📚 目录
- 前端工程化概述
- Webpack深度解析
- Gulp构建实践
- Vite现代化构建
- 工程化最佳实践
- CI/CD与部署自动化
- 实战项目搭建
- 性能优化与监控
前端工程化概述
什么是前端工程化?
前端工程化是指使用工程化方法和工具来规范前端开发流程、提高开发效率、保证代码质量的综合性解决方案。
核心目标:
-
标准化:统一的开发规范和流程
-
自动化:减少重复性手工操作
-
效率化:提升开发和构建性能
-
质量化:保证代码质量和项目稳定性
工程化体系:
graph TD
A[前端工程化] --> B[开发工具链]
A --> C[构建工具]
A --> D[代码规范]
A --> E[自动化测试]
A --> F[部署流程]
B --> B1[代码编辑器]
B --> B2[调试工具]
B --> B3[版本控制]
C --> C1[Webpack]
C --> C2[gulp]
C --> C3[Vite]
D --> D1[ESLint]
D --> D2[Prettier]
D --> D3[Git Hooks]
E --> E1[单元测试]
E --> E2[E2E测试]
E --> E3[性能测试]
F --> F1[CI/CD]
F --> F2[容器化]
F --> F3[监控]
工程化发展历程
// 传统开发方式(手工时代)
function manualDevelopment() {
// 手动下载依赖库
const jquery = downloadFromCDN('jquery.min.js');
const bootstrap = downloadFromCDN('bootstrap.min.js');
// 手动合并文件
const combinedJS = concatenateFiles([jquery, bootstrap, 'app.js']);
// 手动压缩
const minifiedJS = minify(combinedJS);
// 手动上传到服务器
uploadToServer(minifiedJS);
}
// 工程化开发方式(自动化)
function modernDevelopment() {
// 包管理器
npmInstall();
// 模块化开发
import { moduleA } from './modules';
// 自动化构建
webpackBuild();
// 自动化测试
runTests();
// 自动化部署
deployToProduction();
}
Webpack深度解析
Webpack核心概念
入口点(Entry):
// webpack.config.js
module.exports = {
// ========== 单入口配置 ==========
// 指定Webpack打包的入口文件,这是整个依赖图的起点
// Webpack会从这个文件开始递归查找所有依赖的模块
entry: './src/index.js',
// ========== 多入口配置 ==========
// 应用于多页面应用或需要分离业务代码和第三方库的场景
// main: 主要的业务逻辑入口
// vendor: 第三方库/公共模块的入口,便于长期缓存
entry: {
main: './src/index.js', // 主应用入口
vendor: './src/vendor.js' // 第三方库入口
},
// ========== 动态入口配置 ==========
// 异步返回入口文件路径,适用于需要根据条件动态选择入口的场景
// 例如:根据环境变量、用户权限等决定加载哪个入口文件
entry: () => new Promise((resolve) => {
// 这里可以根据某些条件动态决定入口文件
// 比如根据环境变量选择不同的入口
resolve('./src/index.js');
})
};
出口点(Output):
module.exports = {
output: {
// ===== 打包输出目录 =====
// path.resolve确保得到绝对路径,__dirname是当前文件所在目录
// 所有打包后的文件都会输出到dist目录下
path: path.resolve(__dirname, 'dist'),
// ===== 主文件命名规则 =====
// [name]: 对应entry中的key(如main、vendor)
// [contenthash]: 根据文件内容生成的hash值,内容变化时hash才会变
// 作用:利于浏览器缓存,只有文件内容变化时才需要重新下载
filename: '[name].[contenthash].js',
// ===== 代码分割文件命名规则 =====
// 用于动态import()或splitChunks分割出来的chunk文件
// 确保这些异步加载的文件也有正确的缓存策略
chunkFilename: '[name].[contenthash].chunk.js',
// ===== 公共路径前缀 =====
// 所有静态资源引用时会添加这个前缀
// 开发环境:'/static/' 表示资源在静态服务器下
// 生产环境:'https://cdn.example.com/' 表示使用CDN
publicPath: '/static/',
// ===== 自动清理输出目录 =====
// true: 每次构建前自动清理dist目录
// 避免旧文件残留,确保输出目录的干净
clean: true,
// ===== 资源文件命名规则 =====
// 针对图片、字体等静态资源文件的命名规则
// [hash]: 文件内容的hash值,[ext]: 文件扩展名,[query]: URL查询参数
assetModuleFilename: 'assets/[hash][ext][query]'
}
};
加载器(Loaders):
module.exports = {
module: {
rules: [
// ========== JavaScript/TypeScript/JSX/TSX处理 ==========
{
// 匹配所有.js, .jsx, .ts, .tsx结尾的文件
test: /\.(js|jsx|ts|tsx)$/,
// 排除node_modules目录,避免对第三方库进行转译,提高构建速度
exclude: /node_modules/,
// 使用多个loader,执行顺序从右到左
use: [
// ===== ESLint代码检查 =====
// 先进行代码规范检查,在Babel转译之前
'eslint-loader',
// ===== Babel转译 =====
{
loader: 'babel-loader',
options: {
// Babel预设配置
presets: [
// @babel/preset-env: 根据目标浏览器自动转换ES6+语法
['@babel/preset-env', {
targets: {
// 浏览器兼容性目标
// '> 1%': 全球使用率大于1%的浏览器
// 'last 2 versions': 每个浏览器的最后2个版本
browsers: ['> 1%', 'last 2 versions']
}
}]
],
// Babel插件配置
plugins: [
// 转换运行时,避免在每个文件中重复引入helper函数
'@babel/plugin-transform-runtime',
// 转换类属性语法(class MyClass { prop = value; })
'@babel/plugin-proposal-class-properties'
]
}
}
]
},
// ========== CSS文件处理 ==========
{
test: /\.css$/,
use: [
// ===== 将CSS注入到DOM =====
// 创建style标签,将CSS插入到页面head中
// 开发环境使用,便于热更新
'style-loader',
// ===== CSS模块化处理 =====
// 解析CSS文件中的@import和url()
// 支持CSS Modules(配置css-loader.options.modules)
'css-loader',
// ===== PostCSS处理 =====
// 自动添加浏览器前缀(-webkit-, -moz-等)
// 使用autoprefixer插件,根据caniuse数据添加前缀
'postcss-loader'
]
},
// ========== SCSS/Sass文件处理 ==========
{
test: /\.scss$/,
use: [
// 将编译后的CSS注入到DOM
'style-loader',
// 解析SCSS中的@import和url()
'css-loader',
// 将SCSS编译为CSS
'sass-loader',
// ===== 全局变量/混合器注入 =====
// 在所有SCSS文件中自动引入全局变量和混合器
// 避免每个文件都要手动@import
{
loader: 'sass-resources-loader',
options: {
// 全局引入的SCSS文件路径
resources: './src/styles/variables.scss'
}
}
]
},
// ========== 图片资源处理 ==========
{
test: /\.(png|jpe?g|gif|webp|svg)$/,
// ===== 资源类型 =====
// 'asset': Webpack4+的统一资源处理方式
// 根据文件大小自动选择内联base64或独立文件
type: 'asset',
// ===== 内联条件设置 =====
// 当文件小于8KB时,转为base64内联到JS中
// 优点:减少HTTP请求,缺点:增加JS体积
// 适用于小图标、小图片等
parser: {
dataUrlCondition: {
maxSize: 8 * 1024 // 8KB以下转为base64
}
},
// ===== 文件输出路径 =====
// 大文件独立输出时的命名规则
// [name]: 原文件名,[hash]: 内容hash,[ext]: 扩展名
generator: {
filename: 'images/[name].[hash][ext]'
}
},
// ========== 字体文件处理 ==========
{
test: /\.(woff2?|eot|ttf|otf)$/,
// ===== 字体文件类型 =====
// 'asset/resource': 始终输出为独立文件,不内联
// 字体文件通常较大,不适合内联
type: 'asset/resource',
// ===== 字体文件输出路径 =====
generator: {
filename: 'fonts/[name].[hash][ext]'
}
}
]
}
};
插件(Plugins):
// ===== 导入所需的Webpack插件 =====
const HtmlWebpackPlugin = require('html-webpack-plugin'); // HTML模板插件
const MiniCssExtractPlugin = require('mini-css-extract-plugin'); // CSS提取插件
const { CleanWebpackPlugin } = require('clean-webpack-plugin'); // 清理目录插件
const CopyWebpackPlugin = require('copy-webpack-plugin'); // 复制文件插件
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; // Bundle分析插件
module.exports = {
plugins: [
// ========== HTML模板处理插件 ==========
// 自动生成HTML文件,并注入打包后的JS和CSS文件
// 省去手动管理script标签和link标签的麻烦
new HtmlWebpackPlugin({
// HTML模板文件路径
template: './public/index.html',
// 生成的HTML文件名
filename: 'index.html',
// 资源注入位置:'head'或'body'
// 'body': 将script标签放在body底部,推荐做法
inject: 'body',
// ===== HTML压缩配置 =====
// 生产环境启用HTML压缩,减少文件体积
minify: {
removeComments: true, // 移除HTML注释
collapseWhitespace: true, // 折叠空白字符
removeRedundantAttributes: true, // 移除冗余属性(如type="text/javascript")
useShortDoctype: true, // 使用短doctype(<!DOCTYPE html>)
removeEmptyAttributes: true, // 移除空属性
removeStyleLinkTypeAttributes: true,// 移除style/link的type属性
keepClosingSlash: true, // 自闭合标签保持/
minifyJS: true, // 压缩内联JS
minifyCSS: true, // 压缩内联CSS
minifyURLs: true // 压缩内联URL
}
}),
// ========== CSS提取插件 ==========
// 将CSS从JS中提取出来,生成独立的CSS文件
// 生产环境使用,便于浏览器并行加载CSS,避免FOUC(无样式内容闪烁)
new MiniCssExtractPlugin({
// 主CSS文件命名规则
filename: 'css/[name].[contenthash].css',
// 异步加载的CSS文件命名规则
chunkFilename: 'css/[name].[contenthash].chunk.css'
}),
// ========== 目录清理插件 ==========
// 在每次构建前清理输出目录
// 确保每次构建都是全新的,避免旧文件残留
new CleanWebpackPlugin(),
// ========== 文件复制插件 ==========
// 将静态文件直接复制到输出目录,不经过Webpack处理
// 适用于不需要处理的静态资源,如favicon、robots.txt等
new CopyWebpackPlugin({
patterns: [
{
// 源目录
from: 'public/static',
// 目标目录(相对于output.path)
to: 'static',
// 忽略的文件模式
globOptions: {
ignore: ['**/index.html'] // 忽略index.html,由HtmlWebpackPlugin处理
}
}
]
}),
// ========== 环境变量定义插件 ==========
// 在编译时将环境变量注入到代码中
// 可以在代码中直接使用process.env.NODE_ENV等变量
new webpack.DefinePlugin({
'process.env': {
// 将环境变量转换为字符串字面量
NODE_ENV: JSON.stringify(process.env.NODE_ENV),
API_URL: JSON.stringify(process.env.API_URL)
}
}),
// ========== Bundle分析插件 ==========
// 生成Bundle分析报告,帮助优化打包体积
// analyzerMode: 'static' 生成静态HTML报告文件
// openAnalyzer: false 构建完成后不自动打开报告
new BundleAnalyzerPlugin({
analyzerMode: 'static',
openAnalyzer: false
})
]
};
Webpack高级配置
环境配置分离:
// webpack.common.js
const path = require('path');
const { VueLoaderPlugin } = require('vue-loader');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[contenthash].js'
},
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader'
},
{
test: /\.js$/,
exclude: /node_modules/,
use: 'babel-loader'
},
{
test: /\.css$/,
use: ['vue-style-loader', 'css-loader']
}
]
},
plugins: [
new VueLoaderPlugin(),
new HtmlWebpackPlugin({
template: './public/index.html'
})
],
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all'
}
}
}
}
};
// webpack.prod.js
const { merge } = require('webpack-merge');
const common = require('./webpack.common.js');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
module.exports = merge(common, {
mode: 'production',
devtool: 'source-map',
module: {
rules: [
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader']
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].[contenthash].css'
})
],
optimization: {
minimizer: [
new TerserPlugin({
parallel: true,
terserOptions: {
compress: {
drop_console: true // 移除console
}
}
}),
new CssMinimizerPlugin()
]
}
});
// webpack.dev.js
const { merge } = require('webpack-merge');
const common = require('./webpack.common.js');
const webpack = require('webpack');
module.exports = merge(common, {
mode: 'development',
devtool: 'eval-source-map',
devServer: {
contentBase: path.resolve(__dirname, 'dist'),
port: 3000,
hot: true,
overlay: true,
historyApiFallback: true,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
pathRewrite: {
'^/api': ''
}
}
}
},
plugins: [
new webpack.HotModuleReplacementPlugin()
]
});
性能优化配置:
module.exports = {
optimization: {
// 代码分割
splitChunks: {
chunks: 'all',
minSize: 30000,
maxSize: 0,
minChunks: 1,
maxAsyncRequests: 5,
maxInitialRequests: 3,
automaticNameDelimiter: '~',
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10,
name(module) {
const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1];
return `npm.${packageName.replace('@', '')}`;
}
},
common: {
name: 'common',
minChunks: 2,
chunks: 'initial',
priority: -20,
reuseExistingChunk: true
}
}
},
// 运行时代码分离
runtimeChunk: {
name: 'runtime'
},
// Tree shaking
usedExports: true,
sideEffects: false,
// 压缩优化
minimize: true,
minimizer: [
new TerserPlugin({
parallel: true,
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true,
pure_funcs: ['console.log', 'console.info']
},
output: {
comments: false,
ascii_only: true
}
},
extractComments: false
})
]
},
// 解析优化
resolve: {
extensions: ['.js', '.jsx', '.ts', '.tsx', '.vue'],
alias: {
'@': path.resolve(__dirname, 'src'),
'@components': path.resolve(__dirname, 'src/components'),
'@utils': path.resolve(__dirname, 'src/utils'),
'@assets': path.resolve(__dirname, 'src/assets')
},
modules: ['node_modules', 'src/vendor']
},
// 缓存配置
cache: {
type: 'filesystem',
buildDependencies: {
config: [__filename]
}
}
};
自定义Loader和Plugin
自定义Loader示例:
// my-custom-loader.js
module.exports = function(source) {
// 去除console.log
const noConsoleSource = source.replace(/console\.log\(.*?\);?/g, '');
// 添加文件信息注释
const filePath = this.resourcePath;
const fileName = path.basename(filePath);
const comment = `/* File: ${fileName} */\n`;
return comment + noConsoleSource;
};
// webpack.config.js中使用
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: [
'babel-loader',
{
loader: path.resolve(__dirname, 'my-custom-loader.js'),
options: {
// loader选项
}
}
]
}
]
}
};
自定义Plugin示例:
// FileListPlugin.js
class FileListPlugin {
constructor(options = {}) {
this.filename = options.filename || 'filelist.md';
}
apply(compiler) {
compiler.hooks.emit.tapAsync('FileListPlugin', (compilation, callback) => {
const filelist = [];
// 遍历所有编译后的资源
for (const filename in compilation.assets) {
const source = compilation.assets[filename].source();
const size = Buffer.byteLength(source, 'utf8');
filelist.push(`- ${filename} (${size} bytes)`);
}
// 创建新的资源
compilation.assets[this.filename] = {
source: () => filelist.join('\n'),
size: () => filelist.join('\n').length
};
callback();
});
}
}
// 使用自定义Plugin
module.exports = {
plugins: [
new FileListPlugin({
filename: 'build-files.md'
})
]
};
Gulp构建实践
Gulp基础配置
// gulpfile.js - Gulp构建配置文件
// ===== 导入Gulp核心方法 =====
const { src, dest, watch, series, parallel } = require('gulp');
const gulp = require('gulp');
// ===== 导入各种Gulp插件 =====
const sass = require('gulp-sass')(require('sass')); // SCSS编译
const postcss = require('gulp-postcss'); // PostCSS处理
const autoprefixer = require('autoprefixer'); // 浏览器前缀自动添加
const cssnano = require('cssnano'); // CSS压缩
const babel = require('gulp-babel'); // ES6+转ES5
const uglify = require('gulp-uglify'); // JS压缩
const concat = require('gulp-concat'); // 文件合并
const imagemin = require('gulp-imagemin'); // 图片优化
const browserSync = require('browser-sync').create(); // 开发服务器
const del = require('del'); // 文件删除
const plumber = require('gulp-plumber'); // 错误处理
const sourcemaps = require('gulp-sourcemaps'); // 源码映射
// ===== 文件路径配置 =====
// 统一管理所有源文件和输出目录路径,便于维护
const paths = {
html: 'src/**/*.html', // HTML源文件路径
styles: 'src/scss/**/*.scss', // SCSS源文件路径
scripts: 'src/js/**/*.js', // JS源文件路径
images: 'src/images/**/*', // 图片源文件路径
fonts: 'src/fonts/**/*', // 字体源文件路径
dist: 'dist' // 输出目录
};
// ===== 清理输出目录 =====
// 在每次构建前删除dist目录,确保输出环境的干净
function clean() {
return del(paths.dist);
}
// ===== HTML文件处理 =====
// 将HTML文件从src复制到dist目录,不做其他处理
function html() {
return src(paths.html) // 读取HTML文件
.pipe(plumber()) // 添加错误处理,防止错误中断构建流程
.pipe(dest(paths.dist)); // 输出到dist目录
}
// ===== SCSS编译处理 =====
// 将SCSS编译为CSS,添加浏览器前缀,压缩,并生成源码映射
function styles() {
return src(paths.styles) // 读取SCSS文件
.pipe(plumber()) // 错误处理
.pipe(sourcemaps.init()) // 初始化源码映射
.pipe(sass({
outputStyle: 'expanded' // 输出格式:expanded(展开式,便于调试)
}).on('error', sass.logError)) // SCSS编译错误处理
.pipe(postcss([ // 使用PostCSS处理
autoprefixer(), // 自动添加浏览器前缀
cssnano() // CSS压缩
]))
.pipe(sourcemaps.write('.')) // 写入源码映射文件
.pipe(dest(`${paths.dist}/css`)) // 输出到dist/css目录
.pipe(browserSync.stream()); // 触发浏览器热更新
}
// ===== JavaScript处理 =====
// 转译ES6+为ES5,合并文件,压缩,并生成源码映射
function scripts() {
return src(paths.scripts) // 读取JS文件
.pipe(plumber()) // 错误处理
.pipe(sourcemaps.init()) // 初始化源码映射
.pipe(babel({ // Babel转译
presets: ['@babel/preset-env'] // 使用env预设,根据目标浏览器自动转译
}))
.pipe(concat('main.min.js')) // 合并所有JS文件为单个文件
.pipe(uglify()) // 压缩JS代码
.pipe(sourcemaps.write('.')) // 写入源码映射
.pipe(dest(`${paths.dist}/js`)) // 输出到dist/js目录
.pipe(browserSync.stream()); // 触发浏览器热更新
}
// ===== 图片优化处理 =====
// 压缩和优化各种格式的图片文件
function images() {
return src(paths.images) // 读取图片文件
.pipe(imagemin([ // 图片压缩处理
// ===== GIF优化 =====
imagemin.gifsicle({
interlaced: true // 交错式GIF,渐进加载
}),
// ===== JPEG优化 =====
imagemin.mozjpeg({
quality: 80, // 压缩质量:80%(平衡质量和文件大小)
progressive: true // 渐进式JPEG
}),
// ===== PNG优化 =====
imagemin.optipng({
optimizationLevel: 5 // 优化级别:1-7,5是较好的平衡点
}),
// ===== SVG优化 =====
imagemin.svgo({
plugins: [
{ removeViewBox: true }, // 移除viewBox(如果可以)
{ cleanupIDs: false } // 保留ID,避免CSS选择器失效
]
})
]))
.pipe(dest(`${paths.dist}/images`)); // 输出到dist/images目录
}
// ===== 字体文件复制 =====
// 将字体文件直接复制到输出目录,不做处理
function fonts() {
return src(paths.fonts) // 读取字体文件
.pipe(dest(`${paths.dist}/fonts`)); // 输出到dist/fonts目录
}
// ===== 开发服务器配置 =====
// 启动本地开发服务器,支持热更新和实时刷新
function serve() {
browserSync.init({
server: {
baseDir: paths.dist // 服务器根目录
},
port: 3000, // 端口号
open: true, // 自动打开浏览器
notify: false // 不显示浏览器通知
});
// ===== 文件监听配置 =====
// 监听不同类型的文件变化,执行相应的构建任务
watch(paths.styles, styles); // SCSS文件变化时,重新编译样式
watch(paths.scripts, scripts); // JS文件变化时,重新编译脚本
// HTML文件变化时,重新复制并刷新浏览器
watch(paths.html, html).on('change', browserSync.reload);
// 图片文件变化时,重新优化并刷新浏览器
watch(paths.images, images).on('change', browserSync.reload);
}
// ===== 构建任务组合 =====
// 使用series(串行)和parallel(并行)组合多个任务
const build = series(
clean, // 首先清理目录
parallel( // 然后并行执行以下任务
html, // HTML处理
styles, // 样式处理
scripts, // 脚本处理
images, // 图片优化
fonts // 字体复制
)
);
// ===== 开发任务 =====
// 先执行完整构建,然后启动开发服务器
const dev = series(build, serve);
// ===== 任务导出 =====
// 将任务导出为Gulp命令,可在命令行中使用
exports.default = dev; // 默认任务:npm run gulp 或 npm start
exports.build = build; // 生产构建:npm run gulp build
exports.serve = serve; // 仅启动服务:npm run gulp serve
高级Gulp配置
// 复杂的Gulp配置
const gulp = require('gulp');
const $ = require('gulp-load-plugins')();
const webpackStream = require('webpack-stream');
const webpack = require('webpack');
const named = require('vinyl-named');
const rev = require('gulp-rev');
const revRewrite = require('gulp-rev-rewrite');
const gulpsizereport = require('gulp-sizereport');
// 环境配置
const isProduction = process.env.NODE_ENV === 'production';
// Webpack配置
const webpackConfig = {
mode: isProduction ? 'production' : 'development',
devtool: isProduction ? 'source-map' : 'eval-source-map',
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
plugins: ['@babel/plugin-transform-runtime']
}
}
}
]
},
plugins: [
new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery'
})
],
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all'
}
}
}
}
};
// 增量构建
function changed(done) {
return $.changed('dist', { hasChanged: $.changed.compareSha1Digest });
}
// 错误处理
function handleError(error) {
$.util.log($.util.colors.red('Error: ' + error.message));
if (isProduction) {
process.exit(1);
}
this.emit('end');
}
// 处理JavaScript
function scripts() {
return gulp.src(['src/js/**/*.js', '!src/js/**/*.min.js'])
.pipe(plumber({ errorHandler: handleError }))
.pipe($.if(!isProduction, $.sourcemaps.init()))
.pipe(named())
.pipe(webpackStream(webpackConfig, webpack))
.pipe($.if(isProduction, $.uglify({
compress: { drop_console: true }
})))
.pipe($.if(!isProduction, $.sourcemaps.write('.')))
.pipe($.if(isProduction, rev()))
.pipe(dest('dist/js'))
.pipe($.if(isProduction, rev.manifest()))
.pipe(dest('dist/js'));
}
// 处理CSS
function styles() {
const postcssPlugins = [
require('autoprefixer')(),
require('cssnano')()
];
return gulp.src('src/scss/**/*.scss')
.pipe(plumber({ errorHandler: handleError }))
.pipe($.if(!isProduction, $.sourcemaps.init()))
.pipe($.sass.sync({ outputStyle: 'expanded' }).on('error', $.sass.logError))
.pipe($.postcss(postcssPlugins))
.pipe($.if(isProduction, $.cleanCss()))
.pipe($.if(isProduction, rev()))
.pipe(dest('dist/css'))
.pipe($.if(isProduction, rev.manifest()))
.pipe(dest('dist/css'));
}
// 版本控制
function revision() {
const manifest = gulp.src('dist/**/*.json');
return gulp.src(['dist/**/*.html'])
.pipe($.revRewrite({ manifest }))
.pipe(gulp.dest('dist'));
}
// 文件大小报告
function sizeReport() {
return gulp.src('dist/**/*')
.pipe($.sizereport({
gzip: true,
total: true,
title: 'Build Size Report'
}));
}
// 压缩检查
function gzip() {
return gulp.src('dist/**/*.{js,css,html}')
.pipe($.gzip({ append: true }))
.pipe(gulp.dest('dist'));
}
// 完整的生产构建
const production = series(
clean,
parallel(html, styles, scripts, images),
revision,
sizeReport,
gzip
);
// 导出任务
exports.default = dev;
exports.build = build;
exports.production = production;
Vite现代化构建
Vite基础配置
// vite.config.js - Vite构建配置文件
// ===== 导入Vite相关模块 =====
import { defineConfig } from 'vite'; // Vite配置工具函数
import vue from '@vitejs/plugin-vue'; // Vue单文件组件支持
import { resolve } from 'path'; // Node.js路径解析工具
export default defineConfig({
// ===== 插件配置 =====
// Vite使用插件系统来扩展功能
plugins: [
vue(), // 启用Vue单文件组件(.vue文件)支持
],
// ===== 路径别名配置 =====
// 设置模块导入的别名,简化相对路径引用
resolve: {
alias: {
// '@' 指向src目录,便于文件引用
'@': resolve(__dirname, 'src'),
// 各个子模块的别名,提高代码可读性
'@components': resolve(__dirname, 'src/components'),
'@utils': resolve(__dirname, 'src/utils'),
'@assets': resolve(__dirname, 'src/assets')
}
},
// ===== 开发服务器配置 =====
// 配置Vite的开发服务器行为
server: {
host: '0.0.0.0', // 监听所有网络接口,便于局域网访问
port: 3000, // 开发服务器端口号
open: true, // 启动时自动打开浏览器
cors: true, // 启用CORS,允许跨域请求
// ===== 代理配置 =====
// 将特定路径的请求代理到后端服务器,解决开发环境跨域问题
proxy: {
// API请求代理
'/api': {
target: 'http://localhost:8080', // 后端API服务器地址
changeOrigin: true, // 改变请求头的Origin字段
rewrite: (path) => path.replace(/^\/api/, '') // 重写路径,移除/api前缀
},
// 文件上传代理
'/upload': {
target: 'http://localhost:8080', // 文件上传服务器地址
changeOrigin: true // 改变请求头Origin
}
}
},
// ===== 构建配置 =====
// 生产环境打包的相关配置
build: {
target: 'es2015', // 构建目标:支持ES2015语法的浏览器
outDir: 'dist', // 输出目录
assetsDir: 'assets', // 静态资源输出目录(相对于outDir)
sourcemap: false, // 是否生成源码映射文件,生产环境通常关闭
// ===== 压缩配置 =====
minify: 'terser', // 使用Terser进行代码压缩(也可以用'esbuild')
// ===== Terser压缩选项 =====
terserOptions: {
compress: {
drop_console: true, // 移除所有console语句
drop_debugger: true // 移除所有debugger语句
}
},
// ===== 代码分割配置 =====
// 使用Rollup的manualChunks进行手动代码分割
rollupOptions: {
output: {
// 手动分割第三方库,优化缓存策略
manualChunks: {
// Vue核心库单独分包
'vendor': ['vue', 'vue-router', 'vuex'],
// UI组件库单独分包
'ui': ['element-plus'],
// 工具库单独分包
'utils': ['lodash-es', 'axios']
}
}
},
// ===== 构建优化配置 =====
chunkSizeWarningLimit: 1000, // Chunk大小警告阈值(KB)
assetsInlineLimit: 4096 // 小于4KB的资源转为base64内联
},
// ===== CSS配置 =====
// CSS处理和预处理器配置
css: {
// ===== 预处理器选项 =====
preprocessorOptions: {
// SCSS全局变量注入
scss: {
// 在每个SCSS文件开头自动导入全局变量文件
// 避免每个文件都要手动@import
additionalData: `@import "@/styles/variables.scss";`
}
},
// ===== CSS模块配置 =====
modules: {
// CSS类名转换规则:使用驼峰命名
localsConvention: 'camelCaseOnly'
}
},
// ===== 环境变量定义 =====
// 在构建时定义全局常量,可在代码中直接使用
define: {
// 应用版本号,从package.json中读取
__APP_VERSION__: JSON.stringify(process.env.npm_package_version)
},
// ===== 依赖优化配置 =====
// Vite在开发时会预构建依赖,优化启动性能
optimizeDeps: {
// ===== 强制预构建的依赖 =====
// 这些依赖会在开发时被预构建,避免按需转换
include: [
'vue', // Vue核心
'vue-router', // 路由库
'vuex', // 状态管理库
'axios', // HTTP客户端
'lodash-es' // 工具库
],
// ===== 排除预构建的依赖 =====
// 这些依赖不会被预构建,按需加载
exclude: ['@babel/polyfill'] // Babel polyfill通常需要按需加载
}
});
Vite环境配置
// ===== 开发环境变量文件 =====
// .env.development - 开发环境专用配置
VITE_APP_TITLE=My App Development // 应用标题(开发环境)
VITE_API_BASE_URL=http://localhost:8080/api // API基础URL(开发环境)
VITE_APP_ENV=development // 环境标识
// ===== 生产环境变量文件 =====
// .env.production - 生产环境专用配置
VITE_APP_TITLE=My App // 应用标题(生产环境)
VITE_API_BASE_URL=https://api.example.com // API基础URL(生产环境)
VITE_APP_ENV=production // 环境标识
// ===== 预发布环境变量文件 =====
// .env.staging - 预发布环境专用配置
VITE_APP_TITLE=My App Staging // 应用标题(预发布环境)
VITE_API_BASE_URL=https://staging-api.example.com // API基础URL(预发布环境)
VITE_APP_ENV=staging // 环境标识
// ===== 在Vite配置中使用环境变量 =====
// vite.config.js - 根据不同环境动态配置
export default defineConfig(({ mode }) => {
// 根据命令行参数判断当前环境
const isProduction = mode === 'production';
const isStaging = mode === 'staging';
return {
// ===== 环境变量定义 =====
// 将环境变量注入到代码中,可在构建时使用
define: {
// 是否为生产环境标识
__IS_PROD__: isProduction,
// 环境标识字符串
__APP_ENV__: JSON.stringify(process.env.VITE_APP_ENV)
},
// ===== 根据环境配置构建选项 =====
build: {
// 生产环境启用压缩,开发环境不压缩便于调试
minify: isProduction ? 'terser' : false,
// 生产环境关闭源码映射,开发环境启用内联源码映射
sourcemap: isProduction ? false : 'inline'
},
// ===== 根据环境配置服务器 =====
server: {
// 不同环境使用不同的代理配置
proxy: {
'/api': {
// 根据环境选择不同的API地址
target: isProduction
? 'https://api.example.com'
: isStaging
? 'https://staging-api.example.com'
: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
}
};
});
Vite插件开发
// vite-plugin-custom.js - 自定义Vite插件示例
// ===== 插件主函数 =====
export function customPlugin(options = {}) {
return {
// ===== 插件名称 =====
// 用于在日志和调试中识别插件
name: 'vite-plugin-custom',
// ===== 配置钩子 =====
// 在Vite配置解析之前被调用,可以修改配置
config(config, { command }) {
// command: 'serve' | 'build',表示当前是开发还是构建模式
// 如果是开发模式,添加开发服务器配置
if (command === 'serve') {
config.server = {
...config.server,
...options.devServer // 合并插件传入的服务器配置
};
}
return config; // 返回修改后的配置
},
// ===== 配置解析完成钩子 =====
// 在Vite配置解析完成后被调用
configResolved(config) {
console.log('Vite config resolved:', config);
// 这里可以验证配置或进行额外的初始化工作
},
// ===== 开发服务器配置钩子 =====
// 在开发服务器创建后被调用,可以添加自定义中间件
configureServer(server) {
// 添加自定义API路由
server.middlewares.use('/api/custom', (req, res, next) => {
// 自定义API响应
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({
message: 'Custom middleware response',
timestamp: Date.now()
}));
});
},
// ===== 代码转换钩子 =====
// 对模块的源码进行转换
transform(code, id) {
// code: 文件内容
// id: 文件路径(含查询参数)
// 只处理.custom.js文件
if (id.endsWith('.custom.js')) {
// 替换代码中的占位符
const transformedCode = code.replace(
/__VERSION__/g,
options.version || '1.0.0'
);
return {
code: transformedCode, // 转换后的代码
map: null // 源码映射(这里不需要)
};
}
// 返回null表示不处理此文件
return null;
},
// ===== 构建开始钩子 =====
// 在构建过程开始时被调用
buildStart() {
console.log('🚀 Build started...');
// 可以在这里进行构建前的准备工作
// 比如清理临时文件、检查依赖等
},
// ===== 构建结束钩子 =====
// 在构建过程结束时被调用
buildEnd() {
console.log('✅ Build completed!');
// 可以在这里进行构建后的清理工作
// 比如发送构建通知、生成构建报告等
},
// ===== 生成钩子 =====
// 在构建完成后,生成最终文件时被调用
generateBundle(options, bundle) {
// options: 生成选项
// bundle: 包含所有生成文件的信息
console.log(`Generated ${Object.keys(bundle).length} files`);
// 可以在这里分析打包结果或生成额外文件
}
};
}
// ===== 使用自定义插件 =====
// vite.config.js
import { defineConfig } from 'vite';
import { customPlugin } from './vite-plugin-custom';
export default defineConfig({
plugins: [
// 使用自定义插件,传入配置选项
customPlugin({
version: '1.0.0', // 插件版本号
devServer: { // 开发服务器配置
port: 3001, // 自定义端口
open: false // 不自动打开浏览器
}
})
]
});
工程化最佳实践
代码规范和质量控制
ESLint配置:
// .eslintrc.js - ESLint代码检查配置文件
module.exports = {
// ===== 运行环境配置 =====
// 指定代码运行的环境,ESLint会根据环境自动确定全局变量
env: {
browser: true, // 浏览器环境:支持window、document等全局变量
es2021: true, // ES2021语法环境:支持Promise、Set等
node: true // Node.js环境:支持require、process等全局变量
},
// ===== 继承的规则集 =====
// 从已有的规则集继承,避免重复配置
extends: [
'eslint:recommended', // ESLint推荐规则
'@vue/standard', // Vue.js标准规则
'@vue/typescript/recommended', // Vue + TypeScript推荐规则
'prettier' // Prettier规则集成,避免格式冲突
],
// ===== 解析器选项 =====
parserOptions: {
ecmaVersion: 'latest', // 支持最新的ECMAScript语法
sourceType: 'module' // 使用ES模块语法(import/export)
},
// ===== 自定义规则 =====
rules: {
// ===== 调试语句规则 =====
// 根据环境严格程度控制调试语句
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'warn',
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'warn',
// ===== 代码质量规则 =====
'no-unused-vars': 'warn', // 未使用的变量警告(不报错,允许临时调试)
'prefer-const': 'error', // 优先使用const而不是let
'no-var': 'error', // 禁止使用var,强制使用let/const
// ===== 代码风格规则 =====
'object-shorthand': 'error', // 对象方法使用简写语法
'prefer-arrow-callback': 'error' // 回调函数优先使用箭头函数
},
// ===== 文件覆盖规则 =====
// 针对特定文件类型使用不同的规则配置
overrides: [
{
// Vue单文件组件的特殊规则
files: ['*.vue'],
rules: {
// Vue属性每行数量限制
'vue/max-attributes-per-line': ['error', {
singleline: 3, // 单行最多3个属性
multiline: 1 // 多行每行1个属性
}]
}
}
]
};
Prettier配置:
// .prettierrc - Prettier代码格式化配置文件
{
// ===== 分号配置 =====
// false: 不使用分号
// true: 使用分号
"semi": false,
// ===== 引号配置 =====
// true: 优先使用单引号
// false: 优先使用双引号
"singleQuote": true,
// ===== 缩进配置 =====
"tabWidth": 2, // 缩进空格数
"useTabs": false, // 使用空格而不是Tab进行缩进
// ===== 行宽配置 =====
"printWidth": 100, // 每行最大字符数
// ===== 括号配置 =====
"bracketSpacing": true, // 对象字面量括号内添加空格
"bracketSameLine": false, // JSX标签的>不与属性在同一行
// ===== 箭头函数配置 =====
// "avoid": 单参数时不加括号 (x) => {} 改为 x => {}
// "always": 总是加括号
"arrowParens": "avoid",
// ===== 行尾配置 =====
"endOfLine": "lf", // 使用LF换行符(Unix风格)
// ===== 尾随逗号配置 =====
// "es5": ES5支持的对象/数组最后元素加逗号
// "none": 不加尾随逗号
// "all": 所有地方都加尾随逗号
"trailingComma": "es5"
}
Husky和lint-staged:
// package.json - Git钩子和代码质量检查配置
{
"scripts": {
// ===== 代码检查脚本 =====
"lint": "eslint src --ext .js,.vue,.ts",
// ===== 代码修复脚本 =====
"lint:fix": "eslint src --ext .js,.vue,.ts --fix",
// ===== 代码格式化脚本 =====
"format": "prettier --write src/**/*.{js,vue,ts,scss}",
// ===== Pre-commit钩子脚本 =====
"pre-commit": "lint-staged"
},
// ===== Git钩子配置 =====
"husky": {
"hooks": {
// 提交前执行代码检查
"pre-commit": "lint-staged",
// 提交信息格式检查
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
},
// ===== 暂存文件处理配置 =====
// 只对Git暂存区的文件执行检查,提高性能
"lint-staged": {
// JavaScript/Vue/TypeScript文件处理
"*.{js,vue,ts}": [
"eslint --fix", // 自动修复ESLint错误
"prettier --write", // 格式化代码
"git add" // 将修复后的文件重新添加到暂存区
],
// 样式文件处理
"*.{scss,css}": [
"prettier --write", // 格式化样式
"git add" // 重新添加到暂存区
]
}
}
Git工作流规范
# .github/workflows/ci.yml - GitHub Actions CI/CD配置文件
# ===== 流水线名称 =====
name: CI/CD Pipeline
# ===== 触发条件 =====
# 定义什么时候触发此工作流
on:
push:
# 推送到以下分支时触发
branches: [main, develop]
pull_request:
# 创建针对以下分支的PR时触发
branches: [main]
# ===== 工作定义 =====
jobs:
# ===== 测试任务 =====
test:
runs-on: ubuntu-latest # 使用Ubuntu最新版作为运行环境
# ===== 矩阵策略 =====
# 在多个Node.js版本上并行测试
strategy:
matrix:
node-version: [14.x, 16.x, 18.x] # 测试多个Node.js版本
# ===== 执行步骤 =====
steps:
# 步骤1:检出代码
- uses: actions/checkout@v3
# 步骤2:设置Node.js环境
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'npm' # 启用npm缓存加速
# 步骤3:安装依赖
- name: Install dependencies
run: npm ci # 使用npm ci进行快速、可靠的安装
# 步骤4:运行代码检查
- name: Run lint
run: npm run lint
# 步骤5:运行测试并生成覆盖率报告
- name: Run tests
run: npm run test:coverage
# 步骤6:上传覆盖率报告
- name: Upload coverage
uses: codecov/codecov-action@v3
# 步骤7:构建项目
- name: Build
run: npm run build
# 步骤8:上传构建产物
- name: Upload build artifacts
uses: actions/upload-artifact@v3
with:
name: build-${{ matrix.node-version }}
path: dist/
# ===== 部署任务 =====
deploy:
# 依赖测试任务完成
needs: test
runs-on: ubuntu-latest
# ===== 条件部署 =====
# 只有推送到main分支才执行部署
if: github.ref == 'refs/heads/main'
steps:
# 步骤1:检出代码
- uses: actions/checkout@v3
# 步骤2:部署到生产环境
- name: Deploy to production
run: |
echo "Deploying to production..."
# 这里可以是实际的部署脚本
# 例如:上传到CDN、部署到服务器等
# docker build && docker push
# kubectl apply -f deployment.yaml
CI/CD与部署自动化
Docker化部署
# Dockerfile
# 构建阶段
FROM node:18-alpine as build-stage
WORKDIR /app
# 复制package文件
COPY package*.json ./
RUN npm ci --only=production
# 复制源代码
COPY . .
# 构建应用
RUN npm run build
# 生产阶段
FROM nginx:alpine as production-stage
# 复制构建产物
COPY --from=build-stage /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;
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# 启用缓存
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# 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;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
}
Kubernetes部署
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: frontend-app
labels:
app: frontend
spec:
replicas: 3
selector:
matchLabels:
app: frontend
template:
metadata:
labels:
app: frontend
spec:
containers:
- name: frontend
image: my-frontend-app:latest
ports:
- containerPort: 80
resources:
requests:
memory: "64Mi"
cpu: "250m"
limits:
memory: "128Mi"
cpu: "500m"
env:
- name: API_URL
value: "https://api.example.com"
---
apiVersion: v1
kind: Service
metadata:
name: frontend-service
spec:
selector:
app: frontend
ports:
- protocol: TCP
port: 80
targetPort: 80
type: LoadBalancer
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: frontend-ingress
spec:
rules:
- host: example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: frontend-service
port:
number: 80
实战项目搭建
脚手架工具开发
// create-vue-app.js
#!/usr/bin/env node
const { Command } = require('commander');
const inquirer = require('inquirer');
const chalk = require('chalk');
const fs = require('fs-extra');
const path = require('path');
const spawn = require('cross-spawn');
const program = new Command();
program
.name('create-vue-app')
.description('Create a new Vue.js project')
.version('1.0.0')
.argument('[project-name]', 'Project name')
.action(async (projectName) => {
if (!projectName) {
const { name } = await inquirer.prompt([
{
type: 'input',
name: 'name',
message: 'Project name:',
validate: input => input.trim() ? true : 'Project name is required'
}
]);
projectName = name;
}
const targetDir = path.resolve(process.cwd(), projectName);
if (fs.existsSync(targetDir)) {
const { overwrite } = await inquirer.prompt([
{
type: 'confirm',
name: 'overwrite',
message: 'Directory already exists. Overwrite?',
default: false
}
]);
if (!overwrite) {
console.log(chalk.yellow('Project creation cancelled.'));
process.exit(0);
}
await fs.remove(targetDir);
}
const answers = await inquirer.prompt([
{
type: 'list',
name: 'preset',
message: 'Please pick a preset:',
choices: [
{
name: 'Default (Vue 3 + Vite)',
value: 'default'
},
{
name: 'Default Plus (Vue 3 + Vite + TypeScript + Router + Pinia)',
value: 'default-plus'
},
{
name: 'Manually select features',
value: 'manual'
}
]
}
]);
if (answers.preset === 'manual') {
const manualAnswers = await inquirer.prompt([
{
type: 'checkbox',
name: 'features',
message: 'Check the features needed for your project:',
choices: [
{ name: 'TypeScript', value: 'typescript' },
{ name: 'Router', value: 'router' },
{ name: 'Pinia (状态管理)', value: 'pinia' },
{ name: 'ESLint', value: 'eslint' },
{ name: 'Prettier', value: 'prettier' },
{ name: 'Unit Testing', value: 'testing' },
{ name: 'E2E Testing', value: 'e2e' }
]
}
]);
answers.features = manualAnswers.features;
}
console.log(chalk.blue('\nCreating project...'));
await createProject(projectName, targetDir, answers);
console.log(chalk.green('\n✨ Project created successfully!'));
console.log(chalk.cyan('\nNext steps:'));
console.log(chalk.white(` cd ${projectName}`));
console.log(chalk.white(' npm install'));
console.log(chalk.white(' npm run dev'));
});
async function createProject(name, targetDir, answers) {
// 创建项目目录
await fs.ensureDir(targetDir);
// 生成package.json
const packageJson = generatePackageJson(name, answers);
await fs.writeJSON(path.join(targetDir, 'package.json'), packageJson, { spaces: 2 });
// 生成配置文件
await generateConfigFiles(targetDir, answers);
// 生成源代码
await generateSourceFiles(targetDir, answers);
// 安装依赖
console.log(chalk.blue('Installing dependencies...'));
await installDependencies(targetDir, answers);
}
function generatePackageJson(name, answers) {
const base = {
name,
version: '0.0.1',
private: true,
scripts: {
dev: 'vite',
build: 'vite build',
preview: 'vite preview'
}
};
const dependencies = [];
const devDependencies = ['vite'];
if (answers.preset === 'default' || answers.preset === 'default-plus' || answers.features?.includes('typescript')) {
devDependencies.push('@vitejs/plugin-vue');
dependencies.push('vue');
}
if (answers.preset === 'default-plus' || answers.features?.includes('router')) {
dependencies.push('vue-router');
}
if (answers.preset === 'default-plus' || answers.features?.includes('pinia')) {
dependencies.push('pinia');
}
if (answers.features?.includes('typescript')) {
devDependencies.push('typescript', 'vue-tsc');
base.scripts.typecheck = 'vue-tsc --noEmit';
}
return {
...base,
dependencies: dependencies.length ? dependencies : undefined,
devDependencies
};
}
async function installDependencies(targetDir, answers) {
return new Promise((resolve, reject) => {
const child = spawn('npm', ['install'], {
cwd: targetDir,
stdio: 'inherit'
});
child.on('close', (code) => {
if (code !== 0) {
reject(new Error('npm install failed'));
} else {
resolve();
}
});
});
}
program.parse();
项目结构模板
my-project/
├── public/ # 静态资源
│ ├── favicon.ico
│ └── index.html
├── src/ # 源代码
│ ├── api/ # API接口
│ ├── assets/ # 静态资源
│ ├── components/ # 组件
│ │ ├── common/ # 公共组件
│ │ └── business/ # 业务组件
│ ├── composables/ # 组合式函数
│ ├── layouts/ # 布局组件
│ ├── pages/ # 页面
│ ├── router/ # 路由配置
│ ├── stores/ # 状态管理
│ ├── styles/ # 样式文件
│ ├── utils/ # 工具函数
│ ├── types/ # TypeScript类型
│ ├── App.vue # 根组件
│ └── main.ts # 入口文件
├── tests/ # 测试文件
│ ├── unit/ # 单元测试
│ └── e2e/ # E2E测试
├── .env # 环境变量
├── .env.development
├── .env.production
├── .eslintrc.js # ESLint配置
├── .prettierrc # Prettier配置
├── package.json
├── tsconfig.json # TypeScript配置
├── vite.config.ts # Vite配置
└── README.md
性能优化与监控
性能监控配置
// 性能监控工具
class PerformanceMonitor {
constructor() {
this.metrics = {
navigation: {},
resources: [],
paint: {},
memory: {}
};
this.init();
}
init() {
this.observeNavigation();
this.observeResources();
this.observePaint();
this.observeMemory();
this.observeLayoutShift();
}
observeNavigation() {
if (performance.timing) {
const timing = performance.timing;
this.metrics.navigation = {
dns: timing.domainLookupEnd - timing.domainLookupStart,
tcp: timing.connectEnd - timing.connectStart,
request: timing.responseStart - timing.requestStart,
response: timing.responseEnd - timing.responseStart,
dom: timing.domContentLoadedEventEnd - timing.domContentLoadedEventStart,
load: timing.loadEventEnd - timing.loadEventStart,
total: timing.loadEventEnd - timing.navigationStart
};
}
}
observeResources() {
const resources = performance.getEntriesByType('resource');
this.metrics.resources = resources.map(resource => ({
name: resource.name,
type: this.getResourceType(resource.name),
duration: resource.duration,
size: resource.transferSize || 0
}));
}
observePaint() {
const paintEntries = performance.getEntriesByType('paint');
paintEntries.forEach(entry => {
this.metrics.paint[entry.name] = entry.startTime;
});
}
observeMemory() {
if (performance.memory) {
this.metrics.memory = {
used: Math.round(performance.memory.usedJSHeapSize / 1048576),
total: Math.round(performance.memory.totalJSHeapSize / 1048576),
limit: Math.round(performance.memory.jsHeapSizeLimit / 1048576)
};
}
}
observeLayoutShift() {
let cumulativeLayoutShift = 0;
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
cumulativeLayoutShift += entry.value;
}
}
this.metrics.cls = cumulativeLayoutShift;
});
observer.observe({ entryTypes: ['layout-shift'] });
}
getResourceType(url) {
if (url.includes('.css')) return 'css';
if (url.includes('.js')) return 'javascript';
if (url.match(/\.(png|jpg|jpeg|gif|svg|webp)$/)) return 'image';
if (url.match(/\.(woff|woff2|ttf|eot)$/)) return 'font';
return 'other';
}
getReport() {
return {
...this.metrics,
timestamp: Date.now(),
userAgent: navigator.userAgent,
url: window.location.href
};
}
sendReport() {
const report = this.getReport();
// 发送到监控系统
fetch('/api/performance', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(report)
}).catch(error => {
console.error('Failed to send performance report:', error);
});
}
}
// 初始化监控
const monitor = new PerformanceMonitor();
// 页面加载完成后发送报告
window.addEventListener('load', () => {
setTimeout(() => {
monitor.sendReport();
}, 1000);
});
Bundle分析工具
// webpack-bundle-analyzer集成
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin({
analyzerMode: 'static',
openAnalyzer: false,
reportFilename: 'bundle-report.html',
defaultSizes: 'parsed',
generateStatsFile: true,
statsFilename: 'bundle-stats.json',
statsOptions: {
source: false,
modules: true,
chunks: true,
chunkModules: true
}
})
]
};
🚀 落地实战项目案例
微前端架构实践
// 主应用配置 - qiankun
import { registerMicroApps, start } from 'qiankun';
registerMicroApps([
{
name: 'vue-app',
entry: '//localhost:8081',
container: '#vue-app',
activeRule: '/vue',
props: { data: 'main-app-data' }
},
{
name: 'react-app',
entry: '//localhost:8082',
container: '#react-app',
activeRule: '/react'
}
]);
start({
sandbox: {
experimentalStyleIsolation: true,
strictStyleIsolation: false
},
prefetch: true
});
// 微应用配置
// vue-app/src/main.js
import Vue from 'vue';
import App from './App.vue';
import { registerMicroApps, start } from 'qiankun';
let instance = null;
function render(props = {}) {
const { container } = props;
instance = new Vue({
render: h => h(App)
}).$mount(container ? container.querySelector('#app') : '#app');
}
if (!window.__POWERED_BY_QIANKUN__) {
render();
}
export async function bootstrap() {
console.log('[vue-app] bootstrap');
}
export async function mount(props) {
console.log('[vue-app] mount', props);
render(props);
}
export async function unmount() {
console.log('[vue-app] unmount');
instance.$destroy();
instance.$el.innerHTML = '';
instance = null;
}
Monorepo工程架构
// package.json (根目录)
{
"name": "frontend-monorepo",
"private": true,
"workspaces": [
"packages/*",
"apps/*"
],
"scripts": {
"dev": "turbo run dev",
"build": "turbo run build",
"lint": "turbo run lint",
"test": "turbo run test",
"clean": "turbo run clean && rm -rf node_modules"
},
"devDependencies": {
"turbo": "^1.6.3",
"@changesets/cli": "^2.24.4"
}
}
// turbo.json
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", "build/**"]
},
"dev": {
"cache": false,
"persistent": true
},
"lint": {
"outputs": []
},
"test": {
"dependsOn": ["build"],
"outputs": ["coverage/**"]
}
}
}
frontend-monorepo/
├── packages/
│ ├── ui/ # UI组件库
│ ├── utils/ # 工具库
│ ├── hooks/ # 自定义hooks
│ └── types/ # 类型定义
├── apps/
│ ├── admin/ # 管理后台
│ ├── mobile/ # 移动端应用
│ └── docs/ # 文档站点
├── tools/
│ ├── build/ # 构建工具
│ └── scripts/ # 脚本工具
└── shared/ # 共享配置
├── eslint-config/
├── tsconfig/
└── webpack-config/
组件库工程化
// packages/ui/build.js
const { build } = require('esbuild');
const { glob } = require('glob');
const { resolve } = require('path');
async function buildLibrary() {
// 入口文件
const entryPoints = await glob('./src/**/*.{js,ts,jsx,tsx}');
// 构建ESM
await build({
entryPoints,
bundle: false,
outdir: 'dist/es',
format: 'esm',
target: ['es2018'],
sourcemap: true,
tsconfig: './tsconfig.json'
});
// 构建CJS
await build({
entryPoints,
bundle: false,
outdir: 'dist/lib',
format: 'cjs',
target: ['es2018'],
sourcemap: true,
tsconfig: './tsconfig.json'
});
// 生成类型声明
await build({
entryPoints: './src/index.ts',
bundle: false,
outdir: 'dist/types',
format: 'esm',
target: ['es2018'],
tsconfig: './tsconfig.json',
jsxFactory: 'React.createElement',
jsxFragment: 'React.Fragment'
});
}
buildLibrary();
// packages/ui/.storybook/main.js
module.exports = {
stories: ['../src/**/*.stories.@(js|jsx|ts|tsx)'],
addons: [
'@storybook/addon-essentials',
'@storybook/addon-interactions',
'@storybook/addon-docs'
],
framework: {
name: '@storybook/react-webpack5',
options: {}
},
webpackFinal: async (config) => {
// 自定义webpack配置
config.resolve.alias = {
...config.resolve.alias,
'@': resolve(__dirname, '../src')
};
return config;
}
};
低代码平台工程化
// 低代码引擎核心
class LowCodeEngine {
constructor() {
this.components = new Map();
this.schemas = new Map();
this.plugins = [];
}
// 注册组件
registerComponent(name, component) {
this.components.set(name, component);
}
// 注册插件
registerPlugin(plugin) {
this.plugins.push(plugin);
plugin.init(this);
}
// 解析schema并渲染
render(schema) {
const Component = this.components.get(schema.type);
if (!Component) {
throw new Error(`Component ${schema.type} not found`);
}
const props = {
...schema.props,
children: schema.children?.map(child => this.render(child))
};
return React.createElement(Component, props, props.children);
}
// 页面构建器
buildPage(schema) {
return this.render(schema);
}
}
// 拖拽系统
class DragDropSystem {
constructor(container) {
this.container = container;
this.draggedElement = null;
this.dropZones = [];
this.init();
}
init() {
this.container.addEventListener('dragstart', this.handleDragStart.bind(this));
this.container.addEventListener('dragover', this.handleDragOver.bind(this));
this.container.addEventListener('drop', this.handleDrop.bind(this));
}
handleDragStart(e) {
this.draggedElement = e.target;
e.dataTransfer.effectAllowed = 'move';
}
handleDragOver(e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
}
handleDrop(e) {
e.preventDefault();
if (this.draggedElement && e.target.classList.contains('drop-zone')) {
e.target.appendChild(this.draggedElement);
this.onDrop(this.draggedElement, e.target);
}
}
}
📊 企业级监控体系
错误监控系统
// 错误监控SDK - 前端错误采集和上报系统
class ErrorMonitor {
constructor(config) {
// ===== 配置初始化 =====
// 合并用户配置和默认配置
this.config = {
apiUrl: '/api/errors', // 错误上报API地址
maxErrors: 50, // 本地最大错误缓存数
sampleRate: 1, // 采样率(1=100%,0.1=10%)
...config // 合并用户自定义配置
};
this.errors = []; // 本地错误缓存数组
this.init(); // 初始化监控
}
// ===== 初始化方法 =====
// 启动所有错误监听机制
init() {
this.captureGlobalErrors(); // 监听全局JavaScript错误
this.captureUnhandledRejections(); // 监听未处理的Promise rejection
this.captureResourceErrors(); // 监听资源加载错误
this.startReporting(); // 启动定时上报机制
}
// ===== 全局JavaScript错误监听 =====
// 捕获所有未处理的JavaScript运行时错误
captureGlobalErrors() {
window.addEventListener('error', (event) => {
// 构建错误对象,包含详细的错误信息
this.captureError({
type: 'javascript', // 错误类型:JavaScript运行时错误
message: event.message, // 错误消息
filename: event.filename, // 出错的文件名
lineno: event.lineno, // 错误行号
colno: event.colno, // 错误列号
stack: event.error?.stack // 错误堆栈信息
});
});
}
// ===== Promise rejection监听 =====
// 捕获所有未处理的Promise rejection
captureUnhandledRejections() {
window.addEventListener('unhandledrejection', (event) => {
this.captureError({
type: 'promise', // 错误类型:Promise错误
message: event.reason?.message || 'Unhandled Promise Rejection', // 错误消息
stack: event.reason?.stack // 错误堆栈
});
});
}
// ===== 资源加载错误监听 =====
// 捕获图片、脚本、样式等资源加载失败
captureResourceErrors() {
// 使用捕获阶段监听,能获取到资源加载错误
window.addEventListener('error', (event) => {
// event.target !== window 确保是资源错误,不是JS错误
if (event.target !== window) {
this.captureError({
type: 'resource', // 错误类型:资源加载错误
message: `Failed to load ${event.target.tagName}`, // 错误描述
resource: event.target.src || event.target.href, // 失败的资源URL
type: event.target.tagName.toLowerCase() // 资源类型(img, script等)
});
}
}, true); // true表示使用捕获阶段
}
// ===== 错误捕获核心方法 =====
// 统一处理所有类型的错误
captureError(error) {
// ===== 采样控制 =====
// 根据采样率决定是否处理此错误,减少上报量
if (Math.random() > this.config.sampleRate) {
return;
}
// ===== 错误信息增强 =====
// 添加环境信息和上下文信息
const enrichedError = {
...error, // 原始错误信息
timestamp: Date.now(), // 错误发生时间戳
url: window.location.href, // 当前页面URL
userAgent: navigator.userAgent, // 浏览器用户代理
sessionId: this.getSessionId(), // 会话ID(用于关联同一用户的错误)
userId: this.getUserId() // 用户ID(需要用户自己实现)
};
// ===== 错误缓存 =====
// 将错误添加到本地缓存
this.errors.push(enrichedError);
// ===== 缓存大小控制 =====
// 如果缓存超过最大值,移除最旧的错误
if (this.errors.length > this.config.maxErrors) {
this.errors.shift();
}
}
// ===== 错误上报方法 =====
// 将缓存中的错误批量发送到服务器
async reportErrors() {
// 如果没有错误,直接返回
if (this.errors.length === 0) return;
// 复制错误数组并清空缓存
const errorsToSend = [...this.errors];
this.errors = [];
try {
// 发送错误到监控服务器
await fetch(this.config.apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
errors: errorsToSend, // 错误数组
timestamp: Date.now() // 上报时间戳
})
});
} catch (error) {
// ===== 上报失败处理 =====
// 将错误重新添加到缓存头部,下次重试
console.error('Failed to report errors:', error);
this.errors.unshift(...errorsToSend);
}
}
// ===== 启动定时上报 =====
startReporting() {
// 定时上报:每30秒上报一次
setInterval(() => this.reportErrors(), 30000);
// 页面卸载时上报:确保用户离开页面前上报所有错误
window.addEventListener('beforeunload', () => {
this.reportErrors();
});
}
// ===== 会话ID生成 =====
// 生成唯一的会话标识符
getSessionId() {
// 从sessionStorage获取或生成新的
if (!sessionStorage.getItem('sessionId')) {
const sessionId = 'session_' +
Math.random().toString(36).substr(2, 9) +
Date.now();
sessionStorage.setItem('sessionId', sessionId);
return sessionId;
}
return sessionStorage.getItem('sessionId');
}
// ===== 用户ID获取 =====
// 获取当前用户ID(需要根据具体业务实现)
getUserId() {
// 可以从cookie、localStorage、全局变量等获取
return localStorage.getItem('userId') || 'anonymous';
}
}
// ===== 初始化错误监控 =====
// 在应用入口处初始化监控系统
new ErrorMonitor({
apiUrl: 'https://monitor.example.com/api/errors', // 监控API地址
sampleRate: 0.1 // 10%采样率,减少上报量
});
性能监控系统
// Web Vitals监控 - 核心Web性能指标监控
// 导入web-vitals库的核心指标函数
import { getCLS, getFID, getFCP, getLCP, getTTFB } from 'web-vitals';
class PerformanceMonitor {
constructor() {
this.metrics = {}; // 存储性能指标
this.init(); // 初始化监控
}
// ===== 初始化方法 =====
init() {
this.observeWebVitals(); // 监控核心Web性能指标
this.observeUserInteractions(); // 监控用户交互性能
this.observeAPIPerformance(); // 监控API请求性能
}
// ===== Web Vitals指标监控 =====
// 监控Google推荐的核心Web性能指标
observeWebVitals() {
// CLS - Cumulative Layout Shift(累积布局偏移)
// 衡量页面视觉稳定性,值越小越好(<0.1为良好)
getCLS((metric) => this.recordMetric('CLS', metric));
// FID - First Input Delay(首次输入延迟)
// 衡量页面交互性,值越小越好(<100ms为良好)
getFID((metric) => this.recordMetric('FID', metric));
// FCP - First Contentful Paint(首次内容绘制)
// 衡量页面加载速度,值越小越好(<1.8s为良好)
getFCP((metric) => this.recordMetric('FCP', metric));
// LCP - Largest Contentful Paint(最大内容绘制)
// 衡量页面主要内容加载速度,值越小越好(<2.5s为良好)
getLCP((metric) => this.recordMetric('LCP', metric));
// TTFB - Time to First Byte(首字节时间)
// 衡量服务器响应速度,值越小越好(<800ms为良好)
getTTFB((metric) => this.recordMetric('TTFB', metric));
}
// ===== 用户交互性能监控 =====
// 监控用户的点击、输入等交互的响应延迟
observeUserInteractions() {
// 监控点击响应延迟
document.addEventListener('click', (event) => {
const startTime = performance.now(); // 记录点击开始时间
// 使用requestAnimationFrame确保在下一帧测量
requestAnimationFrame(() => {
const clickDelay = performance.now() - startTime;
this.recordMetric('ClickDelay', { value: clickDelay });
});
});
// 监控键盘输入响应延迟
document.addEventListener('keydown', (event) => {
const startTime = performance.now();
requestAnimationFrame(() => {
const keyDelay = performance.now() - startTime;
this.recordMetric('KeyDelay', { value: keyDelay });
});
});
}
// ===== API性能监控 =====
// 拦截fetch API,监控所有网络请求的性能
observeAPIPerformance() {
// 保存原始的fetch函数
const originalFetch = window.fetch;
// 重写fetch函数,添加性能监控
window.fetch = async (...args) => {
const startTime = performance.now(); // 记录请求开始时间
try {
// 执行原始fetch请求
const response = await originalFetch(...args);
const endTime = performance.now(); // 记录请求结束时间
// 记录成功的API请求指标
this.recordMetric('APIRequest', {
url: args[0], // 请求URL
method: args[1]?.method || 'GET', // 请求方法
status: response.status, // 响应状态码
duration: endTime - startTime, // 请求耗时
size: response.headers.get('content-length') // 响应大小
});
return response;
} catch (error) {
const endTime = performance.now();
// 记录失败的API请求指标
this.recordMetric('APIError', {
url: args[0], // 请求URL
error: error.message, // 错误信息
duration: endTime - startTime // 请求耗时
});
// 继续抛出原始错误
throw error;
}
};
}
// ===== 指标记录方法 =====
// 统一记录各种性能指标
recordMetric(name, metric) {
// 存储指标数据
this.metrics[name] = {
...metric, // 原始指标数据
timestamp: Date.now(), // 记录时间
url: window.location.href // 页面URL
};
// 立即发送指标到服务器
this.sendMetric(name, metric);
}
// ===== 指标上报方法 =====
// 将性能指标发送到监控服务器
async sendMetric(name, metric) {
try {
await fetch('/api/metrics', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: name, // 指标名称
metric: metric, // 指标数据
url: window.location.href, // 页面URL
userAgent: navigator.userAgent, // 浏览器信息
timestamp: Date.now() // 上报时间
})
});
} catch (error) {
console.error('Failed to send metric:', error);
// 可以考虑添加重试逻辑或本地缓存
}
}
// ===== 性能报告生成 =====
// 生成性能报告摘要
getPerformanceReport() {
return {
// 核心Web Vitals指标
vitals: {
cls: this.metrics.CLS?.value || 0,
fid: this.metrics.FID?.value || 0,
fcp: this.metrics.FCP?.value || 0,
lcp: this.metrics.LCP?.value || 0,
ttfb: this.metrics.TTFB?.value || 0
},
// 交互性能指标
interactions: {
clickDelay: this.metrics.ClickDelay?.value || 0,
keyDelay: this.metrics.KeyDelay?.value || 0
},
// 网络请求性能
network: {
avgResponseTime: this.calculateAverageResponseTime(),
errorRate: this.calculateErrorRate()
},
// 页面信息
page: {
url: window.location.href,
timestamp: Date.now()
}
};
}
// ===== 计算平均响应时间 =====
calculateAverageResponseTime() {
const requests = Object.values(this.metrics)
.filter(metric => metric.name === 'APIRequest');
if (requests.length === 0) return 0;
const totalTime = requests.reduce((sum, req) => sum + req.duration, 0);
return totalTime / requests.length;
}
// ===== 计算错误率 =====
calculateErrorRate() {
const allRequests = Object.values(this.metrics)
.filter(metric => metric.name.startsWith('API'));
if (allRequests.length === 0) return 0;
const errors = allRequests.filter(metric => metric.name === 'APIError');
return (errors.length / allRequests.length) * 100;
}
}
// ===== 初始化性能监控 =====
// 在应用入口处初始化性能监控系统
new PerformanceMonitor();
用户行为分析
// 用户行为追踪 - 用户交互行为数据采集系统
class UserBehaviorTracker {
constructor() {
this.events = []; // 本地事件缓存
this.sessionId = this.generateSessionId(); // 会话唯一标识
this.init(); // 初始化追踪
}
// ===== 初始化方法 =====
init() {
this.trackPageViews(); // 页面访问追踪
this.trackClicks(); // 点击行为追踪
this.trackScrolls(); // 滚动行为追踪
this.trackFormInteractions(); // 表单交互追踪
this.startBatching(); // 启动批量上报
}
// ===== 页面访问追踪 =====
trackPageViews() {
// 页面首次加载时的访问记录
this.track('page_view', {
path: window.location.pathname, // 页面路径
referrer: document.referrer, // 来源页面
title: document.title, // 页面标题
timestamp: Date.now(), // 访问时间
userAgent: navigator.userAgent // 浏览器信息
});
// SPA路由变化监听(监听浏览器历史记录变化)
window.addEventListener('popstate', () => {
this.track('page_view', {
path: window.location.pathname, // 新页面路径
type: 'spa_navigation' // 标记为SPA路由跳转
});
});
// 监听pushState和replaceState(大多数SPA框架的路由方法)
const originalPushState = history.pushState;
const originalReplaceState = history.replaceState;
history.pushState = function(...args) {
originalPushState.apply(this, args);
window.dispatchEvent(new Event('popstate'));
};
history.replaceState = function(...args) {
originalReplaceState.apply(this, args);
window.dispatchEvent(new Event('popstate'));
};
}
// ===== 点击行为追踪 =====
trackClicks() {
document.addEventListener('click', (event) => {
const target = event.target;
// 构建点击事件数据
this.track('click', {
elementType: target.tagName.toLowerCase(), // 元素类型
elementClass: target.className, // 元素类名
elementId: target.id, // 元素ID
text: target.textContent?.slice(0, 100), // 元素文本(截取前100字符)
attributes: this.getElementAttributes(target), // 元素属性
coordinates: { // 点击坐标
x: event.clientX,
y: event.clientY
},
viewportSize: { // 视窗大小
width: window.innerWidth,
height: window.innerHeight
}
});
});
}
// ===== 滚动行为追踪 =====
trackScrolls() {
let lastScrollDepth = 0; // 记录上次滚动深度
let scrollTimeout = null; // 防抖定时器
window.addEventListener('scroll', () => {
// 防抖处理,避免频繁触发
clearTimeout(scrollTimeout);
scrollTimeout = setTimeout(() => {
// 计算当前滚动深度百分比
const scrollDepth = Math.round(
(window.scrollY / (document.body.scrollHeight - window.innerHeight)) * 100
);
// 只有在滚动深度增加时才记录(避免重复记录同一深度)
if (scrollDepth > lastScrollDepth) {
this.track('scroll', {
depth: scrollDepth, // 滚动深度百分比
scrollPosition: window.scrollY, // 滚动位置
pageHeight: document.body.scrollHeight, // 页面总高度
viewportHeight: window.innerHeight, // 视窗高度
timestamp: Date.now() // 滚动时间
});
lastScrollDepth = scrollDepth;
}
}, 100); // 100ms防抖
});
}
// ===== 表单交互追踪 =====
trackFormInteractions() {
// 表单元素变化追踪(input、select、textarea)
document.addEventListener('change', (event) => {
const target = event.target;
if (target.tagName === 'INPUT' || target.tagName === 'SELECT' || target.tagName === 'TEXTAREA') {
this.track('form_interaction', {
fieldType: target.type, // 字段类型
fieldName: target.name, // 字段名称
fieldValue: this.sanitizeValue(target.value), // 字段值(脱敏处理)
elementId: target.id, // 元素ID
formId: target.form?.id, // 表单ID
timestamp: Date.now() // 交互时间
});
}
});
// 表单提交追踪
document.addEventListener('submit', (event) => {
const form = event.target;
this.track('form_submit', {
formId: form.id, // 表单ID
formName: form.name, // 表单名称
formAction: form.action, // 表单提交地址
formMethod: form.method, // 提交方法
fieldCount: form.elements.length, // 字段数量
timestamp: Date.now() // 提交时间
});
});
// 表单聚焦/失焦追踪(可选)
document.addEventListener('focus', (event) => {
if (['INPUT', 'SELECT', 'TEXTAREA'].includes(event.target.tagName)) {
this.track('form_focus', {
fieldName: event.target.name,
fieldType: event.target.type,
timestamp: Date.now()
});
}
}, true);
}
// ===== 事件追踪核心方法 =====
// 统一处理所有用户行为事件
track(eventName, data) {
// 构建完整的事件对象
const event = {
eventName, // 事件名称
data, // 事件数据
timestamp: Date.now(), // 事件时间戳
sessionId: this.sessionId, // 会话ID
userId: this.getUserId(), // 用户ID
url: window.location.href, // 当前页面URL
userAgent: navigator.userAgent, // 浏览器信息
screenResolution: { // 屏幕分辨率
width: screen.width,
height: screen.height
}
};
// 添加到本地缓存
this.events.push(event);
}
// ===== 批量上报机制 =====
startBatching() {
// 定时批量上报:每30秒上报一次
const batchInterval = setInterval(() => {
this.sendBatch();
}, 30000);
// 页面可见性变化时上报(用户切换标签页)
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
this.sendBatch();
}
});
// 页面卸载时上报:确保数据不丢失
window.addEventListener('beforeunload', () => {
this.sendBatch();
clearInterval(batchInterval);
});
}
// ===== 事件上报方法 =====
async sendBatch() {
// 如果没有事件,直接返回
if (this.events.length === 0) return;
// 复制事件数组并清空缓存
const eventsToSend = [...this.events];
this.events = [];
try {
// 发送批量事件到服务器
await fetch('/api/events', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
events: eventsToSend, // 事件数组
batchId: this.generateBatchId(), // 批次ID
timestamp: Date.now() // 上报时间
})
});
} catch (error) {
// 上报失败处理:将事件重新添加到缓存头部
console.error('Failed to send events:', error);
this.events.unshift(...eventsToSend);
}
}
// ===== 工具方法 =====
// 生成会话ID
generateSessionId() {
return 'session_' +
Math.random().toString(36).substr(2, 9) + // 随机字符串
Date.now(); // 时间戳
}
// 生成批次ID
generateBatchId() {
return 'batch_' + Date.now() + '_' + Math.random().toString(36).substr(2, 5);
}
// 获取用户ID
getUserId() {
return localStorage.getItem('userId') || 'anonymous';
}
// 获取元素属性
getElementAttributes(element) {
const attributes = {};
for (let attr of element.attributes) {
if (['id', 'class', 'href', 'src', 'alt', 'title'].includes(attr.name)) {
attributes[attr.name] = attr.value;
}
}
return attributes;
}
// 数据脱敏处理
sanitizeValue(value) {
if (typeof value !== 'string') return value;
// 密码字段完全隐藏
if (value.length > 0 && ['password', 'pwd', 'pass'].some(keyword =>
this.currentFieldName?.toLowerCase().includes(keyword))) {
return '******';
}
// 邮箱部分脱敏
if (value.includes('@')) {
const [username, domain] = value.split('@');
return username.substring(0, 2) + '***@' + domain;
}
// 手机号脱敏
if (/^\d{11}$/.test(value)) {
return value.substring(0, 3) + '****' + value.substring(7);
}
// 其他长文本截断
return value.length > 50 ? value.substring(0, 50) + '...' : value;
}
}
// ===== 初始化用户行为追踪 =====
// 在应用入口处初始化行为追踪系统
const behaviorTracker = new UserBehaviorTracker();
// 暴露全局接口,供业务代码调用自定义事件
window.trackEvent = (eventName, data) => {
behaviorTracker.track(eventName, data);
};
window.trackCustomGoal = (goalName, value) => {
behaviorTracker.track('goal_conversion', {
goalName,
value,
timestamp: Date.now()
});
};
🎯 总结
这个前端工程化终极指南提供了从基础到高级的完整解决方案:
📋 核心覆盖内容
-
构建工具深入:
- Webpack:核心概念、高级配置、性能优化
- Gulp:任务流管理、自动化构建
- Vite:现代构建、开发体验优化
-
工程化最佳实践:
- 代码规范:ESLint、Prettier、Git Hooks
- 工作流规范:分支管理、提交规范
- Monorepo架构:多项目管理
-
CI/CD与部署:
- Docker容器化部署
- Kubernetes集群管理
- 自动化流水线
-
实战项目落地:
-
监控与优化:
🚀 技术亮点
-
深度技术解析:每个工具都有原理解释和实战配置
-
企业级方案:包含Monorepo、微前端等企业架构
-
完整代码示例:所有配置和工具都可以直接使用
-
性能优化专项:从构建到运行的全链路优化
-
监控体系建设:错误、性能、行为的全方位监控