基於 webpack4 搭建 vue2、vuex 多頁應用框架

Youngbo發表於2018-07-30

背景

最近在對公司的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新增,指定打包模式,可選的值有:

  1. development,開發模式
    • 會將 process.env.NODE_ENV 設定成 development
    • 啟用 NamedChunksPlugin、NamedModulesPlugin 外掛
  2. production,生產模式
    • 會將 process.env.NODE_ENV 設定成 production
    • 會啟用最大化的優化(模組的壓縮、串聯等)
  3. 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哈~~

參考資料

相關文章