阅读视图

发现新文章,点击刷新页面。

SSE 实现 AI 对话中的流式输出

SSE 实现 AI 对话中的流式输出

日常使用 deepseek,经常看到聊天机器人的流式输出,感觉很赞,想尝试下自己实现一个类似效果

各个大模型平台,api 调用都支持流式输出,如果用 node.js 如何实现一个流式输出效果呢

整体内容包含服务端实现客户端实现

整体效果

steamOut.gif

SSE 技术原理

Server-Sent Events (SSE) 是一种基于 HTTP 的服务器向客户端推送数据的技术。与 WebSocket 不同,SSE 是单向的,仅支持服务器向客户端推送数据,但实现简单,且天然支持断线重连。

  • 单向通信,由服务器发送数据,客户端接收数据
  • 自动重连
  • 轻量级,相比 websocket,开销更小
  • 主要传输文本数据,适合 JSON 等结构化数据
  • sse 的消息结构
    • id: 事件 id
    • event: 事件名称 如果不传默认为 message,如果设置其他名称,前端也需要修改成对应的名称,进行消息接受
    • data: 事件数据
    • retry: 重连时间

服务端实现

  • node.js 的 koa2 框架

实现步骤

  1. 设置 SSE 响应头 响应内容类型,客户端不缓存,保持长连接
  2. 防止 koa 自动处理响应,手动设置响应状态码为 200
  3. 定时发送消息模拟推流,通过ctx.res.write(data: msg),也可以传入id, event补充消息内容。
  4. 通过ctx.req.on(eventName, callback)进行连接监听,处理连接关闭,连接失败
aiRouer.get("/stream", (ctx) => {
  console.log("进入stream");

  // 设置SSE响应头
  ctx.set({
    "Content-Type": "text/event-stream",
    "Cache-Control": "no-cache",
    Connection: "keep-alive",
    "Access-Control-Allow-Origin": "*",
  });

  // 关键修复:防止Koa自动处理响应
  ctx.respond = false;
  ctx.status = 200;

  // 发送初始连接成功消息
  ctx.res.write(`data: ${JSON.stringify({ message: "Connected to Koa2 SSE server" })}\n\n`);

  // 设置定时器,定期发送消息
  let counter = 0;
  const timer = setInterval(() => {
    counter++;
    const data = {
      message: `Server time: ${new Date().toLocaleTimeString()}`,
      counter: counter,
    };

    // 检查连接是否仍然有效
    if (ctx.res.writable) {
      // 按照SSE格式发送数据
      ctx.res.write(`id: ${counter}\n`);
      ctx.res.write(`event: message\n`);
      ctx.res.write(`data: ${JSON.stringify(data)}\n\n`);

      if (counter > 10) {
        ctx.res.end();
      }
    } else {
      // 如果连接已关闭,清理资源
      clearInterval(timer);
      console.log("SSE connection closed due to client disconnect");
    }
  }, 1000);

  // 处理连接关闭
  ctx.req.on("close", () => {
    clearInterval(timer);
    console.log("SSE connection closed");
  });

  // 处理错误
  ctx.req.on("error", (err) => {
    clearInterval(timer);
    console.error("SSE connection error:", err);
  });
});

客户端实现

  • vue.js

实现步骤

  1. 触发 SSE 推送,创建一个 EventSource 对象,并监听服务端的推送消息。
  2. 通过 inputStr 接受消息,设置连接关闭条件,并结束定时器。
  3. 定时取数据,输出到outStr
<template>
  <div class="steam">
    <h3>服务端回答的消息</h3>
    <div ref="outRef" class="out-textarea" v-if="outStr">{{ outStr }}</div>
    <div>
      <n-button type="info" @click="onSend">发送</n-button>
    </div>
  </div>
</template>
import { NInput, NButton } from "naive-ui";
import { reactive, toRefs } from "vue";

const state = reactive({
  outRef: null,
  outStr: "",
  inputStr: "",
  inputFinish: false,
});
const { outRef, outStr, inputStr, inputFinish } = toRefs(state);

/**
 * 流式输出
 */
function outStream() {
  let i = 0;
  let timer = setInterval(() => {
    // 一直等待输出,直到服务端停止输出
    if (inputFinish.value && i >= inputStr.value.length) {
      clearInterval(timer);
      timer = null;
      return;
    }
    if (i < inputStr.value.length) {
      state.outStr += inputStr.value[i];
      i++;
    }
  }, 100);
}
function onSend() {
  let eventSource = new EventSource("/api/ai/stream");
  inputStr.value = "";
  outStr.value = "";
  inputFinish.value = false;
  eventSource.onmessage = function (e) {
    const data = JSON.parse(e.data);
    inputStr.value += data.message;
    if (data.counter > 10) {
      eventSource.close();
      eventSource = null;
      inputFinish.value = true;
    }
  };
  outStream();
}
.out-textarea {
  display: inline-block;
  background-color: rgba(0, 0, 0, 0.06);
  padding: 12px 16px;
  border-radius: 8px;
  position: relative;
  box-sizing: border-box;
  min-width: 0;
  max-width: 100%;
  color: rgba(0, 0, 0, 0.88);
  font-size: 14px;
  line-height: 1.5714285714285714;
  min-height: 46px;
  word-break: break-word;
  margin-top: 24px;
  margin-bottom: 24px;
  scrollbar-color: rgba(0, 0, 0, 0.45) transparent;
}

ai 组件库

  • 目前大厂都有成熟的 ai 组件库,其中就包括对话流式输出组件
  • antd-design-x-vue 对话气泡框
  • RICH 设计范式思考
    • Role 【角色】以后产品和人交互,更像是一个人。可以通过角色外观,声音,情绪,专业领域知识
    • Intention 【意图】 以前收集用户需求,通过输入框,按钮,鼠标,触摸动作 以后 ai 会做的更多,比如通过对话、语音、结合少量原先图形化交互、更加准确和简单
    • Conversation 【对话】人与 ai 的会话规则 开始/追问/提示/确认/错误/结束
    • Hybrid UI 【混合界面】 Do 为主/Do + Chat 均衡/Chat 为主

Java 后端实现基于 JWT 的用户认证和权限校验(含代码讲解)

在前后端分离的项目中,JWT(JSON Web Token) 是一种非常流行的用户认证机制。相比传统的 Session,JWT 更加轻量、易扩展,特别适合前后端分离、移动端 API 场景。

今天我们基于 Spring Boot + JWT,完整实现一套 登录生成 Token + 认证拦截器校验 Token 的流程,并附完整示例代码,助你轻松上手。


📌 项目背景

  • 技术栈:Spring Boot + MyBatis + JWT

  • 功能目标:

    • 用户登录,生成 Token
    • 后续请求带 Token 自动鉴权
    • 无需 Session,支持跨服务调用

🧩 一、登录接口:生成 JWT Token

@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody Map<String, String> loginRequest) {
    String username = loginRequest.get("username");
    String password = loginRequest.get("password");

    // 1. 数据库查用户
    QueryWrapper<User> queryWrapper = new QueryWrapper<>();
    queryWrapper.eq("username", username);
    User user = userMapper.selectOne(queryWrapper);

    if (user == null) {
        return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("用户名不存在");
    }

    // 2. 验证密码
    if (!passwordEncoder.matches(password, user.getPassword())) {
        return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("密码错误");
    }

    // 3. 登录成功,生成 JWT
    String token = JwtUtils.generateToken(user); // 👈 自定义工具类

    Map<String, String> response = new HashMap<>();
    response.put("token", token);
    response.put("message", "登录成功");
    return ResponseEntity.ok(response);
}
  • 登录成功后,前端拿到 token,后续请求都通过请求头 Authorization: Bearer <token> 携带。

🔑 二、JWT 工具类(JwtUtils)

public class JwtUtils {

    private static final String SECRET_KEY = "your-secret-key";
    private static final long EXPIRATION_TIME = 3600000; // 1 小时

    // 生成 Token
    public static String generateToken(User user) {
        return Jwts.builder()
                .setSubject(user.getUsername())
                .claim("role", user.getRole()) // 可扩展更多字段
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
                .signWith(SignatureAlgorithm.HS256, SECRET_KEY)
                .compact();
    }

    // 解析 Token
    public static Claims parseToken(String token) {
        return Jwts.parser()
                .setSigningKey(SECRET_KEY)
                .parseClaimsJws(token)
                .getBody();
    }
}

🧱 三、JWT 认证过滤器:拦截每个请求校验 Token

public class JwtAuthenticationFilter implements Filter {

    private static final String SECRET_KEY = "your-secret-key";

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {

        HttpServletRequest httpRequest = (HttpServletRequest) request;
        String token = getTokenFromRequest(httpRequest);

        System.out.println("JWT token: " + token); // ✅ 打印调试

        if (StringUtils.hasText(token)) {
            try {
                Claims claims = Jwts.parser()
                        .setSigningKey(SECRET_KEY)
                        .parseClaimsJws(token)
                        .getBody();

                String username = claims.getSubject();
                String role = claims.get("role", String.class);

                UsernamePasswordAuthenticationToken authentication =
                        new UsernamePasswordAuthenticationToken(
                                username,
                                null,
                                Collections.emptyList() // 可以配置权限
                        );

                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpRequest));
                SecurityContextHolder.getContext().setAuthentication(authentication);

            } catch (Exception e) {
                System.out.println("JWT解析失败: " + e.getMessage());
            }
        }

        chain.doFilter(request, response);
    }

    private String getTokenFromRequest(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7); // 去掉 Bearer 前缀
        }
        return null;
    }
}

🧠 拦截器作用:

  • 自动从请求头解析 Token
  • 校验签名、过期时间
  • 如果合法,向 Spring Security 注入 Authentication 对象,代表当前用户已认证

⚙️ 四、注册过滤器(Spring Security 环境)

如果使用 Spring Security,可以将 JwtAuthenticationFilter 注册到过滤链中:

@Configuration
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
                .csrf(csrf -> csrf.disable())
                .authorizeHttpRequests(auth -> auth
                    .antMatchers("/login").permitAll()
                    .anyRequest().authenticated()
                )
                .addFilterBefore(new JwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
                .build();
    }
}

📋 五、前端如何携带 Token 请求

前端登录后,拿到后端返回的 token,之后的所有请求都加上请求头:

axios.get('/api/user/info', {
  headers: {
    Authorization: 'Bearer ' + token
  }
})

如果你用的是 Fetch:

fetch('/api/user/info', {
  headers: {
    'Authorization': 'Bearer ' + token
  }
});

✅ 最终效果演示

  1. 调用 /login,返回:
{
  "token": "eyJhbGciOiJIUzI1NiIsInR...",
  "message": "登录成功"
}
  1. 访问需要认证的接口,如 /user/info,带上 token,返回成功数据。
  2. 如果 token 错误或过期,会被过滤器拦截,返回未认证提示。

✍️ 总结

步骤 内容
1️⃣ 登录成功生成 JWT
2️⃣ 前端保存 token 并携带请求
3️⃣ 后端过滤器拦截校验 JWT
4️⃣ 注入认证信息,实现无状态登录

🎁 附加建议:

  • 可以使用 Redis 存储 Token 黑名单,实现退出登录/踢人下线。
  • 生产环境务必设置合适的 Token 过期时间、密钥强度。
  • 登录成功返回 token 的同时,也可以返回用户信息。

📌 如果你觉得这篇文章对你有帮助,欢迎点赞 + 收藏 + 关注!我会持续更新更多 Java 项目实战和技术干货。

CSS/JS/图片全挂了,部署后页面白屏/资源加载失败?这两个配置项坑了多少人!

很多同学在本地开发时一切正常,但一把项目部署到服务器上就各种崩溃:页面打不开、图片不显示、甚至连JS文件都404了。其实这些问题大概率都跟两个配置有关,而且都和路径相关!

今天就用大白话讲清楚这两个配置到底管啥用,以及怎么设置才能避免部署翻车。


问题一:为什么页面能打开,但CSS/JS/图片全挂了?

这通常是因为资源路径错了

想象一下:
你写完代码打包后,生成一个dist文件夹。里面虽然有很多资源,但最终只有一个index.html(因为是单页面应用)。

部署到服务器后,你访问首页(比如 www.example.com)能正常打开,但一点其他路由(比如 www.example.com/about)就白屏,或者资源加载失败。

为什么?
因为你打包时资源用的是相对路径(比如 ./static/js/main.js)。当你访问首页时,浏览器拼接出来的路径是:
www.example.com/static/js/main.js ✅ 能访问到。

但如果你访问的是/about页面,浏览器会尝试拼接:
www.example.com/about/static/js/main.js ❌ 这个路径根本不存在!

怎么解决?
在打包工具的配置中(比如Vue的vue.config.js或React的webpack.config.js)设置**公共路径(publicPath)**为绝对路径:

// vue.config.js
module.exports = {
  publicPath: '/'
  // 或者如果你的项目部署在子路径(如/www/demo/),则设为 '/demo/'
};

这样打包后所有资源都会从根路径开始查找(比如 /static/js/main.js),不管在哪个页面访问,资源路径都是对的。


问题二:为什么页面能打开,资源也加载了,但路由跳转失效?

这是因为路由配置少了基础路径

比如你的项目部署在服务器子目录(如 www.example.com/demo/),但你的路由配置还是本地开发时的根路径:

const router = new VueRouter({
  routes: [...],
  mode: 'history'
});

结果你访问 www.example.com/demo/about 时,路由拿着/about去匹配,但实际部署的路径是/demo/about,当然匹配不上!

怎么解决?
在路由配置中加上base选项:

const router = new VueRouter({
  routes: [...],
  mode: 'history',
  base: '/demo/' // 这里填你的部署子路径
});

更聪明的做法是通过环境变量动态设置

base: process.env.BASE_URL

然后在开发环境设为空,生产环境设为你的子路径(比如/demo/),这样本地和部署都能正常工作。


