vue多頁面開發和打包的正確姿勢

乘風gg發表於2018-04-20

本文大約二千字,看完本文大概需要二十分鐘,動手嘗試需要一小時

前段時間做專案,技術棧是vue+webpack,主要就是官網首頁加後臺管理系統 根據當時情況,分析出三種方案

  1. 一個專案程式碼裡面嵌兩個spa應用(官網和後臺系統)
  2. 分開兩套專案原始碼
  3. 一套專案原始碼裡面就一個spa應用

思考:

  1. 直接否定了一套專案原始碼裡一個spa應用(ui樣式會相互覆蓋,如果沒有程式碼規範後期比較難維護)
  2. 兩套原始碼的話,後臺可能開兩個埠,然後需要用nginx反向代理可能比較麻煩,而且前端開發也比較麻煩麻煩,畢竟需要維護兩個git倉庫,兩套git上線流程,可能會損耗很多時間。
  3. 對自己的技術(盲目)自信,也想嚐嚐鮮,分析出需求也不算很複雜。選了第一種方案,就是多個單頁面應用在一套原始碼裡面

上一張多頁面的結構圖

vue多頁面開發和打包的正確姿勢

下載vue spa模板

npm install vue-cli -g
vue init webpack multiple-vue-amazing
複製程式碼

改造多頁面應用

npm install glob --save-dev
複製程式碼

修改src資料夾下面的目錄結構

vue多頁面開發和打包的正確姿勢
在util.js裡面加入


/* 這裡是新增的部分 ---------------------------- 開始 */

// glob是webpack安裝時依賴的一個第三方模組,還模組允許你使用 *等符號, 例如lib/*.js就是獲取lib資料夾下的所有js字尾名的檔案
var glob = require('glob')
// 頁面模板
var HtmlWebpackPlugin = require('html-webpack-plugin')
// 取得相應的頁面路徑,因為之前的配置,所以是src資料夾下的pages資料夾
var PAGE_PATH = path.resolve(__dirname, '../src/pages')
// 用於做相應的merge處理
var merge = require('webpack-merge')


//多入口配置
// 通過glob模組讀取pages資料夾下的所有對應資料夾下的js字尾檔案,如果該檔案存在
// 那麼就作為入口處理
exports.entries = function () {
    var entryFiles = glob.sync(PAGE_PATH + '/*/*.js')
    var map = {}
    entryFiles.forEach((filePath) => {
        var filename = filePath.substring(filePath.lastIndexOf('\/') + 1, filePath.lastIndexOf('.'))
        map[filename] = filePath
    })
    return map
}

//多頁面輸出配置
// 與上面的多頁面入口配置相同,讀取pages資料夾下的對應的html字尾檔案,然後放入陣列中
exports.htmlPlugin = function () {
    let entryHtml = glob.sync(PAGE_PATH + '/*/*.html')
    let arr = []
    entryHtml.forEach((filePath) => {
        let filename = filePath.substring(filePath.lastIndexOf('\/') + 1, filePath.lastIndexOf('.'))
        let conf = {
            // 模板來源
            template: filePath,
            // 檔名稱
            filename: filename + '.html',
            // 頁面模板需要加對應的js指令碼,如果不加這行則每個頁面都會引入所有的js指令碼
            chunks: ['manifest', 'vendor', filename],
            inject: true
        }
        if (process.env.NODE_ENV === 'production') {
            conf = merge(conf, {
                minify: {
                    removeComments: true,
                    collapseWhitespace: true,
                    removeAttributeQuotes: true
                },
                chunksSortMode: 'dependency'
            })
        }
        arr.push(new HtmlWebpackPlugin(conf))
    })
    return arr
}
/* 這裡是新增的部分 ---------------------------- 結束 */
複製程式碼

webpack.base.conf.js 檔案

  /* 修改部分 ---------------- 開始 */
  entry: utils.entries(),
  /* 修改部分 ---------------- 結束 */
