Кривая, чтобы спасти страну: черная технология оптимизации упаковки webpack

Webpack

Болевые точки, возникающие при упаковке webpack

По мере того, как наши проекты становятся все более и более сложными, когда мы используем webpack для упаковки, мы обнаружим, что скорость упаковки становится все медленнее и медленнее.В конце концов, упаковка за один раз занимает несколько минут или даже больше. скорость упаковки серьезно влияет на эффективность, тогда как улучшить скорость упаковки стало нашей проблемой.Как правило, все используют HappyPack, Dellplugin и UglifyJsPlugin (ранее ParallelUglifyPlugin, но теперь он не поддерживается и объединен с UglifyJsPlugin). чтобы улучшить скорость упаковки, но это все еще не может решить нас Большой проект приводит к низкой скорости упаковки Фактически, фундаментальная причина низкой скорости упаковки заключается в том, что все файлы должны быть упакованы каждый раз, когда они упакованы, поэтому, если мы хотим чтобы повысить скорость упаковки, мы можем упаковывать и изменять только недавно добавленные файлы, в этой статье представлено решение, основанное на этом.

предисловие

Когда мы используем webpack для упаковки, будет один или несколько входных файлов, упакованных в соответствующий html, и мы знаем, что наиболее трудоемким пакетом является плагин UglifyJsPlugin, который сжимает и запутывает js.Если наш проект огромен, запись Если файлов слишком много, скорость упаковки js будет очень низкой, поэтому мы можем использовать некоторые средства, чтобы сообщить webpack, что мы хотим упаковать только указанный файл записи, сгенерировать соответствующий html и улучшить скорость упаковки, «сделав большие дела на маленькие».

Как правило, когда мы пишем входные файлы веб-пакета, мы не пишем их вручную один за другим, как это

 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, а ключ — это путь к файлу представлений до соответствующего index.js, например, ключ ./src/views/test/index.js — test. В соответствии с этим правилом он автоматически получит все входные файлы index.js в src/view и сгенерирует HTML-код, соответствующий этим входным файлам.

Затем, если мы изменим весь код в том же каталоге, что и файл входа, мы хотим упаковать этот файл входа при упаковке, а другие немодифицированные файлы входа не будут упакованы, чтобы мы могли точно упаковать,

Итак, мы договорились, что все наши бизнес-коды, связанные с файлом ввода, размещаются в том же каталоге, что и файл ввода, чтобы при изменении кода мы упаковывали только файл ввода, соответствующий измененному коду.

Как определить, был ли файл изменен или создан

метод первый

Пользователь знает, какой код был изменен. Мы можем сообщить программе, какие модули мы изменили, запустив упаковщик. Мы можем использовать опросник, чтобы пользователь мог ввести его вручную или ввести через командную строку. input, Теперь команда npm может принимать ввод параметров, в узле нам нужно только получить параметры, введенные пользователем через 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

Таким образом, мы можем сопоставить эти измененные и недавно добавленные файлы, написав некоторые регулярные выражения, а затем выполнить некоторую целевую обработку, чтобы получить содержимое того файла записи, который необходимо переупаковать. Опираясь на второй способ, поговорим о том, как это сделать.

Шаг 1: Используйте модуль shelljs, чтобы получить строку, напечатанную статусом git.

1.安装shelljs 
npm install shelljs --save-dev
2.获取git status打印出来的字符串
let shell = require('shelljs')
const result = shell.exec('git status')

Шаг 2: Сопоставьте список измененных файлов

// 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)  
    }
}

Шаг 3: Сопоставьте новый список файлов

Здесь я беру каталог 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)
        }
    }
}

Шаг 4: Целевая упаковка/дополнительная упаковка

Имея путь измененного файла, полученный на втором этапе, после некоторой обработки мы можем узнать, какие ключи входа были изменены, а затем при упаковке упаковывать только файлы входов, соответствующие этим измененным ключам.

// 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
}

Чтобы получить запись нового файла, мы можем получить недавно добавленный список файлов через git, а затем мы получим вновь добавленную запись в соответствии со списком файлов, поэтому мы расширяем метод getEntry, но когда входящим параметром является список файлов , мы начинаем со вновь добавленного списка файлов Получить новую запись в

/**
*
*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/Вики Ли/ACC…