总结一下:

  1. 资源加载失败 → 检查打包配置的 publicPath,改成绝对路径(一般是 '/'
  2. 路由跳转失败 → 检查路由配置的 base,确保和部署路径一致

这两个配置项改起来就一行代码,但如果不理解背后的原理,出了问题根本不知道从哪下手。希望这篇能帮你少走弯路!

ConcurrentHashMap 1.7 vs 1.8:分段锁到 CAS+红黑树的演进与性能差异

原文来自于:zha-ge.cn/java/78

ConcurrentHashMap 1.7 vs 1.8:分段锁到 CAS+红黑树的演进与性能差异

讲真,老程序员都得靠点岁月滤镜。我第一次深扒 ConcurrentHashMap 的源码,还是在 JDK 1.7 时代,那时候的哈希并发魂就是“分段锁”,每次和同事聊起,都有点“这玩意多神”的尬自豪。结果等到 1.8 出来,我突然尴尬地发现——这货都变天了!今天,就和大家唠唠我踩过的那些并发哈希坑。


老 ConcurrentHashMap 的分段锁江湖

Java 1.7 的 ConcurrentHashMap 纯粹是“分治思想”的现实写照。它把整个 Map 切成多个 Segment,每个 Segment 都是个小 HashTable,管自己一亩三分地。你 put、get、remove,基本不会打架:

  • 分段锁(Segment):每段一把锁,减少竞争
  • 不支持 null key/null value(冷知识)

比如放数据,大致长这样:

int hash = hash(key);
int segmentIndex = (hash >>> segmentShift) & segmentMask;
Segment<K,V> s = segments[segmentIndex];
s.lock();
try {
    s.put(key, value);
} finally {
    s.unlock();
}

简化后的意思就是:我锁的只是 Segment,不像 HashTable 那样整个表锁死(所以 HashTable 几乎没人用了...)。


JDK 1.8:演变新世界,CAS+链表+红黑树

到了 1.8,这玩意彻底变了个腔调,“分段锁”拜拜,取而代之的是“桶级别”的操作+一身并发黑科技:

  • 不再有 Segment 数组,只有 Node[] table
  • put 的时候,先 CAS 尝试抢占 Node(用 synchronized 兜底)
  • 链表太长自动转红黑树,查找 O(logn) 不是梦

来,来,看核心放数据的套路:

Node<K,V>[] tab = table;
int i = (n - 1) & hash;
Node<K,V> f = tabAt(tab, i);
if (f == null) {
    if (casTabAt(tab, i, null, newNode)) {
        // CAS抢坑成功,无竞争~
    }
} else {
    synchronized (f) {
        // 操作链表或红黑树,争抢得激烈则自动变树!
    }
}

注:这 tabAtcasTabAt 都是 Unsafe 的骚操作~


踩坑瞬间

我自己曾经经历过一次性能“大地震”。那会儿线上压测,两个不同 JDK 下 ConcurrentHashMap,居然结果天差地别。

痛点回忆:

  • 1.7 多线程 put,性能稳定,但线程数过高还是得拼命锁各个段
  • 1.8 急剧提升高并发下的写吞吐,尤其线程超多时,老版本直接锁段,等得人抓耳挠腮,新版 CAS 猛冲
  • 红黑树救过命:有一回 keys 奇葩碰撞扎堆,1.7 直接挂了链表超长数秒。1.8 自动转树,时间稳定 O(logn),查找都不带卡的

有次还诡异碰到过遍历 ConcurrentHashMap 一边 put 的场景,1.7 下因为 Segment 结构脑壳痛,1.8 基本溜了。


性能小对比

偷偷摸鱼做了下对比:

JDK 高并发写延迟 键大量冲突 遍历一致性
1.7 分段锁 查找慢 易被锁阻塞
1.8 CAS+树 稳定O(logn) 比较优秀

简单一句话:1.7 读写分段锁,跑满高速路还堵车;1.8 脱胎换骨,红绿灯智能变道,加点“算法调度”,体验大变脸!


经验启示

  • 多线程首选 1.8 及以上,不用 segment 锁省心
  • Async 大量写入/高冲突 key,红黑树才是真香代码
  • 不想卡链表查找?JDK 1.8 后不怕碰撞怪兽
  • 想偷懒?1.8 遍历、并发用法更随心,不用操心锁分段

唉,老了,偶尔还是怀念 Segment 那点“老锁情怀”,但终究得向先进技术低头。也许代码本来也是进化史——每一版改动后,费神琢磨“为啥这样”,也就有了新技能傍身。大家有啥并发“笑话”,咱评论区扯一扯呗?

写到这儿,键盘都热了,今天就聊到这儿吧。忙到凌晨的程序员,才能继续踩下一个坑不是?

MinIO本地对象存储部署指南

一、下载并安装MinIO

1、访问MinIO官网的下载页面:min.io/download

2、选择对应操作系统的版本进行下载。windows系统,可以下载.exe二进制文件;对于macOS/Liunx,可下载二进制文件或使用Docker方式。

直接运行(Mac/Linux)

下载二进制文件wget dl.min.io/server/mini…

赋予执行权限

chmod +x minio

启动 MinIO 服务器,并指定数据存储目录(例如 ./data)

./minio server ./data`

Docker 方式(推荐,跨平台)

docker run -p 9000:9000 -p 9001:9001 \
-v /path/to/your/data:/data \
minio/minio server /data --console-address ":9001"
  • -p 9000:9000: 将容器的API端口(9000)映射到本地。
  • -p 9001:9001: 将容器的控制台端口(9001)映射到本地。
  • -v ...: 将容器内的 /data目录挂载到本地的一个路径,用于持久化存储。

MinIO 开源版官方下载地址

Windows (AMD64/64位)

  • 下载链接: dl.min.io/server/mini…
  • 推荐做法: 下载后,将 minio.exe放在一个专门的目录,如 C:\MinIO

Linux (AMD64/64位)

# 使用 wget 下载
wget https://dl.min.io/server/minio/release/linux-amd64/minio
# 或使用 curl 下载
curl -O https://dl.min.io/server/minio/release/linux-amd64/minio
# 授予执行权限
chmod +x minio

macOS (Apple Silicon)

# 下载适用于 Apple Silicon 的版本
wget https://dl.min.io/server/minio/release/darwin-arm64/minio
chmod +x minio

验证下载文件(推荐)

为了确保文件完整性和安全性,建议验证文件的 SHA256 校验和。

  1. 下载校验和文件(与 minio.exe同目录):
  1. 在 PowerShell 中计算下载文件的哈希值
Get-FileHash -Path .\minio.exe -Algorithm SHA256

对比:将命令输出的哈希值与下载的 .sha256sum文件中的内容进行对比,两者应该完全一致

快速启动命令(Windows)

打开 PowerShell,导航到你存放 minio.exe的目录:

cd C:\MinIO

启动 MinIO 服务器(将 D:\MinIO-Data替换为你想要的数据目录):

.\minio.exe server D:\MinIO-Data --console-address ":9001"

访问控制台

  • 浏览器打开:http://localhost:9001

  • 使用默认账号登录:

    • 账号: minioadmin
    • 密码: minioadmin

官方源:始终从 dl.min.io或 github.com/minio/minio下载,避免使用第三方镜像,以防捆绑恶意软件。

历史版本dl.min.io/server/mini… 然后选择对应的平台和版本目录。

程序 用途 示例命令
minio.exe 启动 MinIO 服务器 .\minio.exe server D:\MinIO-Data --console-address ":9001"
mc.exe 管理 MinIO/S3 存储 .\mc.exe alias set myminio http://localhost:9000 minioadmin minioadmin

二、访问并登录

1、运行成功后,命令行会输出一个访问地址、Access Key 和 Secret Key;

2、打开浏览器,访问 http://localhost:9001(Docker方式)或 http://localhost:9000(直接运行方式);

3、使用命令行输出的 Access Key 和 Secret Key 登录管理控制台。

三:创建 Bucket 和 Access Key

  1. 在管理控制台中,点击 Buckets -> Create Bucket,创建一个桶(例如 my-local-bucket)。
  2. 点击左侧栏 Access Keys -> Create Access Key,可以创建新的访问密钥对(或者使用初始的root密钥)。记下 Access Key和 Secret Key

四、windows系统中minio自启动

NSSM 是一个将普通应用转换为Windows服务的优秀工具,非常适合管理MinIO

步骤1:下载NSSM

1、NSSM官方地址: nssm.cc/download

2、下载最新版本(例如 nssm-2.24.zip)

3、解压压缩包,根据你的系统位数(通常是 64 位)进入 win64目录,找到 nssm.exe

步骤2:安装MinIO 服务

1、以 管理员身份 打开 PowerShell 或 CMD

2、导航到包含 nssm.exe的目录

3、运行以下命令来创建服务(请根据你的实际路径修改):

.\nssm.exe install MinIO-Server

4、这会打开一个图形化界面,进行如下配置:

  • Path: 浏览选择你的 minio.exe的完整路径(例如 C:\MinIO\minio.exe)。
  • Startup directory: 同上,选择 minio.exe所在的目录(例如 C:\MinIO)。
  • Arguments: 输入 MinIO 的启动参数:
server D:\MinIO-Data --console-address ":9001"
  • (将 D:\MinIO-Data替换为你的数据目录)
  • Service nameMinIO-Server(会自动填充) 5、点击 "Install service" 按钮

步骤 3:管理服务

启动服务

net start MinIO-Server

停止服务

net stop MinIO-Server

前端用 pdf.js 将 PDF 渲染到 Canvas 再转图片,文字消失的坑

一、背景与业务场景

  • 业务流程:上传 PDF → 使用 pdf.js 渲染页面为 canvas → canvas.toDataURL() → 上传为图片 → 页面展示图片。
  • 现象对比:
    • WPS 创建的 PDF:渲染为图片后,文字正常显示。
    • “网页转 PDF”的文件:渲染为图片后,文字不显示(图片里空白或只有图形/线条,没有文字)。

image.png

二、问题现象

  • 代码位置(节选):
    • 使用 pdfjsLib.getDocument 加载 PDF 数据后,在 renderPdfToImages 中调用 page.render 渲染到 canvas,之后 canvas.toDataURL() 上传。

image.png

  • 异常点:
    • 相同的流程下,只有“网页转 PDF”的文档渲染后的图片出现文字缺失。

三、排查线索

  • 之前未为 pdf.js 显式配置 CMaps、标准字体路径等参数。
  • “网页转 PDF”常见特点:使用 CID/子集字体,依赖 CMap 资源映射字形;若运行时找不到对应资源,pdf.js 会出现文字无法正确绘制到 canvas 的情况。
  • WPS 导出的 PDF 通常内嵌常用字体,即使不配置 CMaps 也能绘制出字形,不容易暴露问题。

四、根因分析

  • pdf.js 在渲染涉及 CJK(中文/日/韩)与 CID 字体的 PDF 时,需要 CMap 和标准字体资源辅助解析。未配置 cMapUrl / cMapPacked (以及在部分场景下的 standardFontDataUrl )会导致文本无法正确绘制。
  • 个别环境中直接将 ArrayBuffer 传入 getDocument 可能存在解析不稳定的问题,转为 Uint8Array 更稳妥。
  • 渲染 intent 使用默认值时,在某些 PDF 文本/矢量渲染质量欠佳;使用 intent: 'print' 可提高文本渲染稳定性和清晰度。

五、解决方案(分步骤)

1)准备静态资源(CMaps 与标准字体)

  • 将 node_modules 中的标准字体资源复制到 public (public/lib/pdfjs-dist/web/cmaps/ ) 这样运行时就可以通过 URL 加载这些静态资源,不会被打包工具“吃掉”。

2)为 getDocument 显式配置 CMaps/(可选)标准字体,并把 ArrayBuffer 转为 Uint8Array

  • 要点:
    • data :使用 Uint8Array (由 ArrayBuffer 转),提升兼容性。
    • cMapUrl :指向 public 下的 cmaps/ 目录。
    • cMapPacked: true :使用打包的 .bcmap 文件。
    • standardFontDataUrl :可选。如果极个别 PDF 仍有文字缺失,可打开这一项。
// 将 ArrayBuffer 转成 Uint8Array,并配置 CMaps/标准字体资源(可选)
// 这样可解决“网页转 PDF”使用 CID/中文字体时文字不显示的问题
const pdfBinary = pdfData instanceof ArrayBuffer ? new Uint8Array(pdfData) : pdfData

// 显式配置 pdf.js 的 CMaps 路径与打包模式(.bcmap)
const loadingTask = pdfjsLib.getDocument({
 data: pdfBinary,
 cMapUrl:'/lib/pdfjs-dist/web/cmaps/',
 cMapPacked: true,
})

const pdf = await loadingTask.promise

3)渲染时增加 intent: 'print'

  • 要点:提升文字/矢量渲染清晰度与稳定性,减少文字丢失或模糊。
// 设置渲染意图为 'print',可提升文字/矢量渲染清晰度与稳定性
await page.render({
  canvasContext: context,
  viewport: viewport,
  intent: 'print' // 关键:使用“打印渲染”路径
}).promise

总结

  • “WPS PDF 正常、网页转 PDF 异常”的根因多在字体/CMap 资源缺失。
  • 落地方案核心:
    • 给 pdf.js 配置 cMapUrl / cMapPacked (必要时 standardFontDataUrl )
    • 渲染时使用 intent: 'print'
    • ArrayBuffer 转为 Uint8Array

SQLFE:网页版数据库(VUE3+Node.js)

在现代开发中,"看代码说话"往往比抽象描述更有说服力。今天,让我们深入SQLFE的代码世界,通过真实的代码片段,探索这个数据库管理工具是如何将复杂操作转化为优雅体验的。

image.png

前端核心:Vue 3的魔法实现

1. 数据库连接对话框:ConnectionDialog.vue

<template>
  <el-dialog :title="title" v-model="visible" width="500px">
    <el-form :model="form" label-width="120px" ref="formRef" :rules="rules">
      <el-form-item label="数据库类型" prop="type">
        <el-select v-model="form.type" style="width: 100%">
          <el-option label="MySQL" value="mysql" />
        </el-select>
      </el-form-item>
      
      <el-form-item label="主机" prop="host">
        <el-input v-model="form.host" placeholder="例如:localhost" />
      </el-form-item>
      
      <el-form-item label="端口" prop="port">
        <el-input v-model.number="form.port" placeholder="例如:3306" />
      </el-form-item>
      
      <el-form-item label="用户名" prop="user">
        <el-input v-model="form.user" />
      </el-form-item>
      
      <el-form-item label="密码" prop="password">
        <el-input v-model="form.password" type="password" show-password />
      </el-form-item>
      
      <el-form-item label="数据库名" prop="database">
        <el-input v-model="form.database" placeholder="可选" />
      </el-form-item>
    </el-form>
    
    <template #footer>
      <span class="dialog-footer">
        <el-button @click="closeDialog">取消</el-button>
        <el-button type="primary" @click="handleTest" :loading="testing">测试连接</el-button>
        <el-button type="success" @click="handleSave" :loading="saving">保存连接</el-button>
      </span>
    </template>
    
    <div v-if="testResult" class="test-result" :class="{'success': testResult.success, 'error': !testResult.success}">
      {{ testResult.message }}
    </div>
  </el-dialog>
</template>

<script setup>
import { ref, defineEmits, defineProps } from 'vue'
import { ElMessage } from 'element-plus'
import api from '@/api'

const emit = defineEmits(['close', 'save'])
const props = defineProps({
  connection: Object
})

const visible = ref(true)
const title = props.connection ? '编辑数据库连接' : '新建数据库连接'
const form = ref({
  type: 'mysql',
  host: 'localhost',
  port: 3306,
  user: '',
  password: '',
  database: ''
})
const testing = ref(false)
const saving = ref(false)
const testResult = ref(null)

// 如果是编辑模式,填充表单
if (props.connection) {
  form.value = { ...props.connection }
}

const rules = {
  host: [{ required: true, message: '请输入主机地址', trigger: 'blur' }],
  port: [{ required: true, message: '请输入端口号', trigger: 'blur' }],
  user: [{ required: true, message: '请输入用户名', trigger: 'blur' }]
}

const formRef = ref(null)

const handleTest = async () => {
  try {
    testing.value = true
    const validate = await formRef.value.validate()
    if (!validate) return
    
    const result = await api.testConnection(form.value)
    testResult.value = result
    
    if (result.success) {
      ElMessage.success('连接测试成功!')
    } else {
      ElMessage.error(result.message || '连接测试失败')
    }
  } catch (error) {
    testResult.value = { success: false, message: error.message }
    ElMessage.error('连接测试失败: ' + error.message)
  } finally {
    testing.value = false
  }
}

const handleSave = async () => {
  try {
    saving.value = true
    const validate = await formRef.value.validate()
    if (!validate) return
    
    const result = await api.saveConnection(form.value)
    
    if (result.success) {
      ElMessage.success('连接保存成功!')
      emit('save')
      closeDialog()
    } else {
      ElMessage.error(result.message || '保存失败')
    }
  } catch (error) {
    ElMessage.error('保存失败: ' + error.message)
  } finally {
    saving.value = false
  }
}

const closeDialog = () => {
  visible.value = false
  emit('close')
}
</script>

**代码亮点解析:

  • 使用Vue 3的<script setup>语法,代码更加简洁
  • 表单验证使用Element Plus的内置验证规则
  • api.testConnection()api.saveConnection()封装了与后端的通信
  • 响应式状态管理(testing, saving, testResult)提供流畅的用户反馈

2. 数据库导航树:DatabaseTree.vue

<template>
  <div class="database-tree">
    <el-button type="primary" @click="openConnectionDialog" icon="Plus">添加连接</el-button>
    
    <el-tree
      :data="treeData"
      :props="treeProps"
      node-key="id"
      :expand-on-click-node="false"
      :default-expanded-keys="expandedKeys"
      @node-click="handleNodeClick"
      :load="loadNode"
      :lazy="true"
    >
      <template #default="{ node, data }">
        <span class="custom-tree-node">
          <el-icon v-if="data.icon" :name="data.icon" class="node-icon" />
          <span>{{ node.label }}</span>
        </span>
      </template>
    </el-tree>
    
    <ConnectionDialog 
      v-if="dialogVisible" 
      :connection="editingConnection" 
      @close="dialogVisible = false" 
      @save="handleConnectionSaved"
    />
  </div>
</template>

<script setup>
import { ref, onMounted, computed } from 'vue'
import { ElMessage } from 'element-plus'
import api from '@/api'
import ConnectionDialog from './ConnectionDialog.vue'

const emit = defineEmits(['database-selected', 'table-selected'])
const treeData = ref([])
const expandedKeys = ref([])
const dialogVisible = ref(false)
const editingConnection = ref(null)
const loading = ref(false)

const treeProps = {
  label: 'label',
  children: 'children',
  isLeaf: 'isLeaf'
}

// 初始化加载所有连接
const loadConnections = async () => {
  try {
    loading.value = true
    const connections = await api.getConnections()
    
    treeData.value = connections.map(conn => ({
      id: `conn_${conn.id}`,
      label: conn.name || `${conn.host}:${conn.port}`,
      type: 'connection',
      connection: conn,
      children: []
    }))
    
    // 默认展开第一个连接
    if (treeData.value.length > 0) {
      expandedKeys.value = [treeData.value[0].id]
      loadDatabases(treeData.value[0])
    }
  } catch (error) {
    ElMessage.error('加载连接失败: ' + error.message)
  } finally {
    loading.value = false
  }
}

// 加载数据库
const loadDatabases = async (node) => {
  try {
    const databases = await api.getDatabases(node.connection.id)
    node.children = databases.map(db => ({
      id: `db_${node.connection.id}_${db}`,
      label: db,
      type: 'database',
      connectionId: node.connection.id,
      database: db,
      children: []
    }))
  } catch (error) {
    ElMessage.error('加载数据库失败: ' + error.message)
  }
}

// 加载表
const loadTables = async (node) => {
  try {
    const tables = await api.getTables(node.connectionId, node.database)
    node.children = tables.map(table => ({
      id: `table_${node.connectionId}_${node.database}_${table}`,
      label: table,
      type: 'table',
      connectionId: node.connectionId,
      database: node.database,
      table: table,
      children: []
    }))
  } catch (error) {
    ElMessage.error('加载表失败: ' + error.message)
  }
}

// 加载列
const loadColumns = async (node) => {
  try {
    const columns = await api.getColumns(node.connectionId, node.database, node.table)
    node.children = columns.map(column => ({
      id: `column_${node.connectionId}_${node.database}_${node.table}_${column.Field}`,
      label: `${column.Field} (${column.Type})`,
      type: 'column',
      connectionId: node.connectionId,
      database: node.database,
      table: node.table,
      column: column.Field,
      children: []
    }))
  } catch (error) {
    ElMessage.error('加载列失败: ' + error.message)
  }
}

// 懒加载节点
const loadNode = async (node, resolve) => {
  if (node.level === 0) {
    // 根节点,已经加载过
    return resolve(node.data.children)
  }
  
  if (node.level === 1) {
    // 连接节点,加载数据库
    await loadDatabases(node.data)
    return resolve(node.data.children)
  }
  
  if (node.level === 2) {
    // 数据库节点,加载表
    await loadTables(node.data)
    return resolve(node.data.children)
  }
  
  if (node.level === 3) {
    // 表节点,加载列
    await loadColumns(node.data)
    return resolve(node.data.children)
  }
  
  resolve([])
}

// 节点点击处理
const handleNodeClick = (node) => {
  if (node.type === 'table') {
    emit('table-selected', {
      connectionId: node.connectionId,
      database: node.database,
      table: node.table
    })
  } else if (node.type === 'database') {
    emit('database-selected', {
      connectionId: node.connectionId,
      database: node.database
    })
  }
}

// 打开连接对话框
const openConnectionDialog = (connection = null) => {
  editingConnection.value = connection
  dialogVisible.value = true
}

// 连接保存处理
const handleConnectionSaved = () => {
  loadConnections()
}

onMounted(() => {
  loadConnections()
})
</script>

代码亮点解析:

  • 使用el-tree组件实现树形结构,支持懒加载(lazy="true")
  • loadNode函数实现按需加载,优化性能
  • 响应式数据绑定,自动更新UI
  • 清晰的节点类型管理(connection, database, table, column)
  • 节点点击事件触发相应的视图更新

后端核心:Node.js的API实现

1. 服务启动:index.js

const express = require('express')
const cors = require('cors')
const databaseRoutes = require('./routes/database')
const app = express()
const port = 3001

// 中间件
app.use(cors())
app.use(express.json({ limit: '50mb' }))
app.use(express.urlencoded({ extended: true, limit: '50mb' }))

// 健康检查
app.get('/', (req, res) => {
  res.json({ 
    message: '数据库可视化管理工具API服务', 
    status: 'running',
    version: '1.0.0'
  })
})

// 数据库相关路由
app.use('/api/database', databaseRoutes)

// 全局错误处理中间件
app.use((err, req, res, next) => {
  console.error('全局错误:', err.stack)
  res.status(500).json({ 
    success: false, 
    message: '服务器内部错误',
    error: process.env.NODE_ENV === 'development' ? err.message : undefined
  })
})

// 404处理
app.use((req, res) => {
  res.status(404).json({ 
    success: false, 
    message: '接口不存在',
    path: req.path
  })
})

// 启动服务
app.listen(port, () => {
  console.log(`\n🚀 数据库可视化管理工具后端服务运行在 http://localhost:${port}`)
  console.log('📦 支持的API:')
  console.log('   - POST /api/database/test      测试数据库连接')
  console.log('   - POST /api/database           保存数据库连接')
  console.log('   - GET /api/database            获取所有连接')
  console.log('   - DELETE /api/database/:id     删除连接')
  console.log('   - GET /api/database/:id/databases  获取数据库列表')
  console.log('   - POST /api/database/:id/query   执行SQL查询\n')
})

