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

背景

最近在對公司的H5項目作重構,涉及到構建優化,因爲一些歷史緣由,項目原先使用的打包工具是餓了麼團隊開發的 cooking(基於 webpack 作的封裝,目前已中止維護了。)若是繼續使用,一是項目目前已經比較複雜,如今的構建方式每次打包耗時較長;二是使用一個已經中止維護的工具自己也有風險;另外由於本次重構還要進行 Vue1.0 到 Vue2.0 的框架升級,涉及到一系列依賴包( vue-style-loader 等)的版本兼容問題。折騰了一天也沒啥頭緒,索性將構建工具直接升級到 webpack4,同步搭配 vue2 和 vuex3,一步到位。css

因爲公司業務須要(SEO、頁面主要以投放爲主),咱們項目採用的是多頁面架構,網上基於vue的單頁應用模板,官方提供了 vue-cli,第三方的也很多,多頁模板可參考的卻很少。我先後花了兩週左右時間,參考了一些博客資料和文檔,整理了這套基於webpack4 + vue2 + vuex3的多頁應用模板,記錄下來方便本身之後查看,也分享給有須要的同窗參考。html

webpack的核心概念

受 Parcel 等零配置構建工具的啓發,webpack4 也在向無配置方向努力,作了大量優化,雖然支持零配置的方式,但若是想對模塊進行細粒度的控制,仍然須要手動對一些配置項進行設定。但和 webpack 以前版本相比已經明顯簡化,上手容易了不少。這裏先了解 webpack4 的幾個核心配置項,後面會逐一展開:vue

  • mode
  • entry
  • output
  • loader
  • plugins
  • devServer

接下來我就按照上面的順序,儘可能詳細的列出基於 webpack4 搭建 vue二、vuex 多頁應用的全流程node

mode

webpack4新增,指定打包模式,可選的值有:webpack

  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'
};
複製代碼

更多信息可參考:官方文檔 Modegit

entry

對比多頁應用和單頁應用(SPA),最大的不一樣點,就在於入口的不一樣es6

  • 多頁:最終打包生成多個入口( html 頁面),通常每一個入口文件除了要引入公共的靜態文件( js/css )還要另外引入頁面特有的靜態資源
  • 單頁:只有一個入口( index.html ),頁面中須要引入打包後的全部靜態文件,全部的頁面內容全由 JavaScript 控制

須要注意的是,上面說的入口指的都是最終打包到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官網

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哈~~

參考資料

相關文章
相關標籤/搜索