最近在對公司的H5項目作重構,涉及到構建優化,因爲一些歷史緣由,項目原先使用的打包工具是餓了麼團隊開發的 cooking(基於 webpack 作的封裝,目前已中止維護了。)若是繼續使用,一是項目目前已經比較複雜,如今的構建方式每次打包耗時較長;二是使用一個已經中止維護的工具自己也有風險;另外由於本次重構還要進行 Vue1.0 到 Vue2.0 的框架升級,涉及到一系列依賴包( vue-style-loader 等)的版本兼容問題。折騰了一天也沒啥頭緒,索性將構建工具直接升級到 webpack4,同步搭配 vue2 和 vuex3,一步到位。css
因爲公司業務須要(SEO、頁面主要以投放爲主),咱們項目採用的是多頁面架構,網上基於vue的單頁應用模板,官方提供了 vue-cli,第三方的也很多,多頁模板可參考的卻很少。我先後花了兩週左右時間,參考了一些博客資料和文檔,整理了這套基於webpack4 + vue2 + vuex3的多頁應用模板,記錄下來方便本身之後查看,也分享給有須要的同窗參考。html
受 Parcel 等零配置構建工具的啓發,webpack4 也在向無配置方向努力,作了大量優化,雖然支持零配置的方式,但若是想對模塊進行細粒度的控制,仍然須要手動對一些配置項進行設定。但和 webpack 以前版本相比已經明顯簡化,上手容易了不少。這裏先了解 webpack4 的幾個核心配置項,後面會逐一展開:vue
接下來我就按照上面的順序,儘可能詳細的列出基於 webpack4 搭建 vue二、vuex 多頁應用的全流程node
webpack4新增,指定打包模式,可選的值有:webpack
webpack --mode=production
複製代碼
module.exports = {
mode: 'production'
};
複製代碼
更多信息可參考:官方文檔 Modegit
對比多頁應用和單頁應用(SPA),最大的不一樣點,就在於入口的不一樣es6
須要注意的是,上面說的入口指的都是最終打包到dist目錄下的html文件,而咱們在這裏配置的 entry 實際上是須要被 html 引入的js模塊,這些js模塊、連同抽離的公共js模塊最終還須要利用 html-webpack-plugin 這個插件組合到html文件中:github
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中多頁的配置信息:web
module.exports = {
HTMLDirs: [
{
page: 'index',
title: '首頁'
},
{
page: 'list',
title: '列表頁',
dir: 'content' // 支持設置多級目錄
},
{
page: 'detail',
title: '詳情頁'
}
],
// ...
};
複製代碼
最後再引入相關配置:vuex
module.exports = {
entry: Entries,
// ...
plugins: [
...HTMLPlugins // 利用 HTMLWebpackPlugin 插件合成最終頁面
]
// ...
}
複製代碼
關於公共模塊的抽離後面會單獨介紹
html-webpack-plugin更多配置信息:html-webpack-plugin官網
配置出口的文件名和路徑:
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 用於對模塊的源代碼進行轉換,負責把某種文件格式的內容轉換成 webpack 能夠支持打包的模塊,例如將sass預處理轉換成 css 模塊;將 TypeScript 轉換成 JavaScript;或將內聯圖像轉換爲 data URL等
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的區別
// ...
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,
}
}
]
},
// ...
複製代碼
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的抽取
在webpack打包流程中,模塊代碼轉換的工做由 loader 來處理,除此以外的其餘工做均可以交由 plugin 來完成。經常使用的有:
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 保證模板文件中引用正確的靜態資源地址
})
]
複製代碼
// 抽取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'
})
]
複製代碼
平常開發的時候咱們須要在本地啓動一個靜態服務器,以方便開發調試,咱們使用 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
webpack 4 移除了 CommonsChunkPlugin,取而代之的是兩個新的配置項( optimization.splitChunks 和 optimization.runtimeChunk )用於抽取公共js模塊。 經過 optimization.runtimeChunk: true 選項,webpack 會添加一個只包含運行時(runtime)額外代碼塊到每個入口。(注:這個須要看場景使用,會致使每一個入口都加載多一份運行時代碼)。
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單頁應用只有一個入口,默認入口文件是 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');
複製代碼
爲了不全部狀態都集中到 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哈~~