module.exports = app;

代码亮点解析:

  • 使用cors中间件解决跨域问题
  • 配置了合理的请求体大小限制(limit: '50mb')
  • 详细的启动日志,方便开发者了解服务状态
  • 全局错误处理,避免服务崩溃
  • 清晰的API文档输出

2. 数据库API路由:database.js

const express = require('express')
const router = express.Router()
const dbManager = require('../config/db')

// 测试数据库连接
router.post('/test', async (req, res) => {
  try {
    console.log('[API] 测试数据库连接:', {
      host: req.body.host,
      port: req.body.port,
      user: req.body.user,
      database: req.body.database
    })
    
    const result = await dbManager.testConnection(req.body)
    res.json(result)
  } catch (error) {
    console.error('[API] 测试连接失败:', error.message)
    res.status(500).json({ 
      success: false, 
      message: error.message,
      code: error.code
    })
  }
})

// 保存数据库连接
router.post('/', async (req, res) => {
  try {
    console.log('[API] 保存数据库连接:', {
      host: req.body.host,
      port: req.body.port,
      user: req.body.user
    })
    
    const id = Date.now().toString()
    const result = await dbManager.addConnection(id, req.body)
    
    if (result.success) {
      res.status(201).json({ 
        success: true, 
        id, 
        message: '连接已保存',
        connection: {
          id,
          ...req.body,
          name: req.body.name || `${req.body.host}:${req.body.port}`
        }
      })
    } else {
      res.status(400).json({ 
        success: false, 
        message: result.message 
      })
    }
  } catch (error) {
    console.error('[API] 保存连接失败:', error.message)
    res.status(500).json({ 
      success: false, 
      message: error.message 
    })
  }
})

// 获取所有连接
router.get('/', (req, res) => {
  try {
    console.log('[API] 获取所有连接')
    const connections = dbManager.getAllConnections()
    res.json({ 
      success: true, 
      data: connections 
    })
  } catch (error) {
    console.error('[API] 获取连接失败:', error.message)
    res.status(500).json({ 
      success: false, 
      message: error.message 
    })
  }
})

// 删除连接
router.delete('/:id', async (req, res) => {
  try {
    const { id } = req.params
    console.log(`[API] 删除连接: ${id}`)
    
    const result = await dbManager.removeConnection(id)
    
    if (result) {
      res.json({ 
        success: true, 
        message: '连接已删除',
        id 
      })
    } else {
      res.status(404).json({ 
        success: false, 
        message: '连接不存在',
        id 
      })
    }
  } catch (error) {
    console.error(`[API] 删除连接 ${req.params.id} 失败:`, error.message)
    res.status(500).json({ 
      success: false, 
      message: error.message 
    })
  }
})

// 获取数据库列表
router.get('/:connectionId/databases', async (req, res) => {
  try {
    const { connectionId } = req.params
    console.log(`[API] 获取连接 ${connectionId} 的数据库列表`)
    
    const databases = await dbManager.getDatabases(connectionId)
    res.json({ 
      success: true, 
      data: databases 
    })
  } catch (error) {
    console.error(`[API] 获取连接 ${req.params.connectionId} 的数据库列表失败:`, error.message)
    res.status(500).json({ 
      success: false, 
      message: error.message 
    })
  }
})

// 获取表列表
router.get('/:connectionId/:database/tables', async (req, res) => {
  try {
    const { connectionId, database } = req.params
    console.log(`[API] 获取连接 ${connectionId} 数据库 ${database} 的表列表`)
    
    const tables = await dbManager.getTables(connectionId, database)
    res.json({ 
      success: true, 
      data: tables 
    })
  } catch (error) {
    console.error(`[API] 获取表列表失败:`, error.message)
    res.status(500).json({ 
      success: false, 
      message: error.message 
    })
  }
})

// 执行SQL查询
router.post('/:connectionId/query', async (req, res) => {
  try {
    const { connectionId } = req.params
    const { sql } = req.body
    
    console.log(`[API] 执行查询 (连接: ${connectionId}):`, sql)
    
    if (!sql || typeof sql !== 'string') {
      return res.status(400).json({ 
        success: false, 
        message: 'SQL查询不能为空' 
      })
    }
    
    // 简单的SQL注入防护
    const lowerSql = sql.toLowerCase();
    if (lowerSql.includes('drop') || lowerSql.includes('truncate') || 
        lowerSql.includes('delete') && !lowerSql.includes('from')) {
      return res.status(403).json({ 
        success: false, 
        message: '禁止执行可能有害的SQL语句' 
      })
    }
    
    const result = await dbManager.executeQuery(connectionId, sql)
    res.json({ 
      success: true, 
      ...result 
    })
  } catch (error) {
    console.error(`[API] 执行SQL查询失败:`, error.message)
    res.status(500).json({ 
      success: false, 
      message: error.message 
    })
  }
})

module.exports = router;

代码亮点解析:

  • 详细的API日志记录,便于调试
  • 完善的错误处理和状态码
  • 简单的SQL注入防护机制
  • RESTful风格的API设计
  • 清晰的请求参数验证

3. 数据库连接管理:db.js

const mysql = require('mysql2/promise')
const connections = new Map()

// 测试数据库连接
exports.testConnection = async (config) => {
  try {
    const connection = await mysql.createConnection({
      host: config.host,
      port: config.port,
      user: config.user,
      password: config.password,
      database: config.database || '',
      connectTimeout: 5000
    })
    
    await connection.query('SELECT 1')
    await connection.end()
    
    return { 
      success: true, 
      message: '连接测试成功' 
    }
  } catch (error) {
    console.error('数据库连接测试失败:', error.message)
    return { 
      success: false, 
      message: error.message 
    }
  }
}

// 添加连接
exports.addConnection = async (id, config) => {
  try {
    const connection = await mysql.createConnection({
      host: config.host,
      port: config.port,
      user: config.user,
      password: config.password,
      database: config.database || '',
      connectTimeout: 5000
    })
    
    // 存储连接信息(不存储密码)
    connections.set(id, {
      connection,
      config: {
        host: config.host,
        port: config.port,
        user: config.user,
        database: config.database
      }
    })
    
    return { 
      success: true 
    }
  } catch (error) {
    console.error(`添加连接 ${id} 失败:`, error.message)
    return { 
      success: false, 
      message: error.message 
    }
  }
}

// 获取所有连接(仅返回基本信息)
exports.getAllConnections = () => {
  return Array.from(connections.entries()).map(([id, conn]) => ({
    id,
    ...conn.config
  }))
}

// 移除连接
exports.removeConnection = async (id) => {
  if (connections.has(id)) {
    const { connection } = connections.get(id)
    try {
      await connection.end()
    } catch (error) {
      console.error(`关闭连接 ${id} 时出错:`, error.message)
    }
    connections.delete(id)
    return true
  }
  return false
}

// 获取数据库列表
exports.getDatabases = async (connectionId) => {
  if (!connections.has(connectionId)) {
    throw new Error('连接不存在')
  }
  
  const { connection } = connections.get(connectionId)
  const [rows] = await connection.query('SHOW DATABASES')
  return rows.map(row => row.Database)
}

// 获取表列表
exports.getTables = async (connectionId, database) => {
  if (!connections.has(connectionId)) {
    throw new Error('连接不存在')
  }
  
  const { connection } = connections.get(connectionId)
  await connection.query(`USE \`${database}\``)
  const [rows] = await connection.query('SHOW TABLES')
  return rows.map(row => Object.values(row)[0])
}

// 获取列信息
exports.getColumns = async (connectionId, database, table) => {
  if (!connections.has(connectionId)) {
    throw new Error('连接不存在')
  }
  
  const { connection } = connections.get(connectionId)
  await connection.query(`USE \`${database}\``)
  const [rows] = await connection.query(`DESCRIBE \`${table}\``)
  return rows
}

// 执行SQL查询
exports.executeQuery = async (connectionId, sql) => {
  if (!connections.has(connectionId)) {
    throw new Error('连接不存在')
  }
  
  const { connection } = connections.get(connectionId)
  const [rows, fields] = await connection.query(sql)
  
  // 转换结果为更友好的格式
  return {
    data: rows,
    columns: fields.map(field => ({
      name: field.name,
      type: field.type,
      length: field.length,
      flags: field.flags
    }))
  }
}

代码亮点解析:

  • 使用Map存储连接,便于管理
  • 使用mysql2/promise实现基于Promise的异步操作
  • 连接池管理,避免频繁创建/销毁连接
  • 结果格式化,便于前端使用
  • 详细的错误处理

前后端协作:一次完整的查询之旅

让我们通过一个完整的示例,看看用户从点击表到看到数据的全过程:

1. 前端触发查询

// DatabaseView.vue 中的部分代码
const executeQuery = async (sql = null) => {
  if (!currentConnection.value || !currentDatabase.value) return
  
  const queryToExecute = sql || queryEditor.value
  if (!queryToExecute.trim()) {
    ElMessage.warning('请输入SQL查询')
    return
  }
  
  try {
    loading.value = true
    queryResult.value = null
    queryError.value = null
    
    const result = await api.executeQuery(
      currentConnection.value.id,
      queryToExecute
    )
    
    queryResult.value = {
      data: result.data,
      columns: result.columns.map(col => col.name),
      rowCount: result.data.length,
      executionTime: '0.02s' // 实际应该从后端获取
    }
    
    // 如果是SELECT查询,保存到历史记录
    if (/^\s*SELECT/i.test(queryToExecute)) {
      saveToHistory(queryToExecute)
    }
  } catch (error) {
    queryError.value = error.message
    ElMessage.error('查询执行失败: ' + error.message)
  } finally {
    loading.value = false
  }
}

2. 后端处理请求

// routes/database.js 中的部分代码
router.post('/:connectionId/query', async (req, res) => {
  // ...其他代码
  
  const result = await dbManager.executeQuery(connectionId, sql)
  res.json({ 
    success: true, 
    ...result 
  })
})

3. 数据库操作

// config/db.js 中的部分代码
exports.executeQuery = async (connectionId, sql) => {
  // ...其他代码
  
  const [rows, fields] = await connection.query(sql)
  
  return {
    data: rows,
    columns: fields.map(field => ({
      name: field.name,
      type: field.type,
      length: field.length,
      flags: field.flags
    }))
  }
}

4. 前端渲染结果

<!-- DatabaseView.vue 中的查询结果展示 -->
<div v-if="queryResult" class="query-result">
  <div class="result-header">
    <span>返回 {{ queryResult.rowCount }} 条记录</span>
    <el-button type="primary" size="small" @click="exportToCSV">导出CSV</el-button>
  </div>
  
  <el-table :data="queryResult.data" style="width: 100%" max-height="500">
    <el-table-column 
      v-for="(col, index) in queryResult.columns" 
      :key="index" 
      :prop="col" 
      :label="col"
      :width="getColumnWidth(col)"
    />
  </el-table>
</div>

从代码到体验:技术如何创造价值

通过这些精心设计的代码,SQLFE实现了:

  1. 直观的连接管理:用户无需记忆复杂参数,通过简单的表单即可建立数据库连接
  2. 流畅的导航体验:树形结构清晰展示数据库层次,懒加载确保大型数据库也能快速响应
  3. 安全的查询执行:基本的SQL注入防护,保护数据库安全
  4. 高效的性能:连接池管理避免频繁创建连接,提升响应速度
  5. 友好的错误处理:清晰的错误提示,帮助用户快速定位问题

结语

通过这些真实的代码示例,我们可以看到SQLFE是如何将抽象的技术概念转化为具体的用户体验的。每一行代码背后,都是对开发者体验的深思熟虑。

求人不如靠自己,我命由我不由天。在这个快速变化的技术世界中,理解代码的本质比盲目依赖框架更重要。SQLFE的每一行代码都经过精心设计,不是简单的复制粘贴,而是对问题的深入思考和解决方案的精准实现。

当你在使用SQLFE时,不妨思考一下背后的实现原理。这不仅会让你更好地使用这个工具,也会提升你解决其他问题的能力。毕竟,真正的技术力量,来自于对原理的理解,而非对工具的依赖。

🌐ES6 这 8 个隐藏外挂,知道 3 个算我输!

标签:ES6、JavaScript、性能优化、代码简化


“代码写得少,Bug 自然少。”——鲁迅(并没有说)

今天不聊 React、不聊 Vue,回到语言层,挖一挖那些“官方早就给了,但我们总自己造轮子”的 ES6 冷门 API。
它们每一个都经过浏览器真·原生实现,无 polyfill 也能跑一句顶五句,看完直接复制粘贴就能让同事惊呼“还有这种操作?”。


1. 数组拍平:flat / flatMap

场景:后端把树形结构一股脑塞给你,前端只想拿叶子节点。

// 商品按类目嵌套:[[手机,耳机],[笔记本,鼠标]]
const goods = [['iPhone','AirPods'],['MacBook','MagicMouse']];

// 旧写法
const all = goods.reduce((a, b) => a.concat(b), []); 

// 新写法
const all = goods.flat();          // 默认 1 层
const deep = goods.flat(Infinity); // 无限层

bonusflatMap = map + flat(1),一次循环搞定“一对多”映射。

const users = [{name:'张三',tags:'前端,TS'},{name:'李四',tags:'后端,Go'}];
const pairs = users.flatMap(u => u.tags.split(',').map(t => ({name:u.name, tag:t})));
// [{name:'张三',tag:'前端'}, {name:'张三',tag:'TS'}, ...]

2. 对象 ↔ 数组“瞬移”:entries ↔ fromEntries

场景:只想给对象做“过滤 / 映射 / 排序”,又懒得写 reduce

const score = { 语文:95, 数学:82, 英语:76 };

// 保留 >80 的学科
const pass = Object.fromEntries(
  Object.entries(score).filter(([k,v]) => v > 80)
);
// { 语文:95, 数学:82 }

URL 解析也能一行完成:

const params = Object.fromEntries(new URLSearchParams('name=张三&age=25'));
// { name:'张三', age:'25' }

3. 字符串补全:padStart / padEnd

场景:时间、订单、身份证,位数必须对齐。

const now = new Date();
const time = `${now.getHours().toString().padStart(2,'0')}:${now.getMinutes().toString().padStart(2,'0')}`;
// "09:05" 而不是 "9:5"

固定编号

const orderId = '457';
const fullId = orderId.padStart(8, '0'); // "00000457"

4. 数组去重 + 集合运算:Set

场景:接口返回了 1w 条数据,里面重复 ID 占 30%。

const ids = [3,5,5,7,3,9];
const unique = [...new Set(ids)];        // [3,5,7,9]

// 交集 / 差集,同样一行
const a = new Set([1,2,3]);
const b = new Set([3,4,5]);
const intersect = [...a].filter(v => b.has(v)); // [3]
const diff      = [...a].filter(v => !b.has(v)); // [1,2]

5. 解构“嵌套 + 默认值”

场景:接口字段经常缺失,还要做降级。

function ajax({
  url,
  method = 'GET',          // 默认值
  timeout = 5000,
  headers: { token = '' } = {}  // 嵌套默认值
} = {}) {
  console.log(url, method, token);
}

深层安全取值

const { address: { city, detail = '暂无' } = {} } = user;
// 无论 user.address 是否存在都不会报错

6. 真正“私有”属性:Symbol

场景:写工具库,怕用户覆盖你的内部字段。

const _secret = Symbol('secret');
class Cache {
  [_secret] = new Map();
  set(k,v){ this[_secret].set(k,v); }
  get(k){ return this[_secret].get(k); }
}
const c = new Cache();
c['secret'] = 123;        // 不影响内部
console.log(c[_secret]);  // 外部拿不到

还能改 toString 标签

class Queue {
  [Symbol.toStringTag] = 'Queue';
}
`${new Queue}`; // "[object Queue]"

7. 对象操作“统一入口”:Reflect

场景:写 Proxy 拦截,总担心“死循环”。

const proxy = new Proxy(target, {
  get(t, k){
    console.log('read', k);
    return Reflect.get(t, k); // 调用原始行为,安全
  }
});

安全删除

const ok = Reflect.deleteProperty(obj, 'a'); // 返回布尔,可判断

8. 异步“扫尾”神器:finally()

场景:请求结束必须关 loading,成功失败都得关。

function load(){
  showLoading();
  return fetch('/api/data')
    .then(render)
    .catch(showError)
    .finally(hideLoading); // 只写一次
}

结语:如何无痛养成“新习惯”?

  1. 代码评审刻意问一句:这里能用 flatMap / fromEntries 吗?
  2. 立 Flag:连续三周在业务里用满这 8 个 API,每用一次给自己打 ★。
  3. 团队分享:把本文甩到群里,谁最晚在 PR 里用到,请全组奶茶 🧋。

“轮子”官方已经造好,下次再手写 reduce 去重,就罚自己抄十遍 flat(Infinity) 吧!

