曲線救國:webpack打包優化黑科技

大雄沒了哆啦A夢發表於2019-02-21

webpack打包遇到的痛點

隨著我們專案越來越複雜,我們在用webpack打包的時候,會發現打包的速度越來越慢,最後慢到打包一次要幾分鐘甚至更多的時間,緩慢的打包速度嚴重影響效率,那麼如何提高打包速度就成為了我們的痛點,一般大家都是用HappyPack、Dellplugin和UglifyJsPlugin(之前是ParallelUglifyPlugin,現在不維護合併到UglifyJsPlugin了)的parallel設為true來提高打包速度,但這樣依舊無法解決我們專案龐大而導致打包速度的緩慢問題,實質上打包速度慢的根本原因是因為,每次打包都是要把所有的檔案都打包一遍,所以如果我們想提高打包速度,那麼我們可以只打包修改的或者新加的檔案,本文基於此提供一個方案。

前言

我們使用webpack來打包的時候,會有一個或者多個入口檔案,打包到對應的html中,而我們知道打包最耗時的就是對js進行壓縮和混淆的UglifyJsPlugin外掛,如果我們的專案龐大,入口檔案過多,那麼打包js的速度將嚴重緩慢,so我們可以通過一些手段來告訴webpack我們只想打包指定的入口檔案,生成對應的html,通過“大事化小”的方式提高打包速度。

一般我們在寫webpack的入口檔案的時候,我們不會一個一個手動寫上去,像這樣

 entry: {
    index: './src/views/index/main.js',
    bar: './src/views/bar/main.js',
    ....
  },
複製程式碼

這種方式在專案龐大的時候程式碼管理起來很麻煩,且要手動維護,所以我們會按照某種規則管理我們的檔案,然後寫一個方法來獲取入口檔案,比如:

// utils
// 獲取入口檔案
exports.getEntry = function () {
  let globPath = './src/views/**/index.js'
  return glob.sync(globPath)
    .reduce(function (entry, path) {
      let key = exports.getKey(path)
      entry[key] = path
      return entry
    }, {})
}
// 獲取單個入口檔案對應的key
exports.getKey = (path) => {
    let startIndex = path.indexOf('views') + 6
    let endIndex = 0
    if(path.indexOf('components') > -1){
        // 如果修改的是元件,注意這裡各個頁面的元件是放在各自的目錄下的
        endIndex = path.indexOf('components') + 1
    } else {
        endIndex = path.lastIndexOf('/')
    }
    return path.substring(startIndex, endIndex)
}

// 獲取所有入口檔案對應的keys
exports.getKeys = (filesPath) => {
    let result = []
    for(let path of filesPath) {
        let key = export.getKey(path)
        if(result.indexOf(key) === -1) {
            result.push(key)
        }
    }
    return result
}

// 根據入口檔案生成HtmlWebpackPlugins
exports.getHtmlWebpackPlugins = () => {
    let entyies = exports.getEntry()
    let keys = Object.keys(entry)
    let plugins = keys
      .map((key) => {
        // ejs模板,要和index.js在同個目錄下
        let template = exports.getTemplate(entyies[key])
        let filename = `${__dirname}/dist/${key}.html`
        return new HtmlWebpackPlugin({
          filename,
          template: template,
          inject: true,
          minify: {
            removeComments: true,
            collapseWhitespace: true,
            removeAttributeQuotes: true
          },
          // chunks: globals.concat([key]),
          chunksSortMode: 'dependency',
          excludeChunks: keys.filter(e => e != key)
        })
    })
    return plugins
}

// 獲取入口檔案對應的模板,模板檔案是index.html,本目錄沒有,會往上級目錄找
exports.getTemplate = (path) => {
  path = path.subStr(0, path.lastIndexOf('/'))
  var path = glob.sync(path + '/index.html')
  if(path.length > 0) {
    return path[0]
  } else {
    //取上級目錄下的模板檔案路徑
    if(path.lastIndexOf('/') !== -1) {
      path = path.substr(0, path.lastIndexOf('/'))
      return exports.getTemplate(path)
    }
  }
}
複製程式碼

這裡,我們的所有入口檔案都以index.js為命名,且key為views下到對應index.js的檔案路徑,例如./src/views/test/index.js的key就是test。根據這個規則,它會自動獲取src/view下的所有入口檔案index.js,並生成入口檔案對應的html。

那麼如果我們修改與入口檔案同個目錄的所有程式碼,我們希望打包的時候就打包這個入口檔案,未修改的其他入口檔案統統不打包,這樣就可以做到精確打包了,

所以我們約定,我們所有與入口檔案相關的所有業務程式碼都放在入口檔案相同的目錄下,這樣當我們修改了程式碼以後,我們就只打包修改的程式碼對應的入口檔案。

怎麼判斷修改或者新建了檔案

方法一

修改了哪些程式碼,使用者自己最清楚,我們可以通過執行打包程式時告訴程式我們修改了哪些模組,我們可以使用inquirer來讓使用者手動輸入,也可以通過命令列的方式輸入,關於命令列的輸入,現在npm命令可以接受引數的輸入,在node我們只需要通過process.argv來獲取使用者輸入的引數。

// npm命令通過--接受引數的輸入
npm run build -- module
// node通過process.argv來獲取
let module = process.argv[2]
複製程式碼

這種方式的缺點就是需要使用者輸入,沒有做到自動化。

方法二

我們知道git可以知道使用者修改過哪些檔案和新建了哪些檔案,那麼利用這點我們就可以知道哪些檔案修改過,哪些檔案是新增的,我們針對修改過和新增的檔案進行打包,未改動的忽略,如此我們便可以做到針對性的打包,而避免了全量打包的漫長過程。
我們知道,當我們使用git status命令的時候,git會給我們這樣的提示:

modified: xxx/xx/xx.js
modified: yyy/yy/yy.js

Untracked files:
  (use "git add <file>..." to include in what will be committed)

        xxx/xx/index.js
        yy/yy/index.js
複製程式碼

so,我們可以通過寫一些正規表示式,把這些修改的和新增的檔案給匹配出來,然後針對性的進行一些處理從而得出是哪個入口檔案的內容需要重新打包。下面基於方法二,講下如何做。

第一步:使用shelljs模組獲取git status列印出來的字串

1.安裝shelljs 
npm install shelljs --save-dev
2.獲取git status列印出來的字串
let shell = require('shelljs')
const result = shell.exec('git status')
複製程式碼

第二步:匹配出修改的檔案列表

// build.js
let modifiedFiles = []
// 匹配modified: 後面的修改的檔案路徑
match = result.match(/modified:\s+(.+)/g)
for(let i = 0, len = match.length; i < len; i++) {
    // 匹配views下修改的檔案
    if(/src\/(views|components)/.test(match[i])) {
        let path = match[i].match(/\s+(.+)/)[1]
        modifiedFiles.push(path)  
    }
}
複製程式碼

第三步:匹配出新增的檔案列表

這裡我以src/views目錄下,入口檔案以index.js為例

// build.js
// 獲取新加的檔案
let addFiles = []
// 獲取新建的檔案列表字串
let r = /(?<=\(use "git add <file>\.\.\." to include in what will be committed\))((\n|\t|.)+)/.test(result)
// 獲取新加檔案路徑
if(r) {
    let addFilesListStr = RegExp.$1
    // 匹配src/views下的檔案
    match = addFilesListStr.match(/\n*\t+(src\/views\/.+)\n+/g)
    for(let i = 0, len = match.length; i < len; i++) {
        // 去掉回車換行
        let path = match[i].replace(/(\t|\n)/g, '')
        // 這裡根據你的專案來定義,我這邊的專案入口是index.js,
        // 所以這樣設定,如果新增的檔案沒有index.js入口檔案則下面的glob就匹配不出來
        let paths = glob.sync(`${path}/**/index.js`)
        for(let path of paths) {
            addFiles.push(path)
        }
    }
}
複製程式碼

第四步:針對性打包/增量打包

有了第二步獲取的修改檔案的路徑,經過一些處理,我們就可以知道哪些入口key修改了,然後打包的時候就只打包這些修改的key對應的入口檔案

// utils.js
exports.getModifiedEntry = (modifiedFiles) => {
   let modifiedKeys = exports.getKeys(modifiedFiles)
   let modifiedEntry = {}
   // 全量entry
   let webpackEntry = exports.getEntry()
   for(let key of modifiedKeys) {
    modifiedEntry[key] = webpackEntry[key]
   }
   return modifiedEntry
}
複製程式碼

獲取新建檔案的entry,通過git我們可以獲取新加的檔案列表,然後根據檔案列表我們獲取新加的entry,所以我們擴充套件getEntry方法,但傳入引數為檔案列表的時候,我們從新加的檔案列表中獲取新建的entry

/**
*
*files引數為第三步獲取的新加檔案列表
*/
// utils.js
exports.getEntry = function (files) {
  // 從新加的檔案列表中獲取新建的entry
  if (files) {
    let entry = {}
    for (let path of files) {
      let key = exports.getKey(path)
      entry[key] = './' + path
    }
    return entry
  }
  let globPath = './src/views/**/index.js'
  return glob.sync(globPath)
    .reduce(function (entry, path) {
      let key = exports.getKey(path)
      entry[key] = path
      return entry
    }, entry)
}
複製程式碼

最後我們要根據修改檔案列表和新加的檔案列表生成HtmlWebpackPlugins以打包對應的html

// 根據入口配置獲取對應的htmlWebpackPlugin
// utils.js
exports.getHtmlWebpackPlugins = (entry) => {
    let keys = Object.keys(entry)
    let plugins = keys
      .map((key) => {
        let template = exports.getTemplate(entry[key])
        let filename = `${__dirname}/dist/${key}.html`
        return new HtmlWebpackPlugin({
          filename,
          template: template,
          inject: true,
          minify: {
            removeComments: true,
            collapseWhitespace: true,
            removeAttributeQuotes: true
          },
          chunksSortMode: 'dependency'
        })
    })
    return plugins
}

// build.js
var utils = require('utils')
let newEntry = {}
Object.assign(newEntry, addEntry, modifiedEntry)
htmlWebpackPlugins = utils.getHtmlWebpackPlugins(newEntry)

複製程式碼

其他問題

如果我們修改了一些全域性的程式碼,比如各個元件依賴的js,css等等,這個時候需要進行全量打包了,那麼候我們可以通過引數告訴程式我們要全量打包,參照方法一,通過npm run build -- all

// build.js
var utils = require('utils')
let isBuildAll = process.argv[2] === 'all'
if(isBuildAll) {
    // 全量打包
    let entry = utils.getEntry()
    let plugins = utils.getHtmlWebpackPlugins(entry)
    webpackConfig.plugins = webpackConfig.plugins
        .concat(plugins)
}
複製程式碼

以上是我在開發中遇到打包十分緩慢的一種解決方案,簡略的程式碼請檢視我的git: github.com/VikiLee/acc…


相關文章