15. 前端工程化深入
📋 目录
构建工具深度解析
现代前端工程化离不开构建工具,理解其原理和配置对于提升开发效率至关重要。
Webpack深度配置
// webpack.config.js - 生产级配置
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
const CompressionPlugin = require('compression-webpack-plugin');
module.exports = (env, argv) => {
const isProduction = argv.mode === 'production';
return {
entry: {
main: './src/index.js',
vendor: ['react', 'react-dom', 'lodash']
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: isProduction
? '[name].[contenthash:8].js'
: '[name].js',
chunkFilename: isProduction
? '[name].[contenthash:8].chunk.js'
: '[name].chunk.js',
publicPath: '/',
clean: true
},
optimization: {
minimize: isProduction,
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: isProduction,
drop_debugger: isProduction
}
}
}),
new CssMinimizerPlugin()
],
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
priority: 10
},
common: {
name: 'common',
minChunks: 2,
chunks: 'all',
priority: 5,
reuseExistingChunk: true
}
}
},
runtimeChunk: {
name: 'runtime'
}
},
module: {
rules: [
{
test: /\.(js|jsx|ts|tsx)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env', {
targets: {
browsers: ['> 1%', 'last 2 versions']
},
useBuiltIns: 'usage',
corejs: 3
}],
'@babel/preset-react',
'@babel/preset-typescript'
],
plugins: [
'@babel/plugin-proposal-class-properties',
'@babel/plugin-syntax-dynamic-import',
isProduction && 'babel-plugin-transform-remove-console'
].filter(Boolean)
}
}
},
{
test: /\.css$/,
use: [
isProduction ? MiniCssExtractPlugin.loader : 'style-loader',
{
loader: 'css-loader',
options: {
modules: {
auto: true,
localIdentName: isProduction
? '[hash:base64:8]'
: '[name]__[local]--[hash:base64:5]'
}
}
},
'postcss-loader'
]
},
{
test: /\.scss$/,
use: [
isProduction ? MiniCssExtractPlugin.loader : 'style-loader',
'css-loader',
'postcss-loader',
'sass-loader'
]
},
{
test: /\.(png|jpg|jpeg|gif|svg)$/,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 8 * 1024 // 8KB
}
},
generator: {
filename: 'images/[name].[hash:8][ext]'
}
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/,
type: 'asset/resource',
generator: {
filename: 'fonts/[name].[hash:8][ext]'
}
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
minify: isProduction ? {
removeComments: true,
collapseWhitespace: true,
removeRedundantAttributes: true,
useShortDoctype: true,
removeEmptyAttributes: true,
removeStyleLinkTypeAttributes: true,
keepClosingSlash: true,
minifyJS: true,
minifyCSS: true,
minifyURLs: true
} : false
}),
isProduction && new MiniCssExtractPlugin({
filename: '[name].[contenthash:8].css',
chunkFilename: '[name].[contenthash:8].chunk.css'
}),
isProduction && new CompressionPlugin({
algorithm: 'gzip',
test: /\.(js|css|html|svg)$/,
threshold: 8192,
minRatio: 0.8
}),
process.env.ANALYZE && new BundleAnalyzerPlugin()
].filter(Boolean),
resolve: {
extensions: ['.js', '.jsx', '.ts', '.tsx'],
alias: {
'@': path.resolve(__dirname, 'src'),
'@components': path.resolve(__dirname, 'src/components'),
'@utils': path.resolve(__dirname, 'src/utils'),
'@assets': path.resolve(__dirname, 'src/assets')
}
},
devServer: {
port: 3000,
hot: true,
historyApiFallback: true,
compress: true,
open: true
},
devtool: isProduction ? 'source-map' : 'eval-source-map'
};
};
Vite配置优化
// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { resolve } from 'path';
import { visualizer } from 'rollup-plugin-visualizer';
import { createHtmlPlugin } from 'vite-plugin-html';
export default defineConfig(({ command, mode }) => {
const isProduction = mode === 'production';
return {
plugins: [
react(),
createHtmlPlugin({
minify: isProduction,
inject: {
data: {
title: 'My App',
injectScript: isProduction
? '<script src="/analytics.js"></script>'
: ''
}
}
}),
isProduction && visualizer({
filename: 'dist/stats.html',
open: true,
gzipSize: true
})
].filter(Boolean),
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
'@components': resolve(__dirname, 'src/components'),
'@utils': resolve(__dirname, 'src/utils'),
'@assets': resolve(__dirname, 'src/assets')
}
},
css: {
modules: {
localsConvention: 'camelCase'
},
preprocessorOptions: {
scss: {
additionalData: '@import "@/styles/variables.scss";'
}
}
},
build: {
target: 'es2015',
outDir: 'dist',
assetsDir: 'assets',
sourcemap: isProduction ? 'hidden' : true,
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
utils: ['lodash', 'axios', 'dayjs']
}
}
},
terserOptions: {
compress: {
drop_console: isProduction,
drop_debugger: isProduction
}
}
},
server: {
port: 3000,
open: true,
cors: true,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
},
optimizeDeps: {
include: ['react', 'react-dom', 'lodash'],
exclude: ['@vite/client', '@vite/env']
}
};
});
微前端架构
Single-SPA实现
⚠️
微前端架构能够让大型应用拆分为多个独立的小应用,提高开发效率和可维护性。
// 主应用配置 - main-app/src/index.js
import { registerApplication, start } from 'single-spa';
// 注册微应用
registerApplication({
name: 'react-app',
app: () => import('./microfrontends/react-app/main.js'),
activeWhen: ['/react'],
customProps: {
authToken: 'token123',
apiUrl: 'https://api.example.com'
}
});
registerApplication({
name: 'vue-app',
app: () => import('./microfrontends/vue-app/main.js'),
activeWhen: ['/vue'],
customProps: {
theme: 'dark'
}
});
// 启动single-spa
start({
urlRerouteOnly: true
});
// React微应用 - react-app/src/main.js
import React from 'react';
import ReactDOM from 'react-dom';
import singleSpaReact from 'single-spa-react';
import App from './App';
const lifecycles = singleSpaReact({
React,
ReactDOM,
rootComponent: App,
errorBoundary(err, info, props) {
return <div>Error in React app: {err.message}</div>;
}
});
export const { bootstrap, mount, unmount } = lifecycles;
// 独立运行模式
if (!window.singleSpaNavigate) {
ReactDOM.render(<App />, document.getElementById('root'));
}
// Vue微应用 - vue-app/src/main.js
import { createApp } from 'vue';
import singleSpaVue from 'single-spa-vue';
import App from './App.vue';
const vueLifecycles = singleSpaVue({
createApp,
appOptions: {
render() {
return h(App, {
// 传递props
...this.props
});
}
},
handleInstance: (app, info) => {
app.use(router);
app.use(store);
}
});
export const { bootstrap, mount, unmount } = vueLifecycles;
// 独立运行模式
if (!window.singleSpaNavigate) {
createApp(App).mount('#app');
}
Module Federation实现
// 主应用 webpack.config.js
const ModuleFederationPlugin = require('@module-federation/webpack');
module.exports = {
mode: 'development',
devServer: {
port: 3000
},
plugins: [
new ModuleFederationPlugin({
name: 'shell',
remotes: {
mf_react: 'mf_react@http://localhost:3001/remoteEntry.js',
mf_vue: 'mf_vue@http://localhost:3002/remoteEntry.js'
}
})
]
};
// React微应用 webpack.config.js
module.exports = {
mode: 'development',
devServer: {
port: 3001
},
plugins: [
new ModuleFederationPlugin({
name: 'mf_react',
filename: 'remoteEntry.js',
exposes: {
'./App': './src/App',
'./Button': './src/components/Button'
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true }
}
})
]
};
// 主应用中使用微应用
import React, { Suspense } from 'react';
const ReactApp = React.lazy(() => import('mf_react/App'));
const ReactButton = React.lazy(() => import('mf_react/Button'));
function App() {
return (
<div>
<h1>主应用</h1>
<Suspense fallback={<div>Loading React App...</div>}>
<ReactApp />
</Suspense>
<Suspense fallback={<div>Loading Button...</div>}>
<ReactButton onClick={() => alert('Clicked!')}>
Remote Button
</ReactButton>
</Suspense>
</div>
);
}
export default App;
CI/CD流水线
GitHub Actions配置
# .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:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16, 18, 20]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run linting
run: npm run lint
- name: Run type checking
run: npm run type-check
- name: Run tests
run: npm run test:coverage
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
file: ./coverage/lcov.info
build:
needs: test
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build application
run: npm run build
- name: Upload build artifacts
uses: actions/upload-artifact@v3
with:
name: build-files
path: dist/
security-scan:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Run security audit
run: npm audit --audit-level high
- name: Run Snyk security scan
uses: snyk/actions/node@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
deploy-staging:
needs: [test, build, security-scan]
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/develop'
environment:
name: staging
url: https://staging.example.com
steps:
- name: Download build artifacts
uses: actions/download-artifact@v3
with:
name: build-files
path: dist/
- name: Deploy to staging
run: |
# 部署到staging环境的脚本
echo "Deploying to staging..."
deploy-production:
needs: [test, build, security-scan]
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
environment:
name: production
url: https://example.com
steps:
- name: Download build artifacts
uses: actions/download-artifact@v3
with:
name: build-files
path: dist/
- name: Deploy to production
run: |
# 部署到生产环境的脚本
echo "Deploying to production..."
- name: Notify deployment
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
channel: '#deployments'
webhook_url: ${{ secrets.SLACK_WEBHOOK }}
Docker化部署
# Dockerfile
# 多阶段构建
FROM node:18-alpine AS builder
WORKDIR /app
# 复制package文件
COPY package*.json ./
RUN npm ci --only=production
# 复制源代码
COPY . .
# 构建应用
RUN npm run build
# 生产镜像
FROM nginx:alpine
# 复制构建产物
COPY --from=builder /app/dist /usr/share/nginx/html
# 复制nginx配置
COPY nginx.conf /etc/nginx/nginx.conf
# 暴露端口
EXPOSE 80
# 启动nginx
CMD ["nginx", "-g", "daemon off;"]
# nginx.conf
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Gzip压缩
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css text/xml text/javascript
application/javascript application/xml+rss
application/json;
# 缓存配置
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# SPA路由支持
location / {
try_files $uri $uri/ /index.html;
}
# API代理
location /api/ {
proxy_pass http://backend:8080/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# 健康检查
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
}
}
代码质量保障
ESLint和Prettier配置
// .eslintrc.js
module.exports = {
env: {
browser: true,
es2021: true,
node: true,
jest: true
},
extends: [
'eslint:recommended',
'@typescript-eslint/recommended',
'plugin:react/recommended',
'plugin:react-hooks/recommended',
'plugin:jsx-a11y/recommended',
'plugin:import/recommended',
'plugin:import/typescript',
'prettier'
],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaFeatures: {
jsx: true
},
ecmaVersion: 'latest',
sourceType: 'module',
project: './tsconfig.json'
},
plugins: [
'react',
'@typescript-eslint',
'react-hooks',
'jsx-a11y',
'import'
],
rules: {
// TypeScript规则
'@typescript-eslint/no-unused-vars': 'error',
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
// React规则
'react/react-in-jsx-scope': 'off',
'react/prop-types': 'off',
'react/display-name': 'off',
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn',
// 导入规则
'import/order': [
'error',
{
groups: [
'builtin',
'external',
'internal',
'parent',
'sibling',
'index'
],
'newlines-between': 'always',
alphabetize: {
order: 'asc',
caseInsensitive: true
}
}
],
// 通用规则
'no-console': 'warn',
'no-debugger': 'error',
'prefer-const': 'error',
'no-var': 'error'
},
settings: {
react: {
version: 'detect'
},
'import/resolver': {
typescript: {
alwaysTryTypes: true,
project: './tsconfig.json'
}
}
}
};
// .prettierrc
{
"semi": true,
"trailingComma": "es5",
"singleQuote": true,
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"bracketSpacing": true,
"bracketSameLine": false,
"arrowParens": "avoid",
"endOfLine": "lf"
}
Husky和lint-staged配置
// package.json
{
"scripts": {
"prepare": "husky install",
"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}",
"type-check": "tsc --noEmit"
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"eslint --fix",
"prettier --write"
],
"*.{json,css,md}": [
"prettier --write"
]
}
}
#!/bin/sh
# .husky/pre-commit
. "$(dirname "$0")/_/husky.sh"
npx lint-staged
npm run type-check
#!/bin/sh
# .husky/commit-msg
. "$(dirname "$0")/_/husky.sh"
npx commitlint --edit $1
前端工程化是现代Web开发的基石,通过合理的工具链配置和流程设计,能够大大提升开发效率和代码质量。
📚 参考学习资料
📖 官方文档
🎓 优质教程
- Frontend Build Tools - 构建工具课程
- CI/CD for Frontend - 前端CI/CD指南
- Micro Frontend Architecture - 微前端架构
🛠️ 实践项目
- Create React App - React脚手架
- Vue CLI - Vue脚手架
- Angular CLI - Angular脚手架
- Nx - 单体仓库工具
🔧 开发工具
- GitHub Actions - CI/CD平台
- Jenkins - 自动化服务器
- Docker - 容器化平台
- Husky - Git钩子工具
📝 深入阅读
- Frontend Architecture - 前端架构手册
- Modern Web Development - 现代Web开发基础
- Build Tools Comparison - 构建工具对比
💡 学习建议:建议从基础的构建工具开始,学习Webpack或Vite配置,然后掌握代码质量工具,最后建立完整的CI/CD流水线。
Last updated on