极简三分钟ES6 - ES9中字符串扩展

在ES9中主要是放宽了对模板字符串文字限制

模板字符串的「紧身衣」

在ES9之前,模板字符串遇到转义序列时会有严格限制

// ES8及之前会报错 
const path = `C:\new\templates\`;  
// 错误!因为`\n`和`\t`被解释为换行符和制表符 

这就像在严格安检通道

  • 所有``开头的字符必须符合标准转义规则(如\n换行、\t制表符)
  • 非标准格式(如\x\u不完整)直接报错

ES9的「宽松模式」

ES9引入标记模板字面量的宽松解析规则

// ES9中安全使用 
const path = String.raw`C:\new\templates\`;   
// 成功输出 "C:\new\templates\"

允许非常规转义序列

console.log(`\x`);      // ES9输出 "\\x"(保留原样)
console.log(`\u{61}`);  // 输出 "a"(正常解析Unicode)

支持嵌套模板字符串

// 多层模板嵌套(如生成HTML)
const html = `
  <div>
    ${`<span>${userName}</span>`}
  </div>
`;

一些常见的使用场景

Windows路径处理

// 文件路径安全书写 
const configPath = `C:\Program Files\app\config.json`;   
// 无需写成:C:\\Program Files\\app\\config.json  

正则表达式简化

// 匹配反斜杠的正则 
const regex = new RegExp(`\d+\\\.\\d+`);  
// 无需写成:\\d+\\\.\\d+

代码生成工具

// 生成带转义字符的代码 
const code = `console.log("Hello\\nWorld");`;   
// 输出:console.log("Hello\nWorld"); 

与传统方案相比

场景 旧版方案 ES9方案 优势
书写Windows路径 C:\\Program Files C:\Program Files 减少50%反斜杠
包含反斜杠文本 手动转义\ 直接书写`` 避免转义混淆
嵌套模板 字符串拼接 直接嵌套 提升可读性

牢记

解放转义允许非常规转义序列存在,解放嵌套 支持模板多层嵌套结构,解放创作特殊字符处理更自由

Node.js 版本管理、NPM 命令、与 NVM 完全指南

Node.js 版本概述

版本命名规则

Node.js 采用语义化版本控制(Semantic Versioning),格式为:主版本号.次版本号.修订号

  • 主版本号:不兼容的 API 修改
  • 次版本号:向下兼容的功能性新增
  • 修订号:向下兼容的问题修正

版本类型

LTS 版本(长期支持版本)

  • Active LTS:当前活跃维护的 LTS 版本
  • Maintenance LTS:维护模式的 LTS 版本
  • 特点:稳定性好,生产环境推荐使用
  • 发布周期:每年 10 月发布新的 LTS 版本

Current 版本(当前版本)

  • 特点:包含最新特性,可能不够稳定
  • 适用场景:开发环境、测试新特性

主要版本历史

版本 发布时间 LTS 状态 EOL 时间 主要特性
v18.x 2022.04 Active LTS 2025.04 fetch API、Test Runner
v16.x 2021.04 Maintenance LTS 2024.04 npm 7、Apple Silicon 支持
v14.x 2020.04 EOL 2023.04 Optional Chaining、Nullish Coalescing
v12.x 2019.04 EOL 2022.04 ES6 模块支持、TLS 1.3

NVM 简介与优势

什么是 NVM?

NVM(Node Version Manager)是 Node.js 版本管理工具,允许在同一台机器上安装和管理多个 Node.js 版本。

NVM 的优势

  1. 版本切换:快速切换不同 Node.js 版本
  2. 项目隔离:不同项目使用不同 Node.js 版本
  3. 测试兼容性:测试代码在多个版本下的兼容性
  4. 简化升级:轻松升级或降级 Node.js 版本

NVM 工具对比

工具 平台支持 特点
nvm Linux/macOS 原生 bash 脚本,功能完整
nvm-windows Windows Windows 专用版本
fnm 跨平台 Rust 编写,速度快
n Linux/macOS 简单轻量

NVM 安装指南

Windows 系统安装

方法一:使用 nvm-windows

  1. 下载安装包

    # 访问GitHub下载最新版本
    https://github.com/coreybutler/nvm-windows/releases
    
  2. 运行安装程序

    • 下载 nvm-setup.zip
    • 解压并运行 nvm-setup.exe
    • 按提示完成安装
  3. 验证安装

    nvm version
    

方法二:使用 Chocolatey

# 安装Chocolatey(如果未安装)
Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))

# 安装nvm-windows
choco install nvm

Linux/macOS 系统安装

使用 curl 安装

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash

使用 wget 安装

wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash

配置环境变量

# 添加到 ~/.bashrc 或 ~/.zshrc
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"

# 重新加载配置
source ~/.bashrc

NVM 基础使用

查看版本信息

# 查看NVM版本
nvm --version

# 查看可安装的Node.js版本
nvm list available    # Windows
nvm ls-remote         # Linux/macOS

# 查看已安装的版本
nvm list              # Windows
nvm ls                # Linux/macOS

安装 Node.js 版本

# 安装最新LTS版本
nvm install --lts

# 安装指定版本
nvm install 18.17.0
nvm install 16.20.0

# 安装最新稳定版
nvm install node

# 安装最新版本并设置为默认
nvm install node --latest-npm

切换 Node.js 版本

# 切换到指定版本
nvm use 18.17.0

# 切换到最新LTS版本
nvm use --lts

# 设置默认版本
nvm alias default 18.17.0    # Linux/macOS
nvm use 18.17.0              # Windows每次启动终端需要重新设置

卸载 Node.js 版本

# 卸载指定版本
nvm uninstall 16.20.0

# 查看当前使用的版本
nvm current

npm 完全使用指南

npm 简介

npm(Node Package Manager)是 Node.js 的包管理器,是世界上最大的软件注册表。它不仅用于管理 Node.js 包,也是现代前端开发的核心工具。

npm 版本与更新

查看 npm 版本

# 查看当前npm版本
npm --version
npm -v

# 查看详细信息
npm version

# 查看所有相关版本信息
npm version --json

更新 npm

# 更新到最新版本
npm install -g npm@latest

# 更新到指定版本
npm install -g npm@8.19.2

# 查看可用版本
npm view npm versions --json

包管理命令

安装包

# 安装生产依赖
npm install <package-name>
npm i <package-name>                    # 简写

# 安装开发依赖
npm install <package-name> --save-dev
npm i <package-name> -D                 # 简写

# 安装全局包
npm install -g <package-name>

# 安装指定版本
npm install <package-name>@1.2.3
npm install <package-name>@latest
npm install <package-name>@beta

# 从Git仓库安装
npm install git+https://github.com/user/repo.git
npm install github:user/repo
npm install user/repo

# 从本地路径安装
npm install ./local-package
npm install ../another-package

# 安装所有依赖
npm install                             # 根据package.json安装
npm ci                                  # 从package-lock.json快速安装

卸载包

# 卸载本地包
npm uninstall <package-name>
npm remove <package-name>               # 别名
npm rm <package-name>                   # 简写

# 卸载开发依赖
npm uninstall <package-name> --save-dev

# 卸载全局包
npm uninstall -g <package-name>

# 清理未使用的包
npm prune                               # 移除未在package.json中的包
npm prune --production                  # 移除devDependencies

更新包

# 更新所有包
npm update

# 更新指定包
npm update <package-name>

# 更新全局包
npm update -g

# 检查过期包
npm outdated                            # 检查本地包
npm outdated -g                         # 检查全局包

# 查看包的最新版本
npm view <package-name> version
npm show <package-name> versions        # 查看所有版本

包信息查询

搜索包

# 搜索包
npm search <keyword>
npm s <keyword>                         # 简写

# 在线搜索(更详细)
npm search <keyword> --searchlimit=20
npm search <keyword> --json             # JSON格式输出

查看包信息

# 查看包详细信息
npm info <package-name>
npm view <package-name>                 # 别名
npm show <package-name>                 # 别名

# 查看特定字段
npm view <package-name> version
npm view <package-name> description
npm view <package-name> dependencies
npm view <package-name> repository
npm view <package-name> homepage

# 查看包的所有版本
npm view <package-name> versions
npm view <package-name> time            # 查看发布时间

# 查看包的文档
npm docs <package-name>
npm home <package-name>                 # 打开主页

# 查看包的仓库
npm repo <package-name>

列出已安装的包

# 列出本地包
npm list
npm ls                                  # 简写

# 只显示顶级包
npm list --depth=0

# 列出全局包
npm list -g
npm list -g --depth=0

# 列出特定包
npm list <package-name>
npm list <package-name> -g

# 以JSON格式输出
npm list --json
npm list --json --depth=0

# 列出生产环境包
npm list --prod
npm list --only=production

# 列出开发环境包
npm list --dev
npm list --only=development

脚本管理

运行脚本

# 运行package.json中定义的脚本
npm run <script-name>

# 常用内置脚本
npm start                               # 等同于 npm run start
npm stop                                # 等同于 npm run stop
npm test                                # 等同于 npm run test
npm restart                             # 先stop再start

# 查看所有可用脚本
npm run

# 静默运行(减少输出)
npm run <script-name> --silent
npm run <script-name> -s

# 传递参数给脚本
npm run <script-name> -- --arg1 --arg2

生命周期脚本

# npm会自动运行的生命周期脚本
preinstall                             # 安装前
install                                 # 安装时
postinstall                             # 安装后

preuninstall                            # 卸载前
uninstall                               # 卸载时
postuninstall                           # 卸载后

preversion                              # 版本变更前
version                                 # 版本变更时
postversion                             # 版本变更后

pretest                                 # 测试前
test                                    # 测试时
posttest                                # 测试后

prepublish                              # 发布前(已废弃)
prepublishOnly                          # 仅发布前
publish                                 # 发布时
postpublish                             # 发布后

配置管理

查看配置

# 查看所有配置
npm config list
npm config ls                           # 简写

# 查看详细配置
npm config list -l

# 查看特定配置
npm config get <key>
npm get <key>                           # 简写

# 常用配置查看
npm config get registry
npm config get cache
npm config get prefix

设置配置

# 设置配置
npm config set <key> <value>
npm set <key> <value>                   # 简写

# 常用配置设置
npm config set registry https://registry.npmmirror.com
npm config set cache ~/.npm
npm config set prefix /usr/local

# 设置代理
npm config set proxy http://proxy.company.com:8080
npm config set https-proxy http://proxy.company.com:8080

# 删除配置
npm config delete <key>
npm config rm <key>                     # 简写

# 编辑配置文件
npm config edit

配置文件位置

# 用户级配置文件
~/.npmrc                                # Unix/Linux/macOS
%USERPROFILE%\.npmrc                    # Windows

# 项目级配置文件
<project-root>/.npmrc

# 全局配置文件
/etc/npmrc                              # Unix/Linux
%APPDATA%\npm\etc\npmrc                 # Windows

缓存管理

缓存操作

# 查看缓存信息
npm cache verify

# 清理缓存
npm cache clean
npm cache clean --force                 # 强制清理

# 查看缓存目录
npm config get cache

# 设置缓存目录
npm config set cache /path/to/cache

# 查看缓存内容
npm cache ls                            # 列出缓存内容

发布与版本管理

初始化项目

# 初始化新项目
npm init
npm init -y                             # 使用默认值

# 使用模板初始化
npm init <template-name>
npm create <template-name>              # 等同于上面

# 常用模板
npm init react-app my-app
npm init vue my-app
npm init express-app my-app

版本管理

# 查看当前版本
npm version

# 升级版本
npm version patch                       # 修订版本 1.0.0 -> 1.0.1
npm version minor                       # 次版本   1.0.0 -> 1.1.0
npm version major                       # 主版本   1.0.0 -> 2.0.0

# 预发布版本
npm version prerelease                  # 1.0.0 -> 1.0.1-0
npm version prepatch                    # 1.0.0 -> 1.0.1-0
npm version preminor                    # 1.0.0 -> 1.1.0-0
npm version premajor                    # 1.0.0 -> 2.0.0-0

# 指定版本
npm version 1.2.3

发布包

# 登录npm
npm login
npm adduser                             # 别名

# 查看登录状态
npm whoami

# 发布包
npm publish

# 发布测试版本
npm publish --tag beta
npm publish --tag alpha

# 发布到指定registry
npm publish --registry https://registry.npmjs.org

# 撤销发布(24小时内)
npm unpublish <package-name>@<version>
npm unpublish <package-name> --force    # 撤销所有版本

安全与审计

安全审计

# 检查安全漏洞
npm audit

# 自动修复安全问题
npm audit fix

# 强制修复(可能导致破坏性更改)
npm audit fix --force

# 查看详细审计报告
npm audit --json
npm audit --parseable

# 设置审计级别
npm audit --audit-level moderate        # 只显示中等及以上级别
npm audit --audit-level high            # 只显示高级别

包完整性

# 验证包完整性
npm ls                                  # 检查依赖完整性
npm doctor                              # 运行健康检查

# 检查包的签名
npm verify-signatures

# 安装时验证完整性
npm install --package-lock-only
npm ci                                  # 严格按照lock文件安装

工作空间管理

工作空间操作

# 初始化工作空间
npm init -w ./packages/package-a

# 在工作空间中安装依赖
npm install lodash -w package-a

# 在所有工作空间运行命令
npm run test --workspaces

# 在特定工作空间运行命令
npm run build -w package-a
npm run build --workspace=package-a

# 列出工作空间
npm ls --workspaces

高级功能

npx 使用

# 执行本地安装的包
npx <command>

# 执行远程包(临时下载执行)
npx create-react-app my-app
npx cowsay "Hello World"

# 指定包版本执行
npx <package>@<version>

# 忽略本地包,强制下载
npx --ignore-existing <package>

# 静默模式
npx --quiet <package>
npx -q <package>

链接开发

# 创建全局链接
npm link                                # 在包目录中执行

# 链接到项目
npm link <package-name>                 # 在项目目录中执行

# 取消链接
npm unlink <package-name>
npm unlink                              # 在包目录中执行

私有包管理

# 配置私有registry
npm config set @company:registry https://npm.company.com

# 安装私有包
npm install @company/private-package

# 发布私有包
npm publish --access restricted        # 私有包
npm publish --access public            # 公开包

性能优化

安装优化

# 使用 npm ci 进行快速安装
npm ci                                  # 适用于 CI/CD 环境

# 并行安装
npm install --prefer-offline           # 优先使用离线缓存
npm install --no-optional              # 跳过可选依赖

# 生产环境安装
npm install --only=production
npm install --omit=dev                 # npm 7+

# 减少依赖树深度
npm dedupe                              # 去重依赖
npm ls --depth=0                        # 检查顶级依赖

镜像源配置

# 设置淘宝镜像
npm config set registry https://registry.npmmirror.com

# 设置官方镜像
npm config set registry https://registry.npmjs.org

# 临时使用镜像
npm install --registry https://registry.npmmirror.com

# 查看当前镜像源
npm config get registry

# 使用nrm管理镜像源
npm install -g nrm
nrm ls                                  # 列出可用镜像源
nrm use taobao                         # 切换到淘宝镜像
nrm test                               # 测试镜像源速度

npm 常用配置示例

.npmrc 配置文件示例

# 镜像源配置
registry=https://registry.npmmirror.com

# 缓存配置
cache=/Users/username/.npm-cache

# 安装配置
save-exact=true
package-lock=true

# 安全配置
audit-level=moderate

# 代理配置
proxy=http://proxy.company.com:8080
https-proxy=http://proxy.company.com:8080

# 私有包配置
@company:registry=https://npm.company.com

# 发布配置
access=public

package.json 脚本示例

{
  "scripts": {
    "start": "node server.js",
    "dev": "nodemon server.js",
    "build": "webpack --mode production",
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage",
    "lint": "eslint src/",
    "lint:fix": "eslint src/ --fix",
    "format": "prettier --write src/",
    "clean": "rm -rf dist/",
    "prebuild": "npm run clean",
    "postbuild": "npm run test",
    "deploy": "npm run build && gh-pages -d dist"
  }
}

故障排除

常见问题解决

# 清理并重新安装
rm -rf node_modules package-lock.json
npm install

# 权限问题解决
sudo chown -R $(whoami) ~/.npm
sudo chown -R $(whoami) /usr/local/lib/node_modules

# 网络问题
npm config set proxy http://proxy:8080
npm config set strict-ssl false         # 不推荐,仅用于调试

# 版本冲突
npm ls                                  # 查看依赖树
npm dedupe                              # 去重依赖
npm shrinkwrap                          # 锁定依赖版本

调试信息

# 显示调试信息
npm install --verbose
npm install --loglevel verbose

# 显示详细错误
npm install --loglevel silly

# 查看npm配置路径
npm config get userconfig
npm config get globalconfig

常见问题及解决方案

1. 权限问题

Windows 权限问题

# 以管理员身份运行PowerShell
# 设置执行策略
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser

Linux/macOS 权限问题

# 修复权限
sudo chown -R $(whoami) ~/.nvm
chmod +x ~/.nvm/nvm.sh

2. 环境变量配置问题

Windows 环境变量

# 手动设置环境变量
setx NVM_HOME "C:\Users\%USERNAME%\AppData\Roaming\nvm"
setx NVM_SYMLINK "C:\Program Files\nodejs"
setx PATH "%PATH%;%NVM_HOME%;%NVM_SYMLINK%"

Linux/macOS 环境变量

# 检查配置文件
echo $NVM_DIR
which nvm

# 重新加载NVM
source ~/.nvm/nvm.sh

3. 网络连接问题

设置代理

# 设置npm代理
npm config set proxy http://proxy.company.com:8080
npm config set https-proxy http://proxy.company.com:8080

# 设置NVM代理
export NVM_NODEJS_ORG_MIRROR=https://npm.taobao.org/mirrors/node/

使用国内镜像

# 设置淘宝镜像
export NVM_NODEJS_ORG_MIRROR=https://npmmirror.com/mirrors/node/
export NVM_IOJS_ORG_MIRROR=https://npmmirror.com/mirrors/iojs/

4. 版本切换不生效

检查全局安装的 Node.js

# 卸载全局安装的Node.js
# Windows: 通过控制面板卸载
# Linux/macOS:
sudo rm -rf /usr/local/bin/node
sudo rm -rf /usr/local/bin/npm

清理 PATH 环境变量

# 移除其他Node.js路径
echo $PATH | grep node

5. npm 包管理问题

全局包迁移

# 在新版本中重新安装全局包
npm list -g --depth=0 > global-packages.txt
npm install -g $(cat global-packages.txt | grep -v npm | awk '{print $2}' | cut -d@ -f1)

手动安装 Node.js

Windows 手动安装

方法一:官方安装包

  1. 下载安装包

    • 访问 Node.js 官网
    • 选择 LTS 版本或 Current 版本
    • 下载 Windows Installer (.msi)
  2. 安装过程

    - 运行.msi文件
    - 接受许可协议
    - 选择安装路径(建议默认)
    - 选择组件(建议全选)
    - 完成安装
    
  3. 验证安装

    node --version
    npm --version
    

方法二:便携版安装

  1. 下载便携版

    - 下载Windows Binary (.zip)
    - 解压到指定目录
    
  2. 配置环境变量

    # 添加到PATH
    setx PATH "%PATH%;C:\path\to\node"
    

Linux 手动安装

方法一:包管理器安装

# Ubuntu/Debian
curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -
sudo apt-get install -y nodejs

# CentOS/RHEL
curl -fsSL https://rpm.nodesource.com/setup_lts.x | sudo bash -
sudo yum install -y nodejs

# Arch Linux
sudo pacman -S nodejs npm

方法二:二进制包安装

# 下载二进制包
wget https://nodejs.org/dist/v18.17.0/node-v18.17.0-linux-x64.tar.xz

# 解压
tar -xJf node-v18.17.0-linux-x64.tar.xz

# 创建符号链接
sudo ln -s /path/to/node-v18.17.0-linux-x64/bin/node /usr/local/bin/node
sudo ln -s /path/to/node-v18.17.0-linux-x64/bin/npm /usr/local/bin/npm

macOS 手动安装

方法一:官方安装包

# 下载并安装.pkg文件
# 从官网下载macOS Installer (.pkg)

方法二:Homebrew 安装

# 安装Homebrew(如果未安装)
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

# 安装Node.js
brew install node

# 安装特定版本
brew install node@16

最佳实践建议

1. 版本选择策略

  • 生产环境:使用 LTS 版本,确保稳定性
  • 开发环境:可使用 Current 版本体验新特性
  • 团队协作:统一 Node.js 版本,使用.nvmrc文件

2. 项目版本管理

# 创建.nvmrc文件
echo "18.17.0" > .nvmrc

# 使用项目指定版本
nvm use

# 自动切换版本(需要配置)
# 在.bashrc或.zshrc中添加
autoload -U add-zsh-hook
load-nvmrc() {
  local node_version="$(nvm version)"
  local nvmrc_path="$(nvm_find_nvmrc)"

  if [ -n "$nvmrc_path" ]; then
    local nvmrc_node_version=$(nvm version "$(cat "${nvmrc_path}")")

    if [ "$nvmrc_node_version" = "N/A" ]; then
      nvm install
    elif [ "$nvmrc_node_version" != "$node_version" ]; then
      nvm use
    fi
  elif [ "$node_version" != "$(nvm version default)" ]; then
    echo "Reverting to nvm default version"
    nvm use default
  fi
}
add-zsh-hook chpwd load-nvmrc
load-nvmrc

3. 全局包管理

# 列出全局包
npm list -g --depth=0

# 保存全局包列表
npm list -g --depth=0 --json > global-packages.json

# 在新版本中恢复全局包
# 解析JSON并安装包

4. 性能优化

# 使用更快的镜像源
npm config set registry https://registry.npmmirror.com

# 使用yarn替代npm
npm install -g yarn
yarn config set registry https://registry.npmmirror.com

5. 安全考虑

# 定期更新npm
npm install -g npm@latest

# 检查安全漏洞
npm audit
npm audit fix

# 使用npm ci进行生产环境安装
npm ci --only=production

6. 开发环境配置

# 配置npm初始化默认值
npm config set init.author.name "Your Name"
npm config set init.author.email "your.email@example.com"
npm config set init.license "MIT"

# 设置npm缓存目录
npm config set cache /path/to/npm-cache

故障排除指南

诊断命令

# 检查NVM状态
nvm --version
nvm current
nvm ls

# 检查Node.js和npm
node --version
npm --version
which node
which npm

# 检查环境变量
echo $PATH
echo $NVM_DIR

# 检查npm配置
npm config list
npm config get registry

常见错误及解决

  1. "nvm: command not found"

    • 检查安装是否成功
    • 重新加载 shell 配置文件
    • 检查环境变量配置
  2. 版本切换失败

    • 检查是否有其他 Node.js 安装
    • 清理 PATH 中的冲突路径
    • 重启终端
  3. 权限错误

    • 使用管理员权限
    • 修复文件权限
    • 检查防火墙设置

结语

Node.js 版本管理是现代 JavaScript 开发的重要技能。通过合理使用 NVM 等版本管理工具,可以显著提高开发效率和项目稳定性。建议在实际项目中建立规范的版本管理流程,确保团队协作的一致性。

记住以下关键点:

  • 生产环境优先使用 LTS 版本
  • 使用.nvmrc文件管理项目版本
  • 定期更新和维护开发环境
  • 关注安全更新和最佳实践

前端性能优化之 HTTP/2 多路复用

前端性能优化之 HTTP/2 多路复用

回顾 HTTP/1.x 的队头阻塞问题

要理解多路复用的重要性,我们首先要回顾一下 HTTP/1.x 的工作模式。

在 HTTP/1.x 中,浏览器在同一时间,只能对同一个域名建立有限数量的 TCP 连接(通常是 6-8 个)。而每个 TCP 连接,一次只能处理一个 HTTP 请求。

这种模式导致了一个严重的问题:队头阻塞(Head-of-Line Blocking)

想象一下站在一条单行道的隧道入口,前面有一辆慢悠悠的卡车。虽然身后有许多快速的跑车,但它们都必须排队,等待前面的卡车通过。

在 HTTP/1.x 中,这个“卡车”就是前面那个正在处理的 HTTP 请求。如果一个请求因为某种原因(比如服务器处理慢、网络延迟高)迟迟没有返回,那么它后面的所有请求,即使它们已经准备就绪,也必须等待,无法被处理。

image.png

为了解决这个问题,HTTP/1.x 引入了并发连接,允许浏览器同时建立多个 TCP 连接。但这治标不治本,因为并发连接数有限,而且创建和维护额外的 TCP 连接会带来额外的开销。

HTTP/2 多路复用的核心原理

HTTP/2 引入了二进制分帧(Binary Framing)的概念,核心思想是:在一个 TCP 连接上,可以同时传输多个 HTTP 请求和响应。

这就像把之前的单行道隧道,变成了一条多车道的高速公路。每辆车(每个请求或响应)都有自己的“车道”,互不影响,可以并行通过。

它将所有通信数据流都拆分成小的、独立的帧(Frames)

  1. 流(Streams) :每个 HTTP 请求和响应都被拆分为一个个独立的“流”。流是双向的,可以并行传输。
  2. 消息(Messages) :一个完整的请求或响应,由一个或多个帧组成。
  3. 帧(Frames) :通信的最小单位,包含类型、标志位、流标识符等信息。

当浏览器发起请求时,它会为每个请求分配一个唯一的“流标识符”。请求数据被拆分成多个帧,这些帧可以乱序发送。当服务器接收到这些帧时,会根据流标识符重新组装,还原出完整的请求。

同样,服务器返回的响应数据也会被拆分为帧,乱序发送给浏览器,浏览器再根据流标识符重组。

由于所有数据都封装在一个 TCP 连接中,并且每个帧都有自己的标识,所以浏览器和服务器可以自由地交错发送和接收多个请求的帧,从而实现了真正的并行通信。

image.png

理解模型

  • 浏览器:始发地(货物发送方)。
  • 服务器:终点(货物接收方)。
  • TCP 连接一条多车道的高速公路。这是 HTTP/2 的核心,所有的通信都通过这一条高速公路进行,避免了 HTTP/1.x 时代频繁开辟新“单车道小路”的低效。
  • 流(Streams)高速公路上的多条独立车道。每个请求和响应都拥有自己的车道,它们可以并行地、互不干扰地行驶。
  • 请求/响应(Messages)一定量的货物。这是一个完整的逻辑单元,比如一个完整的图片文件、一个 CSS 文件或一个 JSON 数据。
  • 帧(Frames)运输货物的车辆。每个请求和响应(货物)都会被拆解成许多小的二进制帧(车辆),这些车辆带有唯一的“流 ID”,标识它们属于哪条车道。
  • 乱序到达与重组:不同车道上的车辆(帧)可以交错行驶,甚至乱序到达。但在终点(服务器或浏览器),接收方会根据每个车辆携带的“流 ID”标签,将属于同一条车道的所有车辆重新组合,从而还原出完整的货物。只有当所有车辆都到达,货物才算完整送达。

对比 HTTP/1.x:

  • HTTP/1.x:没有多车道高速公路,只有多条单车道小路
  • 请求/响应:每送一个货物,就必须开辟一条新的小路。
  • 队头阻塞:如果第一辆车(第一个请求)在路上出了故障(网络延迟),那么这条小路上所有的车都必须停下来,等待故障排除,即使它们本身没有任何问题。

浏览器最大并发请求数限制

HTTP/2 时代:流(Streams)并发限制

HTTP/2 的核心是多路复用(Multiplexing) ,它在一个 TCP 连接上可以同时处理多个请求和响应。

那么,为什么浏览器还要限制流的数量呢?这个限制并非为了解决队头阻塞,而是出于以下几个主要原因:

  1. 服务器资源保护 服务器需要为每个并发的分配内存和计算资源。一个服务器不可能无限地处理请求。如果浏览器没有限制,一次性发送数千个流,服务器可能会因资源耗尽而崩溃。这个限制实际上是服务器为防止过载而设定的。
  2. 浏览器资源保护 浏览器同样需要为每个并发的流分配资源。管理数百甚至数千个同时进行的请求会占用大量的内存和 CPU。设置一个合理的上限可以防止浏览器卡顿甚至崩溃。
  3. 流量控制与网络拥塞 虽然 HTTP/2 解决了应用层的队头阻塞,但底层 TCP 协议依然存在流量控制(Flow Control)拥塞控制(Congestion Control) 。如果一条 TCP 连接上的数据量过大,TCP 协议会减慢数据传输速度,导致所有流都变慢。限制并发流的数量,有助于避免过度的网络拥塞。
  4. 实际需求 在大多数情况下,一个页面上的并发请求数量通常不会超过这个上限(通常在 100-200 个)。一个合理的限制既能满足大多数页面的需求,又能保证系统稳定性。

HTTP/1.x 时代:TCP 连接限制

在 HTTP/1.x 中,浏览器限制的是对同一个域名建立的最大 TCP 连接数,通常为 6-8 个。

之所以有这个限制,是因为 HTTP/1.x 存在一个严重的**队头阻塞(Head-of-Line Blocking)**问题。在一个 TCP 连接上,一次只能处理一个请求和响应。如果前面的请求因为网络或服务器原因迟迟没有完成,后面的请求就必须排队等待,即使它们的数据量很小。为了缓解这个问题,浏览器不得不开启多个连接来并行处理请求。但无限开启连接会消耗大量的系统资源(如内存、CPU),因此浏览器设定了一个硬性上限。

对前端开发的影响

HTTP/2 的多路复用,是前端性能优化的一个重大转折点。它对传统的“性能优化黄金法则”产生了深远影响:

  1. 减少请求数不再是首要任务:在 HTTP/1.x 时代,我们通过精灵图(Sprite)、文件合并(JS/CSS Bundling)等方式来减少 HTTP 请求数,以避免队头阻塞和并发连接数的限制。但在 HTTP/2 中,请求数本身带来的开销大大降低,所以我们可以大胆地将文件拆分为更小、更细粒度的模块,实现按需加载
  2. 域名分片(Domain Sharding)失去意义:为了突破浏览器对同一域名 6-8 个连接数的限制,我们曾将资源分散到多个子域名下。在 HTTP/2 中,所有资源都可以通过一个 TCP 连接传输,域名分片反而会增加额外的 DNS 解析和 TCP 连接开销,得不偿失。
  3. HTTP/2 Push:多路复用为服务器推送(Server Push)奠定了基础。服务器可以在浏览器请求 HTML 页面时,提前将页面所需的 CSS、JS 等资源主动推送给浏览器,而无需等待浏览器发起二次请求。这进一步减少了网络延迟。

因此,在 HTTP/2 时代,前端性能优化的重点已经从减少请求数转向了合理拆分资源优化单个资源的大小

例如,可以大胆地将 CSS 和 JavaScript 文件拆分成更小的模块,实现按需加载,从而更好地利用 HTTP/2 的多路复用优势。

总结

HTTP/2 多路复用通过在一个 TCP 连接上并行传输多个请求和响应,彻底解决了 HTTP/1.x 的队头阻塞问题。它让前端开发者可以将注意力从“减少请求数”转移到“按需加载和合理拆分资源”上,从而构建更现代化、性能更优的 Web 应用。

by lichonglou

掌控异步洪流:多请求并发下的顺序控制艺术

Hi,我是前端人类学! 在现代Web开发中,高效地处理多个网络请求是提升应用性能的关键。我们通常会采用并发(Concurrent)或并行(Parallel)的方式同时发起多个请求,以避免漫长的串行等待时间。然而,随之而来一个常见的挑战:“当我有多个请求要发送,并且需要按照特定顺序处理它们的响应时,该怎么办?” 这篇文章将深入探讨这个问题,并提供从基础到高级的多种解决方案。

一、 问题场景:为什么顺序很重要?

首先,我们必须明确“控制发送顺序”和“控制响应处理顺序”的区别。在绝大多数情况下,我们无法也不应该控制HTTP请求的发送顺序。浏览器或Node.js环境会利用底层机制(如HTTP/1.1的管道化、HTTP/2的多路复用)尽可能快地发出所有请求,其到达服务器的顺序是不确定的,并且受网络状况影响。

我们真正需要控制的,其实是响应处理的顺序。即尽管请求A可能比请求B更晚返回,但我们希望先处理A的响应数据,再处理B的。

典型场景包括:

  1. 数据依赖:请求B的参数依赖于请求A的返回结果。
    • 例如:先获取用户ID,再根据用户ID获取个人信息。
  2. UI渲染顺序:需要按顺序渲染内容,如先渲染基础框架,再渲染列表。
    • 例如:一个 dashboard 页面,需要先加载核心指标,再加载详细的图表列表。
  3. 操作依赖:后续操作必须在前一个操作成功之后才能执行。
    • 例如:先创建一条记录,创建成功后再上传该记录的附件。

二、 解决方案集锦

方案一:朴素的链式调用(Callback Hell / Promise Chain)

这是最直接、最易于理解的方式。既然后一个请求依赖前一个,那么就在前一个请求完成之后,再发起下一个。

// 使用 Promise 链
function fetchSequentially() {
  fetch('/api/first')
    .then(response => response.json())
    .then(firstData => {
      // 使用 firstData 的结果作为第二个请求的参数
      return fetch(`/api/second?param=${firstData.id}`);
    })
    .then(response => response.json())
    .then(secondData => {
      console.log('第一个结果:', firstData); // 注意:这里firstData需要闭包或上层变量
      console.log('第二个结果:', secondData);
      // 可以继续链式调用...
    })
    .catch(error => {
      console.error('某个请求失败了:', error);
    });
}

// 使用现代 async/await 语法,更清晰
async function fetchSequentiallyAsync() {
  try {
    const firstResponse = await fetch('/api/first');
    const firstData = await firstResponse.json();

    const secondResponse = await fetch(`/api/second?param=${firstData.id}`);
    const secondData = await secondResponse.json();

    console.log('顺序结果:', firstData, secondData);
  } catch (error) {
    console.error('请求出错:', error);
  }
}

优点:简单明了,代码可读性高。 缺点:请求是串行的,总等待时间是所有请求耗时的总和,性能最差。仅适用于有强依赖关系的场景。

方案二:并发发送,顺序处理(Promise.all + 排序)

当请求之间没有参数依赖,但需要按照发起顺序来处理响应时,我们可以利用 Promise.allPromise.allSettled 来并发请求,然后按固定顺序处理结果。

关键点:我们需要在发送请求时捕获其索引或顺序信息。

async function fetchConcurrentlyButProcessInOrder() {
  const urls = ['/api/a', '/api/b', '/api/c'];

  // 1. 并发发送所有请求,得到一个 Promise 数组
  const fetchPromises = urls.map(url => fetch(url));

  // 2. 等待所有请求完成 (如果有一个失败,Promise.all 会直接 reject)
  try {
    const responses = await Promise.all(fetchPromises);

    // 3. 按顺序提取 JSON 数据
    // 注意:responses 数组的顺序与 urls 的顺序完全一致
    const dataPromises = responses.map(response => response.json());
    const results = await Promise.all(dataPromises);

    // results[0] 对应 urls[0] 的结果,results[1] 对应 urls[1]...
    console.log('按顺序的结果数组:', results);

  } catch (error) {
    // 处理错误
  }
}

更健壮的版本(处理可能失败的请求):

async function fetchWithOrder() {
  const requests = [
    fetch('/api/a'),
    fetch('/api/b'),
    fetch('/api/c'),
  ];

  // 使用 allSettled 即使有失败,也会等待所有Promise完成
  const settledResults = await Promise.allSettled(requests);

  // 然后按顺序处理每个结果
  const finalResults = [];
  for (const result of settledResults) {
    if (result.status === 'fulfilled') {
      const data = await result.value.json();
      finalResults.push(data);
    } else {
      finalResults.push({ error: result.reason });
    }
  }
  console.log(finalResults); // 顺序与 requests 一致
}

优点:充分利用并发,性能好(总耗时约等于最慢的那个请求)。 缺点:必须等所有请求都返回后才能开始处理,如果某个请求特别慢,会阻塞整个结果的处理。并且要求响应处理的顺序必须和发起顺序一致。

方案三:高级模式 - 自定义调度器(如优先级队列)

对于更复杂的场景,例如需要动态调整请求优先级(类似浏览器中资源加载的优先级),我们可以实现一个简单的请求调度器。

这个例子使用一个队列来管理待发送的请求,并控制并发数,但本质上它仍然是按顺序发送的。更高级的可以实现优先级队列。

class RequestScheduler {
  constructor(maxConcurrent = 2) {
    this.maxConcurrent = maxConcurrent; // 最大并发数
    this.currentConcurrent = 0;
    this.queue = [];
  }

  add(requestFn, priority = 0) {
    // 返回一个 Promise,其 resolve/reject 由实际请求决定
    return new Promise((resolve, reject) => {
      this.queue.push({ requestFn, resolve, reject, priority });
      this._tryToRun();
    });
  }

  _tryToRun() {
    // 如果并发数已满或队列为空,则返回
    if (this.currentConcurrent >= this.maxConcurrent || this.queue.length === 0) {
      return;
    }

    // 可以在这里根据 priority 对队列进行排序,实现优先级调度
    this.queue.sort((a, b) => b.priority - a.priority);

    // 取出下一个任务
    const task = this.queue.shift();
    this.currentConcurrent++;

    task.requestFn()
      .then(task.resolve)
      .catch(task.reject)
      .finally(() => {
        this.currentConcurrent--;
        this._tryToRun(); // 一个任务完成,尝试运行下一个
      });
  }
}

// 使用示例
const scheduler = new RequestScheduler(2); // 最大并发数为2

// 添加5个请求,第三个请求优先级最高
scheduler.add(() => fetch('/api/normal1'), 1);
scheduler.add(() => fetch('/api/normal2'), 1);
scheduler.add(() => fetch('/api/important'), 10); // 高优先级
scheduler.add(() => fetch('/api/normal3'), 1);
scheduler.add(() => fetch('/api/normal4'), 1);

// 尽管第三个是添加的,但因为优先级高,它会优先被发出

优点:极其灵活,可以实现复杂的调度策略(如优先级、依赖、重试)。 缺点:实现复杂,属于底层工具,通常只在有非常特殊需求的场景下使用。

三、 总结与选择建议

方案 适用场景 性能 复杂度
链式调用 (async/await) 请求间有强数据依赖 差 (串行)
Promise.all + 排序 请求独立,但需按发起顺序处理 优 (并发)
自定义调度器 复杂场景:优先级调度、流量控制、依赖管理等 可调节

如何选择?

  1. 如果后一个请求需要前一个请求的结果作为参数:毫无疑问,使用方案一(链式调用)
  2. 如果所有请求彼此独立,且你只是希望最终结果是一个有序数组:使用方案二(Promise.all),这是最常见和高效的做法。
  3. 如果你需要实现像“关键请求优先”、“失败自动重试”、“限制并发数以免压垮服务器”等高级功能:那么你需要研究方案三,或直接使用社区成熟的库(如 p-queueaxios 的拦截器与取消功能等)。

理解这些模式背后的核心思想——协调异步操作的完成时机——远比记住代码更重要。这将使你能在面对任何复杂的异步流程时,都能设计出清晰、高效且健壮的解决方案。

干翻 Docker?WebAssembly 3.0 的野心,远不止浏览器,来一起看看吧

今天中午去饭堂吃饭的路上,我照例刷着技术圈的新闻,手指划着划着突然就停住了。一条消息炸了出来:“Wasm 3.0 标准发布”。

发布日期:2025年9月17日。

我心里默念了一下,得,又是一个足以让无数程序员未来几年为之折腾的技术。

可能很多兄弟对 Wasm (WebAssembly) 的印象还停留在“一个能在浏览器里跑 C++/Rust 代码,让网页游戏性能起飞”的阶段。没错,这是它最初的起点,一个为了突破 JavaScript 性能瓶颈而生的“浏览器插件”。

但如果你今天还这么想,那格局就小了。

Wasm 的核心价值是什么?我琢磨了很久,它其实是在实现一个几十年来所有程序员的终极梦想:一种真正与语言无关、与平台无关、高性能、高安全性的二进制格式。

说人话就是:我用 Java、Go、Rust、Python、C# 写的代码,都能编译成一种叫 .wasm 的文件,然后这个文件,你别管是在 Windows、Linux、Mac 上,还是在浏览器、服务器、IoT 设备上,甚至在区块链里,都能跑。而且跑得飞快,还特别安全,每个 Wasm 程序都活在自己的“沙箱”里,翻不出天去。

是不是听着有点耳熟?Java 当年的口号“Write once, run anywhere”?对,就是那个味儿。但 Wasm 野心更大,它想在比 JVM 更底层的地方实现这一切。

而这次的 Wasm 3.0,在我看来,就是它正式“出圈”,从一个“浏览器配角”走向“全场景主角”的成人礼。

我们来看看这次它端出来的几盘“硬菜”。

第一盘硬菜:64位寻址 + 多内存,彻底告别“小打小闹”

以前的 Wasm,用的是 32 位地址,最多只能用 4GB 内存。

这是什么概念?你写个前端应用,做个图片处理,够用。但你想在服务器上跑个大数据分析,或者搞个机器学习模型训练?4GB 内存?开玩笑呢。

这就是为什么之前 Wasm 在后端一直雷声大雨点小,大家觉得它是个“玩具”。

现在,Wasm 3.0 直接给你干到 64 位。理论上是 16EB 的寻址空间,虽然现实中会被限制(比如浏览器里给了 16GB),但这个姿态已经很明确了:我,Wasm,不再是只能在浏览器里过家家的小朋友了,服务器端的活儿,我能接。

还有那个“多内存”,也很有意思。以前一个 Wasm 模块只能操作一块内存,像住在一个单间里。现在一个模块可以同时操作好几块独立的内存。这操作空间就大了去了,比如我可以把敏感数据放在一块隔离的内存里,增加安全性;或者用一块内存做缓冲区,另一块做主逻辑,互不干扰。这为编写更复杂、更安全的系统打开了想象力。

第二盘硬菜,也是最狠的:原生GC(垃圾回收)

这玩意儿,我跟你讲,是划时代的。

之前为啥跑 Go、Java、Kotlin、C# 这些语言到 Wasm 上那么费劲?因为这些语言都是自带 GC 的。你把它们编译到 Wasm,要么就连着庞大的语言运行时一起打包,要么就得自己用 C/Rust 在 Wasm 里模拟一套 GC,那性能和复杂度简直是灾难。

Wasm 3.0 说:“得了,都别瞎折腾了。内存管理这件脏活累活,我来提供基础服务。”

它提供了一套底层的、语言无关的 GC 机制。编译器们(比如 Java 编译器、Go 编译器)只需要告诉 Wasm 运行时:“嘿,我需要一块结构是这样的内存,你帮我管着”,Wasm 就会自动在合适的时候回收它。

这意味着什么?

意味着所有自带 GC 的高级语言,现在都有了一条通往 Wasm 的康庄大道。 未来我们看到用 Java、Scala、Dart 写的应用被编译成 Wasm 包,然后扔到服务器、边缘节点甚至浏览器里去跑,会成为家常便饭。

这扇门一旦打开,整个软件生态可能都要抖三抖。


等会儿,聊到这儿,我得停一下。

作为一个写了十几年代码的老家伙,我见过太多“银弹”了。每次有新技术出来,都有一堆人鼓吹“XXX 已死”、“XXX 是未来”。

说实话,我有点PTSD。

Wasm 3.0 这次端出来的菜,确实香。原生异常处理、尾调用优化……这些都让它越来越像一个“真正的”虚拟平台。

但是,它真的能“干翻 Docker”吗?

Docker 的核心是提供了一个包含完整操作系统环境的隔离容器。你在里面可以用 apt-get,可以访问文件系统,可以起各种进程。它重,但它全。

Wasm 的哲学完全不同。它是一个轻量级的、安全的沙箱,不提供操作系统级别的抽象。它默认不能访问文件、不能访问网络。所有这些能力,都需要宿主环境(比如浏览器、或者一个叫 Wasmtime 的运行时)明确地授权给它。

这既是优点也是缺点。

优点是:

  1. 快。 Wasm 的启动速度是毫秒级,甚至微秒级。Docker 容器启动是秒级。这个差距在 Serverless(函数计算)这种场景下是致命的。
  2. 小。 一个 Wasm 文件可能就几 MB,一个 Docker 镜像动辄几百 MB。
  3. 安全。 Wasm 的沙箱模型默认啥也干不了,权限是“最小可用”,天然就比共享内核的容器要更安全。

这些特性让它在边缘计算、Serverless、插件系统、小程序等领域,简直就是天选之子。想象一下,你可以在 Nginx 里插一个 Wasm 插件来处理请求,或者在数据库里用 Wasm 来执行用户自定义函数,又或者未来的云函数,全都是一个个 Wasm 实例。这比启动一个笨重的 Docker 容器优雅太多了。

但缺点也很明显: 生态。生态。还是TM的生态。 一个技术标准再牛逼,没有配套的工具链、调试器、日志库、监控方案,那都是空中楼阁。我们这些干活儿的人,不是搞科研的,我们关心的是出了问题怎么快速定位,怎么方便地跟现有系统集成。

Docker 和 K8s 经营了这么多年,已经是一整套成熟的工业体系了。Wasm 呢?还在爬坡。


所以,Wasm 3.0 来了,前端会死吗?

不会。别瞎想了。JavaScript 作为“胶水语言”和与 DOM 交互的“总管”地位,短期内无可撼动。Wasm 更像是 JS 身边那个沉默寡言、武功高强的保镖,脏活累活(视频编码、3D渲染、复杂计算)都归他,JS 负责统筹调度。未来的前端,更像是“JS + Wasm”的混合模式。

那后端呢?Docker 会被干翻吗?

我倾向于说,不会被“干翻”,而是“互补”和“侵蚀”。对于需要完整操作系统环境的传统应用,Docker 和 K8s 依然是王道。但对于那些对启动速度、资源占用、安全性要求极高的新场景,Wasm 绝对是一把锋利的匕首,会狠狠地从云原生的版图上,切下一大块属于自己的蛋糕。

说到底,技术圈没有非黑即白。

Wasm 3.0 的发布,不是一声革命的枪响,更像是新大陆被发现后,缓缓驶入港口的第一艘探险船。船上满载着可能性,也充满了未知。

对我等在一线搬砖的工程师来说,现在可能不是需要立刻 all in 的时候,但绝对是需要你抬起头,认真看一看航向的时候了。

可以开始玩玩了。搞个 Rust,编译个 Wasm,在 Wasmtime 里跑跑看。感受一下那种原生性能和跨平台的能力。

至于未来到底会怎样?鬼知道呢。也许几年后,我们简历上的技能栈,都要加上“精通 Wasm 架构设计”了呢。

全屏滚动网站PC端自适应方案

全屏滚动网站的适配要求

既要自适应宽度,也要自适应高度。一般的网站只需要适配宽度即可,高度会自动随着宽度变动。所以一般的网站使用下面两种方案即可。

在实现h5适配常用的方法有:

  1. 插件postcss-px2rem 将px转化为rem, 而rem通常通过插件lib-flexible动态修改根节点html的font-size属性。

    插件lib-flexible核心实现代码如下:

function refreshRem(){
        var width = docEl.getBoundingClientRect().width;
        if (width / dpr > 540) {
            width = 540 * dpr;
        }
        var rem = width / 10;
        docEl.style.fontSize = rem + 'px';
        flexible.rem = win.rem = rem;
    }

    win.addEventListener('resize', function() {
        clearTimeout(tid);
        tid = setTimeout(refreshRem, 300);
    }, false);
  1. 插件postcss-px-to-viewport将px转化为vh(vw/vh/vmin/vmax), 在定屏滚动时,通常将转换后的单位配置为vw。
  • 为什么使用vmin实现全屏滚动网站PC端自适应不行?

    PC的尺寸正常为1920*1080; 插件postcss-px-to-viewport转换vmin的公式是 代码中元素指定的px值 / 插件配置参数viewportWidth;使用vmin,当高度为1080,宽度在1080-1920之间时,都是以高度作为标准计算元素的大小,此时vmin===vh,只要当宽度在0-1080之间时,才会以宽度作为标准计算元素的大小。所以使用vmin的缺点就是在1080-1920区间无法做到自适应。

上面两个方案有各自的优点,但有个缺点,无法同时兼容短屏长屏**。**

优化后的实现方案

  1. 下面这段代码参考异人之下官网(yirenzhixia.qq.com/)实现。

实现逻辑:以PC正常尺寸1920*1080的比率作为衡量标准。当屏幕实际的宽高比大于标准时,以高度作为标准计算fontSize值;当屏幕实际的宽高比小于标准时,以宽度作为标准计算fontSize值。

    function refreshRemNew2() {
        const width = docEl.clientWidth
        const height = docEl.clientHeight
        let basePx = width
        let rem
        if (width / height >= 1.78) {    // > 16:9
            basePx = height * 1.78
        } else {
        }
        rem = basePx / 38.4
        docEl.style.fontSize = rem + 'px'
        window.pxRatio = rem
        if (navigator.userAgent.includes('Firefox') && navigator.userAgent.includes('Mobile')) 
        {
            docEl.style.fontSize = rem * 0.9 + 'px'
            window.pxRatio = rem * 0.9
        }
        flexible.rem = win.rem = rem;
    }

  1. 下面这段代码参考洛克王国世界游戏官网(腾讯互动娱乐)实现。

实现逻辑: 通过宽高计算当前屏幕是短屏还是长屏,如果为正常屏或短屏就以宽度来计算根节点的font-size, 如果为长屏就以高度为准来计算根节点的font-size

- 短屏:高度远远大于宽度,高宽比大于正常的宽高比。
- 长屏:宽度远远大于高度,宽高比大于正常的宽高比。

这里的实现逻辑和上面类似,标准比率也是1.78 (2560 / 1440)。 这里做了更细致的划分。

function refreshRemNew() {
        const screenWidth = document.documentElement.clientWidth;
        const screenHeight = document.documentElement.clientHeight;

        const screenRatio = Math.ceil(screenWidth / screenHeight * 1000) / 1000;
        const standardRatio = Math.ceil(2560 / 1440 * 1000) / 1000;

        const normalHighLimit = Math.ceil((standardRatio + .12) * 1000) / 1000;
        const normalLowLimit = Math.ceil((standardRatio - .218) * 1000) / 1000;

        const longLimit = Math.ceil((standardRatio + .25) * 1000) / 1000;

        console.log(screenRatio, standardRatio)
        console.log(normalHighLimit < screenRatio, longLimit >= screenRatio)
        var rect = docEl.getBoundingClientRect()
        console.log('rect', rect, screenHeight)
        var width = rect.width;
        var height = Math.max(screenHeight, rect.height);
        if (width < 750) {
            width = 750;
        }
        var rem = width / 10;
        if (normalLowLimit <= screenRatio && normalHighLimit >= screenRatio) {
            console.log('不用调整间距')
        } else if (screenRatio < normalLowLimit) {
            console.log('短屏');
        } else if (normalHighLimit < screenRatio && longLimit >= screenRatio) {
            console.log('长屏');
            rem = height / 5;
        } else if (longLimit < screenRatio) {
            console.log('超长屏');
            rem = height / 5;
        } else {
            console.log('意料之外的尺寸范围')
        }
        console.log('rem', rem, 'height', height, 'width', width)
        docEl.style.fontSize = rem + 'px';
        flexible.rem = win.rem = rem;
    }

Vue中集成文字转语音:使用Web Speech API实现功能

实现思路

我将创建一个Vue组件,使用浏览器原生的Web Speech API实现文字转语音功能。这个组件将包含文本输入区域、语音控制选项(语速、音调、音量、声音选择)以及播放控制按钮。

功能概览

image.png

代码实现

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Vue文字转语音功能</title>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
    <style>
        * {
            box-sizing: border-box;
            margin: 0;
            padding: 0;
        }
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            line-height: 1.6;
            color: #333;
            background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
            min-height: 100vh;
            padding: 20px;
        }
        .container {
            max-width: 800px;
            margin: 0 auto;
            background: white;
            border-radius: 10px;
            box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
            padding: 30px;
        }
        h1 {
            text-align: center;
            margin-bottom: 20px;
            color: #2c3e50;
        }
        .description {
            text-align: center;
            margin-bottom: 30px;
            color: #7f8c8d;
        }
        .input-section {
            margin-bottom: 25px;
        }
        textarea {
            width: 100%;
            min-height: 120px;
            padding: 15px;
            border: 1px solid #ddd;
            border-radius: 5px;
            font-size: 16px;
            resize: vertical;
            transition: border-color 0.3s;
        }
        textarea:focus {
            outline: none;
            border-color: #3498db;
            box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.2);
        }
        .controls {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
            gap: 15px;
            margin-bottom: 25px;
        }
        .control-group {
            margin-bottom: 15px;
        }
        label {
            display: block;
            margin-bottom: 5px;
            font-weight: 500;
            color: #2c3e50;
        }
        input[type="range"] {
            width: 100%;
        }
        select {
            width: 100%;
            padding: 8px;
            border: 1px solid #ddd;
            border-radius: 5px;
            background: white;
        }
        .button-group {
            display: flex;
            gap: 10px;
            flex-wrap: wrap;
        }
        button {
            padding: 12px 20px;
            border: none;
            border-radius: 5px;
            cursor: pointer;
            font-size: 16px;
            font-weight: 500;
            transition: all 0.3s;
        }
        .btn-play {
            background: #2ecc71;
            color: white;
        }
        .btn-play:hover {
            background: #27ae60;
        }
        .btn-pause {
            background: #f39c12;
            color: white;
        }
        .btn-pause:hover {
            background: #d35400;
        }
        .btn-resume {
            background: #3498db;
            color: white;
        }
        .btn-resume:hover {
            background: #2980b9;
        }
        .btn-stop {
            background: #e74c3c;
            color: white;
        }
        .btn-stop:hover {
            background: #c0392b;
        }
        button:disabled {
            background: #95a5a6;
            cursor: not-allowed;
        }
        .status {
            margin-top: 20px;
            padding: 15px;
            border-radius: 5px;
            background: #f8f9fa;
            text-align: center;
        }
        .speaking {
            background: rgba(46, 204, 113, 0.1);
            color: #27ae60;
        }
        .paused {
            background: rgba(243, 156, 18, 0.1);
            color: #d35400;
        }
        .stopped {
            background: rgba(231, 76, 60, 0.1);
            color: #c0392b;
        }
        .value-display {
            font-size: 14px;
            color: #7f8c8d;
            text-align: right;
        }
        footer {
            margin-top: 30px;
            text-align: center;
            color: #7f8c8d;
            font-size: 14px;
        }
    </style>
</head>
<body>
    <div id="app" class="container">
        <h1>Vue文字转语音功能</h1>
        <p class="description">使用Web Speech API将文本转换为自然语音</p>
        
        <div class="input-section">
            <textarea v-model="text" placeholder="请输入要转换为语音的文字..."></textarea>
        </div>
        
        <div class="controls">
            <div class="control-group">
                <label for="rate">语速: {{ rate }}</label>
                <input type="range" id="rate" min="0.5" max="2" step="0.1" v-model="rate">
                <div class="value-display">慢 ← → 快</div>
            </div>
            
            <div class="control-group">
                <label for="pitch">音调: {{ pitch }}</label>
                <input type="range" id="pitch" min="0.5" max="2" step="0.1" v-model="pitch">
                <div class="value-display">低 ← → 高</div>
            </div>
            
            <div class="control-group">
                <label for="volume">音量: {{ volume }}</label>
                <input type="range" id="volume" min="0" max="1" step="0.1" v-model="volume">
                <div class="value-display">小 ← → 大</div>
            </div>
            
            <div class="control-group">
                <label for="voice">声音</label>
                <select id="voice" v-model="selectedVoice">
                    <option v-for="(voice, index) in voices" :value="index">
                        {{ voice.name }} ({{ voice.lang }})
                    </option>
                </select>
            </div>
        </div>
        
        <div class="button-group">
            <button class="btn-play" @click="speak" :disabled="isSpeaking || !text">播放</button>
            <button class="btn-pause" @click="pause" :disabled="!isSpeaking || isPaused">暂停</button>
            <button class="btn-resume" @click="resume" :disabled="!isPaused">继续</button>
            <button class="btn-stop" @click="stop" :disabled="!isSpeaking && !isPaused">停止</button>
        </div>
        
        <div class="status" :class="statusClass">
            {{ statusMessage }}
        </div>
        
        <footer>
            <p>基于Web Speech API的语音合成功能 | 浏览器兼容性: Chrome, Edge, Safari</p>
        </footer>
    </div>

    <script>
        new Vue({
            el: '#app',
            data: {
                text: '欢迎使用文字转语音功能!这是一个基于Vue和Web Speech API的示例。您可以输入任何文字,然后点击播放按钮进行语音合成。',
                rate: 1,
                pitch: 1,
                volume: 1,
                voices: [],
                selectedVoice: 0,
                isSpeaking: false,
                isPaused: false,
                synthesis: null
            },
            computed: {
                statusMessage() {
                    if (this.isSpeaking && !this.isPaused) {
                        return '正在播放语音...';
                    } else if (this.isPaused) {
                        return '语音已暂停';
                    } else {
                        return '准备就绪';
                    }
                },
                statusClass() {
                    if (this.isSpeaking && !this.isPaused) {
                        return 'speaking';
                    } else if (this.isPaused) {
                        return 'paused';
                    } else {
                        return 'stopped';
                    }
                }
            },
            mounted() {
                // 初始化语音合成
                this.initSpeechSynthesis();
            },
            methods: {
                initSpeechSynthesis() {
                    if ('speechSynthesis' in window) {
                        this.synthesis = window.speechSynthesis;
                        
                        // 获取可用的语音列表
                        const loadVoices = () => {
                            this.voices = this.synthesis.getVoices();
                            // 尝试选择中文语音
                            const chineseVoice = this.voices.find(voice => 
                                voice.lang.includes('zh') || voice.lang.includes('CN'));
                            if (chineseVoice) {
                                this.selectedVoice = this.voices.indexOf(chineseVoice);
                            }
                        };
                        
                        // 语音列表可能异步加载
                        if (this.synthesis.getVoices().length) {
                            loadVoices();
                        } else {
                            this.synthesis.addEventListener('voiceschanged', loadVoices);
                        }
                    } else {
                        alert('很抱歉,您的浏览器不支持语音合成功能!请使用Chrome、Edge或Safari浏览器。');
                    }
                },
                speak() {
                    if (!this.text) return;
                    
                    // 停止当前正在播放的语音
                    this.stop();
                    
                    // 创建新的语音实例
                    const utterance = new SpeechSynthesisUtterance(this.text);
                    utterance.rate = this.rate;
                    utterance.pitch = this.pitch;
                    utterance.volume = this.volume;
                    
                    if (this.voices[this.selectedVoice]) {
                        utterance.voice = this.voices[this.selectedVoice];
                    }
                    
                    // 设置事件监听
                    utterance.onstart = () => {
                        this.isSpeaking = true;
                        this.isPaused = false;
                    };
                    
                    utterance.onend = () => {
                        this.isSpeaking = false;
                        this.isPaused = false;
                    };
                    
                    utterance.onerror = (event) => {
                        console.error('语音合成错误:', event);
                        this.isSpeaking = false;
                        this.isPaused = false;
                    };
                    
                    // 开始播放
                    this.synthesis.speak(utterance);
                },
                pause() {
                    if (this.synthesis && this.isSpeaking) {
                        this.synthesis.pause();
                        this.isPaused = true;
                    }
                },
                resume() {
                    if (this.synthesis && this.isPaused) {
                        this.synthesis.resume();
                        this.isPaused = false;
                    }
                },
                stop() {
                    if (this.synthesis) {
                        this.synthesis.cancel();
                        this.isSpeaking = false;
                        this.isPaused = false;
                    }
                }
            }
        });
    </script>
</body>
</html>

功能说明

  1. 文本输入:用户可以在文本框中输入或粘贴需要转换为语音的文字内容

  2. 语音控制

    • 语速:控制语音播放速度(0.5-2.0)
    • 音调:控制语音的音调高低(0.5-2.0)
    • 音量:控制语音的音量大小(0.0-1.0)
    • 声音选择:选择不同的语音合成引擎(根据浏览器和设备不同而有所区别)
  3. 控制按钮

    • 播放:开始语音合成
    • 暂停:暂停当前语音
    • 继续:从暂停处继续播放
    • 停止:完全停止语音播放
  4. 状态显示:显示当前语音播放状态

技术要点

  • 使用Web Speech API中的SpeechSynthesis接口
  • 创建SpeechSynthesisUtterance对象来管理语音内容
  • 通过Vue数据绑定实现界面与语音参数的实时同步
  • 处理语音合成相关事件(开始、结束、错误)

注意事项

  • 该功能需要浏览器支持Web Speech API(现代浏览器大多支持)
  • 不同浏览器和设备提供的语音合成引擎可能有所不同
  • 某些浏览器可能需要用户交互(如点击)后才能播放语音

您可以直接将上述代码复制到HTML文件中运行,体验文字转语音功能。

参考链接

developer.mozilla.org/en-US/docs/…

npm私有库创建(docker+verdaccio)

npm私有仓库

前言

私有仓库主要用于托管企业内部不公开的组件或代码库,以实现代码安全隔离和知识产权保护,同时能通过缓存机制显著提升包的下载速度,并通过权限管理加强内部依赖包的版本控制,从而提高团队开发效率和项目可靠性

主要作用与好处

  • 安全性与隐私保护

    • 托管企业内部研发的、不希望公开的私有组件、工具库或sdk,防止核心技术和商业秘密泄露。

    • 只对特定用户或团队可见,保护知识产权。

  • 提升开发效率

    • 通过缓存公共仓库的依赖包,内网下载速度更快,减少对公共npm仓库的依赖和网络不稳定性。

    • 方便管理和复用公司内部的共享组件、通用模块和配置文件,减少重复开发工作。

  • 版本管理与控制

    • 通过权限管理,细化对npm包的下载和发布控制。

    • 提供一个集中的容器来统一管理所需的包和版本,确保版本的一致性。

  • 加速构建与集成

    • 缓存预编译的工具包,用于加速CI/CD流程,避免每次构建都从头安装和编译。

    • 通过代理公共仓库,可以对第三方包进行安全审计和筛选。

  • 促进技术沉淀与复用

    • 鼓励公司内部技术和知识的积累与沉淀,将常用的业务模块或组件集中管理,方便日后维护和引用。

如何快速搭建一个私有仓库

Verdaccio

Verdaccio 是==一款基于Node.js 构建的轻量级、零配置的私有npm 注册中心解决方案==,可用于在本地或团队内部搭建和管理npm 包仓库。它支持缓存公共包,发布私有包,并提供用户认证和权限管理。此外,Verdaccio 具有代理和缓存功能,能连接上游注册中心(如npmjs.org),并可通过插件扩展存储方案,如对接S3 等云服务。

创建流程
使用**npm/yarn/pnpm**创建
  • Node.js v12+

  • 全局安装

npm install -g verdaccio

yarn global add verdaccio

pnpm global add verdaccio
  • 启动
verdaccio

或者使用pm2 start verdaccio

使用浏览器访问web服务,端口号4873,http://0.0.0.0:4873http://127.0.0.1:4873,访问成功就可以

image1.png

使用**docker**镜像安装
  • 安装docker,此处采用docker-compose方式

首先,创建/opt/verdaccio目录,以下在该目录下操作

其次,分别创建conf storage plugins目录,并在conf目录下添加verdaccioconfig.yaml配置文件

接着,创建docker-compose.yml文件,文件配置如下

services:
  verdaccio:
    image: verdaccio/verdaccio
    container_name: verdaccio
    environment:
      - VERDACCIO_PORT=4873
    restart: always
    ports:
      - "4873:4873"
    volumes:
      - /opt/verdaccio/storage:/verdaccio/storage
      - /opt/verdaccio/config:/verdaccio/config
      - /opt/verdaccio/plugins:/verdaccio/plugins

使用docker-compose up -d启动,-d表示在后台运行

启动成功后通过docker ps -a查看正在运行的容器状态

image.png

  • 开启防火墙4873端口 (如在自己电脑本地运行就不用看这一步了)

image3.png

如果你是用的ufw,则使用ufw打开tcp/4873即可

配置好端口后,访问即可

image.png

配置文件

/opt/verdaccio/conf下创建config.yaml文件,此文件修改会映射到容器对应的文件

  • 限制用户

为保证只有一个用户访问,在authhtpasswd设置max_users: -1

以下为config.yaml配置文件

storage: /verdaccio/storage

auth:
  htpasswd:
    file: /verdaccio/storage/htpasswd #用户登录密码
    max_users: -1 #只能有一个用户

uplinks:
  npmjs:
    url: https://registry.npmjs.org/

packages:
  '@*/*':
    access: $authenticated
    publish: $authenticated
    proxy: npmjs

  '**':
    access: $all
    publish: $authenticated
    proxy: npmjs

middlewares:
  audit:
    enabled: true

logs:
  - { type: stdout, format: pretty, level: http }

users:
  admin: #用户名
    groups: admin #分组权限
  • 创建htpasswd文件

在主机上运行

# 安装 htpasswd 工具(Debian/Ubuntu)
sudo apt-get install apache2-utils -y

# 在 /opt/verdaccio/storage/ 下生成 htpasswd 文件
htpasswd -Bbn admin admin123 > /opt/verdaccio/storage/htpasswd

这里生成的用户是:

  • 用户名:admin

  • 密码:admin123(可以自己换)

-B 表示使用 bcrypt 加密,这是 Verdaccio 推荐的。

发布一个包

有了私有库之后,就可以在上面发布一个包

在这之前需要我们登录私有库账号

  • 登录私有仓库
npm login --registry http://xxx:4873 #这里改为你自己的私有仓库地址

以下是登录成功/失败的截图

image.png

image.png

  • 发布包
    现在一般都是用lerna来对包集合统一管理,如果你不想用CJS,可以引入rollup,按照你想用的模块规范输出包 image.png

  • 安装包 image.png

Vue中实现Word、Excel、PDF预览的详细步骤

Thought for 1s### Vue中实现Word、Excel、PDF预览的详细步骤

1. PDF预览实现

方法一:使用PDF.js

npm install pdfjs-dist
<template>
  <div class="pdf-viewer">
    <canvas ref="pdfCanvas"></canvas>
    <div class="controls">
      <button @click="prevPage" :disabled="currentPage <= 1">上一页</button>
      <span>{{ currentPage }} / {{ totalPages }}</span>
      <button @click="nextPage" :disabled="currentPage >= totalPages">下一页</button>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import * as pdfjsLib from 'pdfjs-dist'

// 设置worker路径
pdfjsLib.GlobalWorkerOptions.workerSrc = '/pdf.worker.min.js'

const pdfCanvas = ref(null)
const currentPage = ref(1)
const totalPages = ref(0)
let pdfDoc = null

const props = defineProps({
  pdfUrl: {
    type: String,
    required: true
  }
})

const loadPDF = async () => {
  try {
    pdfDoc = await pdfjsLib.getDocument(props.pdfUrl).promise
    totalPages.value = pdfDoc.numPages
    renderPage(1)
  } catch (error) {
    console.error('PDF加载失败:', error)
  }
}

const renderPage = async (pageNum) => {
  const page = await pdfDoc.getPage(pageNum)
  const canvas = pdfCanvas.value
  const context = canvas.getContext('2d')
  
  const viewport = page.getViewport({ scale: 1.5 })
  canvas.height = viewport.height
  canvas.width = viewport.width
  
  await page.render({
    canvasContext: context,
    viewport: viewport
  }).promise
}

const prevPage = () => {
  if (currentPage.value > 1) {
    currentPage.value--
    renderPage(currentPage.value)
  }
}

const nextPage = () => {
  if (currentPage.value < totalPages.value) {
    currentPage.value++
    renderPage(currentPage.value)
  }
}

onMounted(() => {
  loadPDF()
})
</script>

方法二:使用iframe嵌入

<template>
  <div class="pdf-viewer">
    <iframe 
      :src="pdfUrl" 
      width="100%" 
      height="600px"
      frameborder="0">
    </iframe>
  </div>
</template>

<script setup>
const props = defineProps({
  pdfUrl: String
})
</script>

2. Excel预览实现

使用SheetJS (xlsx)

npm install xlsx
<template>
  <div class="excel-viewer">
    <div class="sheet-tabs">
      <button 
        v-for="(sheet, index) in sheets" 
        :key="index"
        @click="activeSheet = index"
        :class="{ active: activeSheet === index }"
      >
        {{ sheet.name }}
      </button>
    </div>
    <div class="table-container">
      <table v-if="currentSheetData.length">
        <thead>
          <tr>
            <th v-for="(header, index) in headers" :key="index">
              {{ header }}
            </th>
          </tr>
        </thead>
        <tbody>
          <tr v-for="(row, rowIndex) in currentSheetData" :key="rowIndex">
            <td v-for="(cell, cellIndex) in row" :key="cellIndex">
              {{ cell }}
            </td>
          </tr>
        </tbody>
      </table>
    </div>
  </div>
</template>

<script setup>
import { ref, computed, onMounted } from 'vue'
import * as XLSX from 'xlsx'

const sheets = ref([])
const activeSheet = ref(0)
const workbook = ref(null)

const props = defineProps({
  excelUrl: {
    type: String,
    required: true
  }
})

const currentSheetData = computed(() => {
  if (!sheets.value[activeSheet.value]) return []
  return sheets.value[activeSheet.value].data
})

const headers = computed(() => {
  if (!currentSheetData.value.length) return []
  return currentSheetData.value[0]
})

const loadExcel = async () => {
  try {
    const response = await fetch(props.excelUrl)
    const arrayBuffer = await response.arrayBuffer()
    
    workbook.value = XLSX.read(arrayBuffer, { type: 'array' })
    
    sheets.value = workbook.value.SheetNames.map(name => {
      const worksheet = workbook.value.Sheets[name]
      const data = XLSX.utils.sheet_to_json(worksheet, { header: 1 })
      return {
        name,
        data
      }
    })
  } catch (error) {
    console.error('Excel加载失败:', error)
  }
}

onMounted(() => {
  loadExcel()
})
</script>

<style scoped>
.sheet-tabs {
  display: flex;
  gap: 10px;
  margin-bottom: 20px;
}

.sheet-tabs button {
  padding: 8px 16px;
  border: 1px solid #ddd;
  background: #f5f5f5;
  cursor: pointer;
}

.sheet-tabs button.active {
  background: #007bff;
  color: white;
}

.table-container {
  overflow: auto;
  max-height: 500px;
}

table {
  width: 100%;
  border-collapse: collapse;
}

th, td {
  border: 1px solid #ddd;
  padding: 8px;
  text-align: left;
}

th {
  background-color: #f2f2f2;
  position: sticky;
  top: 0;
}
</style>

3. Word预览实现

方法一:使用mammoth.js

npm install mammoth
<template>
  <div class="word-viewer">
    <div class="loading" v-if="loading">加载中...</div>
    <div class="content" v-html="wordContent" v-else></div>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import mammoth from 'mammoth'

const wordContent = ref('')
const loading = ref(true)

const props = defineProps({
  wordUrl: {
    type: String,
    required: true
  }
})

const loadWord = async () => {
  try {
    loading.value = true
    const response = await fetch(props.wordUrl)
    const arrayBuffer = await response.arrayBuffer()
    
    const result = await mammoth.convertToHtml({ arrayBuffer })
    wordContent.value = result.value
    
    if (result.messages.length > 0) {
      console.warn('Word转换警告:', result.messages)
    }
  } catch (error) {
    console.error('Word加载失败:', error)
    wordContent.value = '<p>文档加载失败</p>'
  } finally {
    loading.value = false
  }
}

onMounted(() => {
  loadWord()
})
</script>

<style scoped>
.word-viewer {
  max-width: 800px;
  margin: 0 auto;
  padding: 20px;
}

.content {
  line-height: 1.6;
  font-family: 'Times New Roman', serif;
}

.content :deep(p) {
  margin-bottom: 1em;
}

.content :deep(h1),
.content :deep(h2),
.content :deep(h3) {
  margin-top: 1.5em;
  margin-bottom: 0.5em;
}
</style>

方法二:使用在线预览服务

<template>
  <div class="office-viewer">
    <iframe 
      :src="previewUrl" 
      width="100%" 
      height="600px"
      frameborder="0">
    </iframe>
  </div>
</template>

<script setup>
import { computed } from 'vue'

const props = defineProps({
  fileUrl: {
    type: String,
    required: true
  },
  fileType: {
    type: String,
    required: true // 'word', 'excel', 'pdf'
  }
})

const previewUrl = computed(() => {
  const encodedUrl = encodeURIComponent(props.fileUrl)
  
  // 使用Microsoft Office Online预览
  if (props.fileType === 'word' || props.fileType === 'excel') {
    return `https://view.officeapps.live.com/op/embed.aspx?src=${encodedUrl}`
  }
  
  // 使用Google Docs预览
  return `https://docs.google.com/gview?url=${encodedUrl}&embedded=true`
})
</script>

