什么是工程化?
工程化是指将科学理论、技术和实践经验有系统地应用到各类工程项目的设计、制造、建设和运行过程中。这是一个涵盖多个学科和领域知识的综合性概念,通过标准化、模块化和自动化等方法来提升生产效率、确保产品质量并控制成本。
前端工程化的定义
前端工程化是将开发流程、工具和规范进行标准化,并通过自动化技术优化整个开发过程。这包括代码编写、测试、构建和部署等环节,目的是提升开发效率,同时确保代码质量和可维护性。
为什么要前端工程化?
降低开发成本,提升开发效率。
elips项目工程化——Webpack
什么是Webpack
Webpack 是一个强大的模块打包工具,主要用于 JavaScript 应用程序。它分析项目中的模块依赖关系,将代码和资源(JS、CSS、图片等)打包成一个或多个 bundle 文件。Webpack 支持多种模块规范,通过加载器(Loaders)处理不同类型的文件转换,并提供丰富的插件系统来扩展功能。它还支持代码分割、懒加载和热模块替换等特性,能有效优化应用性能和提升开发效率。简而言之,Webpack 作为现代 Web 开发中不可或缺的构建工具,能够高效管理复杂项目的依赖和资源。
elips中项目结构
app/webpack/
├── config/
│ ├── webpack.base.js # 基础配置
│ ├── webpack.dev.js # 开发环境配置
│ └── webpack.prod.js # 生产环境配置
├── dev.js # 开发服务器脚本
└── prod.js # 生产环境构建脚本
-
config:config 目录包含按环境拆分的配置文件,每个文件专注于特定环境的配置。
-
webpack.base.js:基础配置文件,定义项目的核心构建规则,包括入口文件配置、模块解析规则、公共插件配置和代码分割策略
// 获取 app/pages 目录下所有入口文件 (entry.xxx.js) const entryList = path.resolve(process.cwd(), './app/pages/**/entry.*.js') glob.sync(entryList).forEach(file => { // 构造最终渲染的页面文件 htmlWebpackPluginList.push( // 构造 html-webpack-plugin, 在这里实例化更加模块化和易于维护 new HtmlWebpackPlugin({ ... }) ) }) module.exports = { // 入口文件 多入口 ssr 的项目 entry: pageEntries, // 模块解析配置(决定要交在解析哪些模块,以及如何解析它们) module: { rules:[ ... ] }, // 产物输出路径 output: { ... }, // 配置模块解析具体行为(定义 webpack 在打包时,如何找到并解析具体模块的路径) resolve: { extensions: ['.js', '.jsx', '.less', '.css'], alias: { $pages: path.resolve(process.cwd(), './app/pages'), $common: path.resolve(process.cwd(), './app/pages/common'), $widgets: path.resolve(process.cwd(), './app/pages/widgets'), $store: path.resolve(process.cwd(), './app/pages/store'), '@': path.resolve(process.cwd(), './app') } }, // 插件配置 plugins:[ // 解析vue文件, 解析vue文件中的template、script、style, 并将module中配置的rules应用到对应的部分 new VueLoaderPlugin(), // 定义全局变量, 将三方库暴露到window context中 new webpack.ProvidePlugin({ Vue: 'vue', axios: 'axios', _: 'lodash' }), // 定义全局常量 new webpack.DefinePlugin({ // 支持 vue 解析 optionsAPI __VUE_OPTIONS_API__: 'true', // 禁用 Vue 调试工具 __VUE_PROD_DEVTOOLS__: 'false', // 禁用 vue生产环境显示 "水合"(服务器端渲染 (SSR) 的输出与客户端生成的 DOM 结构不一致时) 信息 __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: 'false' }), ...htmlWebpackPluginList ], // 配置打包输出优化(配置代码分割、模块合并、缓存、TreeShaking、压缩、代码分割等优化) optimization : { /** * 把 js 文件打包成3种类型 * 1. vendor:第三种 lib 库,基本不会改动,除非依赖版本升级 * 2. common:业务组件代码的公共部分抽取出来,改动较少 * 3. entry.{page}: 不用页面 entry 里的业务组件代码的差异部分,会经常改动 * 目的:把改动和引用频率不一样的 js 区分出来,以达到更好利用浏览器缓存的目的 */ splitChunks: { chunks: 'all', // 对同步、异步模块都进行分割 maxAsyncRequests: 10, // 最大异步加载并行请求数 maxInitialRequests: 10, // 入口点的最大并行请求数 cacheGroups: { vendor: { // 打包第三方依赖库 name: 'vendor', // 模块名称 test: /[\\/]node_modules[\\/]/, // 匹配正则,node_modules下的第三方库 priority: 20, // 优先级,越大越优先打包 enforce: true, // 强制执行 reuseExistingChunk: true // 复用已有的公共 chunk }, common:{ //公共模块 name: 'common', minChunks: 2, // 被引用次数大于等于2的模块视为公共模块 minSize: 1, // 最小分割文件大小 (1 字节) priority: 10, reuseExistingChunk: true } } } } } -
webpack.dev.js: 开发环境配置,通过优化 webpack.dev.js 配置,开发者可以享受到更高效的开发体验。启用 Source Map、配置模块热更新、设置开发服务器以及实现实时编译和刷新,所有这些功能都旨在提高开发效率,减少调试时间。
// 基类配置 const baseConfig = require('./webpack.base'); // devserver配置 const DEV_SERVER_CONFIG = { HOST: '127.0.0.1', PORT: 9002, HMR_PATH: '__webpack_hmr__', // 官方规定 TIMEOUT: 20000 } // 开发阶段的 entry 配置需要加入 webpack-dev-server 的 HMR 配置 Object.keys(baseConfig.entry).forEach(key => { // 三方包不作为 hmr 入口 if(key !== 'vendor'){ // 主入口文件 baseConfig.entry[key] = [ baseConfig.entry[key], // hmr更新入口,官方指定的 hmr 路径 `webpack-hot-middleware/client?path=http://${DEV_SERVER_CONFIG.HOST}:${DEV_SERVER_CONFIG.PORT}/${DEV_SERVER_CONFIG.HMR_PATH}?timeout=${DEV_SERVER_CONFIG.TIMEOUT}&reload=true` ] } }) // 合并配置, 并添加自己的配置 const webpackConfig = merge.smart(baseConfig, { // 指定开发环境 mode: 'development', // source-map devtool: 'source-map', output: { filename: 'js/[name]_[chunkhash:8].bundle.js', // 输出目录 path: path.resolve(process.cwd(), './app/public/dist/dev'), // 外部资源公共路径, 存储于内存中 publicPath: `http://${DEV_SERVER_CONFIG.HOST}:${DEV_SERVER_CONFIG.PORT}/public/dist/dev/`, // 多环境打包 globalObject: 'this', }, module: { rules: [ ... ] }, // 开发阶段插件 plugins: [ // 用于实现热更新,模块可以在应用运行时更新 new webpack.HotModuleReplacementPlugin({ // 启用 HMR enabled: true, // 启用 HMR 时显示全屏覆盖层 showFullError: false, showErrors: true, errorDetails: true, reload: true, }) ] }) module.exports = { // webpack 配置 webpackConfig, // devserver 配置,提供给 dev.js 使用的 DEV_SERVER_CONFIG }; -
webpack.prod.js: 生产环境配置,生产环境主要就是代码的压缩优化
// 多线程 build 配置 const threadLoaderOptions = { workers: Math.max(1, os.cpus().length - 1), // worker 数量 poolTimeout: 2000, // 闲置超时 workerParallelJobs: 50, // 并行任务数 poolRespawn: false // 重启挂掉的 worker 线程 } /** * 预热线程池,用于提前初始化和加载线程,以减少首次任务执行时的启动开销 * 没有预热时的执行过程:开始任务 -> 创建线程 -> 加载模块 -> 执行任务 * 预热后的执行过程:开始任务 -> 直接执行任务(线程已准备好) */ require('thread-loader').warmup(threadLoaderOptions, [ 'babel-loader', 'css-loader' ]); // 基类配置 const baseConfig = require('./webpack.base'); // 合并配置, 并添加自己的配置 const webpackConfig = merge.smart(baseConfig, { // 指定生产环境 mode: 'production', // 生产环境输出配置 output: { ... }, module: { rules: [ ... ] }, // webpack 不会大量 hints 信息,默认为 warning performance: { hints: false }, // 插件 plugins:[ // 每次 build 删除 public/dist 目录 new CleanWebpackPlugin(['public/dist'],{...}), // 提取 css 公共部分,利用缓存 new MiniCssExtractPlugin({...}), // 浏览器在请求资源时不发送用户的身份凭证 new HtmlWebpackInjectAttributesPlugin({...}) ], optimization:{ // 将 webpack 运行时生成的代码打包到 runtime.js 中,减少 main.js 的体积 runtimeChunk: true, // 使用 esbubuild 压缩工具,提升压缩速度 // 清除 console.log minimize: true, minimizer:[ new EsbuildPlugin({ target: 'es2015', css: true, // 缩小css minify: true, // 缩小js minifyWhitespace: true, // 改为 true,压缩空白字符 minifyIdentifiers: false, // 保持不变,不压缩标识符 minifySyntax: true, // 改为 true,允许基础语法优化 drop: ['console', 'debugger'], treeShaking: true, legalComments: 'none', //去掉注释 keepNames: false, // 保持函数名和类名 format: 'iife' // 使用立即执行函数表达式格式 }), ] } }) module.exports = webpackConfig;
-
-
构建脚本详解
-
dev.js (开发服务器) 通过启动一个express服务,通过divMiddleware中间件,监控文件改动,并通过hotMiddleware中间件,实现热更新
// 从webpack.dev.js中获取 webpack配置 和 devServer 配置 const { webpackConfig, DEV_SERVER_CONFIG } = webpackProdConfig; // 实例化一个express const app = express(); const compiler = webpack(webpackConfig); // 指定静态文件目录 app.use(express.static(path.join(__dirname, '../public/dist'))); // 引用 divMiddleware中间件,监控文件改动 app.use(divMiddleware(compiler, { // 落地文件 writeToDisk: (filePath) => filePath.endsWith('.tpl') , // 资源路径 publicPath: webpackConfig.output.publicPath, // headers 配置 headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS', 'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization' }, stats: { colors: true, // 彩色输出 chunks: false, // 不显示块信息 modules: false, // 不显示模块信息 children: false, // 不显示子编译任务的信息 warnings: false, // 不显示警告信息 errors: true } })) // 引用 hotMiddleware中间件,实现热更新 app.use(hotMiddleware(compiler, { path: `/${DEV_SERVER_CONFIG.HMR_PATH}`, log: () => { }, }) ); consoler.info('Starting dev server...'); // 启动 devServer const port = DEV_SERVER_CONFIG.PORT; app.listen(port, () => { consoler.success(`> Listening at <http://localhost>:${port}\n`); });type: 'filesystem', buildDependencies: { config: [__filename], package: [path.resolve(__dirname, 'package.json'), path.resolve(__dirname, 'pnpm-lock.yaml')] }, name: 'dev-cache', profile: true, version: `${require('./package.json').version}-${process.version}`, cacheDirectory: path.resolve(__dirname, 'node_modules/.cache/webpack'), cacheLocation: path.resolve(__dirname, 'node_modules/.cache/webpack/dev')type: 'filesystem', buildDependencies: { config: [__filename], package: [path.resolve(__dirname, 'package.json'), path.resolve(__dirname, 'pnpm-lock.yaml')] }, name: 'dev-cache', profile: true, version: `${require('./package.json').version}-${process.version}`, cacheDirectory: path.resolve(__dirname, 'node_modules/.cache/webpack'), cacheLocation: path.resolve(__dirname, 'node_modules/.cache/webpack/dev') -
prod.js (生产构建) 这里就是使用生产的配置文件进行构建
优化点
- 通过 thread-loader 来进行多线程操作
- 使用 esbuild-loader 来进行压缩,提升压缩速度(后续更新 esbuild-loader、swc-loader不同loader的压缩比和时间比较。。。)
继续优化方向
- 构建分析
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;- 缓存优化:开发和生产环境都可以通过缓存来加快编译速度
// 开发环境配置 type: 'filesystem', buildDependencies: { config: [__filename], package: [path.resolve(__dirname, 'package.json'), path.resolve(__dirname, 'pnpm-lock.yaml')] }, name: 'dev-cache', profile: true, version: `${require('./package.json').version}-${process.version}`, cacheDirectory: path.resolve(__dirname, 'node_modules/.cache/webpack'), cacheLocation: path.resolve(__dirname, 'node_modules/.cache/webpack/dev') // 生产环境配置 config.cache = { type: 'filesystem', buildDependencies: { config: [__filename], package: [path.resolve(__dirname, 'package.json'), path.resolve(__dirname, 'pnpm-lock.yaml')] }, name: 'prod-cache', compression: 'gzip', store: 'pack', memoryCacheUnaffected: true, profile: true, version: `${require('./package.json').version}-${process.version}`, cacheDirectory: path.resolve(__dirname, 'node_modules/.cache/webpack'), cacheLocation: path.resolve(__dirname, 'node_modules/.cache/webpack/prod') }- 性能监控
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin');- babel-loader的替换 可以使用swc-loader替换,swc-loader在效率和内存占用上都要优于babel-loader,但是在vue中使用jsx语法会存在问题,需要手写个插件(后续更新。。。)
-
全文特别鸣谢: 抖音“哲玄前端”,《全栈实践课》