Skip to content

前端架构师技术栈项目落地实战手册 ​

适合人群:不满足于背知识点,希望知道 ESLint、Babel、TypeScript、性能优化、工程化、测试、安全、监控、上传、缓存等能力在 Vue2、Vue3、React、Nuxt、小程序、Node/Nest/Express 项目中如何真正落地的学习者。

学习目标:把“前端架构师技术栈全景”转成具体项目实践。每个项目类型都给出推荐目录、关键依赖、配置入口、落地步骤、注意事项和适用边界。

核对时间:2026-06-30。Vue 2 官方已于 2023-12-31 结束生命周期;新项目优先 Vue 3 / React / Nuxt 等现代栈,Vue2 内容主要面向存量项目维护和迁移。

相关文档:权限与动态路由请继续阅读 用户权限动态路由与页面权限控制项目实战文档.md;浏览器、HTTP、CDN、缓存和 BFF 请阅读 浏览器原理HTTP_CDN缓存与BFF系统学习文档.md;AST、webpack 自定义 loader/plugin 请阅读 AST与Webpack自定义Loader和Plugin系统学习文档.md;大文件上传请阅读 大文件上传系统设计与框架落地方案.md。


目录 ​

  1. 如何阅读本文
  2. 不同项目的技术栈选择原则
  3. Vue2 存量项目怎么落地
  4. Vue3 项目怎么落地
  5. React 项目怎么落地
  6. Nuxt 项目怎么落地
  7. 小程序项目怎么落地
  8. Node.js BFF 项目怎么落地
  9. NestJS 项目怎么落地
  10. Express 项目怎么落地
  11. 技术栈在各框架中的落地矩阵
  12. 从零搭建一个架构师级项目模板
  13. 项目升级与迁移策略
  14. 官方参考资料

1. 如何阅读本文 ​

前面生成的文档更像“技术栈地图”,本文更像“落地手册”。

推荐阅读顺序:

  1. 先读 前端架构师技术栈全景学习文档.md,知道架构师要掌握哪些能力。
  2. 再读本文,知道这些能力如何放进具体项目。
  3. 如果要做大文件上传,继续读 大文件上传系统设计与框架落地方案.md。
  4. 如果要深入 ESLint、Babel、性能优化,分别读对应专题文档。

本文不会只说“要做性能优化”,而会说明:

  • Vue2 项目怎么做。
  • Vue3 项目怎么做。
  • React 项目怎么做。
  • Nuxt 项目怎么做。
  • 小程序项目怎么做。
  • Node/Nest/Express 服务端怎么配合。

2. 不同项目的技术栈选择原则 ​

2.1 不同项目类型的重点 ​

项目类型技术重点
Vue2 存量项目稳定维护、依赖锁定、Webpack/Vue CLI、渐进迁移 Vue3
Vue3 新项目Vite、TypeScript、Pinia、Vue Router、组件化、性能与测试
React 新项目Vite/Next、TypeScript、React Router/TanStack Router、TanStack Query、错误边界
Nuxt 项目SSR/SSG、Nitro server、runtimeConfig、routeRules、SEO、缓存
小程序项目分包、setData 优化、上传下载限制、平台 API、弱网体验
Node.js BFF接口聚合、鉴权、缓存、日志、OpenAPI、前后端契约
NestJS 服务模块化、DTO、Pipe、Guard、Interceptor、Swagger、文件上传
Express 服务轻量 API、middleware、Multer、流式处理、错误处理中间件

2.2 不要把同一套方案硬塞给所有项目 ​

例如:

  • 后台管理系统不一定需要 SSR。
  • 小程序不能照搬浏览器 DOM 性能优化方法。
  • Vue2 存量项目不一定适合立即强升 ESLint v10。
  • 组件库项目比业务项目更需要 Storybook、视觉回归和 peer dependency 治理。
  • 大文件上传不能只靠一个 uploadFile,必须有前后端分片协议。

架构师的能力不是“知道更多工具”,而是知道每个工具在什么项目里值得用、怎么用、用到什么程度。


3. Vue2 存量项目怎么落地 ​

3.1 现实判断 ​

Vue 2 已经结束官方生命周期。2026 年不建议新项目继续选择 Vue2。

但很多企业仍有 Vue2 存量项目,重点应放在:

  • 稳定维护。
  • 降低依赖风险。
  • 补齐规范和测试。
  • 优化构建和性能。
  • 为 Vue3 迁移做准备。

3.2 常见技术栈 ​

text
Vue 2
Vue Router 3
Vuex 3
Vue CLI / Webpack
Babel 7 或项目锁定版本
ESLint 旧版配置或渐进迁移 flat config
Jest / Vitest 兼容方案
Element UI / Ant Design Vue 1.x
Axios

3.3 推荐目录 ​

text
src/
  api/
    user.js
    request.js
  assets/
  components/
    base/
    business/
  views/
    user/
      UserList.vue
  router/
    index.js
  store/
    modules/
      user.js
    index.js
  utils/
  plugins/
  directives/
  filters/
  App.vue
  main.js

如果项目较大,可以逐步引入 feature 分层:

text
src/
  features/
    user/
      api.js
      store.js
      components/
      views/
      index.js
  shared/
    request/
    ui/
    utils/

3.4 ESLint 怎么用 ​

Vue2 老项目常见 .eslintrc.js:

js
module.exports = {
  root: true,
  env: {
    browser: true,
    node: true,
  },
  extends: [
    "plugin:vue/recommended",
    "eslint:recommended",
  ],
  parserOptions: {
    ecmaVersion: 2020,
    sourceType: "module",
  },
  rules: {
    "no-debugger": process.env.NODE_ENV === "production" ? "error" : "off",
    "no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
  },
};

存量项目建议:

  • 不要第一步就强行升到 ESLint v10。
  • 先把现有 lint 跑通。
  • 再处理历史问题。
  • 最后再评估 flat config 迁移。

如果项目使用较新的工具链,并且插件已支持 flat config,可以参考 ESLint系统学习文档.md。

3.5 Babel 怎么用 ​

Vue2 + Vue CLI 通常通过 babel.config.js:

js
module.exports = {
  presets: ["@vue/cli-plugin-babel/preset"],
};

如果要明确浏览器兼容目标,在 package.json 中配置 Browserslist:

json
{
  "browserslist": [
    "> 0.5%",
    "last 2 versions",
    "not dead"
  ]
}

Vue2 老项目兼容旧浏览器时,要同时检查:

  • Babel 语法转换。
  • polyfill。
  • CSS 兼容。
  • 第三方依赖是否包含未转译现代语法。

3.6 请求层怎么做 ​

src/api/request.js:

js
import axios from "axios";

const request = axios.create({
  baseURL: process.env.VUE_APP_API_BASE_URL,
  timeout: 15000,
});

request.interceptors.request.use((config) => {
  const token = localStorage.getItem("token");
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

request.interceptors.response.use(
  (response) => response.data,
  (error) => {
    if (error.response && error.response.status === 401) {
      // 跳转登录或刷新 token
    }
    return Promise.reject(error);
  }
);

export default request;

业务 API:

js
import request from "./request";

export function getUsers(params) {
  return request.get("/users", { params });
}

落地时请求层至少要处理三个容易踩的坑:

  1. 401 并发刷新风暴:页面上 5 个请求同时收到 401,如果每个都去触发 refresh token,会出现 5 次刷新调用、后一次的 token 覆盖前一次,甚至触发服务端会话失效。标准做法是全局只保留一个"刷新中"的 Promise,其余 401 请求挂起等同一个 Promise,resolve 后带新 token 重放;reject 则统一跳登录。
  2. 错误码语义分层:拦截器里把 HTTP 状态码和业务 code 两套错误混在一起处理,会导致"HTTP 200 但业务失败"没有提示。响应拦截应区分:网络层失败(无 response)、鉴权类(401/403)、业务错误码(后端约定结构),并保留页面级覆盖入口(有的 404 要静默)。
  3. 重复请求与竞态:同一接口的快速重复触发(搜索框输入)需要取消或去重。可用 AbortController 在请求拦截里登记 pending key,同 key 新请求发出前取消旧请求;组件卸载时也要取消,否则旧响应晚到覆盖新状态(竞态错乱比慢更糟)。

💡 localStorage 存 token 有 XSS 外泄面(详见 前端质量保障安全可访问性与可观测性系统学习文档.md 的安全章节):对安全要求高的系统优先 HttpOnly Cookie + CSRF 防护,或至少把 refresh token 留在 HttpOnly Cookie、localStorage 只放短时效 access token。

3.7 性能优化怎么做 ​

Vue2 项目重点:

  • 路由懒加载。
  • Webpack bundle 分析。
  • 大表格虚拟滚动。
  • 避免过度 watcher。
  • keep-alive 控制缓存范围。
  • 图片懒加载。
  • 第三方库按需引入。
  • Element UI 按需引入。

路由懒加载:

js
const UserList = () => import("@/views/user/UserList.vue");

Webpack 分析:

bash
npm run build -- --report

3.8 测试怎么做 ​

存量 Vue2 项目可以先从高价值测试开始:

  • 工具函数单测。
  • 请求层单测。
  • 权限函数单测。
  • 关键页面 E2E。

不要一开始追求全量覆盖率。

3.9 Vue2 迁移策略 ​

推荐步骤:

  1. 锁定现有 Node 和依赖版本。
  2. 补齐 lint、build、test。
  3. 清理全局 mixin、filter、隐式依赖。
  4. 把业务模块边界整理清楚。
  5. 使用 Composition API 插件或局部重构降低迁移难度。
  6. 评估 Vue Router、Vuex、组件库迁移。
  7. 新模块优先按 Vue3 思维写。
  8. 最后再做框架升级。

4. Vue3 项目怎么落地 ​

4.1 推荐技术栈 ​

text
Vue 3
Vite
TypeScript
Vue Router 4
Pinia
VueUse
Axios 或 fetch 封装
TanStack Query for Vue 可选
ESLint flat config
Prettier
Vitest
Playwright
Sentry / Web Vitals

4.2 创建项目 ​

bash
npm create vue@latest

建议选择:

  • TypeScript。
  • Router。
  • Pinia。
  • ESLint。
  • Prettier。
  • Vitest。
  • Playwright。

4.3 推荐目录 ​

text
src/
  app/
    router/
    providers/
    styles/
  pages/
    user-list/
      index.vue
  features/
    user/
      api/
      model/
      ui/
      index.ts
  shared/
    api/
    config/
    hooks/
    ui/
    utils/
  main.ts

4.4 TypeScript 怎么用 ​

开启严格模式:

json
{
  "compilerOptions": {
    "strict": true,
    "noImplicitOverride": true,
    "noUncheckedIndexedAccess": true
  }
}

API 类型:

ts
export interface User {
  id: string;
  name: string;
  role: "admin" | "member";
}

export interface PageResult<T> {
  list: T[];
  total: number;
}

4.5 Pinia 怎么用 ​

features/user/model/userStore.ts:

ts
import { defineStore } from "pinia";
import type { User } from "../api/types";

export const useUserStore = defineStore("user", {
  state: () => ({
    currentUser: null as User | null,
  }),
  actions: {
    setCurrentUser(user: User) {
      this.currentUser = user;
    },
  },
});

注意:

  • 不要把所有接口列表都塞进 Pinia。
  • 服务端状态更适合 TanStack Query 或页面局部管理。
  • Pinia 适合用户、权限、主题、租户等全局业务状态。

4.6 请求层怎么做 ​

shared/api/request.ts:

ts
export async function request<T>(
  input: string,
  init: RequestInit = {}
): Promise<T> {
  const response = await fetch(`${import.meta.env.VITE_API_BASE_URL}${input}`, {
    ...init,
    headers: {
      "Content-Type": "application/json",
      ...init.headers,
    },
  });

  if (!response.ok) {
    throw new Error(`Request failed: ${response.status}`);
  }

  return response.json() as Promise<T>;
}

业务 API:

ts
import { request } from "@/shared/api/request";
import type { PageResult, User } from "./types";

export function fetchUsers() {
  return request<PageResult<User>>("/users");
}

4.7 ESLint 怎么用 ​

eslint.config.mjs:

js
import js from "@eslint/js";
import pluginVue from "eslint-plugin-vue";
import tseslint from "typescript-eslint";
import { defineConfig } from "eslint/config";

export default defineConfig([
  {
    ignores: ["dist/**", "coverage/**"],
  },
  js.configs.recommended,
  ...pluginVue.configs["flat/recommended"],
  ...tseslint.configs.recommended,
  {
    files: ["**/*.{ts,vue}"],
    rules: {
      "vue/multi-word-component-names": "off",
      "@typescript-eslint/no-unused-vars": [
        "warn",
        { argsIgnorePattern: "^_" }
      ],
    },
  },
]);

4.8 性能怎么做 ​

Vue3 项目重点:

  • 路由懒加载。
  • defineAsyncComponent。
  • 大列表虚拟滚动。
  • shallowRef / markRaw 管理大对象。
  • 减少过深响应式。
  • 分析 Vite 构建产物。
  • Web Vitals 上报。

路由懒加载:

ts
{
  path: "/users",
  component: () => import("@/pages/user-list/index.vue")
}

大对象:

ts
import { markRaw, shallowRef } from "vue";

const chartInstance = shallowRef(null);
chartInstance.value = markRaw(createChart());

为什么需要这样写:Vue 3 响应式基于 Proxy,ref() 包裹对象时 reactive() 会递归地把整个对象图变成响应式代理。ECharts 实例、Three.js 场景、大型表格原始数据这类"不需要被观察的内部结构"一旦陷入深层响应式,会带来三重代价:初始化时递归包装的耗时、运行时每次访问都走 Proxy 拦截、以及内部属性变更触发意外的依赖更新。shallowRef 只对 .value 整体替换做响应,markRaw 给对象打 __v_skip 标记跳过转换——两者配合是"把大对象请出响应式系统"的标准手法。

⚠️ 陷阱:shallowRef 修改 .value 内部属性不会触发更新。需要时手动调用 triggerRef(ref),或整体替换 .value。ECharts 场景的正确用法是让图表实例完全脱离响应式(markRaw 或直接普通变量 + 模板 ref 拿挂载点),而不是指望 watch 深度跟踪它。

4.9 测试怎么做 ​

Vitest 测 composable:

ts
import { describe, expect, it } from "vitest";
import { usePagination } from "./usePagination";

describe("usePagination", () => {
  it("changes page", () => {
    const pagination = usePagination();
    pagination.setPage(2);
    expect(pagination.page.value).toBe(2);
  });
});

Playwright 测关键流程:

ts
import { expect, test } from "@playwright/test";

test("user list is visible", async ({ page }) => {
  await page.goto("/users");
  await expect(page.getByRole("heading", { name: "用户列表" })).toBeVisible();
});

5. React 项目怎么落地 ​

5.1 推荐技术栈 ​

text
React
Vite 或 Next.js
TypeScript
React Router / TanStack Router
TanStack Query
Zustand / Redux Toolkit
React Hook Form
ESLint flat config
Prettier
Vitest
Testing Library
Playwright
Sentry / Web Vitals

如果是纯 SPA,Vite + React 很合适。

如果需要 SSR、SEO、服务端组件或文件路由,考虑 Next.js 或 Remix。

5.2 创建 Vite React 项目 ​

bash
npm create vite@latest my-react-app -- --template react-ts

5.3 推荐目录 ​

text
src/
  app/
    router.tsx
    providers.tsx
  pages/
    users/
      index.tsx
  features/
    user/
      api/
      model/
      ui/
      index.ts
  shared/
    api/
    hooks/
    ui/
    utils/
  main.tsx

5.4 状态怎么用 ​

服务端状态用 TanStack Query:

tsx
import { useQuery } from "@tanstack/react-query";
import { fetchUsers } from "../api/fetchUsers";

export function UserList() {
  const { data, isLoading, error } = useQuery({
    queryKey: ["users"],
    queryFn: fetchUsers,
  });

  if (isLoading) return <div>Loading</div>;
  if (error) return <div>Failed</div>;

  return (
    <ul>
      {data?.list.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

全局 UI / 用户状态用 Zustand:

ts
import { create } from "zustand";

interface AuthState {
  token: string | null;
  setToken: (token: string | null) => void;
}

export const useAuthStore = create<AuthState>((set) => ({
  token: null,
  setToken: (token) => set({ token }),
}));

5.5 错误边界怎么用 ​

tsx
import { Component, type ReactNode } from "react";

export class ErrorBoundary extends Component<
  { children: ReactNode },
  { hasError: boolean }
> {
  state = { hasError: false };

  static getDerivedStateFromError() {
    return { hasError: true };
  }

  componentDidCatch(error: unknown) {
    // reportError(error)
  }

  render() {
    if (this.state.hasError) {
      return <div>页面出现异常</div>;
    }

    return this.props.children;
  }
}

应用入口:

tsx
root.render(
  <ErrorBoundary>
    <App />
  </ErrorBoundary>
);

⚠️ 错误边界的捕获范围(React 官方文档明确的限制):只捕获子组件树在渲染期间抛出的错误,捕获不了事件处理器里的错误(那要用 try/catch 或全局 window.onerror)、异步代码(setTimeout / promise)、服务端渲染期间的错误,以及边界自身抛出的错误。所以"接了 ErrorBoundary 就稳了"是错觉,必须和全局异常捕获(window.addEventListener("error") / unhandledrejection)配合使用。

落地建议:粗粒度边界(整页)+ 细粒度边界(独立区块,如侧栏、图表卡片)嵌套使用——细粒度边界让局部崩溃只降级局部 UI 而不是白屏;边界 componentDidCatch 里必须上报,带 componentStack 定位组件。

5.6 ESLint 怎么用 ​

React 项目要额外关注 Hooks:

js
import js from "@eslint/js";
import reactHooks from "eslint-plugin-react-hooks";
import tseslint from "typescript-eslint";
import { defineConfig } from "eslint/config";

export default defineConfig([
  js.configs.recommended,
  ...tseslint.configs.recommended,
  {
    files: ["**/*.{ts,tsx}"],
    plugins: {
      "react-hooks": reactHooks,
    },
    rules: {
      ...reactHooks.configs.recommended.rules,
    },
  },
]);

5.7 性能怎么做 ​

React 项目重点:

  • 路由级 code splitting。
  • 使用 React Profiler 定位重渲染。
  • 控制 context 更新范围。
  • 大列表虚拟滚动。
  • 避免在 render 中做重计算。
  • memo、useMemo、useCallback 只在有证据时使用。
  • 交互慢时分析 INP 和长任务。

错误示例:

tsx
const filtered = expensiveFilter(list, keyword);

优化:

tsx
const filtered = useMemo(
  () => expensiveFilter(list, keyword),
  [list, keyword]
);

判据而不是口诀:useMemo 本身有成本(依赖比较 + 缓存管理),只有当"重算成本 > 缓存成本"或"引用稳定是下游 memo/effect 生效的前提"时才有收益。两个引用(props 对象、回调函数)每次 render 都是新的,会让子组件的 memo 浅比较直接失效——这类"为了让 memo 链成立"的包裹才是高频正当理由。React 19 起可在函数组件里用 use 读取 Promise/context,React Compiler 稳定后(见性能文档 10.1)手动 memo 的比重还会继续下降。

交互卡顿的定位路径:RUM 里发现某页 INP 差 → DevTools Performance 录一次该交互 → 看瓶颈是 input delay(加载期长任务/第三方)、processing(回调/重渲染)、还是 presentation(DOM 大、样式重算)→ 分别对应"延后 JS、精简回调/拆渲染、控 DOM 规模"。不要跳过归因直接上 memo。

5.8 测试怎么做 ​

tsx
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { LoginForm } from "./LoginForm";

it("submits login form", async () => {
  const user = userEvent.setup();
  render(<LoginForm />);

  await user.type(screen.getByLabelText("邮箱"), "ada@example.com");
  await user.type(screen.getByLabelText("密码"), "secret");
  await user.click(screen.getByRole("button", { name: "登录" }));

  expect(await screen.findByText("登录成功")).toBeInTheDocument();
});

6. Nuxt 项目怎么落地 ​

6.1 推荐技术栈 ​

text
Nuxt 4 / Nuxt 3
Vue 3
TypeScript
Nitro
server/api
runtimeConfig
routeRules
Pinia
ESLint
Vitest
Playwright
Sentry / Web Vitals

Nuxt 适合:

  • SSR。
  • SSG。
  • SEO 页面。
  • 内容站。
  • 电商详情页。
  • 需要前后端同仓轻量 API 的项目。

6.2 创建项目 ​

bash
npx nuxi@latest init my-nuxt-app

6.3 推荐目录 ​

text
app/
  app.vue
  pages/
    users.vue
  components/
  composables/
  middleware/
server/
  api/
    users.get.ts
  middleware/
shared/
  types/
nuxt.config.ts

6.4 runtimeConfig 怎么用 ​

nuxt.config.ts:

ts
export default defineNuxtConfig({
  runtimeConfig: {
    apiSecret: "",
    public: {
      apiBaseUrl: "",
    },
  },
});

使用:

ts
const config = useRuntimeConfig();
const apiBaseUrl = config.public.apiBaseUrl;

注意:

  • runtimeConfig.public 会暴露给浏览器。
  • 服务端 secret 不要放进 public。

6.5 useFetch 怎么用 ​

vue
<script setup lang="ts">
interface User {
  id: string;
  name: string;
}

const { data, pending, error, refresh } = await useFetch<User[]>("/api/users");
</script>

<template>
  <div v-if="pending">Loading</div>
  <div v-else-if="error">Failed</div>
  <ul v-else>
    <li v-for="user in data" :key="user.id">{{ user.name }}</li>
  </ul>
</template>

6.6 server/api 怎么用 ​

server/api/users.get.ts:

ts
export default defineEventHandler(async () => {
  return [
    { id: "1", name: "Ada" },
    { id: "2", name: "Linus" },
  ];
});

适合:

  • BFF。
  • 接口聚合。
  • SSR 数据接口。
  • 简单服务端逻辑。

不适合把复杂后端系统全部塞进 Nuxt server。

6.7 routeRules 怎么用 ​

ts
export default defineNuxtConfig({
  routeRules: {
    "/": { prerender: true },
    "/products/**": { swr: 300 },
    "/admin/**": { ssr: false },
  },
});

用途:

  • 某些页面静态生成。
  • 某些页面缓存。
  • 某些页面关闭 SSR。

6.8 Nuxt 性能怎么做 ​

重点:

  • 正确选择 SSR / SSG / CSR。
  • 使用 routeRules 做缓存。
  • 避免过度客户端 hydration。
  • 图片使用 Nuxt Image 或等效方案。
  • 分析 server response time。
  • 注意 LCP 图片优先级。
  • API 聚合减少请求瀑布。

理解 hydration 成本才能用对前四条:SSR 返回的 HTML 只是"可看的",不是"可用的"——浏览器还要下载组件 JS、重新执行组件、把事件监听绑定到已有 DOM 上(水合),页面才真正可交互。SSR 的 LCP 收益和 hydration 的 INP/交互成本是一体两面:

  • 纯展示内容(文章、商品详情)水合价值低:Nuxt 内置懒水合(lazy/delayed hydration),给组件加 hydrate-on-visible、hydrate-on-idle、hydrate-on-interaction、hydrate-on-media-query、hydrate-on-time、hydrate-never 等策略 prop,即可把水合推迟到滚动进视口、浏览器空闲或用户交互时,非关键组件不再挤占首屏主线程;完全静态的片段可配合 Server Components / Islands(<NuxtIsland>,服务端渲染、客户端不下发 JS)。
  • routeRules 的 swr/isr 缓存命中时 TTFB 大幅下降,LCP 收益直接;ssr: false 则是把该路径整体退回 CSR——后台路由常用,因为登录后的页面 SEO 无意义、且能省掉服务端渲染成本。
  • 服务端拿数据用 useFetch/useAsyncData,payload 会随 HTML 序列化到 window.__NUXT__ 供客户端复用;要避免大对象进 payload(序列化+反序列化双份成本),列表接口做字段裁剪。

6.9 Nuxt 上传怎么做 ​

普通小文件可以走 server/api/upload.post.ts。

大文件不建议直接把整个 multipart 文件读入内存,应使用分片上传协议,或者前端直传对象存储,服务端只负责签名和合并通知。

完整方案见 大文件上传系统设计与框架落地方案.md。


7. 小程序项目怎么落地 ​

7.1 项目类型 ​

常见小程序技术栈:

  • 微信小程序原生。
  • uni-app。
  • Taro。
  • mpvue / 老项目。

小程序不是浏览器环境,不能照搬 Web 项目:

  • 没有完整 DOM。
  • 渲染层和逻辑层分离。
  • setData 成本重要。
  • 包体积限制更明显。
  • 网络和上传 API 受平台限制。
  • 分包非常关键。

7.2 推荐目录 ​

原生小程序:

text
miniprogram/
  app.ts
  app.json
  pages/
    user/
      index.ts
      index.wxml
      index.wxss
      index.json
  components/
  services/
    request.ts
  stores/
  utils/
  subpackages/

uni-app:

text
src/
  pages/
  components/
  services/
  stores/
  static/
  App.vue
  pages.json
  manifest.json

Taro:

text
src/
  pages/
  components/
  services/
  stores/
  app.config.ts
  app.ts

7.3 请求层怎么做 ​

原生小程序封装:

ts
export function request<T>(options: WechatMiniprogram.RequestOption) {
  return new Promise<T>((resolve, reject) => {
    wx.request({
      ...options,
      header: {
        Authorization: wx.getStorageSync("token") || "",
        ...options.header,
      },
      success(res) {
        if (res.statusCode >= 200 && res.statusCode < 300) {
          resolve(res.data as T);
        } else {
          reject(res);
        }
      },
      fail: reject,
    });
  });
}

7.4 setData 性能怎么做 ​

先理解原理:小程序是双线程架构——逻辑层(JS 线程,跑业务代码)和渲染层(WebView 线程,跑 WXML/WXSS 与虚拟 DOM)分离,两者不能直接访问对方,setData 的本质是逻辑层把数据序列化后跨线程发给渲染层,触发一次组件数据更新算法与视图 diff:

text
逻辑层 (JSCore)                    渲染层 (WebView)
  this.setData({...})
      │  序列化 ──►  跨线程通信  ──►  反序列化
                                    │  更新 data → diff → 重渲染
                                    ▼
                                  页面像素更新

所以 setData 的成本 = 序列化 + 通信 + 渲染更新,数据量越大、频率越高越贵。官方性能建议文档明确:避免不必要和过频的数据更新调用(尤其页面滚动回调里每次都 setData);data 只放渲染相关字段,不用 setData 更新与渲染无关的数据(这类数据放普通属性或"纯数据字段");setData 只传发生变化的数据。

原则:

  • 不要频繁大对象 setData。
  • 只更新变化字段。
  • 列表分页加载。
  • 长列表虚拟化或分片渲染。
  • 图片懒加载。
  • 分包加载非首屏页面。

错误:

ts
this.setData({
  list: hugeList,
});

更好:

ts
this.setData({
  [`list[${index}].status`]: "done",
});

7.5 分包怎么做 ​

app.json:

json
{
  "pages": ["pages/home/index"],
  "subpackages": [
    {
      "root": "subpackages/order",
      "pages": ["list/index", "detail/index"]
    }
  ]
}

适合分包:

  • 低频业务。
  • 大型页面。
  • 活动页。
  • 管理功能。

7.6 小程序上传怎么做 ​

平台上传任务一般提供:

  • 开始上传。
  • 监听进度。
  • abort 中断。

真正的暂停、断点续传、秒传,需要自己实现分片协议。

小文件:

ts
const task = wx.uploadFile({
  url: "https://api.example.com/upload",
  filePath,
  name: "file",
  success: console.log,
  fail: console.error,
});

task.onProgressUpdate((progress) => {
  console.log(progress.progress);
});

// 暂停本质是中断当前上传
task.abort();

大文件:

  • 读取文件信息。
  • 按 chunk 上传。
  • 服务端记录已上传 chunk。
  • 暂停时 abort 当前 chunk。
  • 继续时查询已上传 chunk,从缺失部分继续。

完整方案见 大文件上传系统设计与框架落地方案.md。

7.7 小程序监控怎么做 ​

建议:

  • 封装 request 统计错误率和耗时。
  • 页面 onLoad / onReady 记录性能。
  • 上传下载记录进度和失败原因。
  • 关键流程埋点。
  • 使用平台提供的性能和错误监控能力。
  • 自建监控时注意用户隐私和数据量。

8. Node.js BFF 项目怎么落地 ​

8.1 BFF 适合做什么 ​

BFF 适合:

  • 聚合多个后端接口。
  • SSR 数据准备。
  • 前端专用数据裁剪。
  • 鉴权适配。
  • 接口缓存。
  • 文件上传签名。
  • 老系统 API 适配。

不适合:

  • 承担所有业务核心逻辑。
  • 绕过后端权限。
  • 变成无测试的转发层。

8.2 推荐目录 ​

text
src/
  app.ts
  config/
  routes/
  modules/
    user/
      user.controller.ts
      user.service.ts
      user.schema.ts
  middleware/
  clients/
    user-service.client.ts
  utils/
  observability/

8.3 请求聚合示例 ​

ts
app.get("/api/dashboard", async (req, res, next) => {
  try {
    const [user, stats, notices] = await Promise.all([
      userClient.getCurrentUser(req),
      statsClient.getStats(req),
      noticeClient.getNotices(req),
    ]);

    res.json({ user, stats, notices });
  } catch (error) {
    next(error);
  }
});

⚠️ 聚合接口慎用裸 Promise.all:它是"全成全败"——三个下游里最弱的 notices 服务挂了,整个 dashboard 接口 500,用户看到白屏而不是"公告暂不可用"。BFF 聚合的默认姿势应是 Promise.allSettled + 逐字段降级(notices: null + 降级标记),让前端按块渲染兜底 UI;再配合每个下游独立超时(比 BFF 自身的对外超时更短),慢下游不会拖死整个聚合。

8.4 BFF 必备能力 ​

  • 统一错误处理。
  • 日志。
  • trace id。
  • 请求超时。
  • 重试和熔断。
  • 鉴权。
  • 参数校验。
  • OpenAPI。
  • 单元测试。
  • Docker 部署。

每项背后的"为什么"(缺失后的典型故障模式):

能力防的是什么
请求超时下游无响应时连接池被占满,BFF 整体雪崩
重试只对幂等 GET 重试 + 退避 + 次数上限;对非幂等写操作重试就是重复下单
熔断下游持续故障时快速失败,不再把流量灌进火坑,给其恢复窗口
trace id前端 → BFF → 微服务跨层排障时把日志串起来(向下游透传同一 id)
参数校验BFF 是前端与内部服务的信任边界,不能假设上游已过校验
OpenAPI没有契约的 BFF 会退化成"没人敢改的转发脚本"

9. NestJS 项目怎么落地 ​

9.1 NestJS 适合什么 ​

NestJS 适合:

  • 企业级 Node 服务。
  • BFF。
  • API 服务。
  • 微服务。
  • 需要模块化和依赖注入的项目。

9.2 推荐目录 ​

text
src/
  main.ts
  app.module.ts
  common/
    filters/
    guards/
    interceptors/
    pipes/
  modules/
    users/
      users.module.ts
      users.controller.ts
      users.service.ts
      dto/
        create-user.dto.ts
  config/

9.3 DTO 与校验 ​

ts
import { IsEmail, IsString, MinLength } from "class-validator";

export class CreateUserDto {
  @IsEmail()
  email!: string;

  @IsString()
  @MinLength(2)
  name!: string;
}

main.ts:

ts
app.useGlobalPipes(
  new ValidationPipe({
    whitelist: true,
    transform: true,
  })
);

9.4 Swagger / OpenAPI ​

ts
const config = new DocumentBuilder()
  .setTitle("API")
  .setVersion("1.0")
  .build();

const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup("docs", app, document);

前端可以根据 OpenAPI 生成类型和 API client,减少接口契约漂移。

9.5 文件上传 ​

普通文件:

ts
@Post("upload")
@UseInterceptors(FileInterceptor("file"))
uploadFile(@UploadedFile() file: Express.Multer.File) {
  return {
    filename: file.originalname,
    size: file.size,
  };
}

大文件:

  • 不建议一次性 Multer 内存接收。
  • 使用磁盘临时目录或对象存储分片。
  • 每个 chunk 独立上传。
  • 服务端记录上传会话。
  • 完成后合并或调用对象存储 CompleteMultipartUpload。

完整方案见 大文件上传系统设计与框架落地方案.md。


10. Express 项目怎么落地 ​

10.1 Express 适合什么 ​

Express 适合:

  • 轻量 API。
  • BFF。
  • 上传服务。
  • Mock 服务。
  • 老项目维护。

10.2 推荐目录 ​

text
src/
  app.ts
  server.ts
  routes/
    users.route.ts
    upload.route.ts
  services/
  middleware/
    errorHandler.ts
    auth.ts
  validators/
  utils/

10.3 中间件顺序 ​

ts
app.use(requestId());
app.use(logger());
app.use(cors());
app.use(express.json({ limit: "1mb" }));
app.use(auth());
app.use("/api/users", userRouter);
app.use(errorHandler());

顺序很重要:

  • 日志和 request id 尽量靠前。
  • body parser 要限制大小。
  • 路由后面放错误处理中间件。

10.4 参数校验 ​

使用 Zod:

ts
const createUserSchema = z.object({
  email: z.string().email(),
  name: z.string().min(2),
});

app.post("/users", (req, res) => {
  const body = createUserSchema.parse(req.body);
  res.json(body);
});

10.5 文件上传 ​

普通文件可以用 Multer:

ts
const upload = multer({ dest: "uploads/" });

app.post("/upload", upload.single("file"), (req, res) => {
  res.json({
    filename: req.file?.originalname,
    size: req.file?.size,
  });
});

大文件用分片上传,详见 大文件上传系统设计与框架落地方案.md。


11. 技术栈在各框架中的落地矩阵 ​

技术Vue2Vue3ReactNuxt小程序Node/Nest/Express
TypeScript可渐进引入,成本较高推荐默认启用推荐默认启用推荐默认启用Taro/uni/原生均可用推荐默认启用
ESLint先兼容存量版本flat configflat config + hooksNuxt ESLint 模块或 flat configTaro/uni 配 ESLintflat config
BabelVue CLI 内置Vite 通常少配Vite 少配,特殊插件再加通常框架内置Taro/uni 编译链内置后端通常不用 Babel
状态管理VuexPiniaZustand/RTKPinia/useStatePinia/Redux/内置 store服务端 session/cache
服务端状态手写/局部TanStack Query 可选TanStack Query 推荐useFetch/useAsyncData手写缓存数据库/缓存/服务调用
性能路由懒加载、Webpack 分析Vite 分析、响应式优化Profiler、memo、拆包SSR/SSG/cache分包、setData 优化接口耗时、缓存、流式
测试Jest/E2E 先覆盖核心Vitest/PlaywrightVitest/RTL/PlaywrightVitest/Playwrightminiprogram simulate/业务 E2EJest/Supertest
安全XSS、依赖安全XSS、CSPXSS、CSPSSR 安全、runtimeConfig敏感信息、接口鉴权鉴权、校验、CORS
监控Sentry 可接Sentry/Web VitalsSentry/Web VitalsSSR + 客户端监控平台监控 + 自建logs/metrics/traces
上传axios/FormDatafetch/FormData/chunkfetch/FormData/chunkserver/api 或直传wx.uploadFile/分片Multer/stream/chunk

12. 从零搭建一个架构师级项目模板 ​

12.1 Vue3 SPA 模板 ​

bash
npm create vue@latest
pnpm add @tanstack/vue-query axios
pnpm add -D eslint prettier vitest playwright

必须包含:

  • src/shared/api/request.ts
  • src/app/router
  • src/features
  • src/shared/ui
  • eslint.config.mjs
  • playwright.config.ts
  • GitHub Actions。

12.2 React SPA 模板 ​

bash
npm create vite@latest app -- --template react-ts
pnpm add @tanstack/react-query zustand react-router-dom
pnpm add -D eslint prettier vitest @testing-library/react playwright

必须包含:

  • QueryClientProvider。
  • ErrorBoundary。
  • request 封装。
  • 路由懒加载。
  • 测试样例。
  • CI。

12.3 Nuxt 模板 ​

bash
npx nuxi@latest init app
pnpm add pinia
pnpm add -D vitest playwright

必须包含:

  • runtimeConfig。
  • server/api。
  • routeRules。
  • SEO 基础配置。
  • 错误页面。
  • 环境变量示例。

12.4 Node/Nest 模板 ​

必须包含:

  • 参数校验。
  • Swagger/OpenAPI。
  • 全局错误处理。
  • 日志。
  • request id。
  • CORS。
  • 安全 header。
  • 单元测试。
  • e2e 测试。
  • Dockerfile。
  • 健康检查。

13. 项目升级与迁移策略 ​

13.1 Vue2 到 Vue3 ​

不要直接重写。

推荐:

  1. 梳理业务模块。
  2. 升级构建和规范。
  3. 清理全局 mixin/filter。
  4. 组件库迁移评估。
  5. Vue Router / Vuex 迁移评估。
  6. 新模块 Vue3 化。
  7. 分批替换页面。

13.2 Webpack 到 Vite ​

先理解两者差异的本质,迁移清单才不会变成照抄配置:

  • Webpack 是"先打包再 serve":启动时从 entry 开始构建完整依赖图、bundle 全部模块,项目越大冷启动越慢;HMR 也要在 bundle 粒度上重新编译受影响的 chunk。
  • Vite 开发阶段"不打包":利用浏览器原生 ESM,index.html 里的 <script type="module"> 按需请求源码模块,dev server 只实时编译被请求到的文件(esbuild 预构建依赖以合并请求、转换 CJS→ESM);HMR 按模块粒度更新,与项目总规模基本无关。
  • 代价:构建产物阶段 Vite 生产构建走 Rollup(或 Rolldown 方向),分包、依赖预构建(optimizeDeps)、CommonJS 互操作都有和 Webpack 不同的边界行为——迁移问题大多出在"隐式依赖解析差异"(如未声明的依赖、require 动态路径、后缀省略),而不是"换个配置文件"。

步骤:

  1. 梳理 Webpack 自定义能力。
  2. 找出 loader/plugin 替代方案。
  3. 迁移环境变量。
  4. 迁移路径别名。
  5. 迁移静态资源处理。
  6. 迁移测试。
  7. 对比构建产物和运行行为。

13.3 JS 到 TS ​

步骤:

  1. 开启 allowJs。
  2. 先给 API 和公共模块加类型。
  3. 新文件使用 TS。
  4. 逐步提高 strict。
  5. 禁止新增 any。

13.4 普通上传到大文件分片上传 ​

步骤:

  1. 保留原小文件上传接口。
  2. 新增 upload session。
  3. 新增 chunk 上传接口。
  4. 新增已上传 chunk 查询接口。
  5. 新增 complete 合并接口。
  6. 前端新增暂停、继续、失败重试。
  7. 灰度切换大文件走新协议。

14. 官方参考资料 ​

基于 VitePress 构建的个人学习文档知识库