4. 通用文件预览组件

<template>
  <div class="file-preview">
    <div class="file-info">
      <h3>{{ fileName }}</h3>
      <span class="file-type">{{ fileType.toUpperCase() }}</span>
    </div>
    
    <!-- PDF预览 -->
    <PDFViewer v-if="fileType === 'pdf'" :pdf-url="fileUrl" />
    
    <!-- Excel预览 -->
    <ExcelViewer v-else-if="fileType === 'excel'" :excel-url="fileUrl" />
    
    <!-- Word预览 -->
    <WordViewer v-else-if="fileType === 'word'" :word-url="fileUrl" />
    
    <!-- 不支持的文件类型 -->
    <div v-else class="unsupported">
      <p>不支持预览此文件类型</p>
      <a :href="fileUrl" download>下载文件</a>
    </div>
  </div>
</template>

<script setup>
import { computed } from 'vue'
import PDFViewer from './PDFViewer.vue'
import ExcelViewer from './ExcelViewer.vue'
import WordViewer from './WordViewer.vue'

const props = defineProps({
  fileUrl: {
    type: String,
    required: true
  },
  fileName: {
    type: String,
    default: '未知文件'
  }
})

const fileType = computed(() => {
  const extension = props.fileName.split('.').pop().toLowerCase()
  
  switch (extension) {
    case 'pdf':
      return 'pdf'
    case 'doc':
    case 'docx':
      return 'word'
    case 'xls':
    case 'xlsx':
      return 'excel'
    default:
      return 'unknown'
  }
})
</script>

