跳到主要内容

前端基础建设(从 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. 开发规范体系

详细内容参考 代码规范与 LintGit 工作流

3.1 统一 ESLint + Prettier 配置

将团队的 lint 规则发布为 npm 包,所有项目共享同一份配置:

packages/eslint-config/src/index.ts
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;

项目中只需一行配置即可接入:

eslint.config.ts
import companyConfig from '@company/eslint-config';

export default [...companyConfig];

3.2 Git 规范自动化

packages/commitlint-config/src/index.ts
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 在提交前自动检查:

.husky/pre-commit
{
"*.{ts,tsx}": ["eslint --fix", "prettier --write"],
"*.{css,scss}": ["stylelint --fix", "prettier --write"],
"*.{json,md}": ["prettier --write"]
}

3.3 TypeScript 严格模式配置

packages/tsconfig/base.json
{
"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 核心实现

packages/cli/src/index.ts
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();
packages/cli/src/commands/create.ts
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 环境检查

packages/cli/src/commands/doctor.ts
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
pnpm-workspace.yaml
packages:
- 'apps/*'
- 'packages/*'
turbo.json
{
"$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 版本管理

scripts/release.ts
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大型企业功能全面部署复杂
.npmrc
# 公司私有包从私有 registry 安装
@company:registry=https://npm.company.com/
# 其他包从官方 registry 安装
registry=https://registry.npmmirror.com/

6. 构建与发布体系

详细内容参考 CI/CD 与自动化部署环境管理与配置

6.1 统一构建配置

使用 tsup 统一构建公共包:

packages/utils/tsup.config.ts
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 设计

.github/workflows/ci.yml
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 多环境管理

src/config/env.ts
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 灰度发布策略

src/lib/feature-flag.ts
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 核心实现

packages/monitor-sdk/src/index.ts
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 告警规则配置

packages/monitor-server/src/alert-rules.ts
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 组件库设计原则

packages/ui/src/components/Button/Button.tsx
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 业务组件封装思路

packages/business-ui/src/SearchForm/SearchForm.tsx
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 FederationWebpack/Rspack 模块共享无原生沙箱最好同技术栈、新项目
qiankunsingle-spa + Proxy 沙箱JS + CSS 沙箱多技术栈、已有项目
WujieWeb Components + iframeiframe 天然沙箱强隔离需求
Micro AppWeb ComponentsJS + CSS 沙箱轻量级需求
iframe原生隔离天然沙箱最低简单嵌入

9.3 子应用接入规范

子应用入口规范示例 sub-app/src/main.ts
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 共享依赖与通信

shared/event-bus.ts
// 基于 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 推广策略

  1. 试点验证:选一个新项目完整使用基建工具链,收集反馈
  2. 数据说话:用试点项目的效率提升数据说服团队
  3. 降低门槛:好的基建应该是"无感接入"的,不给开发者增加额外负担
  4. 渐进迁移:老项目不强制一次性切换,提供迁移脚本和文档
  5. 持续迭代:定期收集团队反馈,快速响应问题

10.4 常见踩坑与经验

常见踩坑
  1. 规范过于严格:一开始上了 200+ 条 ESLint 规则,开发者怨声载道。建议从最核心的 20-30 条开始,逐步增加。
  2. 脚手架模板不更新:做完就放那了,半年后模板已经过时。建议模板远程托管,每次拉取最新版。
  3. 组件库闭门造车:不听使用者反馈,组件不好用还强推。建议先收集团队高频需求,再设计 API。
  4. 监控数据不消费:接了监控但没人看数据,等于没接。建议配合告警规则,数据驱动改进。
  5. 一次性投入无后续:基建是持续的事,不是做完就结束。建议安排专人持续维护。

常见面试问题

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 方案:

pnpm-workspace.yaml
packages:
- 'apps/*' # 应用项目
- 'packages/*' # 公共包

公共包组织

职责更新频率
@company/eslint-configESLint 统一配置
@company/tsconfigTypeScript 基础配置
@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 任务编排

turbo.json
{
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
}
}
}

"dependsOn": ["^build"] 表示包 A 的 build 依赖其上游包先 build 完成,Turborepo 会自动推断正确的构建顺序并利用缓存加速。


Q5: 如何设计前端监控告警体系?

答案

详细参考 前端监控与埋点

前端监控体系分为数据采集 → 数据上报 → 数据存储 → 数据消费四个环节:

1. 数据采集

监控类型采集方式关键指标
JS 错误window.onerrorunhandledrejection错误率、错误 Top 10
资源错误error 事件捕获阶段监听资源加载失败率
性能指标PerformanceObserverLCP、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 步走

  1. 工具自动化:ESLint + Prettier + Stylelint,通过工具而非人来检查
  2. Git Hooks 拦截:lint-staged + Husky,提交前自动修复和拦截
  3. CI 门禁:PR 必须通过 lint 检查才能合并
  4. 共享配置包:发布 @company/eslint-config,所有项目一行配置接入

遇到阻力怎么办

阻力类型应对策略
"规则太严格了"从宽松规则开始(只有 warn 没有 error),逐步收紧
"迁移成本太高"提供 --fix 自动修复脚本,一键格式化全部代码
"影响开发效率"只在 commit 阶段检查变更的文件,不影响编码过程
"每个人习惯不同"规范一旦确定,写入工具强制执行,避免人为争论
"老项目无法接入"只对新增/修改的文件检查(lint-staged),不动老代码

核心思路:工具 > 人 > 文档。能用工具自动化的绝不靠人去记,能靠人 Code Review 的不靠文档约束。


Q7: 前端 CI/CD 流水线应该包含哪些环节?

答案

详细参考 CI/CD 与自动化部署

完整的 CI/CD Pipeline 包含以下环节:

环节工具失败行为
Installpnpm install --frozen-lockfile阻断
LintESLint + Stylelint阻断
Type Checktsc --noEmit阻断
Unit TestVitest / Jest阻断
BuildVite / Webpack阻断
E2E TestPlaywright告警(不阻断,或看策略)
Preview DeployVercel / Cloudflare Pages自动生成预览链接
Staging DeployDocker + 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: 什么时候需要引入微前端?如何选型?

答案

详细参考 微前端架构

需要微前端的场景

  1. 多团队独立开发:不同团队负责不同业务模块,需要独立开发、独立部署
  2. 渐进式迁移:老项目从 jQuery/Vue 2 迁移到 React/Vue 3,新旧系统共存
  3. 超大型应用:100+ 页面的巨型单体,构建时间过长

不需要微前端的场景

  • 单一团队、单一技术栈 → Monorepo 即可
  • 项目规模不大(< 30 个页面)→ 代码分割 + 路由懒加载即可
  • 仅仅为了"技术先进" → 过度设计

选型决策树


Q10: 如何设计一个前端灰度发布系统?

答案

灰度发布的核心是让新版本只对部分用户可见,验证无问题后再全量放开。

实现方案

  1. 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;
  1. Nginx 分流方式:在 Nginx 层面根据 Cookie/Header 将流量导向不同版本

  2. CDN 多版本方式:构建产物带版本号,通过后端接口控制返回的 HTML 中引用哪个版本

灰度发布流程

阶段灰度比例持续时间观察指标
内部灰度内部员工1-2 天功能正确性
小流量灰度5-10%1-2 天错误率、性能指标
中流量灰度30-50%1 天业务指标(转化率等)
全量发布100%持续监控

回滚策略:灰度期间如果发现问题,立即将灰度比例设为 0%,切回旧版本。CDN 方案下,回滚就是把 HTML 指向旧版本的静态资源。


Q11: 私有 npm 包管理方案如何选择?

答案

方案部署方式适合团队成本优势
Verdaccio自托管(Docker)中小团队低(免费)部署简单、支持缓存上游 registry
GitHub PackagesSaaS已用 GitHub 的团队与 GitHub Actions 深度集成
GitLab PackagesSaaS/私有已用 GitLab 的团队与 GitLab CI 深度集成
cnpm自托管大型企业功能全面、国内使用多
Nexus自托管已有 Nexus 的企业支持多种语言的包管理

推荐选择

  • 10 人以下团队 → Verdaccio(Docker 一键部署,零成本)
  • 使用 GitHub → GitHub Packages(和 CI/CD 无缝集成)
  • 大型企业 → cnpm / Nexus(功能全面、有专人运维)

Verdaccio 快速部署

docker-compose.yml
version: '3'
services:
verdaccio:
image: verdaccio/verdaccio
ports:
- '4873:4873'
volumes:
- ./storage:/verdaccio/storage
- ./conf:/verdaccio/conf
.npmrc
@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 分钟内回复)
  • 每双周发布 基建周报,展示新增功能和使用数据
  • 定期 基建满意度调查,收集改进意见
  • 表彰 基建贡献者,鼓励团队参与共建

相关链接