Чен Хэн, группа поддержки платформы отдела передовых технологий WeDoctor, самый красивый архитектор~
В сообществе фронтендаwebpackМожно сказать, вечная тема. Его мощные и гибкие функции в значительной степени способствовали развитию фронтенд-инжиниринга, что сопровождалось взлетами и падениями бесчисленных фронтенд-проектов. Его сложная конфигурация также обескуражила бесчисленное количество фронтендеров, посмеиваясь над тем, что нужен новый тип работы «инженер по настройке веб-пакетов». Являясь давно зарекомендовавшим себя, наиболее распространенным и классическим инструментом упаковки,webpackОчень ценно для обсуждения. пониматьwebpack,владелецwebpack, будь то в процессе собеседования или в ежедневном процессе построения, разработки и оптимизации проекта, это может принести много пользы. Тогда эта статья начнется с основной концепции и заставит читателей отвести взгляд.webpackпальто, видеть сквозь его сущность.
что это
На самом деле проблема в том,webpackПервый абзац официального сайта дает четкое определение:
At its core, webpack is a static module bundler for modern JavaScript applications. When webpack processes your application, it internally builds a dependency graph which maps every module your project needs and generates one or more bundles.
Это значит:
Ядро webpack предназначено для современных приложений JavaScript.сборщик статических модулей. Когда webpack обрабатывает ваше приложение, он внутренне создаетграфик зависимости, который сопоставляет каждый модуль, требуемый вашим проектом, иСоздать один или несколько пакетов.
Осведомленность об элементе:сборщик статических модулей,график зависимости,Создать один или несколько пакетов. Хотя сегодняшние front-end проекты,webpackОн играет важную роль и включает в себя множество функций, но по своей сути это все же «упаковщик модулей», интегрирующийJavaScriptМодули упакованы в один или несколькоJavaScriptдокумент.
что делать
Итак, зачем вам нужен сборщик модулей?webpackсклад в первые годыREADMEТакже дает ответ:
As developer you want to reuse existing code. As with node.js and web all file are already in the same language, but it is extra work to use your code with the node.js module system and the browser. The goal of
webpackis to bundle CommonJs modules into javascript files which can be loaded by<script>-tags.
можно увидеть,node.jsБольшое количество экологическихJavaScriptкод, а потомуnode.jsконец последовалCommonJSМодульная спецификация не соответствует требованиям браузера, в результате чего код нельзя использовать повторно, что является огромной потерей. тогдаwebpackВсе, что вам нужно сделать, это упаковать эти модули, чтобы их можно было использовать на стороне браузера.<script>Вкладки загружаются и работаютJavaScriptдокумент.
Может быть, это не единственное объяснениеwebpackПричина существования, но достаточно, чтобы дать нам большое вдохновение - положитьCommonJSСтандартизированный код преобразуется в исполняемый в браузереJavaScriptкод
как
Поскольку браузер неCommonJSСпецификация, затем реализуйте ее. отwebpackМы видим идею упакованного продукта.
Создайте три новых файла для наблюдения за их упакованными продуктами:
src/index.js
const printA = require('./a')
printA()
src/a.js
const printB = require('./b')
module.exports = function printA() {
console.log('module a!')
printB()
}
src/b.js
module.exports = function printB() {
console.log('module b!')
}
воплощать в жизньnpx webpack --mode developmentУпакованный выводdist/main.jsдокумент
На приведенном выше рисунке с помощьюwebpackУпаковать 3 простых файла jsindex.js/a.js/b.js, вindex.jsзависел отa.js, иa.jsположился наb.js, образуя полную зависимость.
Так,webpackКак узнать зависимости между файлами и как собрать зависимые файлы, чтобы их не пропустить? Мы все еще можем найти ответ в официальной документации:
When webpack processes your application, it starts from a list of modules defined on the command line or in its configuration file. Starting from these entry points, webpack recursively builds a dependency graph that includes every module your application needs, then bundles all of those modules into a small number of bundles - often, just one - to be loaded by the browser.
Это,webpackбудет настроен изЗапись начинается, рекурсивно строит дерево зависимостей модулей, необходимых приложению.. мы знаем,CommonJSВ спецификации при зависимости от определенного файла нужно использовать толькоrequireКлючевое слово можно ввести, тогда пока мы сталкиваемсяrequireключевое слово, чтобы устранить эту зависимость, и эту зависимость можно использовать сноваrequireКлючевое слово продолжает ссылаться на другую зависимость, поэтому можно рекурсивно полагаться наrequireКлючевое слово находит все зависимые файлы, тем самым завершая построение дерева зависимостей.
Вы можете видеть, что в окончательном выводе на приведенном выше рисунке три файла сохранены в виде пар ключ-значение в__webpack_modules__на объектеkeyпуть к модулю,valueявляется обернутой функцией модуля. функция имеетmodule, module.exports, __webpack_require__три параметра. Это позволяет каждому модулю иметь возможность использоватьmodule.exportsЭкспортируйте этот модуль и используйте__webpack_require__Возможность импортировать другие модули, сохраняя при этом каждый модуль в изолированной области действия.
Почемуwebpackмодифицироватьrequireключевые слова иrequireмаршрут? мы знаемrequireдаnodeПеременную среды, которая поставляется вместе со средой, можно использовать напрямую, но в других средах такой переменной нет, поэтому необходимоwebpackпредоставить такую возможность. Пока предоставляются аналогичные возможности, переменная с именемrequireвсе еще__webpack_require__На самом деле это не имеет значения. Что касается перезаписи пути, конечно, потому что вnodeКонечная система будет загружена в соответствии с путем к файлу, аwebpackВ запакованном файле использование оригинального пути не работает, поэтому путь нужно переписать как__webpack_modules__ключ, чтобы найти соответствующий модуль.
и следующее__webpack_require__функция с__webpack_module_cache__Объект выполняет загрузку модуля. использовать__webpack_require__Модули, загруженные функциями, кэшируются в__webpack_module_cache__объекта, чтобы в следующий раз, если другие модули зависели от этого модуля,Нет необходимости повторно запускать функцию-оболочку модуля, что снижает потребление эффективности выполнения.. В то же время, если между несколькими файлами существуют циклические зависимости, такие какa.jsзависел отb.jsдокумент,b.jsположился наa.js, затем вb.jsиспользовать__webpack_require__нагрузкаa.js, пойдет прямо вif(cachedModule !== undefined)ветвь тогдаreturnкэшированныйa.js, который дальше выполняться не будетa.jsфайл загружается, поэтомуИзбегает возникновения бесконечной рекурсии циклических зависимостей.
Не могу сказать это поwebpackРеализован загрузчик модулей сCommonJSТехнические характеристики одинаковые, и можно лишь сказать, что они неотделимы от десятки. Таким образом, упакованныйJavaScriptфайлы могут быть<script>Вкладка загружается и работает на стороне браузера.
легко реализовать
понялwebpackобработанныйJavaScriptКак это выглядит, давайте разберемся с идеями, и вручную реализуем простой упаковщик по тыкве и нарисуем совок, чтобы помочь понять.
Вот что нужно сделать:
- Прочитайте файл записи и соберите информацию о зависимости
- Рекурсивно читать все зависимые модули, создавая полный список зависимостей
- Упакуйте содержимое каждого модуля в законченный фрагмент исполняемого кода.
Без лишних слов создайте проект и установите необходимые зависимости
npm init -y
npm i @babel/core @babel/parser @babel/traverse webpack webpack-cli -D
в:
-
@babel/parserИспользуется для анализа исходного кода и создания AST -
@babel/traverseИспользуется для обхода AST, чтобы найтиrequireоператор и изменить его на_require_, преобразовать путь импорта в путь относительно корня -
@babel/coreИспользуется для преобразования модифицированного AST в новый вывод кода.
Создайте файл записи myPack.js и импортируйте зависимости
const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const babel = require('@babel/core')
Затем нам нужно проанализировать модуль и сгенерировать информацию о его модуле, включая: путь к модулю, зависимости модуля и преобразованный код модуля.
// 保存根路径,所有模块根据根路径产出相对路径
let root = process.cwd()
function readModuleInfo(filePath) {
// 准备好相对路径作为 module 的 key
filePath =
'./' + path.relative(root, path.resolve(filePath)).replace(/\\+/g, '/')
// 读取源码
const content = fs.readFileSync(filePath, 'utf-8')
// 转换出 AST
const ast = parser.parse(content)
// 遍历模块 AST,将依赖收集到 deps 数组中
const deps = []
traverse(ast, {
CallExpression: ({ node }) => {
// 如果是 require 语句,则收集依赖
if (node.callee.name === 'require') {
// 改造 require 关键字
node.callee.name = '_require_'
let moduleName = node.arguments[0].value
moduleName += path.extname(moduleName) ? '' : '.js'
moduleName = path.join(path.dirname(filePath), moduleName)
moduleName = './' + path.relative(root, moduleName).replace(/\\+/g, '/')
deps.push(moduleName)
// 改造依赖的路径
node.arguments[0].value = moduleName
}
},
})
// 编译回代码
const { code } = babel.transformFromAstSync(ast)
return {
filePath,
deps,
code,
}
}
Далее мы рекурсивно находим все зависимые модули из записи и строим дерево зависимостей
function buildDependencyGraph(entry) {
// 获取入口模块信息
const entryInfo = readModuleInfo(entry)
// 项目依赖树
const graphArr = []
graphArr.push(entryInfo)
// 从入口模块触发,递归地找每个模块的依赖,并将每个模块信息保存到 graphArr
for (const module of graphArr) {
module.deps.forEach((depPath) => {
const moduleInfo = readModuleInfo(path.resolve(depPath))
graphArr.push(moduleInfo)
})
}
return graphArr
}
После вышеуказанного шага мы получили дерево зависимостей, которое может описывать зависимости всего приложения, Наконец, нам нужно только упаковать и вывести в соответствии с целевым форматом.
function pack(graph, entry) {
const moduleArr = graph.map((module) => {
return (
`"${module.filePath}": function(module, exports, _require_) {
eval(\`` +
module.code +
`\`)
}`
)
})
const output = `;(() => {
var modules = {
${moduleArr.join(',\n')}
}
var modules_cache = {}
var _require_ = function(moduleId) {
if (modules_cache[moduleId]) return modules_cache[moduleId].exports
var module = modules_cache[moduleId] = {
exports: {}
}
modules[moduleId](module, module.exports, _require_)
return module.exports
}
_require_('${entry}')
})()`
return output
}
Напрямую используйте шаблоны строк для объединения в классы.CommonJSСтандартные шаблоны, автоматически загружайте модули ввода и используйте IIFE для переноса кода, чтобы убедиться, что модуль кода не повлияет на глобальную область.
Наконец, напишите функцию входаmainначать процесс упаковки
function main(entry = './src/index.js', output = './dist.js') {
fs.writeFileSync(output, pack(buildDependencyGraph(entry), entry))
}
main()
Выполнить и проверить результат
node myPack.js
На данный момент мы использовали в общей сложности менее 90 строк кода (включая комментарии), чтобы завершить минималистичный инструмент для упаковки модулей. Хотя нетWebpackисходный код, но мы начали с принципа проектирования упаковщика и прошлись по основным шагам инструмента упаковки, который прост, но закончен.
Суммировать
Эта статья изwebpackНачиная с концепции дизайна и окончательной реализации, он разбирает свои основные возможности в качестве инструмента упаковки и использует упрощенную версию, чтобы помочь понять его сущность более интуитивно. В целом,webpackВ качестве упаковочного инструмента есть не что иное, какНачиная с записи приложения, рекурсивно найдите все зависимые модули и проанализируйте их в файл JavaScript с возможностью загрузки модуля модульной спецификации CommonJS..
Благодаря отличному дизайну, в реальном производственном процессе,webapckОн также может расширить многие мощные функции. Однако его суть по-прежнему заключается в сборщике модулей. Независимо от того, какие новые функции или новые возможности, пока мы понимаем основную идею инструментов упаковки, любые проблемы будут решены.