前端基础建设(从 0 到 1)
问题
什么是前端基建?如何从零开始为一个团队搭建完整的前端基础设施?
答案
前端基建(Frontend Infrastructure)是指为前端团队提供的一整套工程化基础设施,涵盖从开发规范、脚手架、构建发布到监控告警的完整链路。它的核心目标是提升研发效率、保障代码质量、降低协作成本。
一个成熟的前端基建体系,能让新项目在几分钟内启动、让代码风格保持一致、让发布流程自动化、让线上问题秒级感知。对于 5 人以上的前端团队,基建的投入产出比极高。
面试中问到"前端基建"通常考察的是全局视野和体系化思维。面试官希望看到你能从混乱走向规范、从手动走向自动化、从单点工具走向平台化的完整思路。
1. 什么是前端基建
1.1 定义
前端基建是为前端研发团队提供的标准化、自动化、平台化的工程基础设施。它不是某一个工具,而是一套覆盖开发 → 构建 → 测试 → 部署 → 监控全链路的体系。
1.2 为什么重要
| 没有基建 | 有基建 |
|---|---|
| 每个项目手动配置 ESLint、Prettier | 统一规范包,一键接入 |
| 新项目从零搭建,耗时 1-2 天 | 脚手架创建,5 分钟启动 |
| 手动打包、手动部署 | CI/CD 自动化,PR 合并即部署 |
| 线上出 Bug 靠用户反馈 | 监控告警秒级感知,主动发现 |
| 组件各写各的,重复劳动 | 组件库统一沉淀,按需复用 |
| 代码风格五花八门 | Git Hooks 拦截,风格统一 |
1.3 基建成熟度模型
| 等级 | 名称 | 特征 | 关键动作 |
|---|---|---|---|
| Level 1 | 混乱期 | 无规范、手动部署、各自为战 | 现状梳理,识别痛点 |
| Level 2 | 规范期 | 统一代码规范、Git 规范、文档规范 | 制定规范、Git Hooks 拦截 |
| Level 3 | 工具期 | 脚手架、构建工具、CI/CD 流水线 | 自研 CLI、搭建 Pipeline |
| Level 4 | 平台期 | 组件库、物料市场、监控平台、低代码 | 平台化建设、数据驱动 |
| Level 5 | 智能期 | AI 辅助开发、智能 Code Review、自动化测试 | AI 集成、智能推荐 |
大多数团队处于 Level 2 ~ Level 3。不要追求一步到位,先把规范和 CI/CD 做好,已经能解决 80% 的问题。
2. 基建全景图
基建是自底向上建设的。没有代码规范就去搞组件库,只会让混乱的代码被"标准化地复用"。
3. 开发规范体系
详细内容参考 代码规范与 Lint 和 Git 工作流
3.1 统一 ESLint + Prettier 配置
将团队的 lint 规则发布为 npm 包,所有项目共享同一份配置:
import type { Linter } from 'eslint';
const config: Linter.Config[] = [
{
name: '@company/eslint-config/base',
rules: {
'no-console': ['warn', { allow: ['warn', 'error'] }],
'no-debugger': 'error',
'prefer-const': 'error',
'no-var': 'error',
eqeqeq: ['error', 'always'],
curly: ['error', 'all'],
},
},
{
name: '@company/eslint-config/typescript',
rules: {
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-unused-vars': [
'error',
{ argsIgnorePattern: '^_' },
],
'@typescript-eslint/consistent-type-imports': 'error',
},
},
{
name: '@company/eslint-config/react',
rules: {
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn',
},
},
];
export default config;
项目中只需一行配置即可接入:
import companyConfig from '@company/eslint-config';
export default [...companyConfig];
3.2 Git 规范自动化
import type { UserConfig } from '@commitlint/types';
const config: UserConfig = {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [
2,
'always',
['feat', 'fix', 'docs', 'style', 'refactor', 'perf', 'test', 'build', 'ci', 'chore', 'revert'],
],
'subject-max-length': [2, 'always', 72],
'body-max-line-length': [2, 'always', 100],
},
};
export default config;
配合 Husky + lint-staged 在提交前自动检查:
{
"*.{ts,tsx}": ["eslint --fix", "prettier --write"],
"*.{css,scss}": ["stylelint --fix", "prettier --write"],
"*.{json,md}": ["prettier --write"]
}
3.3 TypeScript 严格模式配置
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "bundler",
"module": "ESNext",
"target": "ES2022",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"jsx": "react-jsx",
"skipLibCheck": true,
"isolatedModules": true,
"verbatimModuleSyntax": true
}
}
3.4 项目目录规范
src/
├── assets/ # 静态资源
├── components/ # 通用组件
│ ├── ui/ # 基础 UI 组件
│ └── business/ # 业务组件
├── hooks/ # 自定义 Hooks
├── lib/ # 工具函数
├── services/ # API 请求层
├── stores/ # 状态管理
├── styles/ # 全局样式
├── types/ # 类型定义
├── pages/ # 页面组件
│ └── [module]/
│ ├── components/ # 页面级组件
│ ├── hooks/ # 页面级 Hooks
│ └── index.tsx
└── app.tsx # 入口
4. 脚手架设计(CLI 工具)
4.1 为什么需要自研脚手架
| 对比项 | 通用脚手架 (create-vite) | 自研脚手架 |
|---|---|---|
| 技术栈 | 通用配置 | 内置团队技术栈 |
| 规范 | 无 | 预置 ESLint/Prettier/Commitlint |
| 模板 | 基础模板 | 包含业务模板(后台管理、H5、小程序) |
| CI/CD | 无 | 预配置 Pipeline |
| 监控 | 无 | 预置 Sentry SDK |
| 更新 | 需手动同步 | 远程模板自动拉取最新版 |
4.2 架构设计
4.3 核心实现
import { Command } from 'commander';
import { createProject } from './commands/create';
import { generateModule } from './commands/generate';
import { checkDoctor } from './commands/doctor';
import { version } from '../package.json';
const program = new Command();
program
.name('fe-cli')
.description('团队前端脚手架工具')
.version(version);
program
.command('create <project-name>')
.description('创建新项目')
.option('-t, --template <template>', '项目模板', 'react-admin')
.option('--no-install', '跳过依赖安装')
.action(createProject);
program
.command('generate <type> <name>')
.alias('g')
.description('生成模块/页面/组件')
.action(generateModule);
program
.command('doctor')
.description('检查开发环境')
.action(checkDoctor);
program.parse();
import inquirer from 'inquirer';
import degit from 'degit';
import chalk from 'chalk';
import ora from 'ora';
import { execSync } from 'node:child_process';
import path from 'node:path';
import fs from 'node:fs';
interface CreateOptions {
template?: string;
install?: boolean;
}
const TEMPLATE_MAP: Record<string, string> = {
'react-admin': 'company/templates/react-admin',
'react-h5': 'company/templates/react-h5',
'react-lib': 'company/templates/react-lib',
'nextjs-app': 'company/templates/nextjs-app',
'node-api': 'company/templates/node-api',
};
export async function createProject(
projectName: string,
options: CreateOptions,
): Promise<void> {
// 1. 交互式选择模板
const answers = await inquirer.prompt([
{
type: 'list',
name: 'template',
message: '请选择项目模板:',
choices: Object.keys(TEMPLATE_MAP),
default: options.template ?? 'react-admin',
},
{
type: 'input',
name: 'description',
message: '请输入项目描述:',
default: 'A frontend project',
},
{
type: 'confirm',
name: 'enableMonitoring',
message: '是否接入监控 SDK?',
default: true,
},
]);
const targetDir = path.resolve(process.cwd(), projectName);
// 2. 检查目录是否存在
if (fs.existsSync(targetDir)) {
const { overwrite } = await inquirer.prompt([
{
type: 'confirm',
name: 'overwrite',
message: `目录 ${projectName} 已存在,是否覆盖?`,
default: false,
},
]);
if (!overwrite) {
return;
}
fs.rmSync(targetDir, { recursive: true });
}
// 3. 拉取远程模板
const spinner = ora('正在拉取项目模板...').start();
try {
const templateRepo = TEMPLATE_MAP[answers.template as string];
const emitter = degit(templateRepo!, { cache: false, force: true });
await emitter.clone(targetDir);
spinner.succeed('模板拉取成功');
} catch (error) {
spinner.fail('模板拉取失败');
throw error;
}
// 4. 修改 package.json
const pkgPath = path.join(targetDir, 'package.json');
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')) as Record<string, unknown>;
pkg.name = projectName;
pkg.description = answers.description;
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
// 5. 安装依赖
if (options.install !== false) {
const installSpinner = ora('正在安装依赖...').start();
execSync('pnpm install', { cwd: targetDir, stdio: 'pipe' });
installSpinner.succeed('依赖安装完成');
}
// 6. 初始化 Git
execSync('git init', { cwd: targetDir, stdio: 'pipe' });
execSync('git add .', { cwd: targetDir, stdio: 'pipe' });
execSync('git commit -m "feat: init project"', {
cwd: targetDir,
stdio: 'pipe',
});
console.log(`\n${chalk.green('✓')} 项目创建成功!\n`);
console.log(` cd ${projectName}`);
console.log(' pnpm dev\n');
}
4.4 Doctor 环境检查
import chalk from 'chalk';
import { execSync } from 'node:child_process';
interface CheckItem {
name: string;
command: string;
minVersion?: string;
required: boolean;
}
const CHECK_LIST: CheckItem[] = [
{ name: 'Node.js', command: 'node -v', minVersion: '18.0.0', required: true },
{ name: 'pnpm', command: 'pnpm -v', minVersion: '8.0.0', required: true },
{ name: 'Git', command: 'git --version', required: true },
{ name: 'Docker', command: 'docker -v', required: false },
];
function getVersion(command: string): string | null {
try {
const output = execSync(command, { encoding: 'utf-8' }).trim();
const match = output.match(/(\d+\.\d+\.\d+)/);
return match?.[1] ?? null;
} catch {
return null;
}
}
function compareVersions(current: string, minimum: string): boolean {
const curr = current.split('.').map(Number);
const min = minimum.split('.').map(Number);
for (let i = 0; i < 3; i++) {
if ((curr[i] ?? 0) > (min[i] ?? 0)) return true;
if ((curr[i] ?? 0) < (min[i] ?? 0)) return false;
}
return true;
}
export function checkDoctor(): void {
console.log(chalk.bold('\n🔍 环境检查\n'));
let allPassed = true;
for (const item of CHECK_LIST) {
const version = getVersion(item.command);
if (!version) {
if (item.required) {
console.log(chalk.red(` ✗ ${item.name}: 未安装(必需)`));
allPassed = false;
} else {
console.log(chalk.yellow(` ⚠ ${item.name}: 未安装(可选)`));
}
continue;
}
if (item.minVersion && !compareVersions(version, item.minVersion)) {
console.log(
chalk.yellow(
` ⚠ ${item.name}: ${version}(建议 >= ${item.minVersion})`,
),
);
} else {
console.log(chalk.green(` ✓ ${item.name}: ${version}`));
}
}
console.log(
allPassed
? chalk.green('\n✓ 环境检查通过\n')
: chalk.red('\n✗ 请修复以上问题后重试\n'),
);
}
5. 公共包管理
详细内容参考 Monorepo 管理
5.1 Monorepo 架构
company-frontend/
├── apps/ # 应用项目
│ ├── web-admin/ # 后台管理
│ ├── web-h5/ # H5 页面
│ └── web-official/ # 官网
├── packages/ # 公共包
│ ├── eslint-config/ # @company/eslint-config
│ ├── tsconfig/ # @company/tsconfig
│ ├── ui/ # @company/ui (组件库)
│ ├── hooks/ # @company/hooks
│ ├── utils/ # @company/utils
│ ├── cli/ # @company/cli (脚手架)
│ └── monitor-sdk/ # @company/monitor-sdk
├── pnpm-workspace.yaml
├── turbo.json
└── package.json
packages:
- 'apps/*'
- 'packages/*'
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
"dev": {
"cache": false,
"persistent": true
},
"lint": {
"dependsOn": ["^build"]
},
"test": {
"dependsOn": ["^build"]
}
}
}
5.2 公共包分类
| 包名 | 用途 | 发布频率 |
|---|---|---|
@company/eslint-config | 统一 ESLint 规则 | 低(规则稳定) |
@company/tsconfig | 统一 TypeScript 配置 | 低 |
@company/ui | 基础 UI 组件库 | 中(功能迭代) |
@company/hooks | 通用 React Hooks | 中 |
@company/utils | 工具函数库 | 中 |
@company/monitor-sdk | 前端监控 SDK | 中 |
@company/cli | 脚手架工具 | 低 |
@company/request | 统一请求封装 (Axios) | 低 |
5.3 Changesets 版本管理
import { execSync } from 'node:child_process';
// 用于 CI/CD 中的自动发布脚本
function release(): void {
// 1. 版本更新
execSync('pnpm changeset version', { stdio: 'inherit' });
// 2. 安装依赖(更新 lock 文件)
execSync('pnpm install --no-frozen-lockfile', { stdio: 'inherit' });
// 3. 构建所有包
execSync('pnpm turbo build --filter="./packages/*"', { stdio: 'inherit' });
// 4. 发布
execSync('pnpm changeset publish', { stdio: 'inherit' });
// 5. 推送 Git tags
execSync('git push --follow-tags', { stdio: 'inherit' });
}
release();
5.4 私有 npm Registry
| 方案 | 适用场景 | 优势 | 劣势 |
|---|---|---|---|
| Verdaccio | 中小团队、自托管 | 部署简单、免费 | 需运维、高可用成本高 |
| GitHub Packages | 使用 GitHub 的团队 | 与 GitHub 深度集成 | 公共包数量有限制 |
| GitLab Packages | 使用 GitLab 的团队 | 与 GitLab CI 深度集成 | 依赖 GitLab 版本 |
| npm Organization | 发布公共包的团队 | 成熟稳定 | 私有包需付费 |
| cnpm/Nexus | 大型企业 | 功能全面 | 部署复杂 |
# 公司私有包从私有 registry 安装
@company:registry=https://npm.company.com/
# 其他包从官方 registry 安装
registry=https://registry.npmmirror.com/
6. 构建与发布体系
详细内容参考 CI/CD 与自动化部署 和 环境管理与配置
6.1 统一构建配置
使用 tsup 统一构建公共包:
import { defineConfig } from 'tsup';
export default defineConfig({
entry: ['src/index.ts'],
format: ['cjs', 'esm'],
dts: true,
splitting: true,
sourcemap: true,
clean: true,
treeshake: true,
minify: false,
external: ['react', 'react-dom'],
});
6.2 CI/CD Pipeline 设计
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
lint-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
# 并行执行 lint、类型检查、测试
- name: Lint
run: pnpm turbo lint
- name: Type Check
run: pnpm turbo typecheck
- name: Test
run: pnpm turbo test -- --coverage
- name: Build
run: pnpm turbo build
# 上传覆盖率报告
- name: Upload Coverage
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
deploy-staging:
needs: lint-and-test
if: github.ref == 'refs/heads/develop'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- run: pnpm turbo build
- name: Deploy to Staging
run: pnpm dlx wrangler pages deploy dist --project-name=my-app --branch=staging
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
deploy-production:
needs: lint-and-test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- run: pnpm turbo build
- name: Deploy to Production
run: pnpm dlx wrangler pages deploy dist --project-name=my-app --branch=main
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
6.3 多环境管理
interface EnvConfig {
apiBaseUrl: string;
sentryDsn: string;
enableMonitoring: boolean;
cdnPrefix: string;
}
const ENV_MAP: Record<string, EnvConfig> = {
development: {
apiBaseUrl: 'http://localhost:3000/api',
sentryDsn: '',
enableMonitoring: false,
cdnPrefix: '',
},
staging: {
apiBaseUrl: 'https://staging-api.company.com',
sentryDsn: 'https://xxx@sentry.company.com/2',
enableMonitoring: true,
cdnPrefix: 'https://staging-cdn.company.com',
},
production: {
apiBaseUrl: 'https://api.company.com',
sentryDsn: 'https://xxx@sentry.company.com/1',
enableMonitoring: true,
cdnPrefix: 'https://cdn.company.com',
},
};
const currentEnv = import.meta.env.MODE ?? 'development';
export const config: EnvConfig = ENV_MAP[currentEnv] ?? ENV_MAP.development!;
6.4 灰度发布策略
import type { FeatureFlags } from '../types';
interface UserContext {
userId: string;
region?: string;
role?: string;
}
// 基于用户 ID 哈希的分桶策略
function hashBucket(userId: string, totalBuckets: number = 100): number {
let hash = 0;
for (let i = 0; i < userId.length; i++) {
const char = userId.charCodeAt(i);
hash = ((hash << 5) - hash + char) | 0; // 转为 32 位整数
}
return Math.abs(hash) % totalBuckets;
}
export function isFeatureEnabled(
featureName: keyof FeatureFlags,
user: UserContext,
flags: FeatureFlags,
): boolean {
const flag = flags[featureName];
if (!flag) return false;
// 全局开关
if (typeof flag === 'boolean') return flag;
// 白名单用户
if (flag.whitelist?.includes(user.userId)) return true;
// 按区域
if (flag.regions && user.region && !flag.regions.includes(user.region)) {
return false;
}
// 按百分比灰度
if (flag.percentage !== undefined) {
const bucket = hashBucket(user.userId);
return bucket < flag.percentage;
}
return false;
}
7. 监控与告警体系
详细内容参考 前端监控与埋点
7.1 监控体系架构
7.2 监控 SDK 核心实现
interface MonitorConfig {
dsn: string;
appId: string;
environment: string;
sampleRate?: number; // 采样率 0-1
enablePerformance?: boolean;
enableError?: boolean;
}
interface ReportData {
type: 'error' | 'performance' | 'event' | 'api';
appId: string;
environment: string;
timestamp: number;
url: string;
userAgent: string;
payload: Record<string, unknown>;
}
class MonitorSDK {
private config: Required<MonitorConfig>;
private queue: ReportData[] = [];
private timer: ReturnType<typeof setTimeout> | null = null;
constructor(config: MonitorConfig) {
this.config = {
sampleRate: 1,
enablePerformance: true,
enableError: true,
...config,
};
this.init();
}
private init(): void {
if (this.config.enableError) {
this.initErrorMonitor();
}
if (this.config.enablePerformance) {
this.initPerformanceMonitor();
}
}
/** 错误监控 */
private initErrorMonitor(): void {
// JS 运行时错误
window.addEventListener('error', (event) => {
this.report({
type: 'error',
payload: {
category: 'js_error',
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
stack: event.error?.stack,
},
});
});
// Promise 未捕获错误
window.addEventListener('unhandledrejection', (event) => {
this.report({
type: 'error',
payload: {
category: 'promise_rejection',
message: event.reason?.message ?? String(event.reason),
stack: event.reason?.stack,
},
});
});
// 资源加载错误
window.addEventListener(
'error',
(event) => {
const target = event.target as HTMLElement;
if (target.tagName && ['SCRIPT', 'LINK', 'IMG'].includes(target.tagName)) {
this.report({
type: 'error',
payload: {
category: 'resource_error',
tagName: target.tagName,
src: (target as HTMLScriptElement).src ?? (target as HTMLLinkElement).href,
},
});
}
},
true, // 捕获阶段
);
}
/** 性能监控 */
private initPerformanceMonitor(): void {
// Web Vitals
if ('PerformanceObserver' in window) {
// LCP
const lcpObserver = new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
if (lastEntry) {
this.report({
type: 'performance',
payload: { metric: 'LCP', value: lastEntry.startTime },
});
}
});
lcpObserver.observe({ type: 'largest-contentful-paint', buffered: true });
// FID → INP
const inpObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
this.report({
type: 'performance',
payload: {
metric: 'INP',
value: (entry as PerformanceEventTiming).duration,
},
});
}
});
inpObserver.observe({ type: 'event', buffered: true });
// CLS
let clsValue = 0;
const clsObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!(entry as LayoutShift).hadRecentInput) {
clsValue += (entry as LayoutShift).value;
}
}
this.report({
type: 'performance',
payload: { metric: 'CLS', value: clsValue },
});
});
clsObserver.observe({ type: 'layout-shift', buffered: true });
}
}
/** 统一上报 */
private report(
data: Pick<ReportData, 'type' | 'payload'>,
): void {
// 采样
if (Math.random() > this.config.sampleRate) return;
const reportData: ReportData = {
...data,
appId: this.config.appId,
environment: this.config.environment,
timestamp: Date.now(),
url: location.href,
userAgent: navigator.userAgent,
};
this.queue.push(reportData);
this.scheduleFlush();
}
/** 批量发送(降低请求频次) */
private scheduleFlush(): void {
if (this.timer) return;
this.timer = setTimeout(() => {
this.flush();
this.timer = null;
}, 2000);
}
private flush(): void {
if (this.queue.length === 0) return;
const data = [...this.queue];
this.queue = [];
// 优先使用 sendBeacon(不阻塞页面卸载)
const blob = new Blob([JSON.stringify(data)], {
type: 'application/json',
});
if (navigator.sendBeacon) {
navigator.sendBeacon(this.config.dsn, blob);
} else {
fetch(this.config.dsn, {
method: 'POST',
body: blob,
keepalive: true,
}).catch(() => {
// 上报失败,静默处理
});
}
}
/** 手动上报自定义事件 */
trackEvent(name: string, properties?: Record<string, unknown>): void {
this.report({
type: 'event',
payload: { name, ...properties },
});
}
}
export function initMonitor(config: MonitorConfig): MonitorSDK {
return new MonitorSDK(config);
}
// LayoutShift 和 PerformanceEventTiming 的类型补充
interface LayoutShift extends PerformanceEntry {
hadRecentInput: boolean;
value: number;
}
interface PerformanceEventTiming extends PerformanceEntry {
duration: number;
}
7.3 告警规则配置
interface AlertRule {
name: string;
metric: string;
condition: 'gt' | 'lt' | 'eq';
threshold: number;
window: number; // 时间窗口(秒)
severity: 'info' | 'warning' | 'critical';
notification: ('feishu' | 'dingtalk' | 'email')[];
}
const ALERT_RULES: AlertRule[] = [
{
name: 'JS 错误率过高',
metric: 'error_rate',
condition: 'gt',
threshold: 1, // 错误率 > 1%
window: 300, // 5 分钟窗口
severity: 'critical',
notification: ['feishu', 'dingtalk'],
},
{
name: 'LCP 超过阈值',
metric: 'lcp_p75',
condition: 'gt',
threshold: 2500, // LCP > 2.5s
window: 600,
severity: 'warning',
notification: ['feishu'],
},
{
name: 'API 成功率下降',
metric: 'api_success_rate',
condition: 'lt',
threshold: 99, // 成功率 < 99%
window: 300,
severity: 'critical',
notification: ['feishu', 'dingtalk', 'email'],
},
];
// 飞书 Webhook 通知
async function sendFeishuAlert(
rule: AlertRule,
currentValue: number,
): Promise<void> {
const webhookUrl = process.env.FEISHU_WEBHOOK_URL!;
const severityEmoji: Record<string, string> = {
info: 'ℹ️',
warning: '⚠️',
critical: '🚨',
};
await fetch(webhookUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
msg_type: 'interactive',
card: {
header: {
title: {
content: `${severityEmoji[rule.severity]} [${rule.severity.toUpperCase()}] ${rule.name}`,
tag: 'plain_text',
},
},
elements: [
{
tag: 'div',
text: {
content: `**指标**: ${rule.metric}\n**当前值**: ${currentValue}\n**阈值**: ${rule.condition === 'gt' ? '>' : '<'} ${rule.threshold}\n**时间**: ${new Date().toISOString()}`,
tag: 'lark_md',
},
},
],
},
}),
});
}
export { ALERT_RULES, sendFeishuAlert };
8. 组件库与物料体系
详细内容参考 组件库建设
8.1 物料体系分层
| 层级 | 说明 | 复用粒度 | 示例 |
|---|---|---|---|
| 基础组件 | 纯 UI 组件,无业务逻辑 | 跨项目 | Button、Input、Select |
| 业务组件 | 封装业务逻辑的可复用组件 | 跨页面 | SearchForm、UserPicker |
| 区块 | 多个组件组合的功能模块 | 复制粘贴 | 登录表单、CRUD 列表 |
| 页面模板 | 完整页面的起步模板 | 脚手架生成 | 后台管理系统、H5 活动页 |
8.2 组件库设计原则
import React from 'react';
// 1. 类型安全:完整的 Props 类型定义
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
/** 按钮类型 */
variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger';
/** 按钮尺寸 */
size?: 'sm' | 'md' | 'lg';
/** 加载状态 */
loading?: boolean;
/** 图标(左侧) */
icon?: React.ReactNode;
}
// 2. 组件实现:forwardRef + 合理默认值
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
(
{
variant = 'primary',
size = 'md',
loading = false,
icon,
children,
disabled,
className,
...rest
},
ref,
) => {
const classes = [
'btn',
`btn--${variant}`,
`btn--${size}`,
loading && 'btn--loading',
disabled && 'btn--disabled',
className,
]
.filter(Boolean)
.join(' ');
return (
<button
ref={ref}
className={classes}
disabled={disabled ?? loading}
{...rest}
>
{loading ? <span className="btn__spinner" /> : icon}
{children && <span className="btn__text">{children}</span>}
</button>
);
},
);
Button.displayName = 'Button';
8.3 业务组件封装思路
import React, { useCallback, useMemo } from 'react';
import { Button, Input, Select } from '@company/ui';
interface FieldConfig {
name: string;
label: string;
type: 'input' | 'select' | 'date' | 'dateRange';
placeholder?: string;
options?: Array<{ label: string; value: string | number }>;
defaultValue?: unknown;
}
interface SearchFormProps {
fields: FieldConfig[];
onSearch: (values: Record<string, unknown>) => void;
onReset?: () => void;
loading?: boolean;
}
export function SearchForm({
fields,
onSearch,
onReset,
loading,
}: SearchFormProps): React.ReactElement {
const [values, setValues] = React.useState<Record<string, unknown>>(() => {
const initial: Record<string, unknown> = {};
for (const field of fields) {
if (field.defaultValue !== undefined) {
initial[field.name] = field.defaultValue;
}
}
return initial;
});
const handleChange = useCallback((name: string, value: unknown) => {
setValues((prev) => ({ ...prev, [name]: value }));
}, []);
const handleReset = useCallback(() => {
setValues({});
onReset?.();
}, [onReset]);
const handleSubmit = useCallback(
(e: React.FormEvent) => {
e.preventDefault();
onSearch(values);
},
[onSearch, values],
);
const renderedFields = useMemo(
() =>
fields.map((field) => (
<div key={field.name} className="search-form__field">
<label>{field.label}</label>
{field.type === 'input' && (
<Input
value={(values[field.name] as string) ?? ''}
onChange={(e) => handleChange(field.name, e.target.value)}
placeholder={field.placeholder}
/>
)}
{field.type === 'select' && (
<Select
value={values[field.name] as string}
onChange={(val) => handleChange(field.name, val)}
options={field.options ?? []}
placeholder={field.placeholder}
/>
)}
</div>
)),
[fields, values, handleChange],
);
return (
<form className="search-form" onSubmit={handleSubmit}>
<div className="search-form__fields">{renderedFields}</div>
<div className="search-form__actions">
<Button type="submit" loading={loading}>
搜索
</Button>
<Button variant="outline" onClick={handleReset}>
重置
</Button>
</div>
</form>
);
}
9. 微前端与应用治理
详细内容参考 微前端架构
9.1 什么时候需要微前端
微前端带来的复杂度远超想象。只有在以下场景才值得考虑:
| 场景 | 是否需要微前端 | 建议 |
|---|---|---|
| 单一技术栈、单一团队 | 不需要 | Monorepo 就够了 |
| 多团队独立开发不同模块 | 需要 | 子应用独立部署 |
| 遗留系统渐进式迁移 | 需要 | 新旧系统共存 |
| 多技术栈共存(React + Vue) | 需要 | 跨框架集成 |
| 超大型应用(100+ 页面) | 可能需要 | 按业务拆分 |
9.2 技术方案选型
| 方案 | 原理 | 沙箱 | 性能 | 接入成本 | 适合场景 |
|---|---|---|---|---|---|
| Module Federation | Webpack/Rspack 模块共享 | 无原生沙箱 | 最好 | 低 | 同技术栈、新项目 |
| qiankun | single-spa + Proxy 沙箱 | JS + CSS 沙箱 | 中 | 中 | 多技术栈、已有项目 |
| Wujie | Web Components + iframe | iframe 天然沙箱 | 中 | 低 | 强隔离需求 |
| Micro App | Web Components | JS + CSS 沙箱 | 中 | 低 | 轻量级需求 |
| iframe | 原生隔离 | 天然沙箱 | 差 | 最低 | 简单嵌入 |
9.3 子应用接入规范
import { createApp, type App as VueApp } from 'vue';
import App from './App.vue';
import router from './router';
let app: VueApp | null = null;
// 微前端生命周期导出
export async function bootstrap(): Promise<void> {
console.log('[sub-app] bootstrapped');
}
export async function mount(props: {
container: HTMLElement;
data?: Record<string, unknown>;
}): Promise<void> {
const { container, data } = props;
app = createApp(App);
app.use(router);
// 注入主应用传递的数据
if (data) {
app.provide('mainAppData', data);
}
// 挂载到指定容器
const mountNode = container.querySelector('#sub-app') ?? container;
app.mount(mountNode);
}
export async function unmount(): Promise<void> {
if (app) {
app.unmount();
app = null;
}
}
// 独立运行时直接挂载
if (!window.__POWERED_BY_QIANKUN__) {
const app = createApp(App);
app.use(router);
app.mount('#app');
}
declare global {
interface Window {
__POWERED_BY_QIANKUN__?: boolean;
}
}
9.4 共享依赖与通信
// 基于 CustomEvent 的跨应用通信
type EventHandler<T = unknown> = (data: T) => void;
class MicroAppEventBus {
private prefix = '__MICRO_APP__';
emit<T>(eventName: string, data: T): void {
const event = new CustomEvent(`${this.prefix}${eventName}`, {
detail: data,
});
window.dispatchEvent(event);
}
on<T>(eventName: string, handler: EventHandler<T>): () => void {
const wrappedHandler = (event: Event) => {
handler((event as CustomEvent<T>).detail);
};
window.addEventListener(
`${this.prefix}${eventName}`,
wrappedHandler,
);
// 返回取消监听函数
return () => {
window.removeEventListener(
`${this.prefix}${eventName}`,
wrappedHandler,
);
};
}
}
export const eventBus = new MicroAppEventBus();
10. 基建落地策略
10.1 优先级排序
先规范 → 再工具 → 后平台。规范是基石,工具是加速器,平台是集大成者。切忌跳过规范直接搞平台。
10.2 ROI 衡量
| 指标 | 基建前 | 基建后 | 提升 |
|---|---|---|---|
| 新项目启动时间 | 1-2 天 | 5 分钟 | 99%+ |
| 构建时间(冷启动) | 5-10 分钟 | 30-60 秒 | 80%+ |
| 发布频率 | 每周 1-2 次 | 每天多次 | 5x+ |
| 线上问题发现时间 | 用户反馈(小时级) | 监控告警(秒级) | 100x+ |
| 代码规范争论 | 频繁 Code Review 讨论 | 自动化检查,零争论 | 归零 |
| 重复代码率 | 30%+ | < 10% | 70%+ |
| 故障恢复时间 (MTTR) | 1-2 小时 | 5-10 分钟 | 90%+ |
10.3 推广策略
- 试点验证:选一个新项目完整使用基建工具链,收集反馈
- 数据说话:用试点项目的效率提升数据说服团队
- 降低门槛:好的基建应该是"无感接入"的,不给开发者增加额外负担
- 渐进迁移:老项目不强制一次性切换,提供迁移脚本和文档
- 持续迭代:定期收集团队反馈,快速响应问题
10.4 常见踩坑与经验
- 规范过于严格:一开始上了 200+ 条 ESLint 规则,开发者怨声载道。建议从最核心的 20-30 条开始,逐步增加。
- 脚手架模板不更新:做完就放那了,半年后模板已经过时。建议模板远程托管,每次拉取最新版。
- 组件库闭门造车:不听使用者反馈,组件不好用还强推。建议先收集团队高频需求,再设计 API。
- 监控数据不消费:接了监控但没人看数据,等于没接。建议配合告警规则,数据驱动改进。
- 一次性投入无后续:基建是持续的事,不是做完就结束。建议安排专人持续维护。
常见面试问题
Q1: 什么是前端基建?为什么需要前端基建?
答案:
前端基建是为前端研发团队提供的一套标准化、自动化、平台化的工程基础设施。它覆盖了开发规范、脚手架、构建发布、监控告警、组件物料等完整链路。
为什么需要:
| 问题 | 没有基建 | 有基建 |
|---|---|---|
| 项目启动 | 从零配置,1-2 天 | 脚手架创建,5 分钟 |
| 代码质量 | 全靠 Code Review 人肉把关 | ESLint + Prettier 自动检查 |
| 部署 | 手动打包上传服务器 | CI/CD 自动化,合并即部署 |
| 线上监控 | 用户反馈才知道出了问题 | 秒级告警,主动发现 |
| 组件复用 | 各写各的,大量重复代码 | 统一组件库,按需引入 |
前端基建的核心价值可以用三个词概括:提效(减少重复劳动)、提质(保障代码质量)、降本(降低协作和运维成本)。
Q2: 如果你来主导一个团队的前端基建,你会怎么做?(从 0 到 1 的路径)
答案:
我会分 5 个阶段推进,每个阶段聚焦最关键的事情:
第一阶段:调研现状(1 周)
- 梳理团队现有项目的技术栈、构建工具、部署方式
- 收集痛点:哪些事情反复做?哪里最容易出问题?
- 确定优先级:根据"痛点严重程度 x 影响人数"排序
第二阶段:规范落地(2 周)
- 统一 ESLint + Prettier + Stylelint 配置,发布为
@company/eslint-config - 统一 Git 规范(Commitlint + Husky + lint-staged)
- 统一 TypeScript 严格模式配置
第三阶段:工具建设(4 周)
- 开发脚手架 CLI(create + generate + doctor)
- 搭建 CI/CD Pipeline(lint → test → build → deploy)
- 搭建私有 npm registry(Verdaccio / GitHub Packages)
第四阶段:效率提升(4 周)
- 沉淀通用 Hooks 库和工具函数库
- 开始建设基础组件库(先做最高频的 10 个组件)
- 接入前端监控(Sentry 或自研 SDK)
第五阶段:平台化(持续迭代)
- 物料市场、区块市场
- 微前端(根据实际需要)
- 低代码平台(面向运营)
关键原则:先试点再推广、数据驱动决策、降低接入门槛。
Q3: 如何设计一个前端脚手架(CLI 工具)?核心功能有哪些?
答案:
前端脚手架的本质是把团队最佳实践固化为工具。核心功能如下:
| 命令 | 功能 | 说明 |
|---|---|---|
create | 创建项目 | 交互式选择模板,拉取远程模板,初始化配置 |
generate | 生成代码 | 生成页面/组件/Store 等模板代码 |
doctor | 环境检查 | 检查 Node/pnpm/Git 版本是否满足要求 |
upgrade | 升级模板 | 对比当前项目和最新模板的差异,增量升级 |
技术栈选择:
// 命令行框架
import { Command } from 'commander'; // 命令行参数解析
import inquirer from 'inquirer'; // 交互式提问
import ora from 'ora'; // Loading 动画
import chalk from 'chalk'; // 终端文字着色
// 模板处理
import degit from 'degit'; // 快速拉取 Git 模板(不带 .git 历史)
import ejs from 'ejs'; // 模板引擎,动态填充变量
模板管理策略:模板放在远程 Git 仓库,脚手架每次创建项目时实时拉取,保证模板始终是最新版本。支持通过 Git tag 指定版本:
// 拉取指定版本的模板
const emitter = degit('company/templates/react-admin#v2.0.0', {
cache: false,
force: true,
});
await emitter.clone(targetDir);
完整实现代码参见上方「脚手架设计」章节。
Q4: 前端 Monorepo 如何管理公共包?
答案:
详细参考 Monorepo 管理
推荐 pnpm workspace + Turborepo 方案:
packages:
- 'apps/*' # 应用项目
- 'packages/*' # 公共包
公共包组织:
| 包 | 职责 | 更新频率 |
|---|---|---|
@company/eslint-config | ESLint 统一配置 | 低 |
@company/tsconfig | TypeScript 基础配置 | 低 |
@company/ui | 基础 UI 组件库 | 中 |
@company/hooks | 通用 React Hooks | 中 |
@company/utils | 工具函数库 | 中 |
@company/request | 统一请求封装 | 低 |
@company/monitor-sdk | 前端监控 SDK | 中 |
版本发布流程(Changesets):
# 1. 开发完成后,描述变更
pnpm changeset
# 2. CI 自动更新版本号和 CHANGELOG
pnpm changeset version
# 3. CI 自动发布到 npm
pnpm changeset publish
Turborepo 任务编排:
{
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
}
}
}
"dependsOn": ["^build"] 表示包 A 的 build 依赖其上游包先 build 完成,Turborepo 会自动推断正确的构建顺序并利用缓存加速。
Q5: 如何设计前端监控告警体系?
答案:
详细参考 前端监控与埋点
前端监控体系分为数据采集 → 数据上报 → 数据存储 → 数据消费四个环节:
1. 数据采集
| 监控类型 | 采集方式 | 关键指标 |
|---|---|---|
| JS 错误 | window.onerror、unhandledrejection | 错误率、错误 Top 10 |
| 资源错误 | error 事件捕获阶段监听 | 资源加载失败率 |
| 性能指标 | PerformanceObserver | LCP、INP、CLS |
| API 监控 | Fetch/XHR 拦截 | 成功率、P95 耗时 |
| 用户行为 | 手动埋点 / 自动埋点 | PV、UV、点击事件 |
2. 数据上报策略
// 1. 批量上报:攒 2 秒或 10 条再发送,降低请求频次
// 2. 采样率:非核心数据设置采样(如 10% 采样率)
// 3. sendBeacon:页面卸载时用 sendBeacon,不阻塞页面关闭
// 4. 离线缓存:弱网环境先存 IndexedDB,恢复网络后上报
3. 告警规则
- JS 错误率 > 1% → 飞书/钉钉立即通知
- LCP P75 > 2.5s → 性能告警
- API 成功率 < 99% → 即时告警
4. 方案选型
| 方案 | 适合场景 | 成本 |
|---|---|---|
| Sentry | 中小团队,快速接入 | SaaS 按量付费 |
| 自研 SDK + ELK | 大团队,数据定制需求强 | 高,需运维 |
| Grafana + Prometheus | 已有后端基建的团队 | 中 |
Q6: 如何统一团队的代码规范?遇到阻力怎么办?
答案:
详细参考 代码规范与 Lint
统一规范的 4 步走:
- 工具自动化:ESLint + Prettier + Stylelint,通过工具而非人来检查
- Git Hooks 拦截:lint-staged + Husky,提交前自动修复和拦截
- CI 门禁:PR 必须通过 lint 检查才能合并
- 共享配置包:发布
@company/eslint-config,所有项目一行配置接入
遇到阻力怎么办:
| 阻力类型 | 应对策略 |
|---|---|
| "规则太严格了" | 从宽松规则开始(只有 warn 没有 error),逐步收紧 |
| "迁移成本太高" | 提供 --fix 自动修复脚本,一键格式化全部代码 |
| "影响开发效率" | 只在 commit 阶段检查变更的文件,不影响编码过程 |
| "每个人习惯不同" | 规范一旦确定,写入工具强制执行,避免人为争论 |
| "老项目无法接入" | 只对新增/修改的文件检查(lint-staged),不动老代码 |
核心思路:工具 > 人 > 文档。能用工具自动化的绝不靠人去记,能靠人 Code Review 的不靠文档约束。
Q7: 前端 CI/CD 流水线应该包含哪些环节?
答案:
详细参考 CI/CD 与自动化部署
完整的 CI/CD Pipeline 包含以下环节:
| 环节 | 工具 | 失败行为 |
|---|---|---|
| Install | pnpm install --frozen-lockfile | 阻断 |
| Lint | ESLint + Stylelint | 阻断 |
| Type Check | tsc --noEmit | 阻断 |
| Unit Test | Vitest / Jest | 阻断 |
| Build | Vite / Webpack | 阻断 |
| E2E Test | Playwright | 告警(不阻断,或看策略) |
| Preview Deploy | Vercel / Cloudflare Pages | 自动生成预览链接 |
| Staging Deploy | Docker + K8s / 云平台 | — |
| Production Deploy | 灰度发布 | — |
关键实践:
- PR 必须通过 CI 才能合并,这是质量门禁
- 缓存优化:缓存 node_modules 和 Turborepo 的构建缓存
- 并行执行:lint、typecheck、test 可以并行跑
- Preview Deploy:每个 PR 生成独立预览链接,方便 QA 验证
Q8: 如何衡量前端基建的 ROI?
答案:
ROI(投资回报率)= 收益 / 投入成本。前端基建的 ROI 可以从以下维度衡量:
效率指标:
| 指标 | 衡量方式 | 基建前基线 | 目标 |
|---|---|---|---|
| 新项目启动时间 | 从创建到第一次 dev 运行 | 1-2 天 | < 10 分钟 |
| CI 构建时间 | Pipeline 完整执行耗时 | 10-15 分钟 | < 5 分钟 |
| 发布频率 | 每周部署次数 | 1-2 次 | 每天多次 |
| 代码重复率 | 工具扫描 | > 30% | < 10% |
质量指标:
| 指标 | 衡量方式 | 基建前基线 | 目标 |
|---|---|---|---|
| 线上 Bug 率 | 生产环境每周 Bug 数 | 5-10 个 | < 2 个 |
| MTTR | 故障发现到修复的时间 | 1-2 小时 | < 10 分钟 |
| 测试覆盖率 | 代码覆盖率 | < 20% | > 60% |
| LCP P75 | 性能监控数据 | > 3s | < 2.5s |
成本指标:
| 指标 | 衡量方式 |
|---|---|
| 规范争论时间 | Code Review 中格式相关的评论数(应该趋近于 0) |
| 重复劳动时间 | 手动配置项目/手动部署花费的人时 |
| 故障影响面 | 线上故障影响的用户数和持续时间 |
- 项目启动时间:脚手架自带计时
- CI 构建时间:GitHub Actions / GitLab CI 自带
- 发布频率:CI/CD 部署记录
- MTTR:监控平台告警 → 恢复的时间差
Q9: 什么时候需要引入微前端?如何选型?
答案:
详细参考 微前端架构
需要微前端的场景:
- 多团队独立开发:不同团队负责不同业务模块,需要独立开发、独立部署
- 渐进式迁移:老项目从 jQuery/Vue 2 迁移到 React/Vue 3,新旧系统共存
- 超大型应用:100+ 页面的巨型单体,构建时间过长
不需要微前端的场景:
- 单一团队、单一技术栈 → Monorepo 即可
- 项目规模不大(< 30 个页面)→ 代码分割 + 路由懒加载即可
- 仅仅为了"技术先进" → 过度设计
选型决策树:
Q10: 如何设计一个前端灰度发布系统?
答案:
灰度发布的核心是让新版本只对部分用户可见,验证无问题后再全量放开。
实现方案:
- Feature Flag 方式:通过配置控制新功能的可见性
function hashBucket(userId: string, total: number = 100): number {
let hash = 0;
for (let i = 0; i < userId.length; i++) {
hash = ((hash << 5) - hash + userId.charCodeAt(i)) | 0;
}
return Math.abs(hash) % total;
}
// 10% 灰度:bucket 0-9 的用户看到新版本
const isInGray = hashBucket(userId) < 10;
-
Nginx 分流方式:在 Nginx 层面根据 Cookie/Header 将流量导向不同版本
-
CDN 多版本方式:构建产物带版本号,通过后端接口控制返回的 HTML 中引用哪个版本
灰度发布流程:
| 阶段 | 灰度比例 | 持续时间 | 观察指标 |
|---|---|---|---|
| 内部灰度 | 内部员工 | 1-2 天 | 功能正确性 |
| 小流量灰度 | 5-10% | 1-2 天 | 错误率、性能指标 |
| 中流量灰度 | 30-50% | 1 天 | 业务指标(转化率等) |
| 全量发布 | 100% | — | 持续监控 |
回滚策略:灰度期间如果发现问题,立即将灰度比例设为 0%,切回旧版本。CDN 方案下,回滚就是把 HTML 指向旧版本的静态资源。
Q11: 私有 npm 包管理方案如何选择?
答案:
| 方案 | 部署方式 | 适合团队 | 成本 | 优势 |
|---|---|---|---|---|
| Verdaccio | 自托管(Docker) | 中小团队 | 低(免费) | 部署简单、支持缓存上游 registry |
| GitHub Packages | SaaS | 已用 GitHub 的团队 | 中 | 与 GitHub Actions 深度集成 |
| GitLab Packages | SaaS/私有 | 已用 GitLab 的团队 | 中 | 与 GitLab CI 深度集成 |
| cnpm | 自托管 | 大型企业 | 高 | 功能全面、国内使用多 |
| Nexus | 自托管 | 已有 Nexus 的企业 | 高 | 支持多种语言的包管理 |
推荐选择:
- 10 人以下团队 → Verdaccio(Docker 一键部署,零成本)
- 使用 GitHub → GitHub Packages(和 CI/CD 无缝集成)
- 大型企业 → cnpm / Nexus(功能全面、有专人运维)
Verdaccio 快速部署:
version: '3'
services:
verdaccio:
image: verdaccio/verdaccio
ports:
- '4873:4873'
volumes:
- ./storage:/verdaccio/storage
- ./conf:/verdaccio/conf
@company:registry=http://localhost:4873/
Q12: 如何推动团队采纳新的基建工具?
答案:
推动基建落地是一件技术 + 管理的事情。以下是实战验证过的推广策略:
1. 降低接入成本(最关键)
// 不好的接入体验:修改 10 个配置文件
// ❌ 改 webpack.config.js
// ❌ 改 .eslintrc.js
// ❌ 改 .prettierrc
// ❌ 改 tsconfig.json
// ...
// 好的接入体验:一行命令搞定
// ✅ npx @company/cli create my-project
2. 渐进式推进
| 阶段 | 策略 | 做法 |
|---|---|---|
| 试点 | 选一个新项目做试点 | 完整走通基建全链路 |
| 数据 | 收集对比数据 | "构建时间从 10 分钟降到 1 分钟" |
| 宣讲 | 团队内部分享 | 展示效果 + 教程 |
| 推广 | 新项目默认使用 | 老项目提供迁移脚本 |
| 强制 | 写入团队规范 | CI 门禁强制检查 |
3. 解决常见反对意见
| 反对意见 | 应对方式 |
|---|---|
| "学习成本高" | 准备完善的文档 + 示例项目 + 一对一答疑 |
| "影响开发进度" | 从新项目开始,不影响老项目的排期 |
| "没有时间" | 先做最小可用版本(MVP),不追求完美 |
| "我觉得现在挺好的" | 用数据说话:看看每月重复劳动浪费了多少人天 |
4. 持续运营
- 建立 基建反馈群,快速响应问题(5 分钟内回复)
- 每双周发布 基建周报,展示新增功能和使用数据
- 定期 基建满意度调查,收集改进意见
- 表彰 基建贡献者,鼓励团队参与共建