背景
最近在對公司的H5專案做重構,涉及到構建優化,由於一些歷史原因,專案原先使用的打包工具是餓了麼團隊開發的 cooking(基於 webpack 做的封裝,目前已停止維護了。)如果繼續使用,一是專案目前已經比較複雜,現在的構建方式每次打包耗時較長;二是使用一個已經停止維護的工具本身也有風險;另外因為本次重構還要進行 Vue1.0 到 Vue2.0 的框架升級,涉及到一系列依賴包( vue-style-loader 等)的版本相容問題。折騰了一天也沒啥頭緒,索性將構建工具直接升級到 webpack4,同步搭配 vue2 和 vuex3,一步到位。
由於公司業務需要(SEO、頁面主要以投放為主),我們專案採用的是多頁面架構,網上基於vue的單頁應用模板,官方提供了 vue-cli,第三方的也不少,多頁模板可參考的卻不多。我前後花了兩週左右時間,參考了一些部落格資料和文件,整理了這套基於webpack4 + vue2 + vuex3的多頁應用模板,記錄下來方便自己以後檢視,也分享給有需要的同學參考。
webpack的核心概念
受 Parcel 等零配置構建工具的啟發,webpack4 也在向無配置方向努力,做了大量優化,雖然支援零配置的方式,但如果想對模組進行細粒度的控制,仍然需要手動對一些配置項進行設定。但和 webpack 之前版本相比已經明顯簡化,上手容易了很多。這裡先了解 webpack4 的幾個核心配置項,後面會逐一展開:
- mode
- entry
- output
- loader
- plugins
- devServer
接下來我就按照上面的順序,儘量詳細的列出基於 webpack4 搭建 vue2、vuex 多頁應用的全流程
mode
webpack4新增,指定打包模式,可選的值有:
- development,開發模式
- 會將 process.env.NODE_ENV 設定成 development
- 啟用 NamedChunksPlugin、NamedModulesPlugin 外掛
- production,生產模式
- 會將 process.env.NODE_ENV 設定成 production
- 會啟用最大化的優化(模組的壓縮、串聯等)
- none,這種模式不會進行優化處理
mode設定的兩種方式:
- package.json 中通過shell命令引數形式設定
webpack --mode=production
複製程式碼
- 通過配置mode配置項
module.exports = {
mode: 'production'
};
複製程式碼
更多資訊可參考:官方文件 Mode
entry
對比多頁應用和單頁應用(SPA),最大的不同點,就在於入口的不同
- 多頁:最終打包生成多個入口( html 頁面),一般每個入口檔案除了要引入公共的靜態檔案( js/css )還要另外引入頁面特有的靜態資源
- 單頁:只有一個入口( index.html ),頁面中需要引入打包後的所有靜態檔案,所有的頁面內容全由 JavaScript 控制
需要注意的是,上面說的入口指的都是最終打包到dist目錄下的html檔案,而我們在這裡配置的 entry 其實是需要被 html 引入的js模組,這些js模組、連同抽離的公共js模組最終還需要利用 html-webpack-plugin 這個外掛組合到html檔案中:
const config = require('./config'); // 多頁面的配置項
let HTMLPlugins = [];
let Entries = {};
config.HTMLDirs.forEach(item => {
let filename = `${item.page}.html`;
if (item.dir) filename = `${item.dir}/${item.page}.html`;
const htmlPlugin = new HTMLWebpackPlugin({
title: item.title, // 生成的html頁面的標題
filename: filename, // 生成到dist目錄下的html檔名稱,支援多級目錄(eg: `${item.page}/index.html`)
template: path.resolve(__dirname, `../src/template/index.html`), // 模板檔案,不同入口可以根據需要設定不同模板
chunks: [item.page, 'vendor'], // html檔案中需要要引入的js模組,這裡的 vendor 是webpack預設配置下抽離的公共模組的名稱
});
HTMLPlugins.push(htmlPlugin);
Entries[item.page] = path.resolve(__dirname, `../src/pages/${item.page}/index.js`); // 根據配置設定入口js檔案
});
// ...
複製程式碼
config.js中多頁的配置資訊:
module.exports = {
HTMLDirs: [
{
page: 'index',
title: '首頁'
},
{
page: 'list',
title: '列表頁',
dir: 'content' // 支援設定多級目錄
},
{
page: 'detail',
title: '詳情頁'
}
],
// ...
};
複製程式碼
最後再引入相關配置:
module.exports = {
entry: Entries,
// ...
plugins: [
...HTMLPlugins // 利用 HTMLWebpackPlugin 外掛合成最終頁面
]
// ...
}
複製程式碼
關於公共模組的抽離後面會單獨介紹
html-webpack-plugin更多配置資訊:html-webpack-plugin官網
output
配置出口的檔名和路徑:
const env = process.env.BUILD_MODE.trim();
let ASSET_PATH = '/'; // dev 環境
if (env === 'prod') ASSET_PATH = '//abc.com/static/'; // build 時設定成實際使用的靜態服務地址
module.exports = {
entry: Entries,
output: {
publicPath: ASSET_PATH,
filename: 'js/[name].[hash:8].js',
path: path.resolve(__dirname, '../dist'),
},
}
複製程式碼
這裡將生成的js檔案掛上8位的MD5戳,以充分利用CDN快取。
關於hash的幾種計算方式和區別可以參考 webpack中的hash、chunkhash、contenthash區別
loader
loader 用於對模組的原始碼進行轉換,負責把某種檔案格式的內容轉換成 webpack 可以支援打包的模組,例如將sass預處理轉換成 css 模組;將 TypeScript 轉換成 JavaScript;或將內聯影像轉換為 data URL等
具體配置:
- webpack.base.js(基礎配置檔案)
const VueLoaderPlugin = require('vue-loader/lib/plugin');
// ...
module: {
rules: [
{
test: /\.vue$/, // 處理vue模組
use: 'vue-loader',
},
{
test: /\.js$/, //處理es6語法
exclude: /node_modules/,
use: ['babel-loader'],
},
{
test: /\.(png|svg|jpg|gif)$/, // 處理圖片
use: {
loader: 'file-loader', // 解決打包css檔案中圖片路徑無法解析的問題
options: {
// 打包生成圖片的名字
name: '[name].[ext]',
// 圖片的生成路徑
outputPath: config.imgOutputPath,
}
}
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/, // 處理字型
use: {
loader: 'file-loader',
options: {
outputPath: config.fontOutputPath,
}
}
}
]
},
plugins: [
// ...
new VueLoaderPlugin()
]
// ...
複製程式碼
vue-loader要配合 VueLoaderPlugin 外掛一起使用。 babel-loader 要配合 .babelrc 使用。這裡配置“stage-2”以使用es7裡的高階語法,實測如果不配置就無法處理 物件擴充套件符、async和await 等新語法特性。
.babelrc配置:
{
"presets": [
["env", {
"modules": false,
"targets": {
"browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
}
}],
"stage-2"
],
"plugins": ["transform-runtime"]
}
複製程式碼
關於 .babelrc 相關的配置可參考: 官方文件; babel配置-各階段的stage的區別
- webpack.dev.js(開發配置檔案)
// ...
module: {
rules: [
{
test: /\.css$/,
exclude: /node_modules/,
use: [
'vue-style-loader', // 處理vue檔案中的css樣式
'css-loader',
'postcss-loader',
]
},
{
test: /\.scss$/,
exclude: /node_modules/,
use: [ // 這些loader會按照從右到左的順序處理樣式
'vue-style-loader',
'css-loader',
'sass-loader',
'postcss-loader',
{
loader: 'sass-resources-loader', // 將定義的sass變數、mix等統一樣式打包到每個css檔案中,避免在每個頁面中手動手動引入
options: {
resources: path.resolve(__dirname, '../src/styles/lib/main.scss'),
}
}
]
},
{
test: /\.(js|vue)$/,
enforce: 'pre', // 強制先進行 ESLint 檢查
exclude: /node_modules|lib/,
loader: 'eslint-loader',
options: {
// 啟用自動修復
fix: true,
// 啟用警告資訊
emitWarning: true,
}
}
]
},
// ...
複製程式碼
- webpack.prod.js(生產配置檔案)
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const ASSET_PATH = '//abc.com/static/'; // 線上靜態資源地址
// ...
module: {
rules: [
{
test: /\.css$/,
exclude: /node_modules/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'postcss-loader'
]
}, {
test: /\.scss$/,
exclude: /node_modules/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'sass-loader',
'postcss-loader',
{
loader: 'sass-resources-loader',
options: {
resources: path.resolve(__dirname, '../src/styles/lib/main.scss'),
},
}
]
},
{
test: /\.(png|svg|jpg|gif)$/, // 處理圖片
use: {
loader: 'file-loader', // 解決打包css檔案中圖片路徑無法解析的問題
options: {
// 打包生成圖片的名字
name: '[name].[hash:8].[ext]',
// 圖片的生成路徑
outputPath: config.imgOutputPath,
publicPath: ASSET_PATH
}
}
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/, // 處理字型
use: {
loader: 'file-loader',
options: {
outputPath: config.fontOutputPath,
publicPath: ASSET_PATH
}
}
}
]
},
// ...
plugins: [
new MiniCssExtractPlugin({
filename: 'css/[name].[chunkhash:8].css' // css最終以單檔案形式抽離到 dist/css目錄下
})
]
複製程式碼
抽取 css 成單個檔案 之前使用的 extract-text-webpack-plugin 不再支援webpack4,官方出了 mini-css-extract-plugin 來處理css的抽取
plugins
在webpack打包流程中,模組程式碼轉換的工作由 loader 來處理,除此之外的其他工作都可以交由 plugin 來完成。常用的有:
- uglifyjs-webpack-plugin, 處理js程式碼壓縮
- mini-css-extract-plugin, 將css抽離成單檔案
- clean-webpack-plugin, 用於每次 build 時清理 dist 資料夾
- copy-webpack-plugin, copy檔案
- webpack.HotModuleReplacementPlugin, 熱載入
- webpack.DefinePlugin,定義環境變數
具體配置:
- webpack.base.js(基礎配置檔案)
const HTMLWebpackPlugin = require('html-webpack-plugin');
const VueLoaderPlugin = require('vue-loader/lib/plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
// ...
plugins: [
new VueLoaderPlugin(),
new CopyWebpackPlugin([
{
from: path.resolve(__dirname, '../public'),
to: path.resolve(__dirname, '../dist'),
ignore: ['*.html']
},
{
from: path.resolve(__dirname, '../src/scripts/lib'), // 搬運本地類庫資源
to: path.resolve(__dirname, '../dist')
}
]),
...HTMLPlugins, // 利用 HTMLWebpackPlugin 外掛合成最終頁面
new webpack.DefinePlugin({
'process.env.ASSET_PATH': JSON.stringify(ASSET_PATH) // 利用 process.env.ASSET_PATH 保證模板檔案中引用正確的靜態資源地址
})
]
複製程式碼
- webpack.prod.js(生產配置檔案)
// 抽取css extract-text-webpack-plugin不再支援webpack4,官方出了mini-css-extract-plugin來處理css的抽取
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const path = require('path');
const CleanWebpackPlugin = require('clean-webpack-plugin');
plugins: [
// 自動清理 dist 資料夾
new CleanWebpackPlugin(['dist'], {
root: path.resolve(__dirname, '..'),
verbose: true, //開啟在控制檯輸出資訊
dry: false,
}),
new MiniCssExtractPlugin({
filename: 'css/[name].[chunkhash:8].css'
})
]
複製程式碼
devServer
日常開發的時候我們需要在本地啟動一個靜態伺服器,以方便開發除錯,我們使用 webpack-dev-server 這個官方提供的一個工具,基於當前的 webpack 構建配置快速啟動一個靜態服務。當 mode 為 development 時,會具備 hot reload 的功能,所以不需要再手動引入 webpack.HotModuleReplacementPlugin 外掛了。
一般把 webpack-dev-server 作為開發依賴安裝,然後使用 npm scripts 來啟動:
npm install webpack-dev-server -S
複製程式碼
package 中的 scripts 配置:
"scripts": {
"dev": "cross-env BUILD_MODE=dev webpack-dev-server ",
},
複製程式碼
devServer的詳細配置可參考官方文件:dev-server
splitChunks配置
webpack 4 移除了 CommonsChunkPlugin,取而代之的是兩個新的配置項( optimization.splitChunks 和 optimization.runtimeChunk )用於抽取公共js模組。 通過 optimization.runtimeChunk: true 選項,webpack 會新增一個只包含執行時(runtime)額外程式碼塊到每一個入口。(注:這個需要看場景使用,會導致每個入口都載入多一份執行時程式碼)。
splitChunks預設配置介紹:
module.exports = {
// ...
optimization: {
splitChunks: {
chunks: 'async', // 控制webpack選擇哪些程式碼塊用於分割(其他型別程式碼塊按預設方式打包)。有3個可選的值:initial、async和all。
minSize: 30000, // 形成一個新程式碼塊最小的體積
maxSize: 0,
minChunks: 1, // 在分割之前,這個程式碼塊最小應該被引用的次數(預設配置的策略是不需要多次引用也可以被分割)
maxAsyncRequests: 5, // 按需載入的程式碼塊,最大數量應該小於或者等於5
maxInitialRequests: 3, // 初始載入的程式碼塊,最大數量應該小於或等於3
automaticNameDelimiter: '~',
name: true,
cacheGroups: {
vendors: { // 將所有來自node_modules的模組分配到一個叫vendors的快取組
test: /[\\/]node_modules[\\/]/,
priority: -10 // 快取組的優先順序(priotity)是負數,因此所有自定義快取組都可以有比它更高優先順序
},
default: {
minChunks: 2, // 所有重複引用至少兩次的程式碼,會被分配到default的快取組。
priority: -20, // 一個模組可以被分配到多個快取組,優化策略會將模組分配至跟高優先順序別(priority)的快取組
reuseExistingChunk: true // 允許複用已經存在的程式碼塊,而不是新建一個新的,需要在精確匹配到對應模組時候才會生效。
}
}
}
}
};
複製程式碼
關於 SplitChunksPlugin 的詳細配置可參考官方文件: SplitChunksPlugin
Vue && Vuex
Vue:
我們知道vue單頁應用只有一個入口,預設入口檔案是 main.js,在該檔案中處理 vue模板、Vuex 最終構造Vue物件。而多頁應用有多個入口,相當於在每個入口裡都要處理一遍單頁裡 main.js 要處理的事情。 一般的配置類似這樣:
import Vue from 'vue';
import Tpl from './index.vue'; // Vue模板
import store from '../../store'; // Vuex
new Vue({
store,
render: h => h(Tpl),
}).$mount('#app');
複製程式碼
Vuex:
為了避免所有狀態都集中到 store 物件中,導致檔案臃腫,不易維護,這裡將store 分割成多個模組(module)。每個模組擁有自己的 state、mutation、action。同時將getter抽離成單獨檔案。 檔案結構如下:
|- store
| |-modules
| | |-app.js // 單個module
| | |-user.js // // 單個module
| |-getters.js
| |-index.js // 在這裡組織各個module
複製程式碼
單個module的設定如下:
const app = {
state: { // state
count: 0
},
mutations: { // mutations
ADD_COUNT: (state, payload) => {
state.count += payload.amount;
}
},
actions: { // actions
addCount: ({ commit }, payload) => {
commit('ADD_COUNT', {
amount: payload.num
});
}
}
};
export default app;
複製程式碼
最終在index.js中組裝各個module:
import Vue from 'vue';
import Vuex from 'vuex';
import app from './modules/app';
import user from './modules/user';
import getters from './getters';
Vue.use(Vuex);
const store = new Vuex.Store({
modules: {
app,
user
},
getters
});
export default store;
複製程式碼
總結
總算寫完了,中間填了不少坑,但一路走下來還是有不少收穫的,後面有時間會繼續完善。專案原始碼的github地址在這裡:webpack4-vue2-multiPage,有需要的直接拿去,如果對你有一些幫助,也請給個star哈~~