<style scoped>
.file-preview {
  border: 1px solid #ddd;
  border-radius: 8px;
  overflow: hidden;
}

.file-info {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 15px;
  background-color: #f8f9fa;
  border-bottom: 1px solid #ddd;
}

.file-type {
  background-color: #007bff;
  color: white;
  padding: 4px 8px;
  border-radius: 4px;
  font-size: 12px;
}

.unsupported {
  padding: 40px;
  text-align: center;
  color: #666;
}
</style>

5. 使用示例

<template>
  <div class="app">
    <h1>文件预览示例</h1>
    
    <div class="file-list">
      <div v-for="file in files" :key="file.id" class="file-item">
        <FilePreview 
          :file-url="file.url" 
          :file-name="file.name" 
        />
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import FilePreview from './components/FilePreview.vue'

const files = ref([
  {
    id: 1,
    name: 'sample.pdf',
    url: '/files/sample.pdf'
  },
  {
    id: 2,
    name: 'data.xlsx',
    url: '/files/data.xlsx'
  },
  {
    id: 3,
    name: 'document.docx',
    url: '/files/document.docx'
  }
])
</script>

注意事项

  1. 跨域问题:确保文件服务器支持CORS
  2. 文件大小:大文件可能影响加载性能
  3. 浏览器兼容性:某些功能可能需要现代浏览器支持
  4. 安全性:验证文件来源,防止XSS攻击
  5. 移动端适配:考虑响应式设计