複製程式碼

webpack.dev.conf.js 檔案

 /* 註釋這個區域的檔案 ------------- 開始 */
    // new HtmlWebpackPlugin({
    //   filename: 'index.html',
    //   template: 'index.html',
    //   inject: true
    // }),
    /* 註釋這個區域的檔案 ------------- 結束 */
    new FriendlyErrorsPlugin()

    /* 新增 .concat(utils.htmlPlugin()) ------------------ */
  ].concat(utils.htmlPlugin())
複製程式碼

webpack.prod.conf.js 檔案

 /* 註釋這個區域的內容 ---------------------- 開始 */
    // new HtmlWebpackPlugin({
    //   filename: config.build.index,
    //   template: 'index.html',
    //   inject: true,
    //   minify: {
    //     removeComments: true,
    //     collapseWhitespace: true,
    //     removeAttributeQuotes: true
    //     // more options:
    //     // https://github.com/kangax/html-minifier#options-quick-reference
    //   },
    //   // necessary to consistently work with multiple chunks via CommonsChunkPlugin
    //   chunksSortMode: 'dependency'
    // }),
    /* 註釋這個區域的內容 ---------------------- 結束 */
    // copy custom static assets
    new CopyWebpackPlugin([
      {
        from: path.resolve(__dirname, '../static'),
        to: config.build.assetsSubDirectory,
        ignore: ['.*']
      }
    ])
    /* 該位置新增 .concat(utils.htmlPlugin()) ------------------- */
  ].concat(utils.htmlPlugin())
複製程式碼

引入第三方ui庫

npm install element-ui bootstrap-vue --save
複製程式碼

分別在不同的頁面引入不同的ui index.js

import BootstrapVue from 'bootstrap-vue'
Vue.use(BootstrapVue)
複製程式碼

admin.js

import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
Vue.use(ElementUI)
複製程式碼

上面多頁面的配置是參考網上的,而且網上的思路大都很相似,核心就是改多個entry,配置完成了之後,開發的時候也是發現不了問題的,然後大概就開發了一個月,開發完之後對官網進行效能分析時發現,webpack打包的vendor.js網路載入時間特別長,導致首屏的白屏時間非常長,最終通過-webpack-bundle-analyzer分析得到了結論

npm run build --report
複製程式碼

vue多頁面開發和打包的正確姿勢
你會發現vendor.js包含了index.html和admin.html的共同部分,所以這個vendor包註定會很大很冗餘

解決思路

既然是vendor過大引起載入速度慢,那就分離這個vendor就好了。我是這樣想的,把各個頁面中都使用到的第三方程式碼提取至vendor.js中,然後各個頁面中用到的第三方程式碼再打包成各自的vendor-x.js,例如現有頁面index.html、admin.html,則最終會打包出vendor.js、vendor-index.js、vendor-admin.js。

webpack.prod.conf.js 檔案

    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor-admin',
      chunks: ['vendor'],
      minChunks: function (module, count) {
        return (
          module.resource &&
          /\.js$/.test(module.resource) &&
          module.resource.indexOf(path.join(__dirname, '../node_modules')) === 0 &&
          module.resource.indexOf('element-ui') != -1
        )
      }
    }),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor-index',
      chunks: ['vendor'],
      minChunks: function (module, count) {
        return (
          module.resource &&
          /\.js$/.test(module.resource) &&
          module.resource.indexOf(path.join(__dirname, '../node_modules')) === 0 &&
          module.resource.indexOf('bootstrap-vue') != -1
        )
      }
    }),
複製程式碼

再次分析,一切都很ok,vendor.js被分離成了vendor.js、vendor-index、vendor-admin.js

vue多頁面開發和打包的正確姿勢
本來以為解決了CommonsChunkPlugin的分離vendor.js的問題,就可以了,然後打包出來發現index.html和admin.html都少了一個引入(各自對應的那個vendor-xx.js)
vue多頁面開發和打包的正確姿勢

