外观
前端架构师技术栈项目落地实战手册
适合人群:不满足于背知识点,希望知道 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。
目录
- 如何阅读本文
- 不同项目的技术栈选择原则
- Vue2 存量项目怎么落地
- Vue3 项目怎么落地
- React 项目怎么落地
- Nuxt 项目怎么落地
- 小程序项目怎么落地
- Node.js BFF 项目怎么落地
- NestJS 项目怎么落地
- Express 项目怎么落地
- 技术栈在各框架中的落地矩阵
- 从零搭建一个架构师级项目模板
- 项目升级与迁移策略
- 官方参考资料
1. 如何阅读本文
前面生成的文档更像“技术栈地图”,本文更像“落地手册”。
推荐阅读顺序:
- 先读
前端架构师技术栈全景学习文档.md,知道架构师要掌握哪些能力。 - 再读本文,知道这些能力如何放进具体项目。
- 如果要做大文件上传,继续读
大文件上传系统设计与框架落地方案.md。 - 如果要深入 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
Axios3.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 });
}落地时请求层至少要处理三个容易踩的坑:
- 401 并发刷新风暴:页面上 5 个请求同时收到 401,如果每个都去触发 refresh token,会出现 5 次刷新调用、后一次的 token 覆盖前一次,甚至触发服务端会话失效。标准做法是全局只保留一个"刷新中"的 Promise,其余 401 请求挂起等同一个 Promise,resolve 后带新 token 重放;reject 则统一跳登录。
- 错误码语义分层:拦截器里把 HTTP 状态码和业务 code 两套错误混在一起处理,会导致"HTTP 200 但业务失败"没有提示。响应拦截应区分:网络层失败(无 response)、鉴权类(401/403)、业务错误码(后端约定结构),并保留页面级覆盖入口(有的 404 要静默)。
- 重复请求与竞态:同一接口的快速重复触发(搜索框输入)需要取消或去重。可用
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 -- --report3.8 测试怎么做
存量 Vue2 项目可以先从高价值测试开始:
- 工具函数单测。
- 请求层单测。
- 权限函数单测。
- 关键页面 E2E。
不要一开始追求全量覆盖率。
3.9 Vue2 迁移策略
推荐步骤:
- 锁定现有 Node 和依赖版本。
- 补齐 lint、build、test。
- 清理全局 mixin、filter、隐式依赖。
- 把业务模块边界整理清楚。
- 使用 Composition API 插件或局部重构降低迁移难度。
- 评估 Vue Router、Vuex、组件库迁移。
- 新模块优先按 Vue3 思维写。
- 最后再做框架升级。
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 Vitals4.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.ts4.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-ts5.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.tsx5.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 VitalsNuxt 适合:
- SSR。
- SSG。
- SEO 页面。
- 内容站。
- 电商详情页。
- 需要前后端同仓轻量 API 的项目。
6.2 创建项目
bash
npx nuxi@latest init my-nuxt-app6.3 推荐目录
text
app/
app.vue
pages/
users.vue
components/
composables/
middleware/
server/
api/
users.get.ts
middleware/
shared/
types/
nuxt.config.ts6.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.jsonTaro:
text
src/
pages/
components/
services/
stores/
app.config.ts
app.ts7.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. 技术栈在各框架中的落地矩阵
| 技术 | Vue2 | Vue3 | React | Nuxt | 小程序 | Node/Nest/Express |
|---|---|---|---|---|---|---|
| TypeScript | 可渐进引入,成本较高 | 推荐默认启用 | 推荐默认启用 | 推荐默认启用 | Taro/uni/原生均可用 | 推荐默认启用 |
| ESLint | 先兼容存量版本 | flat config | flat config + hooks | Nuxt ESLint 模块或 flat config | Taro/uni 配 ESLint | flat config |
| Babel | Vue CLI 内置 | Vite 通常少配 | Vite 少配,特殊插件再加 | 通常框架内置 | Taro/uni 编译链内置 | 后端通常不用 Babel |
| 状态管理 | Vuex | Pinia | Zustand/RTK | Pinia/useState | Pinia/Redux/内置 store | 服务端 session/cache |
| 服务端状态 | 手写/局部 | TanStack Query 可选 | TanStack Query 推荐 | useFetch/useAsyncData | 手写缓存 | 数据库/缓存/服务调用 |
| 性能 | 路由懒加载、Webpack 分析 | Vite 分析、响应式优化 | Profiler、memo、拆包 | SSR/SSG/cache | 分包、setData 优化 | 接口耗时、缓存、流式 |
| 测试 | Jest/E2E 先覆盖核心 | Vitest/Playwright | Vitest/RTL/Playwright | Vitest/Playwright | miniprogram simulate/业务 E2E | Jest/Supertest |
| 安全 | XSS、依赖安全 | XSS、CSP | XSS、CSP | SSR 安全、runtimeConfig | 敏感信息、接口鉴权 | 鉴权、校验、CORS |
| 监控 | Sentry 可接 | Sentry/Web Vitals | Sentry/Web Vitals | SSR + 客户端监控 | 平台监控 + 自建 | logs/metrics/traces |
| 上传 | axios/FormData | fetch/FormData/chunk | fetch/FormData/chunk | server/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.tssrc/app/routersrc/featuressrc/shared/uieslint.config.mjsplaywright.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
不要直接重写。
推荐:
- 梳理业务模块。
- 升级构建和规范。
- 清理全局 mixin/filter。
- 组件库迁移评估。
- Vue Router / Vuex 迁移评估。
- 新模块 Vue3 化。
- 分批替换页面。
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 动态路径、后缀省略),而不是"换个配置文件"。
步骤:
- 梳理 Webpack 自定义能力。
- 找出 loader/plugin 替代方案。
- 迁移环境变量。
- 迁移路径别名。
- 迁移静态资源处理。
- 迁移测试。
- 对比构建产物和运行行为。
13.3 JS 到 TS
步骤:
- 开启
allowJs。 - 先给 API 和公共模块加类型。
- 新文件使用 TS。
- 逐步提高 strict。
- 禁止新增
any。
13.4 普通上传到大文件分片上传
步骤:
- 保留原小文件上传接口。
- 新增 upload session。
- 新增 chunk 上传接口。
- 新增已上传 chunk 查询接口。
- 新增 complete 合并接口。
- 前端新增暂停、继续、失败重试。
- 灰度切换大文件走新协议。
14. 官方参考资料
- Vue 2 EOL:https://v2.vuejs.org/eol/
- Vue 3 官方文档:https://vuejs.org/
- Vue Router:https://router.vuejs.org/
- Pinia:https://pinia.vuejs.org/
- React 官方文档:https://react.dev/
- Nuxt 官方文档:https://nuxt.com/docs
- Vite:https://vite.dev/guide/
- ESLint:https://eslint.org/docs/latest/
- Babel:https://babeljs.io/docs/
- TypeScript:https://www.typescriptlang.org/docs/
- TanStack Query:https://tanstack.com/query/latest
- Playwright:https://playwright.dev/docs/intro
- Testing Library:https://testing-library.com/docs/guiding-principles
- NestJS 文件上传:https://docs.nestjs.com/techniques/file-upload
- Express:https://expressjs.com/
- Multer:https://github.com/expressjs/multer
- 微信小程序上传 API:https://developers.weixin.qq.com/miniprogram/dev/api/network/upload/wx.uploadFile.html
- 微信小程序性能建议(合理使用 setData):https://developers.weixin.qq.com/miniprogram/dev/framework/performance/tips/runtime_setData.html
- Nuxt 懒水合(Lazy Hydration):https://nuxt.com/docs/guide/directory-structure/components
- uni-app uploadFile:https://uniapp.dcloud.net.cn/api/request/network-file.html
- Taro uploadFile:https://docs.taro.zone/docs/apis/network/upload/uploadFile