предисловие
Мои друзья хотели представить некоторый контент, связанный с webapck, поэтому я потратил более двух месяцев на подготовку и, наконец, завершил серию webapck, которая включает следующие части:
- webpack series 1: рукописный сборщик JavaScript
- вторая серия webpack: лучшее руководство по настройке
- третья серия webpack: оптимизируйте скорость упаковки на 90 %
- четвертая серия webpack: оптимизация размера пакета
- Webpack Series 5: оптимизируйте время загрузки первого экрана и беглость страниц
- webpack series 6: анализ сборочных пакетов
- webpack седьмой серии: подробная конфигурация
- webapck series 8: написать плагин webapck (имитировать реализацию HtmlWebpackPlugin)
- Webpack Series 9: Интерпретация основного исходного кода webpack4
- Десятая серия webpack: внешний вид webpack5
Весь контент будет выпущен один за другим.Если у вас есть какой-либо контент, который вы хотите узнать, или у вас есть какие-либо вопросы, пожалуйста, подпишитесь на официальный аккаунт [Front-end Bottle Gentleman] и ответьте [123], чтобы добавить друзей, и я отвечу на ваши вопросы.
Как фронтенд-разработчик, мы тратим много времени на работу с инструментами упаковки, такими как webpack и gulp, упаковываем высокоуровневые проекты JavaScript в более сложные и трудные для интерпретации пакеты файлов и запускаем их в браузере, поэтому необходим для понимания механизма упаковки JavaScript. , это поможет вам лучше отлаживать проект, быстрее обнаруживать проблемы, а также поможет вам лучше понять и использовать инструменты упаковки, такие как webpack. В этой главе вы получите глубокое понимание того, что такое сборщик JavaScript и каков его механизм упаковки. В чем проблема? Если вы это понимаете, следующая оптимизация веб-пакета будет легкой.
1. Что такое модуль
У модуля может быть много определений, но я думаю:Модуль — это набор кода, связанный с определенной функцией. Он инкапсулирует детали реализации, предоставляет общедоступный API и объединяется с другими модулями для создания более крупных приложений.
Так называемая модульность предназначена для достижения более высокого уровня абстракции.Она инкапсулирует одну или несколько реализаций в модуль.Нам не нужно рассматривать зависимости в модуле, просто вызываем предоставляемый им API.
Например в проекте:
<html>
<script src="/src/man.js"></script>
<script src="/src/person.js"></script>
</html>
вperson.js
средняя зависимостьman.js
, и будет сообщено об ошибке, если вы измените порядок цитат. В больших проектах эта зависимость особенно важна и чрезвычайно сложна в обслуживании, кроме того, она имеет следующие проблемы:
- Все загружается в глобальный контекст, вызывая конфликты имен и переопределения.
- Включает в себя много ручной работы разработчиков, чтобы выяснить зависимости и включить порядок
Поэтому модули особенно важны.
Поскольку внешний и внутренний JavaScript-коды размещаются на обоих концах HTTP, они играют разные роли и имеют разную направленность. Браузерный JavaScript должен быть распределен со стороны сервера на несколько клиентских исполнений, в то время как серверный JS — это тот же самый код, который нужно выполнять несколько раз. Узким местом первого является пропускная способность, а узким местом второго — ресурсы памяти, такие как ЦП. В первом нужно загружать код по сети, во втором — с диска,Скорость загрузки двух не в том же порядке величины. Следовательно, определения внешнего и внутреннего модулей не согласованы, а модуль на стороне сервера определяется как:
- CJS (CommonJS): разработанная для синхронных определений серверного JavaScript, модульная система Node фактически основана на CJS;
Однако CommonJS импортируется синхронно, потому что он используется на стороне сервера, а файлы все локальные Синхронный импорт малоэффективен, даже если основной поток завис, но на стороне браузера, если занимает много время ожидания в процессе загрузки пользовательского интерфейса Сценарий загружается, что может вызвать большие проблемы для пользователя. Ввиду сетевых причин спецификации, разработанные CommonJS для бэкенда JavaScript, не совсем подходят для сценариев применения фронтенда.Давайте представим спецификации JavaScript-фронтенда.
- AMD (определение асинхронного модуля): RequireJS, определяемый как асинхронная модель для модулей в браузерах, является самой популярной реализацией AMD;
- UMD (универсальное определение модуля): по сути, это фрагмент кода JavaScript, который размещается поверх библиотек, чтобы позволить их загружать любому загрузчику в любой среде;
- ES2015 (ES6): определяет семантику асинхронного импорта и экспорта модулей, которые будут скомпилированы в
require/exports
Для выполнения это наиболее часто используемое сегодня определение модуля;
2. Что такое упаковщик
Упаковщик — это инструмент, используемый внешними разработчиками для упаковки модулей JavaScript в оптимизированный файл JavaScript, который можно запустить в браузере, например, webapck, rollup, gulp и т. д.
Например, вы включаете несколько файлов JavaScript в один файл html:
<html>
<script src="/src/entry.js"></script>
<script src="/src/message.js"></script>
<script src="/src/hello.js"></script>
<script src="/src/name.js"></script>
</html>
Между четырьмя импортированными файлами существуют следующие зависимости:
1. Модульный
Когда HTML-импортируется, нам нужно обратить внимание на порядок импорта этих 4 файлов (если порядок неправильный, проект сообщит об ошибке).Если он расширен до пригодного для использования веб-проекта с реальными функциями, это может быть необходимо импортировать десятки файлов.Зависимости сложнее.
Поэтому нам нужно разбить каждую зависимость на модули, пусть упаковщик поможет нам управлять этими зависимостями, чтобы на каждую зависимость можно было правильно ссылаться в нужное время и в нужном месте.
2. Комплектация
Кроме того, когда браузер открывает веб-страницу, для каждого js-файла требуется отдельный http-запрос, то есть 4 запроса туда и обратно, чтобы правильно запустить ваш проект.
Мы знаем, что браузеры медленно загружают модули, и даже поддержка HTTP/2 эффективно загружает много маленьких файлов, но ее производительность не так эффективна, как загрузка одного (даже без каких-либо оптимизаций).
Так что лучше объединить все 4 файла в 1:
<html>
<script src="/dist/bundle.js"></script>
</html>
Для этого требуется только один http-запрос.
Таким образом, модульность и объединение являются двумя наиболее важными функциями, которые необходимо реализовать упаковщику.
3. Как упаковать
Как упаковать в файл гм? Обычно он имеет входной файл, начиная с входного файла, получая все зависимости и упаковывая их в один файл.bundle.js
середина. Например, в приведенном выше примере мы можем/src/entry.js
В качестве входного файла объедините оставшиеся 3 файла JavaScript.
Конечно, слияние не может быть таким простым, как объединение всего содержимого 4 файлов в один.bundle.js
середина. Давайте сначала подумаем, как это должно быть реализовано?
1. Проанализируйте входной файл, чтобы получить все зависимости
Прежде всего, единственное, что мы можем определить, это адрес входного файла.Через адрес входного файла мы можем
- получить содержимое его файла
- Получить относительный адрес его зависимого модуля
Поскольку модули зависимостей импортируются по относительным путям (import './message.js'
), следовательно, нам нужно сохранить путь к файлу входа в сочетании с относительным адресом зависимого модуля, мы можем определить абсолютный адрес зависимого модуля и прочитать его содержимое.
Как представить модуль в зависимостях для удобной ссылки в графе зависимостей
Таким образом, мы можем представить модули как:
- код: анализ содержимого файла, обратите внимание, что анализируемый код может работать в текущих и старых браузерах или средах;
- зависимости: массив зависимостей, путь (относительный) путь для всех зависимых модулей;
- имя файла: абсолютный путь к файлу, когда
import
Зависимый модуль — это относительный путь, и в сочетании с текущим абсолютным путем получается путь зависимого модуля;
Среди них имя файла (абсолютный путь) может использоваться как уникальный идентификатор каждого модуля, а содержимое файла может быть получено напрямую через форму ключ: значение — в зависимости от модуля:
// 模块
'src/entry': {
code: '', // 文件解析后内容
dependencies: ["./message.js"], // 依赖项
}
2. Рекурсивно разрешить все зависимости для создания графа зависимостей
Мы определили представление модуля, так как мы можем связать все модули для создания графа зависимостей, с помощью которого мы можем напрямую получить зависимые модули всех модулей, код зависимых модулей, источник зависимых модулей, и зависимые модули зависимые модули.
Как сохранить отношения между зависимыми файлами
Теперь для каждого модуля единственное, что может быть представлено, этоfilename
, и когда мы рекурсивно анализируем файл записи, мы можем получить массив зависимостей каждого файлаdependencies
, который является относительным путем к каждой зависимости, поэтому нам нужно определить его:
// 关联关系
let mapping = {}
Используется для запуска кодаimport
Относительные пути сопоставляются сimport
абсолютный путь.
Итак, наш модуль можно определить как [имя файла: {}]:
// 模块
'src/entry': {
code: '', // 文件解析后内容
dependencies: ["./message.js"], // 依赖项
mapping:{
"./message.js": "src/message.js"
}
}
Тогда график зависимости:
// graph 依赖关系图
let graph = {
// entry 模块
"src/entry.js": {
code: '',
dependencies: ["./src/message.js"],
mapping:{
"./message.js": "src/message.js"
}
},
// message 模块
"src/message.js": {
code: '',
dependencies: [],
mapping:{},
}
}
Когда проект запущен, содержимое кода файла ввода успешно получено через файл ввода, и его код запускается.import
Если вы полагаетесь на модуль, передайтеmapping
Сопоставьте его с абсолютным путем, и вы сможете успешно прочитать содержимое модуля.
И абсолютный путь имени файла каждого модуля уникален, когда мы подключаем модуль к графу зависимостей.graph
, надо только судитьgraph[filename]
Будь он есть, если он есть, нет необходимости добавлять его дважды, и исключается повторная упаковка модуля.
3. Используя граф зависимостей, верните файл JavaScript, который можно запустить в браузере.
В настоящее время самой популярной формой кода, который может выполняться немедленно, является IIFE (Immediately Executed Function), который также может решить проблему загрязнения глобальных переменных.
IIFE
Так называемая IIFE — это анонимная функция, которая вызывается непосредственно в объявлении city.Поскольку область действия переменных JavaScript ограничена внутренней частью функции, вам не нужно учитывать, что она загрязнит глобальные переменные.
(function(man){
function log(name) {
console.log(`hello ${name}`);
}
log(man.name)
})({name: 'bottle'});
// hello bottle
4. Вывод в dist/bundle.js
fs.writeFile
написатьdist/bundle.js
Вот и все.
Итак, процесс упаковки и план реализации определены, давайте еще раз попрактикуемся!
В-четвертых, создать проект MiniPack
Создайте новую папку minipack иnpm init
, создайте следующие файлы:
- src
- - entry.js // 入口 js
- - message.js // 依赖项
- - hello.js // 依赖项
- - name.js // 依赖项
- index.js // 打包 js
- minipack.config.js // minipack 打包配置文件
- package.json
- .gitignore
вentry.js
:
import message from './message.js'
import {name} from './name.js'
message()
console.log('----name-----: ', name)
message.js
:
import {hello} from './hello.js'
import {name} from './name.js'
export default function message() {
console.log(`${hello} ${name}!`)
}
hello.js
:
export const hello = 'hello'
name.js
:
export const name = 'bottle'
minipack.config.js
:
const path = require('path')
module.exports = {
entry: 'src/entry.js',
output: {
filename: "bundle.js",
path: path.resolve(__dirname, './dist'),
}
}
И установить файл
npm install @babel/core @babel/parser @babel/preset-env @babel/traverse --save-dev
На данный момент весь проект создан. Далее идет упаковка:
- Разобрать файл записи, пройти все зависимости
- Рекурсивно разрешить все зависимости для создания графа зависимостей
- Используя граф зависимостей, возвращает файл JavaScript, который можно запустить в браузере.
- вывод в
/dist/bundle.js
5. Разберите файл записи и просмотрите все зависимости
1. @babel/parser анализирует входной файл и получает AST
В файле ./index.js мы создаем сборщик, который сначала анализирует файл записи, мы используем@babel/parser
Парсер для разбора:
Шаг 1: Прочитайте содержимое входного файла
// 获取配置文件
const config = require('./minipack.config');
// 入口
const entry = config.entry;
const content = fs.readFileSync(entry, 'utf-8');
Шаг 2: Используйте@babel/parser
(Парсер JavaScript) Анализировать код, генерировать ast (Абстрактное синтаксическое дерево)
const babelParser = require('@babel/parser')
const ast = babelParser.parse(content, {
sourceType: "module"
})
в,sourceType
Указывает режим, который должен анализировать код. возможно"script"
, "module"
или"unambiguous"
один из"unambiguous"
это позволить@babel/parser
Угадайте, если используете ES6import
илиexport
слова"module"
, в противном случае"script"
. Здесь используется ES6import
илиexport
, так что, это"module"
.
Поскольку дерево ast более сложное, здесь мы можем пройтиastexplorer.net/Проверять:
Мы получили всю часть входного файла, что нам делать дальше?
- Анализировать ast, анализировать содержимое файла записи (версия JavaScript для обратной совместимости в текущих и старых браузерах или средах)
- получить все его зависимые модули
dependencies
2. Получить содержимое входного файла
Мы уже знаем ast входного файла, который можно передать через@babel/core
изtransformFromAst
метод для анализа содержимого файла записи:
const {transformFromAst} = require('@babel/core');
const {code} = transformFromAst(ast, null, {
presets: ['@babel/preset-env'],
})
3. Получить все его зависимые модули
Нам нужно получить все зависимые модули через ast, то есть нам нужно получить все зависимости в astnode.source.value
, это,import
Относительный путь модуля, по которому можно найти зависимые модули.
Шаг 1. Определите массив зависимостей для хранения всех зависимостей, проанализированных в ast.
const dependencies = []
Шаг 2: Используйте@babel/traverse
, который используется вместе с парсером babel для обхода и обновления каждого дочернего узла.
traverse
функция является обходомAST
метод, поbabel-traverse
При условии, что его схема обхода классическаяvisitor
модель ,visitor
Паттерн должен определять сериюvisitor
, при встречеAST
из type === visitor
имя, он войдет в этоvisitor
Функция. типImportDeclaration
Узел AST на самом деле является нашимimport xxx from xxxx
, и, наконец, адресpush
прибытьdependencies
середина.
const traverse = require('@babel/traverse').default
traverse(ast, {
// 遍历所有的 import 模块,并将相对路径放入 dependencies
ImportDeclaration: ({node}) => {
dependencies.push(node.source.value)
}
})
3. Действительный возврат
{
dependencies,
code,
}
Полный код:
/**
* 解析文件内容及其依赖,
* 期望返回:
* dependencies: 文件依赖模块
* code: 文件解析内容
* @param {string} filename 文件路径
*/
function createAsset(filename) {
// 读取文件内容
const content = fs.readFileSync(filename, 'utf-8')
// 使用 @babel/parser(JavaScript解析器)解析代码,生成 ast(抽象语法树)
const ast = babelParser.parse(content, {
sourceType: "module"
})
// 从 ast 中获取所有依赖模块(import),并放入 dependencies 中
const dependencies = []
traverse(ast, {
// 遍历所有的 import 模块,并将相对路径放入 dependencies
ImportDeclaration: ({
node
}) => {
dependencies.push(node.source.value)
}
})
// 获取文件内容
const {
code
} = transformFromAst(ast, null, {
presets: ['@babel/preset-env'],
})
// 返回结果
return {
dependencies,
code,
}
}
6. Рекурсивно проанализируйте все зависимости и сгенерируйте граф зависимостей
Шаг 1: Получите входной файл:
const mainAssert = createAsset(entry)
Шаг 2: Создайте граф зависимостей:
Поскольку каждый модуль представлен в виде ключ: значение, определенный граф зависимостей выглядит следующим образом:
// entry: 入口文件绝对地址
const graph = {
[entry]: mainAssert
}
Шаг 3: Рекурсивно найдите все зависимые модули и добавьте их в граф зависимостей:
Определите функцию рекурсивного поиска:
/**
* 递归遍历,获取所有的依赖
* @param {*} assert 入口文件
*/
function recursionDep(filename, assert) {
// 跟踪所有依赖文件(模块唯一标识符)
assert.mapping = {}
// 由于所有依赖模块的 import 路径为相对路径,所以获取当前绝对路径
const dirname = path.dirname(filename)
assert.dependencies.forEach(relativePath => {
// 获取绝对路径,以便于 createAsset 读取文件
const absolutePath = path.join(dirname, relativePath)
// 与当前 assert 关联
assert.mapping[relativePath] = absolutePath
// 依赖文件没有加入到依赖图中,才让其加入,避免模块重复打包
if (!queue[absolutePath]) {
// 获取依赖模块内容
const child = createAsset(absolutePath)
// 将依赖放入 queue,以便于继续调用 recursionDep 解析依赖资源的依赖,
// 直到所有依赖解析完成,这就构成了一个从入口文件开始的依赖图
queue[absolutePath] = child
if(child.dependencies.length > 0) {
// 继续递归
recursionDep(absolutePath, child)
}
}
})
}
Начать рекурсивно из входного файла:
// 遍历 queue,获取每一个 asset 及其所以依赖模块并将其加入到队列中,直至所有依赖模块遍历完成
for (let filename in queue) {
let assert = queue[filename]
recursionDep(filename, assert)
}
Седьмое: используйте граф зависимостей, чтобы вернуть файл JavaScript, который можно запустить в браузере.
Шаг 1. Создайте функцию немедленного выполнения для запуска непосредственно в браузере.
const result = `
(function() {
})()
`
Шаг 2. Передайте граф зависимостей в качестве параметра функции немедленного выполнения.
Определите переданные модули параметров:
let modules = ''
траверсgraph
, поставить каждыйmod
отkey: value,
способ добавить кmodules
,
Примечание. Поскольку граф зависимостей необходимо передать в указанную выше функцию немедленного выполнения, а затем записать вdist/bundle.js
беги, значит,code
нужно разместитьfunction(require, module, exports){${mod.code}}
Во избежание загрязнения глобальных переменных или других модулей, в то же время, после преобразования кода в код используется система commonJS, а браузер не поддерживает commonJS (в браузере нет модуля, экспорта, запроса, глобального ), поэтому здесь нам нужно их реализовать и внедрить в функцию-оболочку.
for (let filename in graph) {
let mod = graph[filename]
modules += `'${filename}': [
function(require, module, exports) {
${mod.code}
},
${JSON.stringify(mod.mapping)},
],`
}
Шаг 3: Передайте параметры функции немедленного выполнения и немедленно выполните входной файл:
Сначала реализуйте требуемую функцию,require('${entry}')
Выполните входной файл,entry
Это абсолютный путь к входному файлу, а также уникальный идентификатор модуля.
const result = `
(function(modules) {
require('${entry}')
})({${modules}})
`
Уведомление:modules
это группаkey: value,
, поэтому мы вставляем его{}
середина
Шаг 4: Перепишите браузерrequire
метод, когда код запускаетсяrequire('./message.js')
преобразовать вrequire(src/message.js)
const result = `
(function(modules) {
function require(moduleId) {
const [fn, mapping] = modules[moduleId]
function localRequire(name) {
return require(mapping[name])
}
const module = {exports: {}}
fn(localRequire, module, module.exports)
return module.exports
}
require('${entry}')
})({${modules}})
`
Уведомление:
-
moduleId
для входящихfilename
, уникальный идентификатор модуля - через деконструкцию
const [fn, mapping] = modules[id]
чтобы получить оболочку нашей функции (function(require, module, exports) {${mod.code}}
)иmappings
объект - Потому что в целом
require
обаrequire
относительные пути, а не абсолютные пути, поэтому перепишитеfn
изrequire
метод, будетrequire
Преобразование относительных путей вrequire
абсолютный путь, т.е.localRequire
функция - будет
module.exports
входящий вfn
, когда содержимое зависимого модуля необходимо экспортировать в другие модули для использования, когдаrequire
Когда зависимый модуль, вы можете напрямую передатьmodule.exports
вернуть результат
Восемь, вывод в dist/bundle.js
// 打包
const result = bundle(graph)
// 写入 ./dist/bundle.js
fs.writeFile(`${output.path}/${output.filename}`, result, (err) => {
if (err) throw err;
console.log('文件已被保存');
})
Девять, резюме и исходный код
Изначально хотел написать просто, а в итоге столько правок и правок 🤦♀️🤦♀️🤦♀️, а разбираться надо досконально.
Адрес источника:GitHub.com/сестра и/м…
Ссылкаminipack, который решает проблему повторной упаковки модулей и использует webpack для определения модулей с именем файла в качестве уникального идентификатора.
Хотите увидеть предыдущую серию статей,Нажмите, чтобы перейти на домашнюю страницу блога github.
10. Идти последним
-
❤️ Получайте удовольствие, продолжайте учиться и всегда продолжайте программировать. 👨💻
-
Если у вас есть какие-либо вопросы или уникальные идеи, пожалуйста, прокомментируйте или свяжитесь с бутылкой напрямую (общедоступный номер может ответить на 123)! 👀👇
-
👇Приглашаем обратить внимание: джентльмен с бутылкой переднего плана, обновляется ежедневно! 👇