📦 一、模块化
1. CommonJS、ES Module、AMD、UMD 的区别?
CommonJS (Node.js)
// 导出
module.exports = { name: 'Alice' };
// 或
exports.name = 'Alice';
// 导入
const user = require('./user');
// 特点:
// 1. 同步加载,适合服务端
// 2. 运行时加载,加载的是值的拷贝
// 3. 可以动态引入:require(condition ? './a' : './b')
ES Module (浏览器标准)
// 导出
export const name = 'Alice';
export default function() {}
// 导入
import user from './user.js';
import { name } from './user.js';
// 特点:
// 1. 异步加载,适合浏览器
// 2. 编译时加载(静态分析),加载的是值的引用
// 3. 支持 Tree Shaking
// 4. 导入是只读的,不能修改
AMD (RequireJS)
// 定义模块
define(['jquery'], function($) {
return { name: 'Alice' };
});
// 使用
require(['./user'], function(user) {
console.log(user.name);
});
// 特点:异步加载,依赖前置,主要用于浏览器
UMD (通用模块)
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD
define(['jquery'], factory);
} else if (typeof module === 'object' && module.exports) {
// CommonJS
module.exports = factory(require('jquery'));
} else {
// 浏览器全局变量
root.MyModule = factory(root.jQuery);
}
}(this, function ($) {
return { name: 'Alice' };
}));
// 特点:兼容 AMD、CommonJS、全局变量
核心区别对比表:
| 特性 | CommonJS | ES Module |
|---|---|---|
| 加载方式 | 运行时同步 | 编译时异步 |
| 输出 | 值的拷贝 | 值的引用 |
| this | 指向当前模块 | undefined |
| 循环依赖 | 加载部分已执行代码 | 报错或undefined |
| Tree Shaking | 不支持 | 支持 |
🔧 二、构建工具
2. Webpack 核心概念和工作原理?
核心概念:
// webpack.config.js
module.exports = {
// 1. Entry:入口
entry: './src/index.js',
// 2. Output:输出
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[contenthash].js'
},
// 3. Loader:转换器
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader'] // 从右往左执行
},
{
test: /\.jsx?$/,
use: 'babel-loader',
exclude: /node_modules/
}
]
},
// 4. Plugin:插件
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html'
}),
new MiniCssExtractPlugin()
],
// 5. Mode:模式
mode: 'production' // development | production
};
工作原理(简化版):
// 1. 初始化参数
const options = require('./webpack.config.js');
// 2. 创建 Compiler 对象
const compiler = new Webpack(options);
// 3. 从 entry 开始递归分析依赖
compiler.run((err, stats) => {
// 4. 调用 loader 转换模块
// 5. 解析依赖关系,构建依赖图
// 6. 根据依赖图生成 chunk
// 7. 输出到文件系统
});
打包流程详解:
1. 初始化阶段
├─ 读取配置文件
├─ 合并 shell 参数
└─ 初始化 compiler
2. 编译阶段
├─ 从 entry 开始
├─ 调用 loader 处理模块
├─ 使用 acorn 解析 AST
├─ 找出依赖模块(import/require)
└─ 递归处理所有依赖
3. 输出阶段
├─ 根据依赖关系组装 chunk
├─ 把 chunk 转换成文件
└─ 输出到 output 目录
3. Loader 和 Plugin 的区别?如何自定义?
区别:
Loader:
- 文件转换器,处理单个文件
- 在 module.rules 中配置
- 本质是一个函数,接收源文件,返回转换后的结果
Plugin:
- 功能扩展器,可以访问整个编译生命周期
- 在 plugins 数组中配置
- 本质是一个类,有 apply 方法
自定义 Loader:
// my-loader.js
module.exports = function(source) {
// source 是文件内容
// this 是 loader 上下文
// 同步 loader
return source.replace(/console\.log/g, '');
// 异步 loader
const callback = this.async();
setTimeout(() => {
callback(null, source.replace(/console\.log/g, ''));
}, 1000);
};
// 使用
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: path.resolve(__dirname, 'my-loader.js')
}
]
}
};
自定义 Plugin:
// my-plugin.js
class MyPlugin {
apply(compiler) {
// compiler 是 webpack 实例
// 在编译开始时执行
compiler.hooks.compile.tap('MyPlugin', (params) => {
console.log('开始编译...');
});
// 在生成资源到 output 目录之前
compiler.hooks.emit.tapAsync('MyPlugin', (compilation, callback) => {
// compilation 包含所有模块信息
// 遍历所有即将输出的文件
for (let filename in compilation.assets) {
let content = compilation.assets[filename].source();
// 修改文件内容
compilation.assets[filename] = {
source: () => content + '\n// Added by MyPlugin',
size: () => content.length
};
}
callback();
});
}
}
module.exports = MyPlugin;
// 使用
plugins: [new MyPlugin()]
4. Webpack 性能优化手段?
构建速度优化:
module.exports = {
// 1. 缩小搜索范围
resolve: {
modules: [path.resolve(__dirname, 'node_modules')], // 指定查找目录
extensions: ['.js', '.json'], // 减少后缀尝试
alias: {
'@': path.resolve(__dirname, 'src')
}
},
module: {
rules: [
{
test: /\.js$/,
use: 'babel-loader',
include: path.resolve(__dirname, 'src'), // 只处理 src
exclude: /node_modules/ // 排除 node_modules
}
]
},
// 2. 多线程打包
module: {
rules: [
{
test: /\.js$/,
use: [
{
loader: 'thread-loader',
options: { workers: 3 }
},
'babel-loader'
]
}
]
},
// 3. 缓存
cache: {
type: 'filesystem', // 使用文件系统缓存
cacheDirectory: path.resolve(__dirname, '.webpack_cache')
},
// 4. DLL 预编译(不常变的库)
// 已被 cache 替代,但了解思想
};
打包体积优化:
module.exports = {
optimization: {
// 1. 代码分割
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: 10
},
common: {
minChunks: 2,
priority: 5,
reuseExistingChunk: true
}
}
},
// 2. Tree Shaking(production 默认开启)
usedExports: true,
// 3. 压缩
minimize: true,
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true // 删除 console
}
}
}),
new CssMinimizerPlugin()
]
},
// 4. 动态导入(懒加载)
// 代码中使用 import() 语法
// 5. 外部扩展(CDN)
externals: {
react: 'React',
'react-dom': 'ReactDOM'
}
};
// 组件懒加载示例
const Home = lazy(() => import('./pages/Home'));
运行时优化:
module.exports = {
output: {
// 1. 文件名哈希(利用缓存)
filename: '[name].[contenthash:8].js',
chunkFilename: '[name].[contenthash:8].chunk.js'
},
// 2. Source Map 优化
devtool: 'source-map', // production
// devtool: 'eval-cheap-module-source-map', // development
optimization: {
// 3. 模块 ID 稳定
moduleIds: 'deterministic',
// 4. Runtime chunk 分离
runtimeChunk: 'single'
}
};
5. Vite 为什么比 Webpack 快?原理是什么?
核心区别:
Webpack(打包器):
├─ 开发环境:先打包所有模块 → 启动服务器
├─ 修改文件:重新打包相关模块 → HMR
└─ 冷启动慢(大项目 10s+)
Vite(无打包开发服务器):
├─ 开发环境:直接启动服务器 → 按需编译
├─ 修改文件:只重新编译单个模块 → HMR
└─ 冷启动快(1-2s)
Vite 原理:
// 1. 开发环境利用浏览器原生 ES Module
// index.html
<script type="module" src="/src/main.js"></script>
// 2. 浏览器请求 main.js
// GET /src/main.js
// 3. Vite 拦截请求,实时编译
import { createApp } from 'vue'; // 浏览器不认识裸模块
// ↓ Vite 转换
import { createApp } from '/@modules/vue.js'; // 指向 node_modules
// 4. 预构建依赖(首次)
// Vite 用 esbuild 预构建 node_modules
// vue、react 等打包成单文件,避免请求瀑布
// 5. HMR 原理
// WebSocket 通信,模块热替换
if (import.meta.hot) {
import.meta.hot.accept((newModule) => {
// 只更新当前模块
});
}
对比总结:
// Webpack 开发流程
entry → 递归解析 → loader 转换 → 打包成 bundle → 启动服务 (慢)
// Vite 开发流程
启动服务 (快) → 浏览器请求 → 实时编译单个文件 → 返回
// 生产环境
Vite 用 Rollup 打包(和 Webpack 类似)
🎯 三、代码质量
6. ESLint、Prettier、Husky、lint-staged 的作用和配置?
完整配置流程:
// package.json
{
"scripts": {
"lint": "eslint src --ext .js,.jsx,.ts,.tsx",
"lint:fix": "eslint src --ext .js,.jsx,.ts,.tsx --fix",
"format": "prettier --write \"src/**/*.{js,jsx,ts,tsx,json,css,md}\"",
"prepare": "husky install"
},
"devDependencies": {
"eslint": "^8.0.0",
"prettier": "^2.8.0",
"husky": "^8.0.0",
"lint-staged": "^13.0.0"
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"eslint --fix",
"prettier --write"
],
"*.{json,css,md}": [
"prettier --write"
]
}
}
ESLint 配置:
// .eslintrc.js
module.exports = {
env: {
browser: true,
es2021: true,
node: true
},
extends: [
'eslint:recommended',
'plugin:react/recommended',
'plugin:@typescript-eslint/recommended',
'prettier' // 必须放最后,关闭 ESLint 中与 Prettier 冲突的规则
],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
ecmaFeatures: {
jsx: true
}
},
plugins: ['react', '@typescript-eslint'],
rules: {
'no-console': 'warn',
'no-unused-vars': 'error',
'react/prop-types': 'off'
}
};
Prettier 配置:
// .prettierrc.js
module.exports = {
semi: true, // 分号
singleQuote: true, // 单引号
tabWidth: 2, // 缩进
trailingComma: 'es5', // 尾逗号
printWidth: 80, // 每行宽度
arrowParens: 'avoid' // 箭头函数参数括号
};
Husky + lint-staged:
# 1. 安装
npm install husky lint-staged -D
# 2. 初始化 husky
npx husky install
# 3. 添加 pre-commit hook
npx husky add .husky/pre-commit "npx lint-staged"
# 4. 添加 commit-msg hook (可选,配合 commitlint)
npx husky add .husky/commit-msg 'npx --no -- commitlint --edit $1'
// .husky/pre-commit
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx lint-staged
Commitlint 配置(可选):
// commitlint.config.js
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [
2,
'always',
['feat', 'fix', 'docs', 'style', 'refactor', 'test', 'chore']
],
'subject-case': [0]
}
};
// 提交格式:
// feat: 新功能
// fix: 修复 bug
// docs: 文档修改
// style: 代码格式(不影响功能)
// refactor: 重构
// test: 测试
// chore: 构建/工具
工作流程:
1. 开发者修改代码
2. git add .
3. git commit -m "feat: 添加登录功能"
↓
4. Husky 触发 pre-commit hook
↓
5. lint-staged 只检查暂存区文件
├─ 运行 eslint --fix
├─ 运行 prettier --write
└─ 自动修复并重新 add
↓
6. Husky 触发 commit-msg hook
└─ commitlint 检查提交信息格式
↓
7. 通过则提交成功,否则中断
🔄 四、CI/CD
7. 前端 CI/CD 流程和工具?
完整流程:
代码提交 → 自动化测试 → 构建打包 → 部署上线 → 监控回滚
GitHub Actions 示例:
# .github/workflows/deploy.yml
name: Deploy to Production
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
# 1. 测试和构建
build:
runs-on: ubuntu-latest
steps:
- name: Checkout 代码
uses: actions/checkout@v3
- name: 设置 Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: 安装依赖
run: npm ci
- name: 代码检查
run: npm run lint
- name: 单元测试
run: npm run test
- name: 构建
run: npm run build
- name: 上传构建产物
uses: actions/upload-artifact@v3
with:
name: dist
path: dist/
# 2. 部署到服务器
deploy:
needs: build
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- name: 下载构建产物
uses: actions/download-artifact@v3
with:
name: dist
- name: 部署到阿里云 OSS
uses: fangbinwei/aliyun-oss-website-action@v1
with:
accessKeyId: ${{ secrets.ACCESS_KEY_ID }}
accessKeySecret: ${{ secrets.ACCESS_KEY_SECRET }}
bucket: my-bucket
endpoint: oss-cn-hangzhou.aliyuncs.com
folder: dist/
- name: 清除 CDN 缓存
run: |
curl -X POST https://api.cdn.com/purge \
-H "Authorization: Bearer ${{ secrets.CDN_TOKEN }}"
GitLab CI 示例:
# .gitlab-ci.yml
stages:
- test
- build
- deploy
variables:
NODE_VERSION: "18"
# 缓存依赖
cache:
paths:
- node_modules/
# 测试阶段
test:
stage: test
image: node:$NODE_VERSION
script:
- npm ci
- npm run lint
- npm run test:unit
- npm run test:e2e
coverage: '/Statements\s+:\s+(\d+\.\d+)%/'
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
# 构建阶段
build:
stage: build
image: node:$NODE_VERSION
script:
- npm ci
- npm run build
artifacts:
paths:
- dist/
expire_in: 1 week
only:
- main
- develop
# 部署到测试环境
deploy:test:
stage: deploy
script:
- scp -r dist/* user@test-server:/var/www/html/
environment:
name: test
url: https://test.example.com
only:
- develop
# 部署到生产环境
deploy:prod:
stage: deploy
script:
- scp -r dist/* user@prod-server:/var/www/html/
- ssh user@prod-server "pm2 reload app"
environment:
name: production
url: https://www.example.com
when: manual # 手动触发
only:
- main
Docker 部署:
# Dockerfile
# 多阶段构建
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# 生产镜像
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
# docker-compose.yml
version: '3.8'
services:
web:
build: .
ports:
- "80:80"
environment:
- NODE_ENV=production
restart: always
部署策略:
// 1. 蓝绿部署
// 维护两套环境,快速切换
// 2. 金丝雀发布(灰度发布)
// Nginx 配置示例
upstream backend {
server v1.example.com weight=9; # 90% 流量到旧版本
server v2.example.com weight=1; # 10% 流量到新版本
}
// 3. 滚动更新
// Kubernetes 示例
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1 # 最多 1 个不可用
maxSurge: 1 # 最多超出 1 个
📊 五、性能监控
8. 前端监控体系如何搭建?
监控分类:
// 1. 性能监控
const performanceData = {
// FP: First Paint
FP: performance.getEntriesByType('paint')[0].startTime,
// FCP: First Contentful Paint
FCP: performance.getEntriesByType('paint')[1].startTime,
// LCP: Largest Contentful Paint
LCP: new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
console.log('LCP:', lastEntry.renderTime || lastEntry.loadTime);
}).observe({ entryTypes: ['largest-contentful-paint'] }),
// FID: First Input Delay
FID: new PerformanceObserver((list) => {
list.getEntries().forEach(entry => {
console.log('FID:', entry.processingStart - entry.startTime);
});
}).observe({ entryTypes: ['first-input'] }),
// CLS: Cumulative Layout Shift
CLS: new PerformanceObserver((list) => {
let cls = 0;
list.getEntries().forEach(entry => {
if (!entry.hadRecentInput) {
cls += entry.value;
}
});
console.log('CLS:', cls);
}).observe({ entryTypes: ['layout-shift'] }),
// TTFB: Time to First Byte
TTFB: performance.timing.responseStart - performance.timing.requestStart,
// 页面加载总时间
loadTime: performance.timing.loadEventEnd - performance.timing.navigationStart
};
// 上报数据
function reportPerformance(data) {
navigator.sendBeacon('/api/monitor/performance', JSON.stringify(data));
}
错误监控:
// 2. JS 错误监控
window.addEventListener('error', (event) => {
const errorInfo = {
type: 'jsError',
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
stack: event.error?.stack,
userAgent: navigator.userAgent,
url: window.location.href,
timestamp: Date.now()
};
reportError(errorInfo);
}, true);
// Promise 错误
window.addEventListener('unhandledrejection', (event) => {
const errorInfo = {
type: 'promiseError',
message: event.reason?.message || event.reason,
stack: event.reason?.stack,
timestamp: Date.now()
};
reportError(errorInfo);
});
// 资源加载错误
window.addEventListener('error', (event) => {
if (event.target !== window) {
const errorInfo = {
type: 'resourceError',
tagName: event.target.tagName,
src: event.target.src || event.target.href,
timestamp: Date.now()
};
reportError(errorInfo);
}
}, true);
// React 错误边界
class ErrorBoundary extends React.Component {
componentDidCatch(error, errorInfo) {
reportError({
type: 'reactError',
message: error.message,
stack: error.stack,
componentStack: errorInfo.componentStack
});
}
render() {
return this.props.children;
}
}
用户行为监控:
// 3. PV/UV 统计
function trackPageView() {
const data = {
type: 'pv',
url: window.location.href,
referrer: document.referrer,
userId: getUserId(),
timestamp: Date.now()
};
reportBehavior(data);
}
// 页面停留时间
let startTime = Date.now();
window.addEventListener('beforeunload', () => {
const duration = Date.now() - startTime;
navigator.sendBeacon('/api/monitor/duration', JSON.stringify({
url: window.location.href,
duration
}));
});
// 点击事件埋点
document.addEventListener('click', (event) => {
const target = event.target;
const dataTrack = target.getAttribute('data-track');
if (dataTrack) {
reportBehavior({
type: 'click',
element: dataTrack,
timestamp: Date.now()
});
}
});
// 自定义埋点
function trackEvent(eventName, params) {
reportBehavior({
type: 'customEvent',
eventName,
params,
timestamp: Date.now()
});
}
// 使用
trackEvent('购买商品', { productId: '123', price: 99 });
接口监控:
// 4. 封装 fetch/axios
const originalFetch = window.fetch;
window.fetch = function(...args) {
const startTime = Date.now();
const url = args[0];
return originalFetch.apply(this, args)
.then(response => {
const duration = Date.now() - startTime;
reportAPI({
url,
method: args[1]?.method || 'GET',
status: response.status,
duration,
success: response.ok
});
return response;
})
.catch(error => {
reportAPI({
url,
method: args[1]?.method || 'GET',
error: error.message,
success: false
});
throw error;
});
};
上报策略:
// 批量上报(节流)
class Monitor {
constructor() {
this.queue = [];
this.timer = null;
}
// 添加到队列
addLog(data) {
this.queue.push(data);
// 超过 10 条立即上报
if (this.queue.length >= 10) {
this.flush();
return;
}
// 否则 3 秒后上报
if (!this.timer) {
this.timer = setTimeout(() => {
this.flush();
}, 3000);
}
}
// 上报
flush() {
if (this.queue.length === 0) return;
// 使用 sendBeacon 保证数据发送(页面卸载时也能发)
navigator.sendBeacon(
'/api/monitor/log',
JSON.stringify(this.queue)
);
this.queue = [];
this.timer = null;
}
}
const monitor = new Monitor();
// 页面卸载时上报剩余数据
window.addEventListener('beforeunload', () => {
monitor.flush();
});
完整监控 SDK:
class MonitorSDK {
constructor(options) {
this.appId = options.appId;
this.apiUrl = options.apiUrl;
this.userId = this.getUserId();
this.initPerformance();
this.initError();
this.initBehavior();
}
// 生成用户 ID
getUserId() {
let userId = localStorage.getItem('monitor_user_id');
if (!userId) {
userId = `user_${Date.now()}_${Math.random()}`;
localStorage.setItem('monitor_user_id', userId);
}
return userId;
}
// 初始化性能监控
initPerformance() {
if (window.PerformanceObserver) {
// 监控 LCP
new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
this.report({
type: 'performance',
metric: 'LCP',
value: lastEntry.renderTime || lastEntry.loadTime
});
}).observe({ entryTypes: ['largest-contentful-paint'] });
}
}
// 初始化错误监控
initError() {
window.addEventListener('error', (event) => {
this.report({
type: 'error',
subType: 'jsError',
message: event.message,
stack: event.error?.stack
});
}, true);
}
// 初始化行为监控
initBehavior() {
// PV
this.report({
type: 'behavior',
subType: 'pv',
url: window.location.href
});
}
// 上报
report(data) {
const logData = {
...data,
appId: this.appId,
userId: this.userId,
timestamp: Date.now(),
userAgent: navigator.userAgent,
url: window.location.href
};
navigator.sendBeacon(this.apiUrl, JSON.stringify(logData));
}
}
// 使用
const monitor = new MonitorSDK({
appId: 'my-app',
apiUrl: 'https://api.example.com/monitor'
});
🧪 六、测试
9. 前端测试的分类和工具?
测试金字塔:
/\
/E2E\ 端到端测试 (少量)
/------\
/集成测试 \ 组件集成 (适量)
/----------\
/ 单元测试 \ 函数/组件 (大量)
/--------------\
单元测试(Jest + Testing Library):
// utils.js
export function add(a, b) {
return a + b;
}
export function formatPrice(price) {
return `¥${price.toFixed(2)}`;
}
// utils.test.js
import { add, formatPrice } from './utils';
describe('工具函数测试', () => {
test('add 函数', () => {
expect(add(1, 2)).toBe(3);
expect(add(-1, 1)).toBe(0);
});
test('formatPrice 函数', () => {
expect(formatPrice(100)).toBe('¥100.00');
expect(formatPrice(99.9)).toBe('¥99.90');
});
});
// Button.jsx
function Button({ onClick, children }) {
return <button onClick={onClick}>{children}</button>;
}
// Button.test.jsx
import { render, screen, fireEvent } from '@testing-library/react';
import Button from './Button';
describe('Button 组件', () => {
test('渲染正确的文本', () => {
render(<Button>点击我</Button>);
expect(screen.getByText('点击我')).toBeInTheDocument();
});
test('点击事件触发', () => {
const handleClick = jest.fn();
render(<Button onClick={handleClick}>点击</Button>);
fireEvent.click(screen.getByText('点击'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
});
集成测试:
// TodoList.test.jsx
import { render, screen, fireEvent } from '@testing-library/react';
import TodoList from './TodoList';
test('添加和删除待办事项', () => {
render(<TodoList />);
// 添加待办
const input = screen.getByPlaceholderText('输入待办事项');
const addButton = screen.getByText('添加');
fireEvent.change(input, { target: { value: '买菜' } });
fireEvent.click(addButton);
expect(screen.getByText('买菜')).toBeInTheDocument();
// 删除待办
const deleteButton = screen.getByText('删除');
fireEvent.click(deleteButton);
expect(screen.queryByText('买菜')).not.toBeInTheDocument();
});
E2E 测试(Cypress):
// cypress/e2e/login.cy.js
describe('登录流程', () => {
beforeEach(() => {
cy.visit('http://localhost:3000/login');
});
it('成功登录', () => {
// 输入用户名和密码
cy.get('input[name="username"]').type('admin');
cy.get('input[name="password"]').type('123456');
// 点击登录按钮
cy.get('button[type="submit"]').click();
// 验证跳转到首页
cy.url().should('include', '/home');
cy.contains('欢迎回来').should('be.visible');
});
it('登录失败提示', () => {
cy.get('input[name="username"]').type('wrong');
cy.get('input[name="password"]').type('wrong');
cy.get('button[type="submit"]').click();
cy.contains('用户名或密码错误').should('be.visible');
});
});
覆盖率配置:
// jest.config.js
module.exports = {
collectCoverage: true,
collectCoverageFrom: [
'src/**/*.{js,jsx,ts,tsx}',
'!src/**/*.test.{js,jsx}',
'!src/index.js'
],
coverageThreshold: {
global: {
statements: 80,
branches: 80,
functions: 80,
lines: 80
}
}
};
🎁 七、综合实战题
10. 从0到1搭建一个完整的前端工程?
完整清单:
# 1. 初始化项目
npm create vite@latest my-project -- --template react-ts
cd my-project
# 2. 安装依赖
npm install
npm install -D eslint prettier husky lint-staged @commitlint/cli @commitlint/config-conventional
# 3. 配置代码规范
npm init @eslint/config
echo "module.exports = { semi: true, singleQuote: true };" > .prettierrc.js
# 4. 配置 Git Hooks
npx husky install
npx husky add .husky/pre-commit "npx lint-staged"
npx husky add .husky/commit-msg 'npx --no -- commitlint --edit $1'
# 5. 配置环境变量
echo "VITE_API_URL=http://localhost:3000" > .env.development
echo "VITE_API_URL=https://api.example.com" > .env.production
# 6. 目录结构
mkdir -p src/{components,pages,utils,hooks,store,styles,types,api}
# 7. 配置路径别名
# vite.config.ts 中添加 resolve.alias
# 8. 配置 CI/CD
mkdir .github/workflows
# 创建 deploy.yml
# 9. 配置监控
# 引入 Sentry 或自建监控
# 10. 文档
echo "# Project Documentation" > README.md
目录结构:
my-project/
├── .github/
│ └── workflows/
│ └── deploy.yml
├── .husky/
│ ├── pre-commit
│ └── commit-msg
├── public/
├── src/
│ ├── api/ # API 请求
│ ├── components/ # 通用组件
│ ├── hooks/ # 自定义 Hooks
│ ├── pages/ # 页面组件
│ ├── store/ # 状态管理
│ ├── styles/ # 全局样式
│ ├── types/ # TS 类型定义
│ ├── utils/ # 工具函数
│ ├── App.tsx
│ └── main.tsx
├── .env.development
├── .env.production
├── .eslintrc.js
├── .prettierrc.js
├── commitlint.config.js
├── tsconfig.json
├── vite.config.ts
└── package.json