欢迎使用我的小程序👇👇👇👇👇
引言
在前几篇文章中,我们学习了JavaScript的基础知识、ES6特性、数组对象操作、异步编程以及错误处理。本文将继续深入,探讨现代JavaScript开发中至关重要的模块化与工程化实践,帮助你构建可维护、可扩展的大型应用。
一、JavaScript模块化演进史
1.1 模块化前夜:全局变量污染
// 原始方式:全局命名空间污染
var userData = {
name: 'John',
age: 25
};
function getUserName() {
return userData.name;
}
function updateUser(updates) {
for (var key in updates) {
userData[key] = updates[key];
}
}
// 问题:所有变量和函数都在全局作用域
// 容易造成命名冲突、难以维护
1.2 立即执行函数表达式(IIFE)
// IIFE模式:创建私有作用域
var UserModule = (function() {
// 私有变量
var userData = {
name: 'John',
age: 25
};
// 私有方法
function validateUser(data) {
return data.name && data.age > 0;
}
// 公共API
return {
getUserName: function() {
return userData.name;
},
updateUser: function(updates) {
if (validateUser(updates)) {
for (var key in updates) {
userData[key] = updates[key];
}
return true;
}
return false;
},
getUserInfo: function() {
return { ...userData };
}
};
})();
// 使用
console.log(UserModule.getUserName()); // John
UserModule.updateUser({ age: 26 });
1.3 CommonJS(Node.js)
// math.js
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
// 导出
module.exports = {
add,
subtract
};
// 或者
exports.add = add;
exports.subtract = subtract;
// app.js
// 导入
const math = require('./math.js');
console.log(math.add(5, 3)); // 8
// 或者解构导入
const { add, subtract } = require('./math.js');
1.4 AMD(异步模块定义)
// 使用RequireJS
// math.js
define(['dependency'], function(dependency) {
// 模块代码
function add(a, b) {
return a + b;
}
return {
add: add
};
});
// app.js
require(['math'], function(math) {
console.log(math.add(2, 3)); // 5
});
二、ES6模块系统
2.1 基本语法
// math.js - 命名导出
export const PI = 3.14159;
export function add(a, b) {
return a + b;
}
export function multiply(a, b) {
return a * b;
}
// 或者统一导出
const PI = 3.14159;
const add = (a, b) => a + b;
const multiply = (a, b) => a * b;
export { PI, add, multiply };
// user.js - 默认导出
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
getInfo() {
return `${this.name} <${this.email}>`;
}
}
export default User;
// 或者默认导出函数
export default function createUser(name, email) {
return {
name,
email,
createdAt: new Date()
};
}
2.2 导入方式
// app.js - 各种导入方式
// 1. 命名导入
import { add, multiply, PI } from './math.js';
console.log(add(2, 3)); // 5
console.log(PI); // 3.14159
// 2. 重命名导入
import { add as addNumbers, multiply as times } from './math.js';
// 3. 导入全部
import * as MathUtils from './math.js';
console.log(MathUtils.add(1, 2));
// 4. 默认导入
import User from './user.js';
const user = new User('John', 'john@example.com');
// 或者重命名默认导入
import { default as UserClass } from './user.js';
// 5. 混合导入
import User, { PI } from './user.js'; // 如果user.js有默认导出和命名导出
// 6. 动态导入(按需加载)
async function loadModule() {
const module = await import('./math.js');
console.log(module.add(5, 3));
}
button.addEventListener('click', () => {
loadModule();
});
// 7. 内联导入(Web Worker、Service Worker等)
if (typeof window !== 'undefined') {
import('./browser-module.js').then(module => {
// 浏览器特有代码
});
} else {
import('./node-module.js').then(module => {
// Node.js特有代码
});
}
2.3 模块特性详解
// 1. 静态分析特性
// ES6模块在编译时确定依赖关系,支持静态优化
// 2. 严格模式
// 模块默认在严格模式下运行,无需'use strict'
// 3. 顶级作用域
// 每个模块有自己的顶级作用域,变量不会污染全局
// 4. 值引用 vs 值拷贝
// counter.js
export let counter = 0;
export function increment() {
counter++;
}
// app.js
import { counter, increment } from './counter.js';
console.log(counter); // 0
increment();
console.log(counter); // 1 - 直接引用,值已更新
// 5. 循环依赖处理
// a.js
import { b } from './b.js';
export const a = 'a';
console.log('模块a中b的值:', b);
// b.js
import { a } from './a.js';
export const b = 'b';
console.log('模块b中a的值:', a); // 注意:此时a可能是undefined
三、现代打包工具
3.1 Webpack基础配置
// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
// 入口文件
entry: {
main: './src/index.js',
vendor: './src/vendor.js'
},
// 输出配置
output: {
filename: '[name].[contenthash].js',
path: path.resolve(__dirname, 'dist'),
publicPath: '/',
chunkFilename: '[name].[contenthash].chunk.js'
},
// 开发模式
mode: process.env.NODE_ENV === 'production' ? 'production' : 'development',
// Source Map配置
devtool: process.env.NODE_ENV === 'production'
? 'source-map'
: 'cheap-module-eval-source-map',
// 开发服务器
devServer: {
contentBase: './dist',
hot: true,
port: 3000,
historyApiFallback: true,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true
}
}
},
// 模块解析
resolve: {
extensions: ['.js', '.jsx', '.ts', '.tsx'],
alias: {
'@': path.resolve(__dirname, 'src/'),
'@components': path.resolve(__dirname, 'src/components/'),
'@utils': path.resolve(__dirname, 'src/utils/')
}
},
// 模块处理规则
module: {
rules: [
// JavaScript/TypeScript
{
test: /\.(js|jsx|ts|tsx)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: [
'@babel/preset-env',
'@babel/preset-react',
'@babel/preset-typescript'
],
plugins: [
'@babel/plugin-transform-runtime',
'@babel/plugin-proposal-class-properties',
'babel-plugin-lodash' // 按需加载lodash
]
}
}
},
// 样式文件
{
test: /\.css$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
modules: {
auto: true,
localIdentName: '[name]__[local]--[hash:base64:5]'
}
}
},
'postcss-loader'
]
},
// Sass/SCSS
{
test: /\.scss$/,
use: [
'style-loader',
'css-loader',
'postcss-loader',
'sass-loader'
]
},
// 图片资源
{
test: /\.(png|jpg|jpeg|gif|svg)$/,
use: [
{
loader: 'url-loader',
options: {
limit: 8192, // 小于8KB转为base64
name: 'assets/images/[name].[hash:8].[ext]'
}
},
{
loader: 'image-webpack-loader',
options: {
mozjpeg: {
progressive: true,
quality: 65
},
optipng: {
enabled: false
},
pngquant: {
quality: [0.65, 0.90],
speed: 4
},
gifsicle: {
interlaced: false
}
}
}
]
},
// 字体文件
{
test: /\.(woff|woff2|eot|ttf|otf)$/,
use: [
{
loader: 'file-loader',
options: {
name: 'assets/fonts/[name].[hash:8].[ext]'
}
}
]
}
]
},
// 插件
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template: './public/index.html',
favicon: './public/favicon.ico',
minify: process.env.NODE_ENV === 'production' ? {
removeComments: true,
collapseWhitespace: true,
removeRedundantAttributes: true,
useShortDoctype: true,
removeEmptyAttributes: true,
removeStyleLinkTypeAttributes: true,
keepClosingSlash: true,
minifyJS: true,
minifyCSS: true,
minifyURLs: true
} : false
})
],
// 优化配置
optimization: {
moduleIds: 'deterministic',
runtimeChunk: 'single',
splitChunks: {
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all'
},
common: {
name: 'common',
minChunks: 2,
chunks: 'all',
priority: 10,
reuseExistingChunk: true,
enforce: true
}
}
},
minimizer: [
// 使用TerserPlugin压缩JavaScript
// 使用CssMinimizerPlugin压缩CSS
]
},
// 性能提示
performance: {
hints: process.env.NODE_ENV === 'production' ? 'warning' : false,
maxAssetSize: 250000,
maxEntrypointSize: 250000
}
};
3.2 高级Webpack配置技巧
// webpack.config.advanced.js
const webpack = require('webpack');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
// 多环境配置
const configs = {
development: {
mode: 'development',
devtool: 'cheap-module-source-map'
},
production: {
mode: 'production',
devtool: 'source-map',
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
parallel: true,
terserOptions: {
compress: {
drop_console: true, // 移除console
drop_debugger: true
},
output: {
comments: false // 移除注释
}
},
extractComments: false
}),
new CssMinimizerPlugin()
]
}
},
staging: {
mode: 'production',
devtool: 'source-map',
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: false, // 测试环境保留console
drop_debugger: true
}
}
})
]
}
}
};
// 根据环境选择配置
const environment = process.env.NODE_ENV || 'development';
const envConfig = configs[environment];
module.exports = {
...envConfig,
plugins: [
// 环境变量注入
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(environment),
'process.env.API_URL': JSON.stringify(process.env.API_URL || 'http://localhost:3000')
}),
// 进度条
new webpack.ProgressPlugin(),
// 提取CSS到单独文件(生产环境)
...(environment === 'production' ? [
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash].css',
chunkFilename: 'css/[name].[contenthash].chunk.css'
})
] : []),
// 打包分析(需要时启用)
...(process.env.ANALYZE ? [
new BundleAnalyzerPlugin({
analyzerMode: 'static',
reportFilename: 'bundle-report.html',
openAnalyzer: false
})
] : []),
// 忽略无用的moment.js语言包
new webpack.IgnorePlugin({
resourceRegExp: /^\.\/locale$/,
contextRegExp: /moment$/
})
],
// 缓存配置(提升构建速度)
cache: {
type: 'filesystem',
buildDependencies: {
config: [__filename]
}
},
// 性能优化
performance: {
maxAssetSize: 512000,
maxEntrypointSize: 512000,
hints: environment === 'production' ? 'warning' : false
},
// 排除某些依赖
externals: {
// 从CDN引入的库
react: 'React',
'react-dom': 'ReactDOM',
lodash: '_'
},
// 实验性功能
experiments: {
topLevelAwait: true, // 支持顶层await
lazyCompilation: environment === 'development' // 开发环境懒编译
}
};
3.3 Vite配置示例
// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { visualizer } from 'rollup-plugin-visualizer';
// https://vitejs.dev/config/
export default defineConfig(({ mode }) => ({
// 基础路径
base: mode === 'production' ? '/your-base-path/' : '/',
// 插件
plugins: [
react({
// React刷新
fastRefresh: true,
// JSX运行时
jsxRuntime: 'automatic'
}),
// 打包分析
mode === 'analyze' && visualizer({
open: true,
gzipSize: true,
brotliSize: true
})
].filter(Boolean),
// 解析配置
resolve: {
alias: {
'@': '/src',
'@components': '/src/components',
'@assets': '/src/assets'
}
},
// 开发服务器
server: {
port: 3000,
open: true,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
},
hmr: {
overlay: true
}
},
// 构建配置
build: {
outDir: 'dist',
assetsDir: 'assets',
sourcemap: mode !== 'production',
minify: mode === 'production' ? 'terser' : false,
terserOptions: mode === 'production' ? {
compress: {
drop_console: true,
drop_debugger: true
}
} : undefined,
rollupOptions: {
output: {
manualChunks(id) {
// 将node_modules中的包分块
if (id.includes('node_modules')) {
if (id.includes('react') || id.includes('react-dom')) {
return 'react-vendor';
}
if (id.includes('lodash')) {
return 'lodash';
}
return 'vendor';
}
},
chunkFileNames: 'assets/js/[name]-[hash].js',
entryFileNames: 'assets/js/[name]-[hash].js',
assetFileNames: ({ name }) => {
if (/\.(gif|jpe?g|png|svg)$/.test(name ?? '')) {
return 'assets/images/[name]-[hash][extname]';
}
if (/\.css$/.test(name ?? '')) {
return 'assets/css/[name]-[hash][extname]';
}
return 'assets/[name]-[hash][extname]';
}
}
},
// 分块大小警告阈值
chunkSizeWarningLimit: 1000
},
// 预览服务器
preview: {
port: 3001,
open: true
},
// CSS配置
css: {
modules: {
localsConvention: 'camelCaseOnly',
generateScopedName: mode === 'production'
? '[hash:base64:8]'
: '[name]__[local]--[hash:base64:5]'
},
preprocessorOptions: {
scss: {
additionalData: `@import "./src/styles/variables.scss";`
}
}
},
// 环境变量
define: {
__APP_VERSION__: JSON.stringify(process.env.npm_package_version),
__BUILD_TIME__: JSON.stringify(new Date().toISOString())
}
}));
四、代码质量工具
4.1 ESLint配置
// .eslintrc.js
module.exports = {
// 解析器配置
parser: '@babel/eslint-parser',
parserOptions: {
ecmaVersion: 2022,
sourceType: 'module',
ecmaFeatures: {
jsx: true
},
requireConfigFile: false,
babelOptions: {
presets: ['@babel/preset-env', '@babel/preset-react']
}
},
// 运行环境
env: {
browser: true,
node: true,
es2022: true,
jest: true
},
// 扩展规则集
extends: [
'eslint:recommended',
'plugin:react/recommended',
'plugin:react-hooks/recommended',
'plugin:jsx-a11y/recommended',
'plugin:import/recommended',
'plugin:prettier/recommended'
],
// 插件
plugins: [
'react',
'react-hooks',
'jsx-a11y',
'import',
'prettier'
],
// 自定义规则
rules: {
// 代码风格
'prettier/prettier': 'error',
// 最佳实践
'no-console': ['warn', { allow: ['warn', 'error', 'info'] }],
'no-debugger': 'warn',
'no-alert': 'warn',
'no-var': 'error',
'prefer-const': 'error',
'prefer-template': 'error',
'object-shorthand': 'error',
// import/export
'import/order': [
'error',
{
groups: [
'builtin',
'external',
'internal',
'parent',
'sibling',
'index'
],
'newlines-between': 'always',
alphabetize: {
order: 'asc',
caseInsensitive: true
}
}
],
'import/no-unresolved': 'error',
'import/named': 'error',
'import/default': 'error',
'import/export': 'error',
// React
'react/prop-types': 'off', // TypeScript项目中可以关闭
'react/react-in-jsx-scope': 'off', // React 17+不需要
'react/jsx-uses-react': 'off',
'react/display-name': 'off',
'react/jsx-key': 'error',
'react/jsx-no-target-blank': 'error',
// React Hooks
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn',
// 可访问性
'jsx-a11y/anchor-is-valid': [
'error',
{
components: ['Link'],
specialLink: ['hrefLeft', 'hrefRight'],
aspects: ['invalidHref', 'preferButton']
}
]
},
// 覆盖特定文件的规则
overrides: [
{
files: ['*.ts', '*.tsx'],
parser: '@typescript-eslint/parser',
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/recommended-requiring-type-checking'
],
parserOptions: {
project: ['./tsconfig.json']
},
rules: {
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'@typescript-eslint/no-non-null-assertion': 'warn'
}
},
{
files: ['*.test.js', '*.test.jsx', '*.test.ts', '*.test.tsx'],
env: {
jest: true
},
rules: {
'import/no-extraneous-dependencies': 'off'
}
},
{
files: ['*.config.js', '*.config.ts'],
env: {
node: true
},
rules: {
'import/no-extraneous-dependencies': 'off'
}
}
],
// 设置
settings: {
react: {
version: 'detect' // 自动检测React版本
},
'import/resolver': {
node: {
extensions: ['.js', '.jsx', '.ts', '.tsx'],
moduleDirectory: ['node_modules', 'src/']
},
typescript: {
alwaysTryTypes: true
}
}
}
};
4.2 Prettier配置
// .prettierrc.js
module.exports = {
// 每行最大字符数
printWidth: 100,
// 缩进空格数
tabWidth: 2,
// 使用制表符而不是空格缩进
useTabs: false,
// 语句末尾是否添加分号
semi: true,
// 使用单引号
singleQuote: true,
// 对象属性引号:as-needed(仅在需要时添加)
quoteProps: 'as-needed',
// JSX中使用单引号
jsxSingleQuote: false,
// 尾随逗号
trailingComma: 'es5',
// 对象花括号内的空格
bracketSpacing: true,
// JSX标签的'>'换行
jsxBracketSameLine: false,
// 箭头函数参数括号:avoid(能省略时省略)
arrowParens: 'avoid',
// 换行符:lf(Linux/macOS风格)
endOfLine: 'lf',
// HTML空白敏感度
htmlWhitespaceSensitivity: 'css',
// Vue文件脚本和样式标签缩进
vueIndentScriptAndStyle: false,
// 格式化嵌入的代码
embeddedLanguageFormatting: 'auto',
// 超过printWidth时是否折行
proseWrap: 'preserve'
};
// package.json中的脚本配置
/*
{
"scripts": {
"format": "prettier --write \"src/**/*.{js,jsx,ts,tsx,css,md,json}\"",
"format:check": "prettier --check \"src/**/*.{js,jsx,ts,tsx,css,md,json}\"",
"lint": "eslint \"src/**/*.{js,jsx,ts,tsx}\"",
"lint:fix": "eslint \"src/**/*.{js,jsx,ts,tsx}\" --fix"
}
}
*/
4.3 TypeScript配置
// tsconfig.json
{
"compilerOptions": {
/* 基本选项 */
"target": "ES2022", // 编译目标
"lib": ["DOM", "DOM.Iterable", "ES2022"], // 使用的库
"module": "ESNext", // 模块系统
"moduleResolution": "node", // 模块解析策略
"baseUrl": ".", // 解析非相对模块的基础路径
"paths": { // 路径映射
"@/*": ["src/*"],
"@components/*": ["src/components/*"],
"@utils/*": ["src/utils/*"]
},
/* 严格类型检查 */
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"alwaysStrict": true,
/* 额外检查 */
"noUnusedLocals": true, // 未使用的局部变量报错
"noUnusedParameters": true, // 未使用的参数报错
"noImplicitReturns": true, // 不是所有路径都返回值时报错
"noFallthroughCasesInSwitch": true, // switch语句中缺少break报错
/* 模块解析 */
"allowSyntheticDefaultImports": true, // 允许从没有默认导出的模块进行默认导入
"esModuleInterop": true, // 启用ES模块互操作性
"resolveJsonModule": true, // 允许导入JSON模块
/* 源代码映射 */
"sourceMap": true,
"declaration": true, // 生成.d.ts声明文件
"declarationMap": true, // 为声明文件生成sourcemap
"inlineSources": true, // 将源代码包含在sourcemap中
"declarationDir": "./dist/types", // 声明文件输出目录
/* 实验性特性 */
"experimentalDecorators": true, // 启用装饰器
"emitDecoratorMetadata": true, // 为装饰器发出元数据
/* 高级选项 */
"skipLibCheck": true, // 跳过库文件的类型检查
"forceConsistentCasingInFileNames": true, // 强制文件名大小写一致
"allowJs": true, // 允许编译JavaScript文件
"checkJs": false, // 在.js文件中报告错误
"outDir": "./dist", // 输出目录
"rootDir": "./src", // 输入文件根目录
"removeComments": true, // 移除注释
"newLine": "lf", // 换行符
/* JSX支持 */
"jsx": "react-jsx", // React 17+ JSX转换
/* 编译器性能 */
"incremental": true, // 启用增量编译
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo" // 增量编译信息文件
},
/* 包含和排除的文件 */
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.js",
"src/**/*.jsx",
"types/**/*.d.ts"
],
"exclude": [
"node_modules",
"dist",
"build",
"coverage",
"**/*.test.ts",
"**/*.test.tsx",
"**/*.spec.ts",
"**/*.spec.tsx"
]
}
五、测试工具与策略
5.1 Jest测试配置
// jest.config.js
module.exports = {
// 测试环境
testEnvironment: 'jsdom',
// 测试文件匹配模式
testMatch: [
'<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}',
'<rootDir>/src/**/*.{spec,test}.{js,jsx,ts,tsx}'
],
// 测试路径忽略
testPathIgnorePatterns: [
'<rootDir>/node_modules/',
'<rootDir>/dist/',
'<rootDir>/build/'
],
// 模块解析
moduleNameMapper: {
// 支持路径别名
'^@/(.*)$': '<rootDir>/src/$1',
'^@components/(.*)$': '<rootDir>/src/components/$1',
'^@utils/(.*)$': '<rootDir>/src/utils/$1',
// 处理样式文件
'\\.(css|less|scss|sass)$': 'identity-obj-proxy',
// 处理静态资源
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
'<rootDir>/__mocks__/fileMock.js'
},
// 设置文件
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
// 收集测试覆盖率
collectCoverage: true,
collectCoverageFrom: [
'src/**/*.{js,jsx,ts,tsx}',
'!src/**/*.d.ts',
'!src/**/*.stories.{js,jsx,ts,tsx}',
'!src/**/index.{js,jsx,ts,tsx}',
'!src/**/*.test.{js,jsx,ts,tsx}',
'!src/**/*.spec.{js,jsx,ts,tsx}',
'!src/test/**/*'
],
coverageDirectory: 'coverage',
coverageReporters: ['text', 'lcov', 'html'],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
},
// 变换配置
transform: {
'^.+\\.(js|jsx|ts|tsx)$': 'babel-jest'
},
// 变换忽略
transformIgnorePatterns: [
'node_modules/(?!(lodash-es|@testing-library)/)' // 需要转换的node_modules
],
// 重置模块
resetMocks: true,
resetModules: true,
// 清除模拟
clearMocks: true,
// 快照序列化器
snapshotSerializers: ['@emotion/jest/serializer'],
// 慢测试阈值(秒)
slowTestThreshold: 5,
// 测试运行前和运行后的脚本
globalSetup: '<rootDir>/jest.global-setup.js',
globalTeardown: '<rootDir>/jest.global-teardown.js'
};
// jest.setup.js
import '@testing-library/jest-dom';
import { jest } from '@jest/globals';
// 全局测试配置
global.jest = jest;
// 模拟window对象方法
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation(query => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(),
removeListener: jest.fn(),
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn()
}))
});
// 模拟localStorage
const localStorageMock = {
getItem: jest.fn(),
setItem: jest.fn(),
removeItem: jest.fn(),
clear: jest.fn(),
length: 0,
key: jest.fn()
};
global.localStorage = localStorageMock;
// 模拟sessionStorage
const sessionStorageMock = {
getItem: jest.fn(),
setItem: jest.fn(),
removeItem: jest.fn(),
clear: jest.fn(),
length: 0,
key: jest.fn()
};
global.sessionStorage = sessionStorageMock;
// 模拟IntersectionObserver
global.IntersectionObserver = jest.fn(() => ({
observe: jest.fn(),
unobserve: jest.fn(),
disconnect: jest.fn()
}));
// 模拟ResizeObserver
global.ResizeObserver = jest.fn(() => ({
observe: jest.fn(),
unobserve: jest.fn(),
disconnect: jest.fn()
}));
5.2 测试编写示例
// utils/__tests__/math.test.js
import { add, subtract, divide, multiply } from '../math';
describe('数学工具函数', () => {
describe('add函数', () => {
test('两个正数相加', () => {
expect(add(2, 3)).toBe(5);
});
test('正数与负数相加', () => {
expect(add(5, -3)).toBe(2);
});
test('两个负数相加', () => {
expect(add(-2, -3)).toBe(-5);
});
test('浮点数相加', () => {
expect(add(0.1, 0.2)).toBeCloseTo(0.3);
});
});
describe('subtract函数', () => {
test('正数相减', () => {
expect(subtract(5, 3)).toBe(2);
});
test('负数相减', () => {
expect(subtract(-2, -3)).toBe(1);
});
});
describe('divide函数', () => {
test('正常除法', () => {
expect(divide(10, 2)).toBe(5);
});
test('除数为0', () => {
expect(() => divide(5, 0)).toThrow('除数不能为零');
});
test('浮点数除法', () => {
expect(divide(1, 3)).toBeCloseTo(0.333333);
});
});
describe('multiply函数', () => {
test('正常乘法', () => {
expect(multiply(3, 4)).toBe(12);
});
test('乘以0', () => {
expect(multiply(5, 0)).toBe(0);
});
});
// 参数化测试
const addCases = [
[1, 2, 3],
[0, 0, 0],
[-1, 1, 0],
[2.5, 2.5, 5]
];
test.each(addCases)('当参数为 %i 和 %i 时,返回 %i', (a, b, expected) => {
expect(add(a, b)).toBe(expected);
});
});
// components/__tests__/Button.test.jsx
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Button from '../Button';
describe('Button组件', () => {
const defaultProps = {
onClick: jest.fn(),
children: '点击我'
};
beforeEach(() => {
jest.clearAllMocks();
});
test('渲染正确的文本', () => {
render(<Button {...defaultProps} />);
expect(screen.getByText('点击我')).toBeInTheDocument();
});
test('点击按钮触发onClick回调', async () => {
const user = userEvent.setup();
render(<Button {...defaultProps} />);
const button = screen.getByRole('button');
await user.click(button);
expect(defaultProps.onClick).toHaveBeenCalledTimes(1);
});
test('禁用状态下不能点击', () => {
render(<Button {...defaultProps} disabled />);
const button = screen.getByRole('button');
fireEvent.click(button);
expect(defaultProps.onClick).not.toHaveBeenCalled();
expect(button).toBeDisabled();
});
test('加载状态显示加载指示器', () => {
render(<Button {...defaultProps} loading />);
expect(screen.getByRole('button')).toBeDisabled();
expect(screen.getByTestId('loading-spinner')).toBeInTheDocument();
});
test('支持不同的变体样式', () => {
const { rerender } = render(<Button {...defaultProps} variant="primary" />);
expect(screen.getByRole('button')).toHaveClass('btn-primary');
rerender(<Button {...defaultProps} variant="secondary" />);
expect(screen.getByRole('button')).toHaveClass('btn-secondary');
});
test('快照测试', () => {
const { asFragment } = render(<Button {...defaultProps} />);
expect(asFragment()).toMatchSnapshot();
});
});
// hooks/__tests__/useCounter.test.js
import { renderHook, act } from '@testing-library/react-hooks';
import useCounter from '../useCounter';
describe('useCounter自定义Hook', () => {
test('使用默认初始值0', () => {
const { result } = renderHook(() => useCounter());
expect(result.current.count).toBe(0);
});
test('使用自定义初始值', () => {
const { result } = renderHook(() => useCounter(10));
expect(result.current.count).toBe(10);
});
test('increment函数增加计数', () => {
const { result } = renderHook(() => useCounter(5));
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(6);
});
test('decrement函数减少计数', () => {
const { result } = renderHook(() => useCounter(5));
act(() => {
result.current.decrement();
});
expect(result.current.count).toBe(4);
});
test('reset函数重置计数', () => {
const { result } = renderHook(() => useCounter(5));
act(() => {
result.current.increment();
result.current.increment();
});
expect(result.current.count).toBe(7);
act(() => {
result.current.reset();
});
expect(result.current.count).toBe(5);
});
test('设置最大限制', () => {
const { result } = renderHook(() => useCounter(0, { max: 3 }));
act(() => {
result.current.increment();
result.current.increment();
result.current.increment();
result.current.increment(); // 应该不超过3
});
expect(result.current.count).toBe(3);
});
test('设置最小限制', () => {
const { result } = renderHook(() => useCounter(5, { min: 3 }));
act(() => {
result.current.decrement();
result.current.decrement();
result.current.decrement(); // 应该不低于3
});
expect(result.current.count).toBe(3);
});
});
// e2e/__tests__/login.e2e.test.js
describe('登录端到端测试', () => {
beforeAll(async () => {
await page.goto('http://localhost:3000/login');
});
test('登录表单正常显示', async () => {
const emailInput = await page.$('input[type="email"]');
const passwordInput = await page.$('input[type="password"]');
const submitButton = await page.$('button[type="submit"]');
expect(emailInput).toBeTruthy();
expect(passwordInput).toBeTruthy();
expect(submitButton).toBeTruthy();
});
test('登录成功重定向', async () => {
await page.type('input[type="email"]', 'test@example.com');
await page.type('input[type="password"]', 'password123');
await Promise.all([
page.click('button[type="submit"]'),
page.waitForNavigation()
]);
expect(page.url()).toContain('/dashboard');
});
test('无效凭证显示错误消息', async () => {
await page.goto('http://localhost:3000/login');
await page.type('input[type="email"]', 'wrong@example.com');
await page.type('input[type="password"]', 'wrongpassword');
await page.click('button[type="submit"]');
await page.waitForSelector('.error-message');
const errorMessage = await page.$eval('.error-message', el => el.textContent);
expect(errorMessage).toContain('无效的凭证');
});
});
六、项目结构与架构
6.1 推荐的项目结构
my-app/
├── public/ # 静态资源
│ ├── favicon.ico
│ ├── index.html
│ └── robots.txt
├── src/ # 源代码
│ ├── assets/ # 静态资源
│ │ ├── images/
│ │ ├── fonts/
│ │ └── styles/
│ │ ├── base/
│ │ ├── components/
│ │ ├── layouts/
│ │ └── variables.scss
│ ├── components/ # 可复用组件
│ │ ├── common/ # 通用组件
│ │ │ ├── Button/
│ │ │ ├── Input/
│ │ │ └── Modal/
│ │ ├── layout/ # 布局组件
│ │ │ ├── Header/
│ │ │ ├── Footer/
│ │ │ └── Sidebar/
│ │ └── features/ # 功能组件
│ ├── containers/ # 容器组件(连接状态)
│ ├── contexts/ # React Context
│ ├── hooks/ # 自定义Hooks
│ ├── pages/ # 页面组件
│ │ ├── Home/
│ │ ├── Login/
│ │ └── Dashboard/
│ ├── routes/ # 路由配置
│ ├── services/ # API服务
│ │ ├── api/
│ │ ├── auth/
│ │ └── storage/
│ ├── store/ # 状态管理
│ │ ├── slices/ # Redux切片
│ │ └── index.ts
│ ├── utils/ # 工具函数
│ │ ├── helpers/
│ │ ├── validators/
│ │ └── constants.ts
│ ├── types/ # TypeScript类型定义
│ ├── test/ # 测试相关
│ │ ├── __mocks__/
│ │ ├── fixtures/
│ │ └── utils/
│ ├── App.tsx
│ ├── main.tsx
│ └── vite-env.d.ts
├── .husky/ # Git hooks
├── .github/ # GitHub配置
│ └── workflows/
├── dist/ # 构建输出
├── coverage/ # 测试覆盖率报告
├── node_modules/
├── .env # 环境变量
├── .env.development
├── .env.production
├── .eslintrc.js
├── .prettierrc.js
├── .babelrc
├── tsconfig.json
├── vite.config.js
├── package.json
└── README.md
6.2 模块化架构示例
// src/services/api/httpClient.js
import axios from 'axios';
class HttpClient {
constructor(baseURL) {
this.client = axios.create({
baseURL,
timeout: 30000,
headers: {
'Content-Type': 'application/json'
}
});
// 请求拦截器
this.client.interceptors.request.use(
config => {
const token = localStorage.getItem('auth_token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
error => Promise.reject(error)
);
// 响应拦截器
this.client.interceptors.response.use(
response => response.data,
error => {
if (error.response?.status === 401) {
// 处理未授权
localStorage.removeItem('auth_token');
window.location.href = '/login';
}
return Promise.reject(error);
}
);
}
get(url, config = {}) {
return this.client.get(url, config);
}
post(url, data, config = {}) {
return this.client.post(url, data, config);
}
put(url, data, config = {}) {
return this.client.put(url, data, config);
}
patch(url, data, config = {}) {
return this.client.patch(url, data, config);
}
delete(url, config = {}) {
return this.client.delete(url, config);
}
}
export default HttpClient;
// src/services/api/index.js
import HttpClient from './httpClient';
const BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000/api';
export const httpClient = new HttpClient(BASE_URL);
// 用户服务
export const userService = {
getProfile: () => httpClient.get('/users/profile'),
updateProfile: (data) => httpClient.put('/users/profile', data),
getUsers: (params) => httpClient.get('/users', { params })
};
// 产品服务
export const productService = {
getProducts: (params) => httpClient.get('/products', { params }),
getProduct: (id) => httpClient.get(`/products/${id}`),
createProduct: (data) => httpClient.post('/products', data),
updateProduct: (id, data) => httpClient.put(`/products/${id}`, data),
deleteProduct: (id) => httpClient.delete(`/products/${id}`)
};
// 订单服务
export const orderService = {
getOrders: (params) => httpClient.get('/orders', { params }),
createOrder: (data) => httpClient.post('/orders', data),
cancelOrder: (id) => httpClient.post(`/orders/${id}/cancel`)
};
6.3 组件设计模式
// src/components/common/Button/index.jsx
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import './Button.scss';
const Button = React.forwardRef(({
children,
variant = 'primary',
size = 'medium',
disabled = false,
loading = false,
fullWidth = false,
startIcon,
endIcon,
className,
onClick,
type = 'button',
...props
}, ref) => {
const handleClick = (event) => {
if (!disabled && !loading && onClick) {
onClick(event);
}
};
const buttonClasses = classNames(
'btn',
`btn--${variant}`,
`btn--${size}`,
{
'btn--disabled': disabled,
'btn--loading': loading,
'btn--full-width': fullWidth
},
className
);
return (
<button
ref={ref}
type={type}
className={buttonClasses}
disabled={disabled || loading}
onClick={handleClick}
aria-busy={loading}
{...props}
>
{loading && (
<span className="btn__loader" aria-label="加载中">
<span className="btn__loader-dot" />
<span className="btn__loader-dot" />
<span className="btn__loader-dot" />
</span>
)}
{!loading && startIcon && (
<span className="btn__icon btn__icon--start">
{startIcon}
</span>
)}
<span className="btn__content">{children}</span>
{!loading && endIcon && (
<span className="btn__icon btn__icon--end">
{endIcon}
</span>
)}
</button>
);
});
Button.propTypes = {
children: PropTypes.node.isRequired,
variant: PropTypes.oneOf(['primary', 'secondary', 'outline', 'text', 'danger']),
size: PropTypes.oneOf(['small', 'medium', 'large']),
disabled: PropTypes.bool,
loading: PropTypes.bool,
fullWidth: PropTypes.bool,
startIcon: PropTypes.node,
endIcon: PropTypes.node,
className: PropTypes.string,
onClick: PropTypes.func,
type: PropTypes.oneOf(['button', 'submit', 'reset'])
};
Button.displayName = 'Button';
export default Button;
// src/components/common/Button/Button.scss
.btn {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
border: none;
border-radius: 4px;
font-family: inherit;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
text-decoration: none;
user-select: none;
white-space: nowrap;
vertical-align: middle;
outline: none;
&:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
}
&--disabled {
opacity: 0.6;
cursor: not-allowed;
}
&--full-width {
width: 100%;
}
// 变体
&--primary {
background-color: var(--color-primary);
color: var(--color-white);
&:hover:not(.btn--disabled) {
background-color: var(--color-primary-dark);
}
&:active:not(.btn--disabled) {
background-color: var(--color-primary-darker);
}
}
&--secondary {
background-color: var(--color-secondary);
color: var(--color-white);
&:hover:not(.btn--disabled) {
background-color: var(--color-secondary-dark);
}
}
&--outline {
background-color: transparent;
border: 1px solid var(--color-border);
color: var(--color-text);
&:hover:not(.btn--disabled) {
background-color: var(--color-bg-hover);
}
}
&--text {
background-color: transparent;
color: var(--color-primary);
&:hover:not(.btn--disabled) {
background-color: var(--color-bg-hover);
}
}
&--danger {
background-color: var(--color-error);
color: var(--color-white);
&:hover:not(.btn--disabled) {
background-color: var(--color-error-dark);
}
}
// 尺寸
&--small {
padding: 6px 12px;
font-size: 12px;
line-height: 1.5;
}
&--medium {
padding: 8px 16px;
font-size: 14px;
line-height: 1.5;
}
&--large {
padding: 12px 24px;
font-size: 16px;
line-height: 1.5;
}
// 加载状态
&--loading {
.btn__content {
visibility: hidden;
}
}
&__loader {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
&-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background-color: currentColor;
animation: btn-loader-dot 1.4s ease-in-out infinite;
&:nth-child(2) {
animation-delay: 0.2s;
}
&:nth-child(3) {
animation-delay: 0.4s;
}
}
}
&__icon {
display: inline-flex;
align-items: center;
&--start {
margin-right: 4px;
}
&--end {
margin-left: 4px;
}
}
}
@keyframes btn-loader-dot {
0%, 60%, 100% {
transform: scale(1);
opacity: 1;
}
30% {
transform: scale(1.2);
opacity: 0.8;
}
}
七、性能优化
7.1 代码分割与懒加载
// 路由懒加载
import { lazy, Suspense } from 'react';
const HomePage = lazy(() => import('./pages/Home'));
const AboutPage = lazy(() => import('./pages/About'));
const ContactPage = lazy(() => import('./pages/Contact'));
const LoadingFallback = () => (
<div className="loading-spinner">
<div className="spinner" />
<p>加载中...</p>
</div>
);
function App() {
return (
<Suspense fallback={<LoadingFallback />}>
<Router>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/about" element={<AboutPage />} />
<Route path="/contact" element={<ContactPage />} />
</Routes>
</Router>
</Suspense>
);
}
// 组件懒加载与预加载
const HeavyComponent = lazy(() =>
import('./components/HeavyComponent')
.then(module => {
// 可以在这里添加一些初始化逻辑
console.log('HeavyComponent加载完成');
return module;
})
);
// 预加载策略
function PrefetchComponent() {
const [showComponent, setShowComponent] = useState(false);
// 鼠标悬停时预加载
const handleMouseEnter = () => {
import('./components/HeavyComponent');
};
return (
<div>
<button
onMouseEnter={handleMouseEnter}
onClick={() => setShowComponent(true)}
>
显示重组件
</button>
{showComponent && (
<Suspense fallback={<div>加载中...</div>}>
<HeavyComponent />
</Suspense>
)}
</div>
);
}
// 基于路由的代码分割
const router = createBrowserRouter([
{
path: "/",
element: <Layout />,
children: [
{
index: true,
element: <HomePage />
},
{
path: "dashboard",
async lazy() {
const { DashboardPage } = await import("./pages/Dashboard");
return { Component: DashboardPage };
}
},
{
path: "analytics",
async lazy() {
const { AnalyticsPage } = await import("./pages/Analytics");
return {
Component: AnalyticsPage,
// 可以同时加载数据
loader: ({ request }) => {
return fetchAnalyticsData(request);
}
};
}
}
]
}
]);
7.2 性能监控与优化
// 性能监控组件
import { useEffect, useRef } from 'react';
import { reportMetrics } from '../services/analytics';
function PerformanceMonitor({ componentName }) {
const mountTime = useRef(Date.now());
const renderCount = useRef(0);
useEffect(() => {
const mountDuration = Date.now() - mountTime.current;
// 报告组件挂载性能
reportMetrics('component_mount', {
component: componentName,
duration: mountDuration,
timestamp: new Date().toISOString()
});
return () => {
// 组件卸载时报告
reportMetrics('component_unmount', {
component: componentName,
mountTime: mountDuration,
renderCount: renderCount.current,
timestamp: new Date().toISOString()
});
};
}, [componentName]);
useEffect(() => {
renderCount.current += 1;
// 报告渲染次数(用于检测不必要的重渲染)
if (renderCount.current > 10) {
console.warn(`${componentName} 组件渲染次数过多: ${renderCount.current}`);
}
});
return null;
}
// React.memo优化
import React, { memo } from 'react';
const ExpensiveComponent = memo(function ExpensiveComponent({ data, onAction }) {
console.log('ExpensiveComponent渲染');
// 使用useMemo缓存计算结果
const processedData = React.useMemo(() => {
return data.map(item => ({
...item,
processed: heavyComputation(item)
}));
}, [data]);
// 使用useCallback缓存函数
const handleClick = React.useCallback(() => {
onAction(processedData);
}, [onAction, processedData]);
return (
<div>
{processedData.map(item => (
<div key={item.id}>{item.name}</div>
))}
<button onClick={handleClick}>执行操作</button>
</div>
);
}, (prevProps, nextProps) => {
// 自定义比较函数
return prevProps.data === nextProps.data &&
prevProps.onAction === nextProps.onAction;
});
// 虚拟列表组件(处理大量数据)
import { FixedSizeList, VariableSizeList } from 'react-window';
function VirtualList({ items, itemHeight = 50 }) {
const Row = ({ index, style }) => (
<div style={style}>
行 {index}: {items[index]}
</div>
);
return (
<FixedSizeList
height={400}
width={300}
itemCount={items.length}
itemSize={itemHeight}
>
{Row}
</FixedSizeList>
);
}
// 图片懒加载组件
function LazyImage({ src, alt, placeholder, ...props }) {
const [isLoaded, setIsLoaded] = useState(false);
const [isInView, setIsInView] = useState(false);
const imgRef = useRef();
const observerRef = useRef();
useEffect(() => {
if (!imgRef.current) return;
observerRef.current = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
setIsInView(true);
observerRef.current.unobserve(entry.target);
}
});
},
{ rootMargin: '50px' } // 提前50px加载
);
observerRef.current.observe(imgRef.current);
return () => {
if (observerRef.current) {
observerRef.current.disconnect();
}
};
}, []);
return (
<div ref={imgRef} className="lazy-image-container">
{!isLoaded && placeholder && (
<div className="image-placeholder">
{placeholder}
</div>
)}
{isInView && (
<img
src={src}
alt={alt}
loading="lazy"
onLoad={() => setIsLoaded(true)}
className={`lazy-image ${isLoaded ? 'loaded' : 'loading'}`}
{...props}
/>
)}
</div>
);
}
八、部署与CI/CD
8.1 Docker配置
# Dockerfile
# 构建阶段
FROM node:18-alpine AS builder
# 安装构建依赖
RUN apk add --no-cache python3 make g++
# 设置工作目录
WORKDIR /app
# 复制包管理文件
COPY package*.json ./
# 安装依赖
RUN npm ci --only=production
# 复制源代码
COPY . .
# 构建应用
RUN npm run build
# 生产阶段
FROM nginx:alpine AS production
# 复制构建产物
COPY --from=builder /app/dist /usr/share/nginx/html
# 复制nginx配置
COPY nginx.conf /etc/nginx/nginx.conf
# 复制健康检查脚本
COPY healthcheck.sh /healthcheck.sh
RUN chmod +x /healthcheck.sh
# 暴露端口
EXPOSE 80
# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD ["/healthcheck.sh"]
# 启动nginx
CMD ["nginx", "-g", "daemon off;"]
# nginx.conf
worker_processes auto;
events {
worker_connections 1024;
use epoll;
multi_accept on;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# 日志格式
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
error_log /var/log/nginx/error.log warn;
# 基础设置
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
client_max_body_size 100m;
# Gzip压缩
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_proxied any;
gzip_comp_level 6;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/javascript
application/xml+rss
application/json
application/atom+xml
image/svg+xml;
# 缓存设置
open_file_cache max=1000 inactive=20s;
open_file_cache_valid 30s;
open_file_cache_min_uses 2;
open_file_cache_errors on;
# 服务器配置
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# 安全头部
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:;" always;
# 静态文件缓存
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# HTML文件不缓存
location ~* \.(html)$ {
expires -1;
add_header Cache-Control "no-store, no-cache, must-revalidate";
}
# 单页应用路由支持
location / {
try_files $uri $uri/ /index.html;
}
# API代理
location /api/ {
proxy_pass http://api-server:3000/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
# 错误页面
error_page 404 /index.html;
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
}
8.2 GitHub Actions CI/CD
# .github/workflows/ci-cd.yml
name: CI/CD Pipeline
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
env:
NODE_VERSION: '18'
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
# 代码质量检查
lint-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: ESLint检查
run: npm run lint
- name: TypeScript类型检查
run: npm run type-check
- name: 单元测试
run: npm run test:unit -- --coverage
- name: 上传测试覆盖率
uses: codecov/codecov-action@v3
- name: 构建测试
run: npm run build
# E2E测试
e2e-tests:
runs-on: ubuntu-latest
needs: lint-and-test
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: 启动服务并运行E2E测试
run: |
npm run test:e2e
- name: 上传E2E测试结果
uses: actions/upload-artifact@v3
if: always()
with:
name: e2e-test-results
path: test-results/
# 构建和推送Docker镜像
build-and-push:
runs-on: ubuntu-latest
needs: [lint-and-test, e2e-tests]
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- name: 设置Docker构建x
uses: docker/setup-buildx-action@v2
- name: 登录到容器注册表
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: 提取元数据
id: meta
uses: docker/metadata-action@v4
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha,prefix={{branch}}-
- name: 构建和推送Docker镜像
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
# 部署到生产环境
deploy-production:
runs-on: ubuntu-latest
needs: build-and-push
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
environment: production
steps:
- name: 部署到Kubernetes
uses: azure/k8s-deploy@v1
with:
namespace: production
manifests: |
k8s/deployment.yaml
k8s/service.yaml
k8s/ingress.yaml
images: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
- name: 等待部署完成
run: |
kubectl rollout status deployment/my-app -n production --timeout=300s
- name: 运行健康检查
run: |
curl -f https://api.example.com/health || exit 1
- name: 发送部署通知
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
author_name: CI/CD Pipeline
fields: repo,message,commit,author,action,eventName,ref,workflow
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
结语
现代JavaScript开发已经远不止是编写代码,它涉及到模块化设计、工程化实践、代码质量保障、性能优化和自动化部署等多个方面。通过本文的学习,你应该能够:
- 理解JavaScript模块化的演进和各种模块系统
- 掌握现代打包工具(Webpack、Vite)的配置和使用
- 建立完整的代码质量保障体系(ESLint、Prettier、TypeScript)
- 编写全面的测试套件(单元测试、集成测试、E2E测试)
- 设计可维护的项目结构和组件架构
- 实施性能优化策略
- 搭建自动化CI/CD流水线
这些工程化实践将帮助你构建更健壮、更可维护、更高效的前端应用。记住,好的工程实践是项目成功的关键,持续学习和实践这些技能将使你在前端开发领域保持竞争力。