工程化——webpack基本配置

117 阅读8分钟

什么是工程化?

工程化是指将科学理论、技术和实践经验有系统地应用到各类工程项目的设计、制造、建设和运行过程中。这是一个涵盖多个学科和领域知识的综合性概念,通过标准化、模块化和自动化等方法来提升生产效率、确保产品质量并控制成本。

前端工程化的定义

前端工程化是将开发流程、工具和规范进行标准化,并通过自动化技术优化整个开发过程。这包括代码编写、测试、构建和部署等环节,目的是提升开发效率,同时确保代码质量和可维护性。

为什么要前端工程化?

降低开发成本,提升开发效率。

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               # 生产环境构建脚本
  1. config:config 目录包含按环境拆分的配置文件,每个文件专注于特定环境的配置。

    1. 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
                      }
                  }
              }
          }
          
      }
      
    2. 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
      };
      
    3. 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;
      
  2. 构建脚本详解

    1. 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')
      
    2. prod.js (生产构建) 这里就是使用生产的配置文件进行构建

    优化点

    1. 通过 thread-loader 来进行多线程操作
    2. 使用 esbuild-loader 来进行压缩,提升压缩速度(后续更新 esbuild-loader、swc-loader不同loader的压缩比和时间比较。。。)

    继续优化方向

    1. 构建分析
    const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
    
    1. 缓存优化:开发和生产环境都可以通过缓存来加快编译速度
    // 开发环境配置
    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')
    }
    
    1. 性能监控
    const SpeedMeasurePlugin = require('speed-measure-webpack-plugin');
    
    1. babel-loader的替换 可以使用swc-loader替换,swc-loader在效率和内存占用上都要优于babel-loader,但是在vue中使用jsx语法会存在问题,需要手写个插件(后续更新。。。)

全文特别鸣谢: 抖音“哲玄前端”,《全栈实践课》