Автор: Мяо Диан
В настоящее время наиболее широко используемым и наиболее широко используемым инструментом упаковки приложений являетсяwebpackТеперь, помимо возможностей оптимизации, уже предоставляемых самим веб-пакетом (например, Tree Shaking, Code Splitting и т. д.), что еще мы можем сделать?Эта статья в основном знакомит с некоторыми исследованиями команды Didi WebApp на этом пути.
предисловие
В настоящее время все больше и больше проектов разрабатываются с использованием ES2015+, с webpack + babel в качестве инженерной основы, а сторонние библиотеки зависимостей загружаются через NPM. В то же время, чтобы достичь цели повторного использования кода, мы извлечем некоторые самостоятельно разработанные библиотеки компонентов или JSSDK в независимое обслуживание хранилища и загрузим их через NPM.
Большинство людей привыкли к такому способу развития и находят его очень удобным и практичным. Но за удобством скрываются две проблемы:
-
избыточность кода
Вообще говоря, эти пакеты NPM также разрабатываются на основе ES2015+, каждый пакет должен быть скомпилирован и выпущен Babel, прежде чем его можно будет использовать в основном приложении, и этот процесс компиляции часто добавляет много «скомпилированного кода»; каждый пакет будет имеют один и тот же скомпилированный код, что приводит к значительной избыточности кода, и эту часть избыточного кода нельзя удалить с помощью таких методов, как встряхивание дерева.
-
ненужные зависимости
При рассмотрении сценария библиотеки компонентов мы обычно для удобства вводим все компоненты, но на практике для приложения могут использоваться только некоторые компоненты, в этом случае введение всех компонентов также приведет к избыточности кода.
Избыточность кода приведет к увеличению времени загрузки и времени выполнения пакетов статических ресурсов, что напрямую повлияет на производительность и опыт. Теперь, когда мы признаем, что такие проблемы существуют, давайте посмотрим, как решить их обе.
основной
Для двух вышеупомянутых проблем наша основная схема оптимизации решения такова:пост-компиляцияа такжеВнедрить по требованию.
Эффект
Давайте сначала посмотрим на данные до и после оптимизации проекта тикета Didi (пользователь тикета) (без gzip, размер всего проекта после сжатия):
- Обычная упаковка: 455 КБ
- После компиляции: 423 КБ
- Посткомпиляция и импорт по запросу: 388 КБ
- пост-компиляция и импорт по запросу и babel-preset-env: 377 КБ
В итоге было уменьшено около 80 КБ, а эффект от оптимизации все равно весьма впечатляет.
Приведенные выше данные в основном используются для библиотек компонентов и некоторых внутренних общих JSSDK.пост-компиляцияа такжеВнедрить по требованиюПосле стратегии следует отметить, чтоВнедрить по требованиюЭффект зависит от ситуации в проекте, данные здесь только для справки.
Давайте посмотрим на конкретные детали этих двух точек соответственно.
пост-компиляция
Позвольте мне сначала объяснить:
Пост-компиляция: это означает, что пакеты NPM, от которых зависит приложение, не нужно компилировать перед выпуском, а компилируют вместе, когда приложение компилируется и упаковывается.
Суть пост-компиляции заключается в том, чтобы отсрочить время компиляции зависимых пакетов и скомпилировать их единообразно; давайте сначала взглянем на его конфигурацию веб-пакета.
настроить
Для конкретных проектных приложений пост-компиляцию делать особо не нужно, нужно лишь включить в конфигурационный файл webpack (webpack 2+) пакеты зависимостей, которые необходимо скомпилировать:
// webpack.config.js
module.exports = {
// ...
module: {
rules: [
// ...
{
test: /\.js$/,
loader: 'babel-loader',
// 注意这里的 include
// 除了 src 还包含了额外的 node_modules 下的两个包
include: [
resolve('src'),
resolve('node_modules/A'),
resolve('node_modules/B')
]
},
// ...
]
},
// ...
}
Нам просто нужно включить посткомпилированные модули A и B через конфигурацию включения веб-пакета.
Но здесь будут некоторые проблемы, например, как показано ниже:
Приложение, показанное выше, зависит от пакетов A и B, требующих посткомпиляции, в то время как A зависит от пакетов C и D, требующих посткомпиляции, а B зависит от пакета E, который не требует посткомпиляции; давайте сосредоточимся на случае полагаться на пакет A: сам A нуждается в пост-компиляции, а затем зависимые от A пакеты C и D также нуждаются в пост-компиляции. Мы можем назвать этот сценарийКомпиляция после вложения, в настоящее время, если вы все еще используете вышеуказанный метод настройки веб-пакета, вы также должны включить пакеты C и D, но для приложения оно знает только пакеты A и B, которые ему нужно скомпилировать позже, и оно не знает что A также будут пакеты C и D, которые нуждаются в пост-компиляции, поэтому приложение не должно явно включать пакеты C и D, но должно объявлять, какие модули пост-компиляции ему нужны.
Для решения вышеуказанногоКомпиляция после вложенияВопрос, мы разработали плагин WebPackwebpack-post-compile-plugin, используемый для автоматического сбора посткомпилированных пакетов зависимостей и их вложенных зависимостей; посмотрите на основной код этого плагина:
var util = require('./util')
function PostCompilePlugin (options) {
// ...
}
PostCompilePlugin.prototype.apply = function (compiler) {
var that = this
compiler.plugin(['before-run', 'watch-run'], function (compiler, callback) {
// ...
var dependencies = that._collectCompileDependencies(compiler)
if (dependencies.length) {
var rules = compiler.options.module.rules
rules && rules.forEach(function (rule) {
if (rule.include) {
if (!Array.isArray(rule.include)) {
rule.include = [rule.include]
}
rule.include = rule.include.concat(dependencies)
}
})
}
callback()
})
}
Принцип в компиляторе webpackbefore-run
а такжеwatch-run
В обработчике событий зависимости собираются, а затем присоединяются к включению webpack module.rule; собранное правило состоит в том, чтобы найти compileDependencies, объявленные в package.json приложения или зависимого пакета как зависимости после компиляции.
Итак, в случае с вышеуказанным приложением используйте конфигурацию webpack плагина webpack-post-compile-plugin:
var PostCompilePlugin = require('webpack-post-compile-plugin')
// webpack.config.js
module.exports = {
// ...
module: {
rules: [
// ...
{
test: /\.js$/,
loader: 'babel-loader',
include: [
resolve('src')
]
},
// ...
]
},
// ...
plugins: [
new PostCompilePlugin()
]
}
Добавьте поле compileDependencies в package.json текущего проекта, чтобы указать зависимости после компиляции:
// app package.json
{
// ...
"compileDependencies": ["A", "B"]
// ...
}
A также имеет зависимости после компиляции, поэтому вам нужно указать compileDependencies в package.json пакета A:
// A package.json
{
// ...
"compileDependencies": ["C", "D"]
// ...
}
преимущество
- Общие зависимости могут быть общими, и требуется только одна копия.Важно скомпилировать их только один раз.Рекомендуется управлять зависимостями через peerDependencies.
- Существует только одна копия API преобразования Babel (например, babel-plugin-transform-runtime или babel-polyfill).
- Нет необходимости настраивать ссылки компиляции и упаковки для каждого зависимого пакета, и его даже можно выпустить непосредственно на уровне исходного кода.
PS:Что касается выбора babel-plugin-transform-runtime и babel-polyfill, для приложений мы рекомендуем использовать babel-polyfill. Потому что зависимости некоторых сторонних пакетов будут определять, поддерживаются ли определенные функции глобально, и не выполнять обработку полифилла. Например: vuex проверит наличие поддержкиPromise
, если не поддерживается, будет выдано сообщение об ошибке или в коде есть что-то похожее"foobar".includes("foo")
Кодовые слова babel-plugin-transform-runtime не обрабатываются должным образом.
Конечно, техническое решение после компиляции конечно не идеально, и у него тоже будут некоторые недостатки.
недостаток
- Конфигурация Babel основного приложения должна быть совместима с конфигурацией Babel зависимостей.
- Зависимые пакеты не могут использовать псевдонимы и не могут удобно использовать DefinePlugin (он может быть просто скомпилирован, но не обработан Babel).
- Время компиляции приложения будет больше.
Несмотря на некоторые недостатки, с учетом соотношения цена/качество, пост-компиляция в настоящее время все еще является хорошим выбором.
Внедрить по требованию
Основная проблема, решающая после компиляции, является избыточность кода, а введение по требованию в основном используется для решения проблемы ненужных зависимостей.
Сценарии для внедрения по требованию в основном представляют собой библиотеки компонентов и пакеты зависимостей классов инструментов. Потому что, будь то библиотека компонентов или пакет зависимостей, он часто «большой и полный». При разработке приложения мы можем использовать только часть его возможностей. Если ввести их все, будет много отходов. ресурсов.
Чтобы решить эту проблему, нам нужно импортировать по требованию. В настоящее время основные библиотеки компонентов или инструментальные средства также предоставляют возможность импорта по запросу, но в основном они обеспечивают введение скомпилированных модулей.
Что мы рекомендуем, так это ввод исходного кода по запросу,пост-компиляцияплан упаковки.
Но на самом деле мы можем столкнуться с некоторыми проблемами обратной совместимости, которые нельзя убить одним выстрелом.Например, для проектов, которые были созданы ранее, в настоящее время нет ни сил, ни времени для выполнения соответствующих апгрейдов.Тогда какой-то наш внутренний компонент библиотеки или инструментальные средства В настоящее время требуется небольшая жертва: предоставить две записи, одну скомпилированную и одну исходную.
Битва за входы
Это связано с проблемой, что NPM-пакет имеет две записи, но, к счастью, webpack 2+ или rollup уже помогли нам справиться с этой проблемой, то есть запись по-прежнему использует основное поле в package.json после компиляции, а затем исходный код. запись использует поле модуля, см.rollup pkg.module wiki. Таким образом, мы можем реализовать совместное использование двух записей, что может не только обеспечить обратную совместимость, но и гарантировать, что запись, использующая webpack 2+ или rollup, напрямую указывает на исходный код, который можно напрямую использовать на этой основе.пост-компиляция.
Компиляция библиотеки компонентов Vue
пост-компиляцияа такжеВнедрить по требованиюОдин из самых типичных сценариев — наша библиотека компонентов, здесь мы делимся нашим практическим опытом работы с библиотекой компонентов (на основе Vue).
Внедрить по запросу.Когда нет пост-компиляции, фактически мы добились автоматической компиляции по каждому модулю при компиляции и публикации, так что пользователь может напрямую импортировать файл записи соответствующего каталога. Принцип очень прост: перейдите в каталог модуля в каталоге исходного кода, получите каждую запись и динамически измените запись конфигурации веб-пакета библиотеки компонентов. И этот процесспост-компиляцияЕго нет в сцене, и его можно напрямую импортировать в запись модуля, соответствующую исходному коду, потому чтопост-компиляцияВам не нужно полагаться на пакет для самостоятельной компиляции, вам нужно только скомпилировать приложение.
Для компонентов, если они предварительно скомпилированы, обычно мы компилируем JS-файлы входа и выхода, а также файлы стилей CSS, поэтому, если мы хотим импортировать по требованию, это может быть так:
import Dialog from 'cube-ui/lib/dialog'
import 'cube-ui/lib/dialog/style.css'
даже впост-компиляцияВ этом сценарии, хотя нет необходимости решать проблемы со стилем, путь недостаточно элегантен, когда он импортируется по требованию:
import Dialog from 'cube-ui/src/modules/dialog'
Что бы это ни было, это всегда недостаточно элегантно, к счастью, есть плагин для Babel.babel-plugin-transform-importsчтобы помочь нам изящно представить по требованию. Но для нашей скомпилированной сцены нам также нужно ввести стили, для этого мы ее унифицировали и расширили на babel-plugin-transform-imports.babel-plugin-transform-modulesПлагин, добавлен элемент настройки стиля.
Так используется он или нетпост-компиляция, мы хотим сделатьВнедрить по требованию,нужно только:
import { Dialog } form 'cube-ui'
Вы можете написать так, если используетепост-компиляция, исходный код импортируется напрямую, тогда вам нужно только добавить следующую конфигурацию в файл .babelrc:
"plugins": [
["transform-modules", {
"cube-ui": {
"transform": "cube-ui/src/modules/${member}",
"preventFullImport": true,
"kebabCase": true
}
}]
]
А если это webpack 1 или используемая библиотека компонентов уже скомпилирована, то нужно только добавитьstyle
Элементы конфигурации могут быть:
"plugins": [
["transform-modules", {
"cube-ui": {
"transform": "cube-ui/lib/${member}",
"preventFullImport": true,
"kebabCase": true,
"style": true
}
}]
]
Таким образом, мы можем добиться элегантного введения по запросу через плагин, независимо от того, используется он или нет.пост-компиляция, разработчикам нужно только изменить конфигурацию babel, и им не нужно изменять путь импорта в исходном коде.
Суммировать
Выше приведено небольшое исследование нашей оптимизации компиляции на основе веб-пакета.Здесь мы можем обобщить «лучшие практики» использования веб-пакета для компиляции и упаковки приложений:
Пост-компиляция + импорт по запросу
В сочетании с модулями babel-preset-env, babel-plugin-transform-modules опыт разработки и преимущества становятся лучше.
Добро пожаловать в блог Didi FE:GitHub.com/DD Fe/DD Fe — нет…