Автор: Bumpman - Фэнмо Кодзиро
задний план
Webpack
После итерации до версии 4.x ее исходный код уже очень велик, а различные сценарии разработки сильно абстрагированы, а стоимость чтения становится все дороже и дороже. Но чтобы понять его внутреннюю работу, давайте попробуем начать с самой простой конфигурации веб-пакета и разработать низкопрофильную версию с точки зрения разработчика инструментов.Webpack
.
Точка зрения разработчика
Предположим, однажды мы получили требование и нам нужно разработатьreact
Одностраничное приложение, страница содержит строку текста и кнопку и должна поддерживать изменение текста при каждом нажатии кнопки. Поэтому мы создали новый проект и[根目录]/src
Создайте новый JS-файл в разделе. имитироватьWebpack
Отслеживая процесс упаковки зависимостей модулей, мы создали 3 новых компонента React и установили между ними простую зависимость.
// index.js 根组件
import React from 'react'
import ReactDom from 'react-dom'
import App from './App'
ReactDom.render(<App />, document.querySelector('#container'))
// App.js 页面组件
import React from 'react'
import Switch from './Switch.js'
export default class App extends React.Component {
constructor(props) {
super(props)
this.state = {
toggle: false
}
}
handleToggle() {
this.setState(prev => ({
toggle: !prev.toggle
}))
}
render() {
const { toggle } = this.state
return (
<div>
<h1>Hello, { toggle ? 'NervJS' : 'O2 Team'}</h1>
<Switch handleToggle={this.handleToggle.bind(this)} />
</div>
)
}
}
// Switch.js 按钮组件
import React from 'react'
export default function Switch({ handleToggle }) {
return (
<button onClick={handleToggle}>Toggle</button>
)
}
Затем нам нужен файл конфигурации, чтобы позволитьWebpack
Зная, как мы хотим, чтобы это работало, мы создаем новый файл в корневом каталогеwebpack.config.js
И напишите на него базовую конфигурацию. (Если вы не знакомы с содержанием конфигурации, вы можете сначала изучить егокитайская документация webpack)
// webpack.config.js
const resolve = dir => require('path').join(__dirname, dir)
module.exports = {
// 入口文件地址
entry: './src/index.js',
// 输出文件地址
output: {
path: resolve('dist'),
fileName: 'bundle.js'
},
// loader
module: {
rules: [
{
test: /\.(js|jsx)$/,
// 编译匹配include路径的文件
include: [
resolve('src')
],
use: 'babel-loader'
}
]
},
plugins: [
new HtmlWebpackPlugin()
]
}
вmodule
рольtest
Когда поле и имя файла совпадают, соответствующий загрузчик скомпилируется с соответствующим загрузчиком.Webpack
только знаю.js
,.json
Эти два типа файлов и через загрузчик мы можем обрабатывать файлы в других форматах, таких как css.
И дляReact
Что касается файлов, нам нужно преобразовать синтаксис JSX в чистый синтаксис JS, т.е.React.createElement
метод, код может быть распознан браузером. Обычно мы проходимbabel-loader
и настроенreact
Правила синтаксического анализа для выполнения этого шага.
После вышеуказанной обработки. Код компонента кнопки, который на самом деле читает браузер, вероятно, выглядит так.
...
function Switch(_ref) {
var handleToggle = _ref.handleToggle;
return _nervjs["default"].createElement("button", {
onClick: handleToggle
}, "Toggle");
}
А что касаетсяplugin
Некоторые плагины умеют прописывать функцию обработки результата компиляции вWebpack
В хуках жизненного цикла выполните некоторую обработку результата компиляции перед созданием окончательного файла. Например, в большинстве сценариев нам нужно вставить сгенерированный файл JS в файл Html. нужно использоватьhtml-webpack-plugin
Для этого плагина нам нужно написать это в конфигурации.
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpackConfig = {
entry: 'index.js',
output: {
path: path.resolve(__dirname, './dist'),
filename: 'index_bundle.js'
},
// 向plugins数组中传入一个HtmlWebpackPlugin插件的实例
plugins: [new HtmlWebpackPlugin()]
};
так,html-webpack-plugin
Он будет зарегистрирован на этапе завершения упаковки, получит путь к JS-файлу записи окончательной упаковки и сгенерирует файл, например<script src="./dist/bundle_[hash].js"></script>
Тег script вставляется в файл Html. Таким образом, браузер может отображать содержимое страницы через файл html.
ок, тут написано, для разработчика все элементы конфигурации и файлы кода проекта, которые нужно запаковать, подготовлены, и дальше дело сдать работу инструменту упаковкиWebpack
,пройти черезWebpack
Упакуйте код так, как мы и браузер хотим, чтобы он выглядел
Перспектива инструмента
Прежде всего, нам нужно понять процесс упаковки Webpack.
отWebpack
Как видно из рабочего процесса, нам нужно реализоватьCompiler
Класс, этот класс должен собрать всю информацию о конфигурации, переданную разработчиком, а затем управлять общим процессом компиляции. мы можем поставитьCompiler
Поймите его как босса компании, он командует общей ситуацией и владеет общей информацией (потребностями клиентов). Зная всю информацию, он вызывает другой классCompilation
создать экземпляр и доверить ему всю информацию и рабочий процесс,Compilation
По сути, это эквивалент секретаря начальника, которому необходимо мобилизовать различные отделы, чтобы начать работу по мере необходимости, иloader
а такжеplugin
Это эквивалентно различным отделам и будет иметь дело с ними только тогда, когда появится их специализированная работа ( js , css , scss , jpg , png...)
Для реализации обоихWebpack
Упакованная функция реализует только основной код. Мы делаем некоторые упрощения в этом процессе
Сначала мы создали новыйwebpack
В качестве открытого метода функция принимает два параметра, один из которых является объектом элемента конфигурации, а другой — обратным вызовом ошибки.
const Compiler = require('./compiler')
function webpack(config, callback) {
// 此处应有参数校验
const compiler = new Compiler(config)
// 开始编译
compiler.run()
}
module.exports = webpack
1. Информация о конфигурации сборки
нам нужно сначалаCompiler
Конструктор класса собирает информацию, переданную пользователем.
class Compiler {
constructor(config, _callback) {
const {
entry,
output,
module,
plugins
} = config
// 入口
this.entryPath = entry
// 输出文件路径
this.distPath = output.path
// 输出文件名称
this.distName = output.fileName
// 需要使用的loader
this.loaders = module.rules
// 需要挂载的plugin
this.plugins = plugins
// 根目录
this.root = process.cwd()
// 编译工具类Compilation
this.compilation = {}
// 入口文件在module中的相对路径,也是这个模块的id
this.entryId = getRootPath(this.root, entry, this.root)
}
}
При этом ставим всеplugin
прикреплен к экземпляруhooks
характеристики.Webpack
Управление жизненным циклом основано наtapable
Библиотека, через эту библиотеку мы можем легко создать хук модели публикации-подписки, а затем смонтировать функцию в экземпляр (обратный вызов события хука поддерживает синхронный запуск, асинхронный запуск и даже обратный вызов цепочки), в соответствующем Время запускает соответствующий обработчик событий. мы вhooks
Объявите некоторые хуки жизненного цикла:
const { AsyncSeriesHook } = require('tapable') // 此处我们创建了一些异步钩子
constructor(config, _callback) {
...
this.hooks = {
// 生命周期事件
beforeRun: new AsyncSeriesHook(['compiler']), // compiler代表我们将向回调事件中传入一个compiler参数
afterRun: new AsyncSeriesHook(['compiler']),
beforeCompile: new AsyncSeriesHook(['compiler']),
afterCompile: new AsyncSeriesHook(['compiler']),
emit: new AsyncSeriesHook(['compiler']),
failed: new AsyncSeriesHook(['compiler']),
}
this.mountPlugin()
}
// 注册所有的plugin
mountPlugin() {
for(let i=0;i<this.plugins.length;i++) {
const item = this.plugins[i]
if ('apply' in item && typeof item.apply === 'function') {
// 注册各生命周期钩子的发布订阅监听事件
item.apply(this)
}
}
}
// 当运行run方法的逻辑之前
run() {
// 在特定的生命周期发布消息,触发对应的订阅事件
this.hooks.beforeRun.callAsync(this) // this作为参数传入,对应之前的compiler
...
}
Общая информация:
Каждыйplugin Class
должны реализоватьapply
метод, этот метод получаетcompiler
instance, а затем смонтируйте настоящую функцию ловушки вcompiler.hook
на определенном цикле объявления.
Если мы объявим хук, но не смонтируем какой-либо метод, он сообщит об ошибке при срабатывании функции вызова. Но по фактуWebpack
Каждый хук жизненного цикла, кроме настроенных пользователемplugin
, смонтирует хотя бы одинWebpack
мой собственныйplugin
, так что такой проблемы не будет. больше оtapable
Использование также может быть перемещеноTapable
2. Скомпилируйте
Далее нам нужно объявитьCompilation
Класс, этот класс в основном для выполнения работы по компиляции. существуетCompilation
В конструкторе мы сначала получаем от боссаCompiler
Информация выдана и смонтирована в собственных свойствах.
class Compilation {
constructor(props) {
const {
entry,
root,
loaders,
hooks
} = props
this.entry = entry
this.root = root
this.loaders = loaders
this.hooks = hooks
}
// 开始编译
async make() {
await this.moduleWalker(this.entry)
}
// dfs遍历函数
moduleWalker = async () => {}
}
Поскольку нам нужно скомпилировать все файлы, на которые есть ссылки в процессе упаковки, в окончательный пакет кода, нам нужно объявить функцию глубокого обхода.moduleWalker
(Это имя взято автором, а не вебпаком официально), как следует из названия, этот метод будет начинаться с файла входа, компилировать файлы на первом и втором шагах по очереди и собирать другие модули, на которые есть ссылки, рекурсивно сделать ту же обработку.
Этап компиляции делится на два этапа
- Первый шаг – использовать все
loader
Скомпилируйте его и верните скомпилированный исходный код - Второй шаг эквивалентен
Webpack
Целью собственного шага компиляции является построение отношения вызова зависимостей между каждым независимым модулем. Что нам нужно сделать, так это поставить всеrequire
метод заменен наWebpack
самоопределяющийся__webpack_require__
функция. потому что все скомпилированные модули будутWebpack
Объекты хранятся в замыканииmoduleMap
в то время как__webpack_require__
Функция является единственной, которая имеет разрешение на доступmoduleMap
Методы.
объяснение одним предложением__webpack_require__
Функция состоит в том, чтобы изменить оригинал между модулями文件地址 -> 文件内容
Отношения заменяются на对象的key -> 对象的value(文件内容)
такое отношение.
Когда второй шаг компиляции завершен, ссылки в текущем модуле собираются и возвращаются вCompilation
, такmoduleWalker
Эти зависимые модули могут быть скомпилированы рекурсивно. Конечно, существует высокая вероятность циклических ссылок и повторных ссылок, мы будем генерировать уникальное значение ключа в соответствии с путем файла, на который указывает ссылка, и пропускать при повторении значения ключа.
i. moduleWalker
Функция обхода
// 存放处理完毕的模块代码Map
moduleMap = {}
// 根据依赖将所有被引用过的文件都进行编译
async moduleWalker(sourcePath) {
if (sourcePath in this.moduleMap) return
// 在读取文件时,我们需要完整的以.js结尾的文件路径
sourcePath = completeFilePath(sourcePath)
const [ sourceCode, md5Hash ] = await this.loaderParse(sourcePath)
const modulePath = getRootPath(this.root, sourcePath, this.root)
// 获取模块编译后的代码和模块内的依赖数组
const [ moduleCode, relyInModule ] = this.parse(sourceCode, path.dirname(modulePath))
// 将模块代码放入ModuleMap
this.moduleMap[modulePath] = moduleCode
this.assets[modulePath] = md5Hash
// 再依次对模块中的依赖项进行解析
for(let i=0;i<relyInModule.length;i++) {
await this.moduleWalker(relyInModule[i], path.dirname(relyInModule[i]))
}
}
Если мы логируем путь dfs, то можем увидеть такой процесс
ii. Первый этап компиляцииloaderParse
функция
async loaderParse(entryPath) {
// 用utf8格式读取文件内容
let [ content, md5Hash ] = await readFileWithHash(entryPath)
// 获取用户注入的loader
const { loaders } = this
// 依次遍历所有loader
for(let i=0;i<loaders.length;i++) {
const loader = loaders[i]
const { test : reg, use } = loader
if (entryPath.match(reg)) {
// 判断是否满足正则或字符串要求
// 如果该规则需要应用多个loader,从最后一个开始向前执行
if (Array.isArray(use)) {
while(use.length) {
const cur = use.pop()
const loaderHandler =
typeof cur.loader === 'string'
// loader也可能来源于package包例如babel-loader
? require(cur.loader)
: (
typeof cur.loader === 'function'
? cur.loader : _ => _
)
content = loaderHandler(content)
}
} else if (typeof use.loader === 'string') {
const loaderHandler = require(use.loader)
content = loaderHandler(content)
} else if (typeof use.loader === 'function') {
const loaderHandler = use.loader
content = loaderHandler(content)
}
}
}
return [ content, md5Hash ]
}
Однако здесь есть небольшой эпизод, которым мы обычно пользуемся.babel-loader
не похожеWebpack
Сцена, отличная от сумки, используется вbabel-loader
См. это предложение в документации
This package allows transpiling JavaScript files using Babel and webpack.
Но к счастью@babel/core
а такжеwebpack
Соединения нет, поэтому я могу только усердно работать и писать метод загрузчика для его разбора.JS
а такжеES6
синтаксис.
const babel = require('@babel/core')
module.exports = function BabelLoader (source) {
const res = babel.transform(source, {
sourceType: 'module' // 编译ES6 import和export语法
})
return res.code
}
Конечно, правила компиляции можно передать как элементы конфигурации, но для имитации реальных сценариев разработки нам необходимо настроитьbabel.config.js
документ
module.exports = function (api) {
api.cache(true)
return {
"presets": [
['@babel/preset-env', {
targets: {
"ie": "8"
},
}],
'@babel/preset-react', // 编译JSX
],
"plugins": [
["@babel/plugin-transform-template-literals", {
"loose": true
}]
],
"compact": true
}
}
Итак, получивloader
После обработки кода любой модуль теоретически можно использовать прямо в браузере или в юнит-тестах. Но наш код — это единое целое, и нам также нужен разумный способ организовать взаимосвязь между кодами.
Вышеизложенное также объясняет, почему мы используем__webpack_require__
функция.这里我们得到的代码仍然是字符串的形式,为了方便我们使用eval
Функция анализирует строку в прямо читаемый код. Конечно, это просто способ найти, для этого объяснительного языка для JS, если модуль объясняет компиляцию, скорость будет очень медленной. Фактически, реальная производственная среда будет инкапсулировать содержимое модуля в одинIIFE
(непосредственно самовыполняющееся функциональное выражение)
В общем, во второй части компиляцияparse
То, что нам нужно сделать в функции, на самом деле очень просто, это поместить все модули вrequire
Имя функции метода заменяется на__webpack_require__
Вот и все. То, что мы используем на этом этапе,babel
Полная домашняя бочка.babel
Как лучший в отрасли компилятор JS, этапы анализа кода в основном разделены на два этапа, а именно лексический анализ и синтаксический анализ. Проще говоря, это анализ фрагмента кода слово за словом и создание контекста на основе текущего слова. Затем приступайте к оценке роли следующего слова в контексте.
Обратите внимание, что на этом этапе мы также можем «кстати» собрать массив зависимостей модуля и вернуть его вместе (для рекурсии dfs)
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const types = require('@babel/types')
const generator = require('@babel/generator').default
...
// 解析源码,替换其中的require方法来构建ModuleMap
parse(source, dirpath) {
const inst = this
// 将代码解析成ast
const ast = parser.parse(source)
const relyInModule = [] // 获取文件依赖的所有模块
traverse(ast, {
// 检索所有的词法分析节点,当遇到函数调用表达式的时候执行,对ast树进行改写
CallExpression(p) {
// 有些require是被_interopRequireDefault包裹的
// 所以需要先找到_interopRequireDefault节点
if (p.node.callee && p.node.callee.name === '_interopRequireDefault') {
const innerNode = p.node.arguments[0]
if (innerNode.callee.name === 'require') {
inst.convertNode(innerNode, dirpath, relyInModule)
}
} else if (p.node.callee.name === 'require') {
inst.convertNode(p.node, dirpath, relyInModule)
}
}
})
// 将改写后的ast树重新组装成一份新的代码, 并且和依赖项一同返回
const moduleCode = generator(ast).code
return [ moduleCode, relyInModule ]
}
/**
* 将某个节点的name和arguments转换成我们想要的新节点
*/
convertNode = (node, dirpath, relyInModule) => {
node.callee.name = '__webpack_require__'
// 参数字符串名称,例如'react', './MyName.js'
let moduleName = node.arguments[0].value
// 生成依赖模块相对【项目根目录】的路径
let moduleKey = completeFilePath(getRootPath(dirpath, moduleName, this.root))
// 收集module数组
relyInModule.push(moduleKey)
// 替换__webpack_require__的参数字符串,因为这个字符串也是对应模块的moduleKey,需要保持统一
// 因为ast树中的每一个元素都是babel节点,所以需要使用'@babel/types'来进行生成
node.arguments = [ types.stringLiteral(moduleKey) ]
}
3. emit
Создать пакетный файл
Выполните этот шаг,compilation
Миссия фактически выполнена. Если мы обычно наблюдаем за сгенерированным файлом js, мы обнаружим, что упакованный вид является функцией немедленного выполнения, тело основной функции является замыканием, а загруженный модуль кэшируется в замыкании.installedModules
и определяет__webpack_require__
функция, конечным результатом является модуль, соответствующий записи функции. Параметры функции — это параметры каждого модуля.key-value
составной объект.
мы проходим здесьejs
Шаблон используется для сращивания, а ранее собранныйmoduleMap
Объект проходится и вставляется в строку шаблона ejs.
код шаблона
// template.ejs
(function(modules) { // webpackBootstrap
// The module cache
var installedModules = {};
// The require function
function __webpack_require__(moduleId) {
// Check if module is in cache
if(installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// Create a new module (and put it into the cache)
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};
// Execute the module function
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
// Flag the module as loaded
module.l = true;
// Return the exports of the module
return module.exports;
}
// Load entry module and return exports
return __webpack_require__(__webpack_require__.s = "<%-entryId%>");
})({
<%for(let key in modules) {%>
"<%-key%>":
(function(module, exports, __webpack_require__) {
eval(
`<%-modules[key]%>`
);
}),
<%}%>
});
Сгенерировать пакет.js
/**
* 发射文件,生成最终的bundle.js
*/
emitFile() { // 发射打包后的输出结果文件
// 首先对比缓存判断文件是否变化
const assets = this.compilation.assets
const pastAssets = this.getStorageCache()
if (loadsh.isEqual(assets, pastAssets)) {
// 如果文件hash值没有变化,说明无需重写文件
// 只需要依次判断每个对应的文件是否存在即可
// 这一步省略!
} else {
// 缓存未能命中
// 获取输出文件路径
const outputFile = path.join(this.distPath, this.distName);
// 获取输出文件模板
// const templateStr = this.generateSourceCode(path.join(__dirname, '..', "bundleTemplate.ejs"));
const templateStr = fs.readFileSync(path.join(__dirname, '..', "template.ejs"), 'utf-8');
// 渲染输出文件模板
const code = ejs.render(templateStr, {entryId: this.entryId, modules: this.compilation.moduleMap});
this.assets = {};
this.assets[outputFile] = code;
// 将渲染后的代码写入输出文件中
fs.writeFile(outputFile, this.assets[outputFile], function(e) {
if (e) {
console.log('[Error] ' + e)
} else {
console.log('[Success] 编译成功')
}
});
// 将缓存信息写入缓存文件
fs.writeFileSync(resolve(this.distPath, 'manifest.json'), JSON.stringify(assets, null, 2))
}
}
На этом этапе мы генерируем содержимое файла на основеMd5Hash
Чтобы сравнить предыдущий кэш для ускорения упаковки, внимательные студенты найдутWebpack
Файл кеша создается каждый раз, когда он упакованmanifest.json
,В форме
{
"main.js": "./js/main7b6b4.js",
"main.css": "./css/maincc69a7ca7d74e1933b9d.css",
"main.js.map": "./js/main7b6b4.js.map",
"vendors~main.js": "./js/vendors~main3089a.js",
"vendors~main.css": "./css/vendors~maincc69a7ca7d74e1933b9d.css",
"vendors~main.js.map": "./js/vendors~main3089a.js.map",
"js/28505f.js": "./js/28505f.js",
"js/28505f.js.map": "./js/28505f.js.map",
"js/34c834.js": "./js/34c834.js",
"js/34c834.js.map": "./js/34c834.js.map",
"js/4d218c.js": "./js/4d218c.js",
"js/4d218c.js.map": "./js/4d218c.js.map",
"index.html": "./index.html",
"static/initGlobalSize.js": "./static/initGlobalSize.js"
}
Это также часто используемое суждение при возобновлении файловой точки останова, поэтому я не буду подробно его здесь раскрывать.
тестовое задание
После этого шага мы в основном закончили (ошибка: если не учитывать умопомрачительный процесс отладки), то мы находимся вpackage.json
Настройте скрипт пакета внутри
"scripts": {
"build": "node build.js"
}
бегатьyarn build
(@ο@) Вау~ Пришло захватывающее время.
Однако...
Увидев, что эта странная штуковина, которая была упакована, сделала ошибку, мне все же захотелось рассмеяться в душе. Я проверил и обнаружил, что объединенная строка закончилась преждевременно, потому что обратные кавычки встретились с обратными кавычками в комментарии. хорошо, тогда яbabel traverse
Я добавил несколько строк кода и удалил все комментарии в коде. Но потом пришли другие проблемы.
Ну, наверное, на самом делеreact
Есть еще несколько шагов в производстве упаковки, но это выходит за рамки темы сегодняшнего обсуждения. В этот момент на ум пришла призрачная рамка. На мой взгляд, я думал о высокой производительности, разработанной JD.react
версия фреймворка, похожего на реакциюNervJS
,возможноNervJS
Доступный (неправильный) код для поддержки этого жалкого упаковщика
По этому мыbabel.config.js
Настроить псевдоним для заменыreact
зависимости. (React
передача проектаNervJS
Это так просто)
module.exports = function (api) {
api.cache(true)
return {
...
"plugins": [
...
[
"module-resolver", {
"root": ["."],
"alias": {
"react": "nervjs",
"react-dom": "nervjs",
// Not necessary unless you consume a module using `createClass`
"create-react-class": "nerv-create-class"
}
}
]
],
"compact": true
}
}
бегатьyarn build
(@ο@) вау~ наконец-то код запускается успешно, хотя проблем много, но хотя бы этоwebpack
Благодаря такому простому дизайну он может поддерживать большинство фреймворков JS. Заинтересованные студенты также могут попробовать написать самостоятельно или напрямую изздесьПод клоном зрения
без сомнений,Webpack
Это очень хороший инструмент для упаковки модулей кода (хотя его официальный веб-сайт очень сдержан и не содержит никаких слоганов). Очень хороший инструмент должен сохранять свои характеристики, давая другим разработчикам возможность расширять свои возможности на его основе. Если у нас будет возможность подробно изучить эти инструменты, наши знания в области разработки кода также значительно улучшатся.
Добро пожаловать в блог Bump Labs:aotu.io
Или обратите внимание на официальный аккаунт AOTULabs и время от времени публикуйте статьи: