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

webpack打包遇到的痛點

隨着咱們項目愈來愈複雜,咱們在用webpack打包的時候,會發現打包的速度愈來愈慢,最後慢到打包一次要幾分鐘甚至更多的時間,緩慢的打包速度嚴重影響效率,那麼如何提升打包速度就成爲了咱們的痛點,通常你們都是用HappyPack、Dellplugin和UglifyJsPlugin(以前是ParallelUglifyPlugin,如今不維護合併到UglifyJsPlugin了)的parallel設爲true來提升打包速度,但這樣依舊沒法解決咱們項目龐大而致使打包速度的緩慢問題,實質上打包速度慢的根本緣由是由於,每次打包都是要把全部的文件都打包一遍,因此若是咱們想提升打包速度,那麼咱們能夠只打包修改的或者新加的文件,本文基於此提供一個方案。css

前言

咱們使用webpack來打包的時候,會有一個或者多個入口文件,打包到對應的html中,而咱們知道打包最耗時的就是對js進行壓縮和混淆的UglifyJsPlugin插件,若是咱們的項目龐大,入口文件過多,那麼打包js的速度將嚴重緩慢,so咱們能夠經過一些手段來告訴webpack咱們只想打包指定的入口文件,生成對應的html,經過「大事化小」的方式提升打包速度。html

通常咱們在寫webpack的入口文件的時候,咱們不會一個一個手動寫上去,像這樣node

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

這種方式在項目龐大的時候代碼管理起來很麻煩,且要手動維護,因此咱們會按照某種規則管理咱們的文件,而後寫一個方法來獲取入口文件,好比:webpack

// 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。git

那麼若是咱們修改與入口文件同個目錄的全部代碼,咱們但願打包的時候就打包這個入口文件,未修改的其餘入口文件通通不打包,這樣就能夠作到精確打包了,github

因此咱們約定,咱們全部與入口文件相關的全部業務代碼都放在入口文件相同的目錄下,這樣當咱們修改了代碼之後,咱們就只打包修改的代碼對應的入口文件。

怎麼判斷修改或者新建了文件

方法一

修改了哪些代碼,用戶本身最清楚,咱們能夠經過運行打包程序時告訴程序咱們修改了哪些模塊,咱們可使用inquirer來讓用戶手動輸入,也能夠經過命令行的方式輸入,關於命令行的輸入,如今npm命令能夠接受參數的輸入,在node咱們只須要經過process.argv來獲取用戶輸入的參數。web

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

這種方式的缺點就是須要用戶輸入,沒有作到自動化。正則表達式

方法二

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

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,咱們能夠經過寫一些正則表達式,把這些修改的和新增的文件給匹配出來,而後針對性的進行一些處理從而得出是哪一個入口文件的內容須要從新打包。下面基於方法二,講下如何作。npm

第一步:使用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…

相關文章
相關標籤/搜索