跨過前端工程化建設的三座大山
現如今前端構建打包工具層出不窮,有 Webpack
、Vite
、Rollup
...,但無論是哪一個工具,還是從一個工具切換到一個新穎的工具,實際上最終都離不開 解析編譯,模塊分包,壓縮優化三個階段。本文使用 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 文件打包成三個類型:
-
vendor: 第三方庫,基本不會改,除非依賴升級。
-
common: 業務組件的公共部分抽取出來,改動較少。
-
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