解決方案

這個問題其實就是HtmlWebpackPlugin的問題 把原來的 chunksSortMode: 'dependency'改成自定義函式的配置,如下

util.js檔案

chunksSortMode: function (chunk1, chunk2) {
          var order1 = chunks.indexOf(chunk1.names[0])
          var order2 = chunks.indexOf(chunk2.names[0])
          return order1 - order2
        },
複製程式碼

抽離多個頁面之間公共的模組common-api (2018/4/23)更新

其實多頁面之間抽離公共的檔案的場景,中大型專案會用的比較多,最初是看到下面評論說需要抽離common-api的時候會多打包出來common-api.css,並且有同學私信我遇到一些關於commonChunk的問題,我做一個更新,並提供思路

需求:專案中一些公共的js甚至是css,可複用到每一個page裡邊,例如admin.js引用common-api.js,index.js也引用了common-api.js。現在抽離多個頁面之間公共的模組common-api

新增common目錄

新建一個common/index.js 直接引用一個本地檔案js(我這裡用jquery來代替公共js)

vue多頁面開發和打包的正確姿勢
vue多頁面開發和打包的正確姿勢

引用公共檔案

在admin.js index.js裡引用

import $ from '../../common'
console.log($('body'))
複製程式碼

打包明顯能發現jquery被打包了兩次,資源浪費了。

vue多頁面開發和打包的正確姿勢

解決方案

  • 因為是多頁面,所以必須新增一個公共的入口common-api
  • commonChunkPlugin 抽取common-api(有坑,下面會講)
  • 修改htmlWebpackPlugin的chunks順序
    vue多頁面開發和打包的正確姿勢
    vue多頁面開發和打包的正確姿勢

坑就是必須指定chunks 不然會報錯webpack ERROR in CommonsChunkPlugin: While running in normal mode it's not allowed to use a non-entry chunk

一開始有位同學問我這個錯誤怎麼解決,一開始我也不知道,後來查閱了一些資料,發現只要指定了chunks就可以解決這個錯誤,貼一段github的issues,感興趣的可以深入瞭解

重新指定util.js 裡面的htmlPlugin的順序

 let chunks = filename === 'admin' ?
      ['manifest', 'vendor', 'vendor-admin', 'common-api', filename] :
      ['manifest', 'vendor', 'vendor-index', 'common-api', filename]
複製程式碼

最後看看結果,抽離出來了common-api,jq只被載入了一次, 並且並沒有多打包出來common-api.css,html裡面script的順序也是對的

vue多頁面開發和打包的正確姿勢

vue多頁面開發和打包的正確姿勢
最終實現

  • 每個頁面載入各自的chunk
  • 每個頁面有不同的引數
  • 每個頁面能共享公共chunk
  • 瀏覽器快取,效能更好
  • 如果還嫌慢的話,開啟gzip
  • 每個頁面抽離公共函式common-api(更新於2018/4/23)

感想

大功告成了,雖然配置看起來很簡單,不過我當時開發的時候,思考了很久,所以假如你CommonsChunkPlugin和HtmlWebpackPlugin不熟悉或者只會用別人第三方的配置表,估計會踩大坑,比如說,CommonsChunkPlugin不指定chunks,預設是什麼?minChunks大多數人只會寫一個數值,然而自定義一個函式的寫法其實才是最強大的,根據我個人的經驗chunks結合minChunks自定義函式的寫法,能解決幾乎所有CommonsChunkPlugin靈異的事件。

webpack4

雖然本篇文章是基於webpack3來講的,但是webpack4多頁面配置和優化打包的思想其實也是一樣的,放心大膽的使用吧,有坑就解決它。

原始碼

本文原始碼喜歡點個贊

參考

多頁面配置參考

HtmlWebpackPlugin順序問題

相關文章