跨過前端工程化建設的三座大山

現如今前端構建打包工具層出不窮,有 WebpackViteRollup...,但無論是哪一個工具,還是從一個工具切換到一個新穎的工具,實際上最終都離不開 解析編譯模塊分包壓縮優化三個階段。本文使用 Webpack5 解釋並配置這三個階段。

三大階段

整篇文章都會圍繞以下這張圖講解。最左邊是多入口的業務文件,中間是構建打包三個階段,最右邊是產物文件。

解析編譯

入口

Webpack 在讀取配置的時候會先讀取 entry 字段,該字段就是入口文件地址。

entry: {
  'entry1': path.resolve(process.cwd()'./app/entry1/entry1.js'),
  'entry2': path.resolve(process.cwd()'./app/entry2/entry2.js')
}

輸出路徑

不同環境文件的輸出路徑有所不同。

生產環境
output: {
    // 文件名
    filename: 'js/[name]_[chunkhash:8].bundle.js',
    // 輸出路徑
    path: path.resolve(process.cwd()'./app/public/dist/prod/'),
    // 公共路徑
    publicPath: '/dist/prod/'
},
開發環境

開發環境不需要文件落地,通過 devServer (下面會講如何使用中間件實現) 放置到內存中。

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/`, // 外部資源文件公共路徑
},

模塊解析

有一些模塊需要通過解析器進行被更好的識別 / 兼容 / 優化。比如 .vue 結尾的文件需要轉換成 .js 結尾才能被瀏覽器識別,.less 結尾的文件需要轉換成 .css 結尾的文件,.css 結尾的文件需要轉換成 style 標籤 ...

模塊解析需要配置在 module 字段下面。

// 模塊解析配置(決定了要加載解析哪些模塊, 以及用什麼方式去解釋)
module: {
    rules: [],
},
處理 .vue 文件
{
    test: /.vue$/,
    use: {
      loader: 'vue-loader',
    },
},
處理 .js 文件
{
    test: /.js$/,
    include: [
      // 只對業務代碼進行 babel 處理
      path.resolve(process.cwd()'./app/pages'),
    ],
    use: {
      loader: 'babel-loader',
    },
},
處理資源文件

對小圖片進行 base64 轉換。

{
    test: /.(png|jpe?g|gif|svg)(?.+)?$/,
    use: {
      loader: 'url-loader',
      options: {
        limit: 300
      },
    },
},

在引入其他靜態文件的時候,輸出到 output 目錄,並且修改成正確的 url。

{
    test: /.(eot|svg|ttf|woff|woff2)(?\S*)?$/,
    use: 'file-loader',
},
處理 css 相關文件

less -> css

css -> style

{
    test: /.css$/,
    use: ['style-loader''css-loader'],
},

{
    test: /.less$/,
    use: ['style-loader''css-loader''less-loader'],
},

文件擴展名和路徑別名

resolve: {
    // import xxx from './xxx.vue' -> import xxx from './xxx'
    extensions: ['.js''.vue''.less''.css'],
    alias: {
      // 配置別名 ./app/pages/xxx -> $pages/xxx
      $pages: path.resolve(process.cwd()'./app/pages'),
      $store: path.resolve(process.cwd()'./app/pages/store'),
    },
},

plugins

plugins: [
    // 處理 .vue 文件
    // 它的職責是將你定義過的其它規則複製並應用到 .vue 文件裏。
    // 例如,如果你有一條匹配 /.js$/ 的規則,那麼它會應用到 .vue 文件裏的 <script> 塊。
    new VueLoaderPlugin(),
    // 把第三方庫暴露到 window context 下
    new webpack.ProvidePlugin({
      Vue: 'vue',
      axios: 'axios',
      _: 'lodash',
    }),
    // 構造最終渲染的頁面模板 entry1
    new HtmlWebpackPlugin({
      // 產物 (最終模板) 輸出路徑
      filename: path.resolve(process.cwd()'./app/public/dist/''entry.page1.tpl'),
      // 指定要使用的模板文件
      template: path.resolve(process.cwd()'./app/view/entry.tpl'),
      // 要注入的代碼塊 與入口對應
      chunks: ['entry1'],
    }),
    // 構造最終渲染的頁面模板 entry2
    new HtmlWebpackPlugin({
      // 產物 (最終模板) 輸出路徑
      filename: path.resolve(process.cwd()'./app/public/dist/''entry.page2.tpl'),
      // 指定要使用的模板文件
      template: path.resolve(process.cwd()'./app/view/entry.tpl'),
      // 要注入的代碼塊 與入口對應
      chunks: ['entry2'],
    })
],

模塊分包

把 js 文件打包成三個類型:

  1. vendor: 第三方庫,基本不會改,除非依賴升級。

  2. common: 業務組件的公共部分抽取出來,改動較少。

  3. entry.{page}: 不同頁面 entry 裏的業務組件代碼的差異部分,會經常改動。

目的:把改動和引用頻率不一樣的 js 區分出來,以達到更好利用瀏覽器緩存的效果

// 配置打包輸出優化 (代碼分割, 模塊合併 等優化策略)
optimization: {
    splitChunks: {
      chunks: 'all', // 對同步和異步模塊都進行分割
      maxAsyncRequests: 10, // 每個異步加載模塊最多的並行請求數
      maxInitialRequests: 10, // 一個入口的最大並行請求數
      cacheGroups: {
        vendor: {
          // 第三方依賴庫
          test: /[\/]node_modules[\/]/, // 匹配 node_modules 目錄下的模塊
          name: 'vendor', // 模塊名稱
          priority: 20, // 優先級 數字越大優先級越高
          enforce: true, // 強制執行
          reuseExistingChunk: true, // 複用已經存在的 chunk
        },
        common: {
          // 公共模塊
          test: /[\/]common|widgets[\/]/,
          name: 'common',
          minChunks: 2, // 最少引用次數
          minSize: 1, // 最小分割文件大小 字節爲單位
          priority: 10,
          reuseExistingChunk: true,
        },
      },
    },
    // 將 webpack 運行時的代碼單獨抽離出來 runtime.js
    runtimeChunk: true,
}

壓縮 / 優化

生產環境

css

多線程處理 happypack

抽離公共部分 MiniCssExtractPlugin

壓縮 CSSMinimizerPlugin

// 多線程 build 設置
const happypackCommonConifig = {
debug: false,
threadPool: HappyPack.ThreadPool({ size: os.cpus().length }),
};

module: {
    rules: [{
        test: /.css$/,
        use: [MiniCssExtractPlugin.loader, 'happypack/loader?id=css'],
    }]
},

plugins: [{
    // 提取 css 的公共部分
    new MiniCssExtractPlugin({
      chunkFilename: 'css/[name]_[chunkhash:8].bundle.css', // 非入口 chunk 的名稱
    }),
    // 優化並壓縮 css 資源
    new CSSMinimizerPlugin(),
    // 多線程打包 CSS,加快打包速度
    new HappyPack({
      ...happypackCommonConifig,
      id: 'css',
      loaders: [
        {
          path: 'css-loader',
          options: {
            importLoaders: 1,
          },
        },
      ],
    }),
}]
js

多線程處理 loader happypack

並行壓縮 TerserWebpackPlugin

module: {
    rules: [{
        test: /.js$/,
        include: [
          // 只對業務代碼進行 babel 處理,加快 webpack 打包速度
          path.resolve(process.cwd()'./app/pages'),
        ],
        use: ['happypack/loader?id=js'],
    }]
},
plugins: [
    // 多線程打包 JS,加快打包速度
    new HappyPack({
      ...happypackCommonConifig,
      id: 'js',
      loaders: [
        `babel-loader?${JSON.stringify({
          presets: ['@babel/preset-env'],
          plugins: [
            '@babel/plugin-transform-runtime'
          ],
        })}`,
      ],
    }),
]
optimization: {
    // 使用 TerserPlugin 的併發和緩存,提升壓縮階段性能
    // 清除 console.log
    minimize: true,
    minimizer: [
      new TerserWebpackPlugin({
        parallel: true, // 利用多核 CPU 進行壓縮
        cache: true, // 啓動緩存來加速構建過程
        terserOptions: {
          compress: {
            drop_console: true, // 刪除所有的 `console` 語句
          },
        },
      }),
    ],
}
其他優化

打包前清空目錄

plugins: [
    // 每次 build 前,清空 public/dist 目錄
    new CleanWebpackPlugin(['public/dist'], {
      root: path.resolve(process.cwd()'./app/'),
      exclude: [],
      verbose: true,
      dry: false,
    }),
]

開發環境

熱更新 HMR

開啓 devServer 去熱更新,需要具備兩種能力,一種是監控文件變化的能力,一種是通知頁面可以去更新代碼的能力。

啓動 devServer 的時候,可以將打包構建的其他代碼放入內存,將雙向通信的代碼片段注入模板文件,當啓動模塊頁的服務時就能建立與 devServer 雙向通信的橋樑。

devServer

前置配置

const DEV_SERVER_CONFIG = {
  HOST: '127.0.0.1',
  PORT: 9002,
  HMR_PATH: '__webpack_hmr', // 官方規定
  TIMEOUT: 20000,
};

這裏使用 express 啓動服務器,然後使用下面兩個中間件:

監聽文件改動:webpack-dev-middleware

通知文件更新:webpack-hot-middleware

// 本地開發啓動 devServer 配置
const express = require('express');
const path = require('path');
const webpack = require('webpack');
const consoler = require('consoler');
const devMiddleware = require('webpack-dev-middleware');
const hotMiddleware = require('webpack-hot-middleware');
const webpackDevConfig = require('./config/webpack.dev.js');
const app = express();

// 從 webpack.dev.js 獲取 webpack 配置 和 devServer 配置
const { webpackConfig, DEV_SERVER_CONFIG } = webpackDevConfig;

const compiler = webpack(webpackConfig);

// 指定靜態文件目錄
app.use(express.static(path.join(__dirname, '../public/dist')));
// 引用 webpack-dev-middleware 中間件  (監控文件改動)
app.use(
  devMiddleware(compiler, {
    // 落地文件: 生成的 tpl
    writeToDisk: (filePath) => filePath.endsWith('.tpl'),
    // 資源路徑
    publicPath: webpackConfig.output.publicPath,

    // headers 配置
    headers: {
      'Access-Control-Allow-Origin''*',
      'Access-Control-Allow-Method''GET,POST,PUT,DELETE,OPTIONS,PATCH',
      'Access-Control-Allow-Headers':
        'X-Requested-With,content-type,Authorization',
    },
    // 控制檯輸出
    stats: {
      colors: true,
    },
  }),
);
// 引用 webpack-hot-middleware 中間件  (熱更新通訊)
app.use(
  hotMiddleware(compiler, {
    log: () ={},
    path: `/${DEV_SERVER_CONFIG.HMR_PATH}`,
  }),
);

consoler.info('請等待 webpack 初次構建完成提示...');

// 啓動 devServer
const port = DEV_SERVER_CONFIG.PORT;
app.listen(port, () => {
console.log(`app listening on port ${port}`);
});

HMR 相關的 Webpack 配置

entry: { 
    // 注入代碼
    'entry1': [
        path.resolve(process.cwd()'./app/entry1/entry1.js'),
        `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`,
    ],
    'entry2': [
        path.resolve(process.cwd()'./app/entry2/entry2.js'),
        `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`,
    ]
},
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/`, // 外部資源文件公共路徑
},
// 開發階段插件
plugins: [
    // 實現熱模塊替換
    // 模塊熱替換允許在運行時更新各種模塊
    new webpack.HotModuleReplacementPlugin({
      multiStep: false,
    }),
]
其他優化

開啓 sourceMap,呈現代碼的映射關係,便於在開發過程中調試代碼

devtool: 'eval-cheap-module-source-map',

總結

以上就是使用 Webpack5 實現前端構建打包過程中的三個階段。這裏無論是哪種構建工具都可以實現這三個階段,工具可以變,使用方式可以變,但核心原理核心階段都是不變的

作者:龍爍

https://juejin.cn/post/7466010329232605235

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/sCfh4REYOXkb_x756kCbkA