前端工程化最佳实践详解
介绍
前端工程化最佳实践是经过实践检验的有效方法和策略,能够帮助团队提高开发效率、代码质量和用户体验。随着前端技术的快速发展,最佳实践也在不断演进。本章将详细介绍前端工程化各个方面的最佳实践,包括代码组织、性能优化、依赖管理、团队协作等,帮助开发者构建高质量的前端应用。
代码组织与架构
模块化设计
核心原则:
-
单一职责原则(SRP)
// ❌ 违反单一职责原则
class UserManager {
validateUser(user) { /* 验证逻辑 */ }
saveUser(user) { /* 保存逻辑 */ }
sendEmail(user) { /* 邮件逻辑 */ }
}
// ✅ 遵循单一职责原则
class UserValidator {
validate(user) { /* 验证逻辑 */ }
}
class UserRepository {
save(user) { /* 保存逻辑 */ }
}
class EmailService {
send(user) { /* 邮件逻辑 */ }
} -
最小化暴露:只暴露必要的接口,隐藏内部实现细节
// utils/dateHelper.js
function formatDate(date, format) {
// 内部实现
return formattedDate;
}
function parseDate(dateString) {
// 内部实现
return parsedDate;
}
// 私有函数,不对外暴露
function _validateFormat(format) {
// 验证逻辑
}
// 只暴露公共接口
export { formatDate, parseDate }; -
依赖清晰:明确模块之间的依赖关系,避免循环依赖
// 使用依赖注入避免循环依赖
class OrderService {
constructor(userService, paymentService) {
this.userService = userService;
this.paymentService = paymentService;
}
} -
粒度适中:模块粒度既不过大也不过小,便于维护和复用
-
一致命名:使用一致的命名规范,提高可读性
文件夹结构
推荐的项目结构:
src/
├── assets/ # 静态资源
│ ├── images/ # 图片资源
│ ├── icons/ # 图标资源
│ ├── fonts/ # 字体文件
│ └── videos/ # 视频资源
├── components/ # 通用组件
│ ├── common/ # 基础组件
│ │ ├── Button/
│ │ ├── Input/
│ │ └── Modal/
│ ├── layout/ # 布局组件
│ │ ├── Header/
│ │ ├── Sidebar/
│ │ └── Footer/
│ └── business/ # 业务组件
│ ├── UserCard/
│ ├── ProductList/
│ └── OrderForm/
├── pages/ # 页面组件
│ ├── Home/
│ ├── User/
│ └── Product/
├── hooks/ # 自定义hooks
│ ├── useAuth.js
│ ├── useApi.js
│ └── useLocalStorage.js
├── utils/ # 工具函数
│ ├── helpers/ # 通用帮助函数
│ ├── validators/ # 验证函数
│ └── formatters/ # 格式化函数
├── services/ # API服务
│ ├── api/ # API接口
│ ├── auth/ # 认证服务
│ └── storage/ # 存储服务
├── store/ # 状态管理
│ ├── modules/ # 状态模块
│ ├── middleware/ # 中间件
│ └── index.js # 状态入口
├── routes/ # 路由配置
│ ├── index.js # 路由入口
│ └── guards.js # 路由守卫
├── styles/ # 全局样式
│ ├── variables.scss # 样式变量
│ ├── mixins.scss # 样式混入
│ └── global.scss # 全局样式
├── types/ # TypeScript类型定义
│ ├── api.ts
│ ├── user.ts
│ └── common.ts
└── constants/ # 常量定义
├── api.js # API常量
├── routes.js # 路由常量
└── config.js # 配置常量
组件设计模式
详细实现示例:
-
容器组件与展示组件分离
// 容器组件 - 处理数据逻辑
const UserListContainer = () => {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
fetchUsers().then(setUsers);
}, []);
return (
<UserList
users={users}
loading={loading}
onUserSelect={handleUserSelect}
/>
);
};
// 展示组件 - 负责UI渲染
const UserList = ({ users, loading, onUserSelect }) => {
if (loading) return <Loading />;
return (
<div className="user-list">
{users.map(user => (
<UserCard
key={user.id}
user={user}
onClick={() => onUserSelect(user)}
/>
))}
</div>
);
}; -
高阶组件(HOC)
// 权限控制HOC
const withAuth = (WrappedComponent, requiredRole) => {
return (props) => {
const { user } = useAuth();
if (!user || !user.roles.includes(requiredRole)) {
return <AccessDenied />;
}
return <WrappedComponent {...props} />;
};
};
// 使用HOC
const AdminPanel = withAuth(AdminPanelComponent, 'admin'); -
自定义Hooks
// 自定义Hook - 数据获取
const useApi = (url, options = {}) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
setLoading(true);
try {
const response = await fetch(url, options);
const result = await response.json();
setData(result);
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
};
fetchData();
}, [url]);
return { data, loading, error };
};
// 使用自定义Hook
const UserProfile = ({ userId }) => {
const { data: user, loading, error } = useApi(`/api/users/${userId}`);
if (loading) return <Loading />;
if (error) return <Error message={error.message} />;
return <UserCard user={user} />;
}; -
组件组合模式
// 复合组件模式
const Card = ({ children, className }) => {
return <div className={`card ${className}`}>{children}</div>;
};
Card.Header = ({ children }) => {
return <div className="card-header">{children}</div>;
};
Card.Body = ({ children }) => {
return <div className="card-body">{children}</div>;
};
Card.Footer = ({ children }) => {
return <div className="card-footer">{children}</div>;
};
// 使用组合组件
const UserCard = ({ user }) => {
return (
<Card>
<Card.Header>
<h3>{user.name}</h3>
</Card.Header>
<Card.Body>
<p>{user.email}</p>
<p>{user.role}</p>
</Card.Body>
<Card.Footer>
<Button>Edit</Button>
<Button variant="danger">Delete</Button>
</Card.Footer>
</Card>
);
};
性能优化
构建优化
性能优化策略对比:
| 优化策略 | 优化效果 | 实现难度 | 适用场景 | 注意事项 |
|---|---|---|---|---|
| 代码分割 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | 大型应用 | 避免过度分割 |
| Tree Shaking | ⭐⭐⭐⭐ | ⭐⭐ | 所有项目 | 注意副作用标记 |
| 资源压缩 | ⭐⭐⭐ | ⭐ | 所有项目 | 平衡压缩率和速度 |
| Bundle分析 | ⭐⭐⭐⭐ | ⭐ | 所有项目 | 定期分析依赖 |
| 图片优化 | ⭐⭐⭐⭐ | ⭐⭐ | 图片密集应用 | 选择合适格式 |
-
代码分割:按需加载代码,减小初始包体积
// 路由级别的代码分割
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Contact = lazy(() => import('./pages/Contact'));
function App() {
return (
<Router>
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
</Routes>
</Suspense>
</Router>
);
}
// 组件级别的动态导入
const HeavyComponent = lazy(() =>
import('./HeavyComponent').then(module => ({
default: module.HeavyComponent
}))
);
// 条件加载
const loadChart = async () => {
if (needsChart) {
const { Chart } = await import('./Chart');
return Chart;
}
}; -
Tree Shaking:移除未使用的代码
// package.json - 标记副作用
{
"sideEffects": false, // 标记包没有副作用
// 或指定有副作用的文件
"sideEffects": [
"*.css",
"*.scss",
"./src/polyfills.js",
"./src/setup.js"
]
}
// webpack.config.js - 启用Tree Shaking
module.exports = {
mode: 'production', // 生产模式自动启用
optimization: {
usedExports: true,
sideEffects: false
}
};
// 正确的导入方式
import { debounce } from 'lodash-es'; // ✅ 支持Tree Shaking
import debounce from 'lodash/debounce'; // ✅ 按需导入
import _ from 'lodash'; // ❌ 导入整个库 -
资源压缩与优化
// webpack.config.js - 资源优化配置
const TerserPlugin = require('terser-webpack-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const ImageMinimizerPlugin = require('image-minimizer-webpack-plugin');
module.exports = {
optimization: {
minimizer: [
// JS压缩
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true, // 移除console
drop_debugger: true, // 移除debugger
pure_funcs: ['console.log'] // 移除特定函数
},
mangle: {
safari10: true // Safari 10兼容性
}
}
}),
// CSS压缩
new CssMinimizerPlugin({
minimizerOptions: {
preset: [
'default',
{
discardComments: { removeAll: true },
normalizeWhitespace: true
}
]
}
}),
// 图片压缩
new ImageMinimizerPlugin({
minimizer: {
implementation: ImageMinimizerPlugin.imageminMinify,
options: {
plugins: [
['imagemin-mozjpeg', { quality: 80 }],
['imagemin-pngquant', { quality: [0.6, 0.8] }],
['imagemin-svgo', {
plugins: [
{ name: 'removeViewBox', active: false },
{ name: 'addAttributesToSVGElement', params: {
attributes: [{ xmlns: 'http://www.w3.org/2000/svg' }]
}}
]
}]
]
}
}
})
]
}
}; -
Bundle分析与优化
// 使用webpack-bundle-analyzer分析依赖
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin({
analyzerMode: 'static',
openAnalyzer: false,
reportFilename: 'bundle-report.html'
})
],
// 优化分包策略
optimization: {
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
},
// React相关库单独打包
react: {
test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
name: 'react',
chunks: 'all',
priority: 20
}
}
}
}
}; -
现代化图片优化
// 响应式图片组件
const OptimizedImage = ({ src, alt, className, ...props }) => {
const [imageSrc, setImageSrc] = useState('');
const [isLoaded, setIsLoaded] = useState(false);
useEffect(() => {
// 检测WebP支持
const supportsWebP = () => {
const canvas = document.createElement('canvas');
return canvas.toDataURL('image/webp').indexOf('webp') > -1;
};
// 根据设备像素比选择图片
const getOptimalSrc = (baseSrc) => {
const dpr = window.devicePixelRatio || 1;
const extension = supportsWebP() ? '.webp' : '.jpg';
if (dpr >= 2) {
return `${baseSrc}@2x${extension}`;
}
return `${baseSrc}${extension}`;
};
setImageSrc(getOptimalSrc(src));
}, [src]);
return (
<picture>
<source srcSet={`${src}.webp`} type="image/webp" />
<img
src={imageSrc}
alt={alt}
className={`${className} ${isLoaded ? 'loaded' : 'loading'}`}
onLoad={() => setIsLoaded(true)}
loading="lazy"
{...props}
/>
</picture>
);
};
运行时优化
-
懒加载:延迟加载非关键资源
// 通用懒加载Hook
const useLazyLoad = (options = {}) => {
const [isIntersecting, setIsIntersecting] = useState(false);
const [isLoaded, setIsLoaded] = useState(false);
const elementRef = useRef(null);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting && !isLoaded) {
setIsIntersecting(true);
setIsLoaded(true);
observer.unobserve(elementRef.current);
}
},
{
threshold: options.threshold || 0.1,
rootMargin: options.rootMargin || '50px'
}
);
if (elementRef.current) {
observer.observe(elementRef.current);
}
return () => {
if (elementRef.current) {
observer.unobserve(elementRef.current);
}
};
}, []);
return [elementRef, isIntersecting, isLoaded];
};
// 图片懒加载组件
const LazyImage = ({ src, alt, placeholder, className }) => {
const [imgRef, isIntersecting] = useLazyLoad({ threshold: 0.1 });
const [imageLoaded, setImageLoaded] = useState(false);
const [imageSrc, setImageSrc] = useState(placeholder);
useEffect(() => {
if (isIntersecting && src) {
const img = new Image();
img.onload = () => {
setImageSrc(src);
setImageLoaded(true);
};
img.src = src;
}
}, [isIntersecting, src]);
return (
<div ref={imgRef} className={`lazy-image-container ${className}`}>
<img
src={imageSrc}
alt={alt}
className={`lazy-image ${imageLoaded ? 'loaded' : 'loading'}`}
loading="lazy"
/>
{!imageLoaded && (
<div className="lazy-image-skeleton">
<div className="skeleton-animation"></div>
</div>
)}
</div>
);
};
// 组件懒加载
const LazyComponent = ({ component: Component, fallback, ...props }) => {
const [componentRef, isIntersecting] = useLazyLoad();
return (
<div ref={componentRef}>
{isIntersecting ? (
<Component {...props} />
) : (
fallback || <div className="component-placeholder">Loading...</div>
)}
</div>
);
}; -
虚拟列表:只渲染可视区域内的列表项
// 虚拟列表Hook
const useVirtualList = ({
items,
itemHeight,
containerHeight,
overscan = 5
}) => {
const [scrollTop, setScrollTop] = useState(0);
const [isScrolling, setIsScrolling] = useState(false);
// 计算可见范围
const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - overscan);
const endIndex = Math.min(
items.length - 1,
Math.ceil((scrollTop + containerHeight) / itemHeight) + overscan
);
// 可见项目
const visibleItems = items.slice(startIndex, endIndex + 1).map((item, index) => ({
...item,
index: startIndex + index
}));
// 滚动处理
const handleScroll = useCallback(
throttle((e) => {
setScrollTop(e.target.scrollTop);
setIsScrolling(true);
// 滚动结束检测
clearTimeout(handleScroll.timer);
handleScroll.timer = setTimeout(() => {
setIsScrolling(false);
}, 150);
}, 16),
[]
);
return {
visibleItems,
startIndex,
endIndex,
totalHeight: items.length * itemHeight,
offsetY: startIndex * itemHeight,
handleScroll,
isScrolling
};
};
// 虚拟列表组件
const VirtualList = ({
items,
itemHeight = 50,
height = 400,
renderItem,
className
}) => {
const {
visibleItems,
totalHeight,
offsetY,
handleScroll,
isScrolling
} = useVirtualList({ items, itemHeight, containerHeight: height });
return (
<div
className={`virtual-list ${className} ${isScrolling ? 'scrolling' : ''}`}
style={{ height, overflow: 'auto' }}
onScroll={handleScroll}
>
<div style={{ height: totalHeight, position: 'relative' }}>
<div
style={{
transform: `translateY(${offsetY}px)`,
position: 'absolute',
top: 0,
left: 0,
right: 0
}}
>
{visibleItems.map((item) => (
<div
key={item.id || item.index}
style={{ height: itemHeight }}
className="virtual-list-item"
>
{renderItem(item, item.index)}
</div>
))}
</div>
</div>
</div>
);
}; -
防抖与节流:优化高频事件处理
// 高级防抖函数
function debounce(fn, delay, options = {}) {
let timer = null;
let lastCallTime = 0;
const debounced = function(...args) {
const now = Date.now();
const timeSinceLastCall = now - lastCallTime;
const callNow = options.leading && !timer;
if (timer) clearTimeout(timer);
if (callNow) {
lastCallTime = now;
return fn.apply(this, args);
}
timer = setTimeout(() => {
lastCallTime = Date.now();
timer = null;
if (!options.leading) {
fn.apply(this, args);
}
}, delay);
};
debounced.cancel = () => {
if (timer) {
clearTimeout(timer);
timer = null;
}
};
debounced.flush = () => {
if (timer) {
clearTimeout(timer);
fn.apply(this, arguments);
timer = null;
}
};
return debounced;
}
// 高级节流函数
function throttle(fn, interval, options = {}) {
let lastCallTime = 0;
let timer = null;
const throttled = function(...args) {
const now = Date.now();
const timeSinceLastCall = now - lastCallTime;
const callNow = options.leading !== false && !lastCallTime;
const remainingTime = interval - timeSinceLastCall;
if (callNow || remainingTime <= 0) {
if (timer) {
clearTimeout(timer);
timer = null;
}
lastCallTime = now;
return fn.apply(this, args);
}
if (!timer && options.trailing !== false) {
timer = setTimeout(() => {
lastCallTime = options.leading === false ? 0 : Date.now();
timer = null;
fn.apply(this, args);
}, remainingTime);
}
};
throttled.cancel = () => {
if (timer) {
clearTimeout(timer);
timer = null;
}
lastCallTime = 0;
};
return throttled;
}
// React Hooks版本
const useDebounce = (value, delay) => {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
};
const useThrottle = (value, interval) => {
const [throttledValue, setThrottledValue] = useState(value);
const lastUpdated = useRef(Date.now());
useEffect(() => {
const now = Date.now();
const timeSinceLastUpdate = now - lastUpdated.current;
if (timeSinceLastUpdate >= interval) {
lastUpdated.current = now;
setThrottledValue(value);
} else {
const timer = setTimeout(() => {
lastUpdated.current = Date.now();
setThrottledValue(value);
}, interval - timeSinceLastUpdate);
return () => clearTimeout(timer);
}
}, [value, interval]);
return throttledValue;
}; -
智能缓存策略
// 内存缓存管理器
class MemoryCache {
constructor(maxSize = 100, ttl = 5 * 60 * 1000) {
this.cache = new Map();
this.maxSize = maxSize;
this.ttl = ttl;
}
set(key, value, customTTL) {
const expiry = Date.now() + (customTTL || this.ttl);
// 如果缓存已满,删除最旧的项
if (this.cache.size >= this.maxSize) {
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
this.cache.set(key, { value, expiry });
}
get(key) {
const item = this.cache.get(key);
if (!item) return null;
if (Date.now() > item.expiry) {
this.cache.delete(key);
return null;
}
// 更新访问时间(LRU策略)
this.cache.delete(key);
this.cache.set(key, item);
return item.value;
}
clear() {
this.cache.clear();
}
size() {
return this.cache.size;
}
}
// API缓存Hook
const useApiCache = () => {
const cache = useRef(new MemoryCache());
const cachedFetch = useCallback(async (url, options = {}) => {
const cacheKey = `${url}${JSON.stringify(options)}`;
// 检查缓存
const cachedData = cache.current.get(cacheKey);
if (cachedData && !options.forceRefresh) {
return cachedData;
}
try {
const response = await fetch(url, options);
const data = await response.json();
// 缓存成功响应
if (response.ok) {
cache.current.set(cacheKey, data, options.cacheTTL);
}
return data;
} catch (error) {
// 如果有缓存数据,在网络错误时返回缓存
const fallbackData = cache.current.get(cacheKey);
if (fallbackData) {
return fallbackData;
}
throw error;
}
}, []);
return { cachedFetch, clearCache: () => cache.current.clear() };
}; -
减少重绘和回流
// DOM批量操作工具
const batchDOMUpdates = (() => {
let pending = [];
let isScheduled = false;
const flush = () => {
const updates = pending.slice();
pending = [];
isScheduled = false;
// 使用DocumentFragment减少重绘
const fragment = document.createDocumentFragment();
updates.forEach(update => {
if (typeof update === 'function') {
update();
}
});
};
return (updateFn) => {
pending.push(updateFn);
if (!isScheduled) {
isScheduled = true;
requestAnimationFrame(flush);
}
};
})();
// 优化的样式更新
const updateStyles = (element, styles) => {
batchDOMUpdates(() => {
// 一次性更新所有样式
Object.assign(element.style, styles);
});
};
// 虚拟滚动优化
const useOptimizedScroll = (callback, deps = []) => {
const ticking = useRef(false);
const optimizedCallback = useCallback((...args) => {
if (!ticking.current) {
requestAnimationFrame(() => {
callback(...args);
ticking.current = false;
});
ticking.current = true;
}
}, deps);
return optimizedCallback;
};
依赖管理
版本控制策略
-
语义化版本控制:遵循SemVer规范
{
"dependencies": {
"react": "^18.2.0", // 兼容性更新 (18.2.0 <= version < 19.0.0)
"lodash": "~4.17.21", // 补丁更新 (4.17.21 <= version < 4.18.0)
"axios": "1.6.0", // 精确版本
"@types/node": ">=18.0.0", // 最小版本
"typescript": "^5.0.0"
},
"devDependencies": {
"eslint": "~8.50.0",
"prettier": "^3.0.0"
},
"peerDependencies": {
"react": ">=16.8.0", // 宿主环境依赖
"react-dom": ">=16.8.0"
},
"optionalDependencies": {
"fsevents": "^2.3.0" // 可选依赖(macOS文件监听)
}
} -
智能锁定文件管理
# npm 命令
npm ci # 使用锁定文件安装(CI环境推荐)
npm install --package-lock-only # 只更新锁定文件
npm audit # 安全审计
npm audit fix # 自动修复漏洞
npm outdated # 检查过期依赖
npm ls # 查看依赖树
# yarn 命令
yarn install --frozen-lockfile # 严格按锁定文件安装
yarn audit # 安全审计
yarn upgrade-interactive # 交互式升级
yarn why package-name # 分析依赖原因
# pnpm 命令
pnpm install --frozen-lockfile # 使用锁定文件
pnpm audit # 安全审计
pnpm update --interactive # 交互式更新
pnpm why package-name # 依赖分析 -
自动化依赖更新
// renovate.json - Renovate Bot配置
{
"extends": ["config:base"],
"schedule": ["before 4am on Monday"],
"timezone": "Asia/Shanghai",
"packageRules": [
{
"matchPackagePatterns": ["^@types/"],
"groupName": "definitelyTyped",
"automerge": true,
"schedule": ["at any time"]
},
{
"matchDepTypes": ["devDependencies"],
"updateTypes": ["patch", "minor"],
"automerge": true
},
{
"matchPackageNames": ["react", "react-dom"],
"groupName": "react",
"reviewers": ["team:frontend"],
"schedule": ["before 10am on Monday"]
},
{
"matchPackageNames": ["typescript"],
"schedule": ["before 10am on first day of month"]
}
],
"vulnerabilityAlerts": {
"enabled": true,
"automerge": true
},
"lockFileMaintenance": {
"enabled": true,
"schedule": ["before 4am on Monday"]
}
}
// dependabot.yml - GitHub Dependabot配置
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
time: "04:00"
open-pull-requests-limit: 10
reviewers:
- "frontend-team"
assignees:
- "tech-lead"
commit-message:
prefix: "deps"
include: "scope" -
版本范围最佳实践
// 依赖版本策略配置
const versionStrategies = {
// 核心框架 - 保守更新策略
frameworks: {
"react": "~18.2.0", // 只接受补丁更新
"vue": "~3.3.0",
"angular": "~16.0.0",
"next": "~13.5.0"
},
// 工具库 - 兼容性更新策略
utilities: {
"lodash": "^4.17.21", // 接受兼容性更新
"date-fns": "^2.30.0",
"ramda": "^0.29.0",
"axios": "^1.6.0"
},
// 类型定义 - 激进更新策略
types: {
"@types/react": "^18.0.0", // 类型定义可以更激进
"@types/node": "^20.0.0",
"@types/lodash": "^4.14.0"
},
// 开发工具 - 最新版本策略
devTools: {
"eslint": "^8.50.0",
"prettier": "^3.0.0",
"typescript": "^5.0.0",
"vite": "^4.5.0"
},
// 测试工具 - 稳定版本策略
testing: {
"jest": "~29.7.0",
"@testing-library/react": "^13.4.0",
"cypress": "~13.6.0"
}
};
依赖优化
-
依赖分析工具集
# 安装分析工具
npm install -g npm-check-updates depcheck webpack-bundle-analyzer
npm install -g cost-of-modules bundlephobia-cli
# 检查未使用的依赖
depcheck
# 检查可更新的依赖
ncu
ncu -u # 更新package.json
# 分析依赖成本
cost-of-modules
bundlephobia lodash
# Bundle分析
npx webpack-bundle-analyzer dist/static/js/*.js -
Bundle优化配置
// webpack.config.js - 高级分包策略
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const { DuplicatePackageCheckerPlugin } = require('duplicate-package-checker-webpack-plugin');
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
minSize: 20000,
maxSize: 244000,
cacheGroups: {
// 第三方库基础包
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
priority: 10,
reuseExistingChunk: true
},
// React生态系统
react: {
test: /[\\/]node_modules[\\/](react|react-dom|react-router)[\\/]/,
name: 'react',
chunks: 'all',
priority: 20
},
// UI组件库
ui: {
test: /[\\/]node_modules[\\/](antd|@ant-design|element-ui|@mui)[\\/]/,
name: 'ui',
chunks: 'all',
priority: 15
},
// 工具库
utils: {
test: /[\\/]node_modules[\\/](lodash|date-fns|moment|dayjs)[\\/]/,
name: 'utils',
chunks: 'all',
priority: 12
},
// 公共代码
common: {
name: 'common',
minChunks: 2,
chunks: 'all',
priority: 5,
reuseExistingChunk: true
}
}
},
// 运行时代码单独提取
runtimeChunk: {
name: entrypoint => `runtime-${entrypoint.name}`
}
},
plugins: [
// Bundle分析
process.env.ANALYZE && new BundleAnalyzerPlugin({
analyzerMode: 'static',
openAnalyzer: false,
reportFilename: 'bundle-report.html',
generateStatsFile: true,
statsFilename: 'bundle-stats.json'
}),
// 重复包检测
new DuplicatePackageCheckerPlugin({
verbose: true,
emitError: false,
showHelp: false,
strict: false
})
].filter(Boolean)
}; -
Tree Shaking优化
// package.json - 副作用标记
{
"sideEffects": [
"*.css",
"*.scss",
"*.less",
"./src/polyfills.js",
"./src/global-styles.js",
"./src/i18n/index.js"
]
}
// 正确的导入方式
// ❌ 错误:导入整个库
import _ from 'lodash';
import * as _ from 'lodash';
import { Button } from 'antd';
// ✅ 正确:按需导入
import { debounce, throttle } from 'lodash';
import debounce from 'lodash/debounce';
import Button from 'antd/es/button';
import 'antd/es/button/style/css';
// babel-plugin-import配置
// .babelrc
{
"plugins": [
[
"import",
{
"libraryName": "antd",
"libraryDirectory": "es",
"style": "css"
},
"antd"
],
[
"import",
{
"libraryName": "lodash",
"libraryDirectory": "",
"camel2DashComponentName": false
},
"lodash"
],
[
"import",
{
"libraryName": "date-fns",
"libraryDirectory": "",
"camel2DashComponentName": false
},
"date-fns"
]
]
} -
按需加载策略
// 路由级别代码分割
const HomePage = lazy(() => import('./pages/Home'));
const AboutPage = lazy(() => import('./pages/About'));
const UserPage = lazy(() =>
import('./pages/User').then(module => ({
default: module.UserPage
}))
);
// 组件级别按需加载
const LazyChart = lazy(() => {
return Promise.all([
import('chart.js'),
import('./components/Chart')
]).then(([chartJs, Chart]) => ({
default: Chart.default
}));
});
// 条件加载polyfills
const loadPolyfills = async () => {
const polyfills = [];
if (!window.IntersectionObserver) {
polyfills.push(import('intersection-observer'));
}
if (!window.fetch) {
polyfills.push(import('whatwg-fetch'));
}
if (!Array.prototype.includes) {
polyfills.push(import('core-js/features/array/includes'));
}
await Promise.all(polyfills);
};
// 动态导入工具函数
const loadUtils = {
async loadDateUtils() {
const { format, parseISO, addDays } = await import('date-fns');
return { format, parseISO, addDays };
},
async loadChartLibrary() {
const [{ Chart }, { registerables }] = await Promise.all([
import('chart.js'),
import('chart.js/auto')
]);
Chart.register(...registerables);
return Chart;
},
async loadMarkdownParser() {
const [{ marked }, { highlight }] = await Promise.all([
import('marked'),
import('highlight.js')
]);
marked.setOptions({ highlight });
return marked;
}
}; -
重复依赖检测与优化
// 依赖重复检测脚本
const fs = require('fs');
const path = require('path');
function findDuplicates(lockFile) {
const dependencies = {};
const duplicates = {};
// 解析lock文件
Object.keys(lockFile.dependencies || {}).forEach(name => {
const version = lockFile.dependencies[name].version;
if (!dependencies[name]) {
dependencies[name] = [];
}
dependencies[name].push(version);
});
// 找出重复依赖
Object.keys(dependencies).forEach(name => {
const versions = [...new Set(dependencies[name])];
if (versions.length > 1) {
duplicates[name] = versions;
}
});
return duplicates;
}
// webpack配置中的重复依赖解决
module.exports = {
resolve: {
alias: {
// 强制使用单一版本
'react': path.resolve('./node_modules/react'),
'react-dom': path.resolve('./node_modules/react-dom'),
'lodash': path.resolve('./node_modules/lodash')
}
}
};
安全管理
-
依赖安全扫描
# npm内置安全审计
npm audit
npm audit fix
npm audit fix --force
# 使用snyk进行深度安全扫描
npm install -g snyk
snyk auth
snyk test
snyk monitor
snyk wizard # 交互式修复
# 使用retire.js检查已知漏洞
npm install -g retire
retire --js --node
# 使用audit-ci在CI中进行安全检查
npm install -g audit-ci
audit-ci --moderate -
许可证合规检查
// license-checker配置
const licenseChecker = require('license-checker');
const allowedLicenses = [
'MIT',
'Apache-2.0',
'BSD-2-Clause',
'BSD-3-Clause',
'ISC',
'CC0-1.0',
'Unlicense'
];
licenseChecker.init({
start: './node_modules',
production: true,
excludePrivatePackages: true,
onlyAllow: allowedLicenses.join(';'),
failOn: 'GPL;AGPL;LGPL;CPAL;EPL;MPL;APSL;Ruby;Artistic;OFL'
}, (err, packages) => {
if (err) {
console.error('License check failed:', err);
process.exit(1);
}
console.log('✅ All licenses are compliant');
// 生成许可证报告
const report = Object.keys(packages).map(pkg => ({
package: pkg,
license: packages[pkg].licenses,
repository: packages[pkg].repository
}));
fs.writeFileSync('license-report.json', JSON.stringify(report, null, 2));
});
代码质量与规范
编码规范
-
统一代码风格配置
// .eslintrc.js - ESLint配置
module.exports = {
extends: [
'eslint:recommended',
'@typescript-eslint/recommended',
'plugin:react/recommended',
'plugin:react-hooks/recommended',
'plugin:jsx-a11y/recommended',
'plugin:import/recommended',
'plugin:import/typescript',
'prettier'
],
plugins: [
'@typescript-eslint',
'react',
'react-hooks',
'jsx-a11y',
'import',
'unused-imports'
],
rules: {
// 代码质量规则
'no-console': 'warn',
'no-debugger': 'error',
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': 'error',
'unused-imports/no-unused-imports': 'error',
// React规则
'react/prop-types': 'off',
'react/react-in-jsx-scope': '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
}
}
],
// 复杂度控制
'complexity': ['warn', 10],
'max-depth': ['warn', 4],
'max-lines-per-function': ['warn', 50]
},
settings: {
react: {
version: 'detect'
},
'import/resolver': {
typescript: {
alwaysTryTypes: true
}
}
}
};
// .prettierrc.js - Prettier配置
module.exports = {
semi: true,
trailingComma: 'es5',
singleQuote: true,
printWidth: 80,
tabWidth: 2,
useTabs: false,
bracketSpacing: true,
bracketSameLine: false,
arrowParens: 'avoid',
endOfLine: 'lf',
quoteProps: 'as-needed',
jsxSingleQuote: true,
proseWrap: 'preserve'
}; -
命名规范标准
// 变量和函数命名 - camelCase
const userName = 'john';
const isLoggedIn = true;
const getUserProfile = () => {};
// 常量命名 - SCREAMING_SNAKE_CASE
const API_BASE_URL = 'https://api.example.com';
const MAX_RETRY_COUNT = 3;
// 类命名 - PascalCase
class UserService {
private apiClient: ApiClient;
constructor(apiClient: ApiClient) {
this.apiClient = apiClient;
}
}
// 接口命名 - PascalCase with 'I' prefix (optional)
interface IUserRepository {
findById(id: string): Promise<User>;
}
// 类型别名 - PascalCase
type UserRole = 'admin' | 'user' | 'guest';
// 枚举命名 - PascalCase
enum HttpStatus {
OK = 200,
NOT_FOUND = 404,
INTERNAL_SERVER_ERROR = 500
}
// React组件 - PascalCase
const UserProfile: React.FC<UserProfileProps> = ({ user }) => {
return <div>{user.name}</div>;
};
// Hook命名 - use前缀 + camelCase
const useUserData = (userId: string) => {
const [user, setUser] = useState<User | null>(null);
// ...
return { user, loading, error };
};
// 文件命名规范
// 组件文件: UserProfile.tsx, UserProfile.test.tsx
// Hook文件: useUserData.ts, useUserData.test.ts
// 工具文件: dateUtils.ts, apiClient.ts
// 类型文件: user.types.ts, api.types.ts -
注释规范与文档
/**
* 用户服务类,负责处理用户相关的业务逻辑
* @example
* ```typescript
* const userService = new UserService(apiClient);
* const user = await userService.getUserById('123');
* ```
*/
class UserService {
/**
* 根据用户ID获取用户信息
* @param userId - 用户唯一标识符
* @returns Promise<User> 用户信息
* @throws {UserNotFoundError} 当用户不存在时抛出
* @throws {NetworkError} 当网络请求失败时抛出
*/
async getUserById(userId: string): Promise<User> {
// 参数验证
if (!userId || typeof userId !== 'string') {
throw new Error('Invalid user ID');
}
try {
// 发起API请求
const response = await this.apiClient.get(`/users/${userId}`);
return response.data;
} catch (error) {
// 错误处理和日志记录
console.error(`Failed to fetch user ${userId}:`, error);
throw error;
}
}
/**
* 批量获取用户信息
* TODO: 添加缓存机制以提高性能
* FIXME: 处理大量用户ID时可能导致请求超时
*/
async getUsersByIds(userIds: string[]): Promise<User[]> {
// 实现逻辑...
}
}
// React组件注释
interface UserCardProps {
/** 用户信息对象 */
user: User;
/** 是否显示详细信息,默认为false */
showDetails?: boolean;
/** 点击用户卡片时的回调函数 */
onClick?: (user: User) => void;
}
/**
* 用户卡片组件
* 用于展示用户基本信息,支持点击交互
*/
const UserCard: React.FC<UserCardProps> = ({
user,
showDetails = false,
onClick
}) => {
// 组件实现...
}; -
TypeScript类型安全
// 严格的TypeScript配置 - tsconfig.json
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"exactOptionalPropertyTypes": true,
"noUncheckedIndexedAccess": true
}
}
// 类型定义最佳实践
// 基础类型定义
interface User {
readonly id: string;
name: string;
email: string;
role: UserRole;
createdAt: Date;
updatedAt: Date;
}
// 联合类型
type UserRole = 'admin' | 'moderator' | 'user';
type Theme = 'light' | 'dark' | 'auto';
// 泛型类型
interface ApiResponse<T> {
data: T;
message: string;
status: number;
}
// 条件类型
type NonNullable<T> = T extends null | undefined ? never : T;
// 工具类型的使用
type CreateUserRequest = Omit<User, 'id' | 'createdAt' | 'updatedAt'>;
type UpdateUserRequest = Partial<Pick<User, 'name' | 'email'>>;
// 类型守卫
function isUser(obj: unknown): obj is User {
return (
typeof obj === 'object' &&
obj !== null &&
'id' in obj &&
'name' in obj &&
'email' in obj
);
}
// 断言函数
function assertIsUser(obj: unknown): asserts obj is User {
if (!isUser(obj)) {
throw new Error('Object is not a valid User');
}
} -
错误处理机制
// 自定义错误类
class AppError extends Error {
constructor(
public message: string,
public code: string,
public statusCode: number = 500
) {
super(message);
this.name = this.constructor.name;
Error.captureStackTrace(this, this.constructor);
}
}
class ValidationError extends AppError {
constructor(message: string, public field?: string) {
super(message, 'VALIDATION_ERROR', 400);
}
}
class NotFoundError extends AppError {
constructor(resource: string, id: string) {
super(`${resource} with id ${id} not found`, 'NOT_FOUND', 404);
}
}
// Result模式
type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E };
async function safeApiCall<T>(apiCall: () => Promise<T>): Promise<Result<T>> {
try {
const data = await apiCall();
return { success: true, data };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error : new Error(String(error))
};
}
}
// 使用示例
const result = await safeApiCall(() => userService.getUserById('123'));
if (result.success) {
console.log(result.data.name);
} else {
console.error(result.error.message);
}
// React错误边界
class ErrorBoundary extends React.Component<
React.PropsWithChildren<{}>,
{ hasError: boolean; error?: Error }
> {
constructor(props: React.PropsWithChildren<{}>) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error: Error) {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error('Error caught by boundary:', error, errorInfo);
// 发送错误报告到监控服务
}
render() {
if (this.state.hasError) {
return (
<div className="error-fallback">
<h2>Something went wrong</h2>
<p>{this.state.error?.message}</p>
</div>
);
}
return this.props.children;
}
}
代码审查
-
审查流程标准化
# .github/pull_request_template.md
## 变更描述
<!-- 简要描述本次变更的内容和目的 -->
## 变更类型
- [ ] 🐛 Bug修复
- [ ] ✨ 新功能
- [ ] 💄 UI/样式更新
- [ ] ♻️ 代码重构
- [ ] 📝 文档更新
- [ ] 🔧 配置变更
- [ ] ⚡ 性能优化
- [ ] 🔒 安全修复
## 测试
- [ ] 单元测试已通过
- [ ] 集成测试已通过
- [ ] 手动测试已完成
- [ ] 回归测试已完成
## 检查清单
- [ ] 代码遵循项目编码规范
- [ ] 已添加必要的注释和文档
- [ ] 没有引入新的技术债务
- [ ] 性能影响已评估
- [ ] 安全影响已评估
- [ ] 向后兼容性已考虑
## 相关链接
- 相关Issue: #
- 设计文档:
- 测试报告: -
自动化代码检查
# .github/workflows/code-quality.yml
name: Code Quality
on:
pull_request:
branches: [main, develop]
jobs:
code-quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Lint check
run: npm run lint
- name: Type check
run: npm run type-check
- name: Format check
run: npm run format:check
- name: Test coverage
run: npm run test:coverage
- name: Build check
run: npm run build
- name: Bundle size check
run: npm run bundle-size
- name: Security audit
run: npm audit --audit-level moderate -
代码审查指南
# 代码审查指南
## 审查重点
### 1. 功能正确性
- 代码是否实现了预期功能
- 边界条件是否正确处理
- 错误处理是否完善
### 2. 代码质量
- 代码是否清晰易读
- 是否遵循SOLID原则
- 是否有代码重复
- 复杂度是否合理
### 3. 性能考虑
- 是否有性能瓶颈
- 内存使用是否合理
- 是否有不必要的计算
### 4. 安全性
- 输入验证是否充分
- 是否有安全漏洞
- 敏感信息是否正确处理
### 5. 可维护性
- 代码结构是否合理
- 是否易于扩展
- 文档是否充分
## 反馈原则
### 建设性反馈
- 具体指出问题所在
- 提供改进建议
- 解释问题的影响
### 示例
❌ "这段代码不好"
✅ "这个函数复杂度较高(15),建议拆分为多个小函数以提高可读性和可测试性"
❌ "性能有问题"
✅ "这里使用了O(n²)算法,在数据量大时可能影响性能,建议使用Map来优化查找"
团队协作与工作流
分支策略
- Git Flow 工作流
# Git Flow 命令示例
# 初始化 Git Flow
git flow init
# 开始新功能开发
git flow feature start user-authentication
# 完成功能开发
git flow feature finish user-authentication
# 开始发布准备
git flow release start v1.2.0
# 完成发布
git flow release finish v1.2.0
# 紧急修复
git flow hotfix start critical-bug
git flow hotfix finish critical-bug
-
GitHub Flow 工作流
# .github/workflows/github-flow.yml
name: GitHub Flow
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run tests
run: npm test
deploy:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- name: Deploy to production
run: echo "Deploying to production" -
分支保护规则配置
{
"required_status_checks": {
"strict": true,
"contexts": [
"ci/tests",
"ci/lint",
"ci/type-check",
"ci/security-scan"
]
},
"enforce_admins": true,
"required_pull_request_reviews": {
"required_approving_review_count": 2,
"dismiss_stale_reviews": true,
"require_code_owner_reviews": true,
"require_last_push_approval": true
},
"restrictions": {
"users": [],
"teams": ["core-team"]
},
"allow_force_pushes": false,
"allow_deletions": false
}
协作工具配置
-
版本控制最佳实践
# .gitconfig 全局配置
[user]
name = Your Name
email = your.email@company.com
[core]
editor = code --wait
autocrlf = input
ignorecase = false
[pull]
rebase = true
[push]
default = current
autoSetupRemote = true
[alias]
co = checkout
br = branch
ci = commit
st = status
unstage = reset HEAD --
last = log -1 HEAD
visual = !gitk
# 美化日志
lg = log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit
# 交互式rebase
rb = rebase -i
# 快速提交
ac = !git add -A && git commit -m
# 清理已合并分支
cleanup = !git branch --merged | grep -v "\*\|main\|develop" | xargs -n 1 git branch -d -
项目管理工具集成
# .github/workflows/project-automation.yml
name: Project Automation
on:
issues:
types: [opened, closed]
pull_request:
types: [opened, closed, merged]
jobs:
update-project:
runs-on: ubuntu-latest
steps:
- name: Add to project
uses: actions/add-to-project@v0.4.0
with:
project-url: https://github.com/orgs/company/projects/1
github-token: ${{ secrets.ADD_TO_PROJECT_PAT }}
- name: Update Jira
if: contains(github.event.pull_request.title, 'JIRA-')
run: |
# 提取JIRA票号并更新状态
JIRA_KEY=$(echo "${{ github.event.pull_request.title }}" | grep -o 'JIRA-[0-9]\+')
curl -X POST \
-H "Authorization: Basic ${{ secrets.JIRA_AUTH }}" \
-H "Content-Type: application/json" \
"https://company.atlassian.net/rest/api/2/issue/$JIRA_KEY/transitions" \
-d '{"transition":{"id":"31"}}' -
沟通协作工具配置
// slack-integration.js - Slack集成
const { WebClient } = require('@slack/web-api');
class SlackNotifier {
constructor(token) {
this.client = new WebClient(token);
}
async notifyDeployment(environment, version, status) {
const color = status === 'success' ? 'good' : 'danger';
const emoji = status === 'success' ? '✅' : '❌';
await this.client.chat.postMessage({
channel: '#deployments',
attachments: [{
color,
fields: [
{
title: 'Environment',
value: environment,
short: true
},
{
title: 'Version',
value: version,
short: true
},
{
title: 'Status',
value: `${emoji} ${status}`,
short: true
}
],
footer: 'CI/CD Pipeline',
ts: Math.floor(Date.now() / 1000)
}]
});
}
async notifyCodeReview(prUrl, author, reviewers) {
await this.client.chat.postMessage({
channel: '#code-reviews',
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: `🔍 *New Pull Request* by ${author}\n<${prUrl}|View PR>`
}
},
{
type: 'section',
text: {
type: 'mrkdwn',
text: `*Reviewers:* ${reviewers.map(r => `<@${r}>`).join(', ')}`
}
}
]
});
}
}
CI/CD工作流
-
完整的CI/CD流程
-
高级CI/CD配置
# .github/workflows/advanced-cicd.yml
name: Advanced CI/CD Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
env:
NODE_VERSION: '18'
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
# 代码质量检查
quality-gate:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16, 18, 20]
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # 获取完整历史用于SonarQube分析
- name: Setup Node.js ${{ matrix.node-version }}
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 -- --format=json --output-file=eslint-report.json
- name: Run type checking
run: npm run type-check
- name: Run tests with coverage
run: npm run test:coverage -- --reporter=json --outputFile=test-results.json
- name: SonarQube Scan
uses: sonarqube-quality-gate-action@master
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
- name: Upload test results
uses: actions/upload-artifact@v3
with:
name: test-results-${{ matrix.node-version }}
path: |
test-results.json
coverage/
eslint-report.json
# 安全扫描
security-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
scan-ref: '.'
format: 'sarif'
output: 'trivy-results.sarif'
- name: Upload Trivy scan results
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: 'trivy-results.sarif'
- name: Run Snyk security scan
uses: snyk/actions/node@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
args: --severity-threshold=high
# 构建和发布
build-and-publish:
needs: [quality-gate, security-scan]
runs-on: ubuntu-latest
outputs:
image: ${{ steps.image.outputs.image }}
version: ${{ steps.version.outputs.version }}
steps:
- uses: actions/checkout@v4
- name: Generate version
id: version
run: |
if [[ ${{ github.ref }} == 'refs/heads/main' ]]; then
VERSION=$(date +%Y%m%d)-${GITHUB_SHA::8}
else
VERSION=dev-${GITHUB_SHA::8}
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
- 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
npm run build:storybook
- name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.version }}
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
cache-from: type=gha
cache-to: type=gha,mode=max
- id: image
run: echo "image=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.version }}" >> $GITHUB_OUTPUT
# 部署到Staging
deploy-staging:
needs: build-and-publish
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/develop'
environment: staging
steps:
- name: Deploy to Kubernetes
run: |
kubectl set image deployment/app app=${{ needs.build-and-publish.outputs.image }} -n staging
kubectl rollout status deployment/app -n staging --timeout=300s
- name: Run smoke tests
run: |
npm run test:smoke -- --baseUrl=https://staging.example.com
- name: Notify Slack
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
channel: '#deployments'
webhook_url: ${{ secrets.SLACK_WEBHOOK }}
# 部署到生产
deploy-production:
needs: build-and-publish
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
environment: production
steps:
- name: Blue-Green Deployment
run: |
# 部署到绿色环境
kubectl set image deployment/app-green app=${{ needs.build-and-publish.outputs.image }} -n production
kubectl rollout status deployment/app-green -n production --timeout=300s
# 健康检查
kubectl run health-check --image=curlimages/curl --rm -i --restart=Never -- \
curl -f http://app-green-service:8080/health
# 切换流量
kubectl patch service app-service -p '{"spec":{"selector":{"version":"green"}}}' -n production
# 等待并验证
sleep 30
kubectl run smoke-test --image=curlimages/curl --rm -i --restart=Never -- \
curl -f https://api.example.com/health
- name: Rollback on failure
if: failure()
run: |
kubectl patch service app-service -p '{"spec":{"selector":{"version":"blue"}}}' -n production
- name: Update monitoring
run: |
# 更新Grafana仪表板
curl -X POST "$GRAFANA_URL/api/annotations" \
-H "Authorization: Bearer $GRAFANA_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"text": "Deployed version ${{ needs.build-and-publish.outputs.version }}",
"tags": ["deployment", "production"]
}'
安全最佳实践
代码安全
-
XSS攻击防护
// XSS防护工具类
class XSSProtection {
// HTML实体编码
static escapeHtml(unsafe: string): string {
return unsafe
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
// URL编码
static escapeUrl(url: string): string {
return encodeURIComponent(url);
}
// JavaScript字符串转义
static escapeJs(unsafe: string): string {
return unsafe
.replace(/\\/g, '\\\\')
.replace(/'/g, "\\'")
.replace(/"/g, '\\"')
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r')
.replace(/\t/g, '\\t');
}
}
// React组件中的安全实践
import DOMPurify from 'dompurify';
interface SafeHtmlProps {
html: string;
allowedTags?: string[];
}
const SafeHtml: React.FC<SafeHtmlProps> = ({ html, allowedTags = [] }) => {
const sanitizedHtml = DOMPurify.sanitize(html, {
ALLOWED_TAGS: allowedTags,
ALLOWED_ATTR: ['href', 'target', 'rel'],
ADD_ATTR: ['target'],
ADD_TAGS: ['iframe'],
FORBID_ATTR: ['style', 'onerror', 'onload']
});
return (
<div
dangerouslySetInnerHTML={{ __html: sanitizedHtml }}
className="safe-content"
/>
);
};
// 安全的用户输入处理
const UserComment: React.FC<{ comment: string }> = ({ comment }) => {
// 自动转义,安全显示用户输入
return <div className="user-comment">{comment}</div>;
};
// 链接安全处理
const SafeLink: React.FC<{ href: string; children: React.ReactNode }> = ({
href,
children
}) => {
const isExternalLink = href.startsWith('http') && !href.includes(window.location.hostname);
return (
<a
href={href}
target={isExternalLink ? '_blank' : '_self'}
rel={isExternalLink ? 'noopener noreferrer' : undefined}
onClick={(e) => {
// 验证URL安全性
if (href.startsWith('javascript:') || href.startsWith('data:')) {
e.preventDefault();
console.warn('Blocked potentially dangerous URL:', href);
}
}}
>
{children}
</a>
);
}; -
CSRF攻击防护
// CSRF Token管理
class CSRFProtection {
private static tokenKey = 'csrf-token';
// 获取CSRF Token
static getToken(): string {
return document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '';
}
// 验证CSRF Token
static validateToken(token: string): boolean {
return token === this.getToken() && token.length > 0;
}
// 为请求添加CSRF Token
static addTokenToHeaders(headers: Record<string, string> = {}): Record<string, string> {
return {
...headers,
'X-CSRF-Token': this.getToken(),
'X-Requested-With': 'XMLHttpRequest'
};
}
}
// API客户端集成CSRF保护
class SecureApiClient {
private baseURL: string;
constructor(baseURL: string) {
this.baseURL = baseURL;
}
async request<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
const url = `${this.baseURL}${endpoint}`;
// 为POST、PUT、DELETE请求添加CSRF保护
const method = options.method?.toUpperCase() || 'GET';
const needsCSRFProtection = ['POST', 'PUT', 'DELETE', 'PATCH'].includes(method);
const headers = {
'Content-Type': 'application/json',
...options.headers,
...(needsCSRFProtection ? CSRFProtection.addTokenToHeaders() : {})
};
const response = await fetch(url, {
...options,
headers,
credentials: 'same-origin' // 确保发送cookies
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
}
}
// React Hook for CSRF protection
const useCSRFProtection = () => {
const [token, setToken] = useState<string>('');
useEffect(() => {
const csrfToken = CSRFProtection.getToken();
setToken(csrfToken);
// 监听token变化
const observer = new MutationObserver(() => {
const newToken = CSRFProtection.getToken();
if (newToken !== token) {
setToken(newToken);
}
});
const metaTag = document.querySelector('meta[name="csrf-token"]');
if (metaTag) {
observer.observe(metaTag, { attributes: true });
}
return () => observer.disconnect();
}, [token]);
return { token, isValid: token.length > 0 };
}; -
输入验证与注入攻击防护
// 输入验证工具
class InputValidator {
// 邮箱验证
static isValidEmail(email: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email) && email.length <= 254;
}
// URL验证
static isValidUrl(url: string): boolean {
try {
const urlObj = new URL(url);
return ['http:', 'https:'].includes(urlObj.protocol);
} catch {
return false;
}
}
// SQL注入防护 - 参数化查询示例
static sanitizeForQuery(input: string): string {
return input.replace(/[';"\\]/g, '');
}
// NoSQL注入防护
static sanitizeMongoInput(input: any): any {
if (typeof input === 'string') {
return input.replace(/[${}]/g, '');
}
if (typeof input === 'object' && input !== null) {
const sanitized: any = {};
for (const [key, value] of Object.entries(input)) {
if (!key.startsWith('$')) {
sanitized[key] = this.sanitizeMongoInput(value);
}
}
return sanitized;
}
return input;
}
// 文件上传验证
static validateFileUpload(file: File): { valid: boolean; error?: string } {
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'application/pdf'];
const maxSize = 5 * 1024 * 1024; // 5MB
if (!allowedTypes.includes(file.type)) {
return { valid: false, error: 'File type not allowed' };
}
if (file.size > maxSize) {
return { valid: false, error: 'File size too large' };
}
// 检查文件扩展名
const extension = file.name.split('.').pop()?.toLowerCase();
const allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'pdf'];
if (!extension || !allowedExtensions.includes(extension)) {
return { valid: false, error: 'Invalid file extension' };
}
return { valid: true };
}
}
// React表单验证Hook
const useSecureForm = <T extends Record<string, any>>(initialValues: T) => {
const [values, setValues] = useState<T>(initialValues);
const [errors, setErrors] = useState<Partial<Record<keyof T, string>>>({});
const validateField = (name: keyof T, value: any): string | null => {
// 通用验证规则
if (typeof value === 'string') {
// 检查是否包含潜在的脚本
if (/<script|javascript:|data:/i.test(value)) {
return 'Invalid input detected';
}
// 长度限制
if (value.length > 1000) {
return 'Input too long';
}
}
// 特定字段验证
if (name === 'email' && !InputValidator.isValidEmail(value)) {
return 'Invalid email format';
}
if (name === 'url' && value && !InputValidator.isValidUrl(value)) {
return 'Invalid URL format';
}
return null;
};
const setValue = (name: keyof T, value: any) => {
const error = validateField(name, value);
setValues(prev => ({ ...prev, [name]: value }));
setErrors(prev => ({ ...prev, [name]: error || undefined }));
};
const isValid = Object.values(errors).every(error => !error);
return { values, errors, setValue, isValid };
}; -
依赖安全管理
# package.json 安全配置
{
"scripts": {
"audit": "npm audit",
"audit:fix": "npm audit fix",
"audit:ci": "npm audit --audit-level moderate",
"security:scan": "snyk test",
"security:monitor": "snyk monitor",
"deps:check": "ncu -u",
"deps:outdated": "npm outdated"
},
"overrides": {
"vulnerable-package": "^2.0.0"
}
}# .github/workflows/security.yml
name: Security Scan
on:
schedule:
- cron: '0 2 * * 1' # 每周一凌晨2点
push:
branches: [main]
jobs:
security-audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run npm audit
run: npm audit --audit-level moderate
- name: Run Snyk security scan
uses: snyk/actions/node@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
args: --severity-threshold=high
- name: Check for outdated packages
run: |
npm outdated || true
npx ncu --doctor --upgrade
部署安全
-
HTTPS和证书管理
# nginx.conf - HTTPS配置
server {
listen 80;
server_name example.com www.example.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name example.com www.example.com;
# SSL证书配置
ssl_certificate /path/to/certificate.crt;
ssl_certificate_key /path/to/private.key;
# SSL安全配置
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
# HSTS
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# 其他安全头
add_header X-Frame-Options DENY always;
add_header X-Content-Type-Options nosniff always;
add_header Referrer-Policy strict-origin-when-cross-origin always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
} -
内容安全策略(CSP)
<!-- HTML meta标签方式 -->
<meta http-equiv="Content-Security-Policy" content="
default-src 'self';
script-src 'self' 'unsafe-inline' https://cdn.example.com;
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
img-src 'self' data: https:;
font-src 'self' https://fonts.gstatic.com;
connect-src 'self' https://api.example.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
">// 动态CSP配置
class CSPManager {
private static nonces = new Set<string>();
static generateNonce(): string {
const nonce = btoa(crypto.getRandomValues(new Uint8Array(16)).join(''));
this.nonces.add(nonce);
return nonce;
}
static buildCSP(options: {
allowInlineStyles?: boolean;
allowInlineScripts?: boolean;
trustedDomains?: string[];
} = {}): string {
const {
allowInlineStyles = false,
allowInlineScripts = false,
trustedDomains = []
} = options;
const policies = [
"default-src 'self'",
`script-src 'self' ${allowInlineScripts ? "'unsafe-inline'" : ''} ${trustedDomains.join(' ')}`,
`style-src 'self' ${allowInlineStyles ? "'unsafe-inline'" : ''} https://fonts.googleapis.com`,
"img-src 'self' data: https:",
"font-src 'self' https://fonts.gstatic.com",
"connect-src 'self' https://api.example.com",
"frame-ancestors 'none'",
"base-uri 'self'",
"form-action 'self'"
];
return policies.join('; ');
}
} -
CORS配置
// Express.js CORS配置
const corsOptions = {
origin: function (origin, callback) {
const allowedOrigins = [
'https://example.com',
'https://www.example.com',
'https://app.example.com'
];
// 允许无origin的请求(如移动应用)
if (!origin) return callback(null, true);
if (allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true,
optionsSuccessStatus: 200,
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-CSRF-Token']
};
app.use(cors(corsOptions)); -
权限控制与访问管理
// 基于角色的访问控制(RBAC)
interface Permission {
resource: string;
action: string;
}
interface Role {
name: string;
permissions: Permission[];
}
interface User {
id: string;
roles: Role[];
}
class AccessControl {
static hasPermission(user: User, resource: string, action: string): boolean {
return user.roles.some(role =>
role.permissions.some(permission =>
permission.resource === resource && permission.action === action
)
);
}
static requirePermission(resource: string, action: string) {
return (target: any, propertyName: string, descriptor: PropertyDescriptor) => {
const method = descriptor.value;
descriptor.value = function (...args: any[]) {
const user = this.getCurrentUser(); // 获取当前用户
if (!AccessControl.hasPermission(user, resource, action)) {
throw new Error('Access denied');
}
return method.apply(this, args);
};
};
}
}
// React权限控制组件
const ProtectedComponent: React.FC<{
resource: string;
action: string;
fallback?: React.ReactNode;
children: React.ReactNode;
}> = ({ resource, action, fallback, children }) => {
const { user } = useAuth();
if (!user || !AccessControl.hasPermission(user, resource, action)) {
return <>{fallback || <div>Access denied</div>}</>;
}
return <>{children}</>;
};
实际案例分析
案例1:大型电商应用的前端工程化实践
项目背景:
- 用户量:千万级
- 页面数量:500+
- 团队规模:50+前端开发者
- 技术栈:React + TypeScript + Webpack
工程化方案:
详细实施方案:
-
微前端架构实现
// 主应用配置
import { registerMicroApps, start } from 'qiankun';
const microApps = [
{
name: 'product-app',
entry: '//localhost:8081',
container: '#product-container',
activeRule: '/product',
props: {
routerBase: '/product',
getGlobalState: () => globalStore.getState()
}
},
{
name: 'order-app',
entry: '//localhost:8082',
container: '#order-container',
activeRule: '/order',
props: {
routerBase: '/order',
getGlobalState: () => globalStore.getState()
}
}
];
registerMicroApps(microApps, {
beforeLoad: (app) => {
console.log('before load', app.name);
return Promise.resolve();
},
beforeMount: (app) => {
console.log('before mount', app.name);
return Promise.resolve();
},
afterMount: (app) => {
console.log('after mount', app.name);
return Promise.resolve();
}
});
start({
prefetch: 'all',
sandbox: {
strictStyleIsolation: true,
experimentalStyleIsolation: true
}
}); -
企业级组件库架构
// 组件库结构
├── packages/
│ ├── components/ # 基础组件
│ │ ├── Button/
│ │ ├── Input/
│ │ └── Table/
│ ├── business-components/ # 业务组件
│ │ ├── ProductCard/
│ │ ├── OrderList/
│ │ └── UserProfile/
│ ├── icons/ # 图标库
│ ├── themes/ # 主题系统
│ └── utils/ # 工具函数
// 组件库构建配置
// rollup.config.js
export default {
input: 'src/index.ts',
output: [
{
file: 'dist/index.esm.js',
format: 'esm',
sourcemap: true
},
{
file: 'dist/index.cjs.js',
format: 'cjs',
sourcemap: true
},
{
file: 'dist/index.umd.js',
format: 'umd',
name: 'ECommerceUI',
sourcemap: true
}
],
plugins: [
typescript(),
resolve(),
commonjs(),
postcss({
extract: true,
minimize: true
})
],
external: ['react', 'react-dom']
};
效果:
- 构建时间从30分钟降至5分钟
- 首屏加载时间优化40%
- 代码质量显著提升
- 团队协作效率提升60%
- 部署频率从周级提升到日级
案例2:中小型企业的轻量化工程化方案
项目背景:
- 团队规模:5-10人
- 项目复杂度:中等
- 技术栈:Vue 3 + Vite + TypeScript
工程化方案:
详细配置:
- Vite配置优化
// vite.config.ts
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { resolve } from 'path';
import { visualizer } from 'rollup-plugin-visualizer';
export default defineConfig({
plugins: [
vue(),
visualizer({
filename: 'dist/stats.html',
open: true,
gzipSize: true
})
],
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
'@components': resolve(__dirname, 'src/components'),
'@utils': resolve(__dirname, 'src/utils')
}
},
build: {
target: 'es2015',
outDir: 'dist',
assetsDir: 'assets',
sourcemap: process.env.NODE_ENV === 'development',
rollupOptions: {
output: {
chunkFileNames: 'js/[name]-[hash].js',
entryFileNames: 'js/[name]-[hash].js',
assetFileNames: '[ext]/[name]-[hash].[ext]',
manualChunks: {
vue: ['vue', 'vue-router', 'pinia'],
vendor: ['axios', 'dayjs']
}
}
}
}
});
效果:
- 开发效率提升50%
- 部署自动化,从手动部署到自动化部署
- 代码质量稳定,bug率降低30%
- 构建速度提升3倍(相比Webpack)
案例3:金融科技公司的安全实践
项目背景:
- 行业:金融科技
- 安全等级:高
- 合规要求:严格
- 技术栈:React + TypeScript + Node.js
安全工程化方案:
关键实施措施:
-
多层安全防护
// 安全中间件
class SecurityMiddleware {
// 请求限流
static rateLimit = rateLimit({
windowMs: 15 * 60 * 1000, // 15分钟
max: 100, // 限制每个IP 15分钟内最多100个请求
message: 'Too many requests from this IP'
});
// 安全头设置
static securityHeaders = helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
},
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
}
});
// 输入验证
static validateInput = (schema: any) => {
return (req: Request, res: Response, next: NextFunction) => {
const { error } = schema.validate(req.body);
if (error) {
return res.status(400).json({ error: error.details[0].message });
}
next();
};
};
} -
自动化安全检测
# .github/workflows/security.yml
name: Security Scan
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
schedule:
- cron: '0 2 * * 1' # 每周一凌晨2点
jobs:
security-scan:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Run Snyk to check for vulnerabilities
uses: snyk/actions/node@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
args: --severity-threshold=high
- name: Run SAST scan
uses: github/super-linter@v4
env:
DEFAULT_BRANCH: main
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VALIDATE_JAVASCRIPT_ES: true
VALIDATE_TYPESCRIPT_ES: true
- name: Run dependency check
run: |
npm audit --audit-level moderate
npx audit-ci --moderate
效果:
- 通过了多项金融行业安全认证
- 零安全事故记录
- 合规审计100%通过
- 安全漏洞发现和修复时间缩短80%
总结
前端工程化是一个持续演进的过程,需要根据项目规模、团队情况和业务需求来选择合适的方案。通过以上案例分析,我们可以总结出以下关键要点:
核心原则
最佳实践建议
-
选择合适的技术栈
- 大型项目:React/Vue + TypeScript + Webpack/Vite
- 中小型项目:Vue 3 + Vite + TypeScript
- 组件库:Rollup + TypeScript + Storybook
-
建立完善的工具链
- 代码质量:ESLint + Prettier + Husky
- 测试体系:Jest/Vitest + Testing Library + Cypress
- 构建优化:代码分割 + Tree Shaking + 压缩优化
- 部署流程:CI/CD + 容器化 + 监控告警
-
注重团队协作
- 制定编码规范和最佳实践文档
- 建立代码审查机制
- 定期技术分享和培训
- 使用统一的开发环境和工具
-
持续监控和优化
- 建立性能监控体系
- 定期进行代码质量审计
- 跟踪关键业务指标
- 及时响应和解决问题
-
安全防护措施
- 集成安全扫描工具
- 建立安全开发流程
- 定期进行安全培训
- 制定应急响应预案
实施路径
| 阶段 | 重点内容 | 预期效果 | 时间周期 |
|---|---|---|---|
| 基础阶段 | 代码规范、构建工具、版本控制 | 提升代码质量,统一开发环境 | 1-2个月 |
| 进阶阶段 | 自动化测试、CI/CD、性能优化 | 提高开发效率,保证代码质量 | 2-3个月 |
| 高级阶段 | 微前端、组件库、监控体系 | 支撑大规模开发,提升用户体验 | 3-6个月 |
| 专业阶段 | 安全防护、合规审计、智能化 | 满足企业级需求,降低风险 | 持续优化 |
通过合理的工程化实践,可以显著提升开发效率、代码质量和产品稳定性,为业务发展提供强有力的技术支撑。记住,工程化不是目的,而是手段,最终目标是为用户提供更好的产品体验。
持续学习和改进是前端工程化的关键,只有不断适应新技术和新需求,才能保持竞争力。