这些方案可以根据具体需求选择使用,建议先测试小文件确保功能正常。

终于有人把数据库讲明白了

你是不是刚开始接触数据库时,觉得它听起来挺技术、有点遥远?其实它没那么复杂。说白了,数据库就是用来存数据、管数据、还能高效查数据的一套系统。

现在的各类应用,小到一个App,大到整个银行系统,背后都离不开数据库的支持。

那么数据库到底有哪些类型?分别适用什么场景?又该怎么选?这篇文章就从最基本的分类讲起,带你一步步弄明白数据库到底怎么用。

一、什么是数据库?

数据库,本质上是一种电子化、结构化的数据集合系统。它的核心功能,是存储、管理并高效处理数据。这里要注意,它不是某个具体的软件或某张表格,而是一整套数据处理的逻辑和方法体系。

举个例子:

假设你经营一家小店,每天要记录销售、库存和客户信息。如果只用纸笔,查找、修改和统计将极其繁琐。而如果用到数据库,不仅能安全存储数据,还能在秒级内完成成千上万条记录的检索和更新。

一个典型的数据库包括三个基本组成部分:

  • 数据(Data):信息本身,如数字、文本、日期等;

  • 数据库管理系统(DBMS):负责管理数据库的软件,如MySQL、MongoDB;

  • 应用程序接口(API):允许其他程序与数据库交互的通道。

这里有个需要注意的,数据库的强大并不在于它“储存”了多少数据,而在于它如何管理数据——包括保证一致性、实现快速检索和控制访问权限。

这才是它真正的价值所在。

二、数据库有哪些类型?

既然知道了数据库的基础定义,你可能会问:数据库只有一种吗?当然不是。根据数据的组织方式和适用场景,数据库主要可分为两类:关系型数据库和非关系型数据库。

1. 关系型数据库(SQL数据库)

这类数据库以“表”为基本单位,每张表有明确的列(字段)和行(记录),不同表之间可以建立关联。它使用SQL(结构化查询语言)进行数据操作,强调数据的严格一致和事务处理。

典型代表包括:

  • MySQL:轻量、开源,适合绝大多数Web应用;

  • PostgreSQL:支持更复杂的数据类型和查询,适用地理数据、科研等场景;

  • Oracle Database:是企业级商用的数据库,性能强大、稳定性极高;

  • SQL Server:微软系解决方案,广泛用于Windows生态中。

关系型数据库适合处理高度结构化、逻辑关联强的数据,例如财务系统、交易记录、人事管理等。

2. 非关系型数据库(NoSQL数据库)

NoSQL数据库的出现,是为了解决关系型数据库在扩展性、灵活性和大规模分布式环境中的局限性。它不依赖固定表结构,数据模型更自由。

根据存储方式,可进一步分为四类:

  • 文档数据库(如MongoDB):数据以文档形式存储,适用内容管理、用户配置等;

  • 键值数据库(如Redis):通过Key-Value快速读写,多用于缓存、会话存储;

  • 列存储数据库(如Cassandra):按列组织数据,适合大数据分析与时序业务;

  • 图数据库(如Neo4j):专门处理关系网络,如社交链接、推荐系统。

NoSQL更适合非结构化或半结构化数据,例如日志文件、传感器数据、实时消息流等。

不管是关系型数据库还是非关系型数据库,它们都是将各种数据收起来,但如果这些庞大的数据没有进行严格的分类和管理,结果都会造成数据错乱,想要的数据始终找不到,浪费人力物力;这时候我们可以借助数据集成工具,比如FineDataLink,它不仅能收集多源数据,还能将这些数据进行清洗,还支持 SQL 语句的数据库,并且能够对这些数据进行实时处理和权限管理。

三、数据库的实际应用场景

了解类型之后,更重要的问题是,它们在实际中究竟怎么用?我们可以结合具体的使用场景:

1. 关系型数据库的使用场景

关系型数据库适合需要高度一致性、事务支持和复杂查询的场景。比如:

  • 电商系统:订单、用户账号、库存数据必须准确无误,关系型数据库能通过事务机制确保数据不出错。

  • 金融系统:银行交易、账务记录对一致性要求极高,关系型数据库是首选。

  • 企业管理系统:如ERP、CRM等,需要多表关联查询和报表生成。

用过来人的经验告诉你,如果你的项目涉及大量结构化数据,并且业务逻辑复杂,关系型数据库通常更稳妥。

2. 非关系型数据库的使用场景

非关系型数据库更适合需要高性能、可扩展性或灵活数据模型的场景。比如:

  • 社交媒体平台:用户生成的内容(文字、图片、视频)结构多变,文档型数据库如MongoDB可以轻松应对。

  • 缓存和会话存储:键值数据库读写速度极快,适合用作缓存层提升应用性能。

  • 实时大数据处理:物联网传感器数据、日志数据量巨大,列存储或键值数据库能高效写入和查询。

此外,在需要处理复杂关系网络的场景(比如社交关系推荐),图数据库可能更有优势。

3. 混合使用场景

在实际项目中,很多系统会同时使用多种数据库。

比如,

一个大型电商平台往往会采用组合式数据存储策略:利用 MySQL 存储用户账户、订单及交易记录,依托其强事务特性保证核心数据的一致性;通过 Redis 缓存高频访问的商品信息和秒杀库存,显著提升响应速度与并发能力;同时借用 Elasticsearch (高性能搜索引擎)实现商品的全文检索、复杂筛选和排序功能,增强搜索体验。这种多类型数据库协同的架构,充分发挥各自优势,在保障数据可靠性的同时,大幅提升了系统的整体性能与可扩展性。

这种多数据库协作的架构,可以充分发挥各自长处。

总结

相信通过以上的内容,你已经对数据库是什么、有哪些类型以及适用场景有了更清晰的认识。

说到底,数据库就是帮你管好数据的工具。无论是关系型还是非关系型,关键得结合实际业务,不然数据一多就容易乱,再好的系统也发挥不出价值。

最重要的是,我们要知道业务不是一直不变的,数据库也是,只有跟着需求持续调整和优化,数据库才能真正帮到你,不然投入再多也很容易变成摆设,否则既浪费资源,又拖累效率。

行业拓展

分享一个面向研发人群使用的前后端分离的低代码软件——JNPF

基于 Java Boot/.Net Core双引擎,它适配国产化,支持主流数据库和操作系统,提供五十几种高频预制组件,内置了常用的后台管理系统使用场景和实用模版,通过简单的拖拉拽操作,开发者能够高效完成软件开发,提高开发效率,减少代码编写工作。

JNPF基于SpringBoot+Vue.js,提供了一个适合所有水平用户的低代码学习平台,无论是有经验的开发者还是编程新手,都可以在这里找到适合自己的学习路径。

此外,JNPF支持全源码交付,完全支持根据公司、项目需求、业务需求进行二次改造开发或内网部署,具备多角色门户、登录认证、组织管理、角色授权、表单设计、流程设计、页面配置、报表设计、门户配置、代码生成工具等开箱即用的在线服务。

Prompt设计实战指南:三大模板与进阶技巧

🎯 Prompt设计实战指南:三大模板与进阶技巧

一、需求+细节+目标模板

这个模板适合目标明确、需要具体细节的任务,通过清晰描述需求、细化内容、设定明确目标,引导模型生成高质量输出。

示例1:小红书护肤种草文案

初始Prompt

写一篇小红书护肤文案,介绍一款美白精华

优化思路

  • 补充量化参数:添加具体成分和效果数据
  • 增加情绪指令:使用情感词激发用户兴趣
  • 添加反向约束:避免夸大宣传保证合规性

优化后Prompt

【需求】为25-30岁女性写一篇小红书美白精华种草文案;
【细节】突出成分如烟酰胺+VC衍生物,搭配实测28天提亮1个度、毛孔减少20%的数据,备注早晚使用步骤;
【目标】文案要带动500+点赞和100+收藏,引导用户点击淘口令购买

请使用口语化语气,加入"惊艳""必入"等情感词,但避免使用"最白""完全去除"等夸大宣传词汇

后续优化方案

  • 如果点击率不高,可以加入动态调整:添加"根据近期用户反馈,强调淡化痘印效果"
  • 多维度添加:增加与同价位产品的对比表格

示例2:得物平台球鞋测评

初始Prompt

生成得物球鞋测评,要有科技点和穿搭建议

优化思路

  • 添加量化参数:具体科技参数和穿搭场景
  • 多维度添加:涵盖科技、穿搭、尺码建议多个维度
  • 设定可衡量目标:明确互动量指标

优化后Prompt

【需求】为得物平台创作一篇Air Jordan新款球鞋测评;
【细节】重点描述中底Zoom Air缓震科技+碳板支撑结构,搭配3套穿搭风格(街头、运动、休闲),附尺码建议;
【目标】测评要获得800+点赞,引导用户点击得物购买链接

请加入实测数据:"实测缓震性能提升30%,回弹反馈明显",并标注"#球鞋测评 #穿搭攻略"话题标签

后续优化方案

  • 添加动态调整指令:"若用户反馈尺码不准,后续补充详细尺码对照表"
  • 加入情绪指令:"用'踩屎感'等球鞋圈术语描述脚感"

二、角色+任务+风格模板

这个模板通过赋予模型特定角色,明确任务和输出风格,适合需要专业调性或特定风格内容的场景。

示例1:微信公众号健康科普文章

初始Prompt

写一篇关于护肝的公众号文章

优化思路

  • 明确专业角色:三甲医院肝病科医生
  • 任务分解:分步骤讲解肝损伤征兆、护肝方案、熬夜技巧
  • 添加权威背书:引用权威期刊数据

优化后Prompt

【角色】你是一名三甲医院肝病科医生,有15年临床经验;
【任务】撰写一篇护肝科普文章,分三部分:肝损伤征兆→护肝饮食方案(列举5种食物)→熬夜护肝技巧;
【风格】语言严谨但亲切,引用《中华肝病杂志》数据,结尾用'别忘了转发给熬夜的朋友'引导传播

请避免使用绝对性词汇如'100%有效',加入'研究表明''临床数据显示'等权威表述

后续优化方案

  • 添加多维度添加:"增加中医角度护肝建议"
  • 反向约束:"避免推荐特定商业产品,保持中立性"

示例2:React组件开发指导

初始Prompt

帮我写一个React表格组件

优化思路

  • 明确技术角色:资深前端工程师
  • 任务具体化:列出具体功能需求和技术栈
  • 定义代码风格:符合主流规范

优化后Prompt

【角色】你是资深前端工程师,精通React和TypeScript;
【任务】创建一个可复用的数据表格组件,支持分页、排序和筛选功能;
【风格】代码符合ESLint Airbnb规范,使用Hook写法,添加详细注释

技术要求:
1. 使用React 18+和TypeScript 5+
2. 支持客户端分页(每页10条)
3. 实现按列排序(升序/降序)
4. 添加关键词筛选功能
5. 导出为Named Export

请提供完整代码示例和Props类型定义

后续优化方案

  • 加入量化参数:"处理1000条数据时渲染性能保持在60fps"
  • 动态调整:"如果用户需要服务端分页,提供相应修改方案"

三、问题+限制+期待模板

这个模板适合解决具体问题或生成创意内容,通过明确问题、约束条件和期望效果,引导模型定向输出。

示例1:社交媒体节日营销方案

初始Prompt

帮我想个情人节营销方案

优化思路

  • 明确具体问题:小众香水品牌的情人节营销
  • 添加预算限制:明确不超过5万预算
  • 定义可衡量期待:销量增长30%

优化后Prompt

【问题】如何为小众香水品牌策划情人节社交媒体营销?
【限制】预算不足5万,主打小红书和微信朋友圈;
【期待】方案需包含UGC征集玩法(如#晒情侣香水照)、KOC投放策略(选择5万粉丝以下达人),期待拉动销量环比增长30%

请避免传统打折促销方式,侧重情感共鸣和用户体验分享

后续优化方案

  • 添加动态调整:"如果前期互动量低,增加赠品投放策略"
  • 多维度添加:"加入线上线下联动方案"

示例2:电商平台详情页优化

初始Prompt

优化电商详情页

优化思路

  • 量化问题:转化率低于行业平均水平
  • 明确限制:不能更改产品价格和主图
  • 具体期待:提升到2.5%转化率

优化后Prompt

【问题】天猫店铺详情页转化率仅1.2%,低于行业3%平均值,如何优化?
【限制】不能更改产品价格和主图;
【期待】提出5点优化建议:如视频评测植入、痛点场景文案、买家秀置顶,期待点击购买转化率提升至2.5%

请聚焦于文案和布局优化,提供具体修改示例和A/B测试方案

后续优化方案

  • 加入量化参数:"每个优化点预计提升转化率0.3%"
  • 反向约束:"避免建议完全重构页面,保持低成本优化"

💡 进阶技巧综合应用指南

1. 情绪指令的精准使用

  • 正向激发:"用'惊艳'、'必入'等情感词激发购买欲望"
  • 避免过度:"避免使用'最白'、'完全去除'等夸大词汇"

2. 量化参数的具体设定

  • 效果量化:"28天提亮1个度"、"毛孔减少20%"
  • 性能量化:"处理1000条数据时渲染性能保持在60fps"

3. 反向约束的合理应用

  • 合规约束:"避免夸大宣传"
  • 技术约束:"不能更改产品价格和主图"

4. 动态调整的灵活运用

  • 数据驱动:"根据近期用户反馈调整强调点"
  • 条件触发:"如果前期互动量低,增加赠品策略"

5. 多维度添加的全面考虑

  • 内容维度:增加产品对比表格
  • 技术维度:添加服务端分页方案

总结

这三个Prompt模板提供了清晰的结构化框架,而进阶技巧则让输出更加精准和实用。在实际应用中:

  1. 从简单开始:先用基础模板确保方向正确
  2. 逐步添加技巧:根据效果逐步融入量化参数、情绪指令等
  3. 持续迭代优化:基于输出结果和用户反馈不断调整Prompt

记住,好的Prompt工程不是一次性的工作,而是一个持续的迭代过程。通过不断测试和优化,你会逐渐掌握如何与AI模型进行更有效的"对话",从而获得更符合期望的高质量输出。

希望这些示例和技巧能帮助你更好地设计Prompt!

❌