Основное содержание, обсуждаемое в этой статье, выглядит следующим образом:
-
webpack
Основы упаковки - Как реализовать самостоятельно
loader
а такжеplugin
Примечание. В этой статье используетсяwebpack
версияv4.43.0
, webpack-cli
версияv3.3.11
,node
версияv12.14.1
,npm
Версияv6.13.4
(Если хочешьyarn
также возможно), для демонстрационных целейchrome
версия браузера81.0.4044.129(正式版本) (64 位)
1. Основные принципы упаковки webpack
Одной из основных функций веб-пакета является упаковка модульного кода, который мы пишем, для генерации кода, который можно запустить в браузере Здесь мы также начинаем с простого и шаг за шагом изучаем принцип упаковки веб-пакета.
1.1 Простое требование
Сначала мы создаем пустой проект, используяnpm init -y
Быстро инициализировать одинpackage.json
, затем установитеwebpack webpack-cli
Далее создайте в корневом каталогеsrc
содержание,src
Создано в каталогеindex.js
,add.js
,minus.js
, созданный в корневом каталогеindex.html
,вindex.html
представлятьindex.js
,существуетindex.js
представлятьadd.js
,minus.js
,
Структура каталогов следующая:
Содержимое файла следующее:
// add.js
export default (a, b) => {
return a + b
}
// minus.js
export const minus = (a, b) => {
return a - b
}
// index.js
import add from './add.js'
import { minus } from './minus.js'
const sum = add(1, 2)
const division = minus(2, 1)
console.log('sum>>>>>', sum)
console.log('division>>>>>', division)
<!--index.html-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>demo</title>
</head>
<body>
<script src="./src/index.js"></script>
</body>
</html>
прямо вindex.html
представлятьindex.js
код, который явно не работает в браузере, вы увидите такие ошибки
Uncaught SyntaxError: Cannot use import statement outside a module
да мы не можемscript
представилjs
файл, использоватьes6
модульный синтаксис
1.2 Реализовать основную функцию упаковки webpack
Сначала мы создаем файл bundle.js в корневом каталоге проекта, который используется для модуляции модуля, который мы только что написали.js
файлы кода для упаковки
Давайте сначала посмотрим на описание процесса упаковки на официальном сайте webpack:
it internally builds a dependency graph which maps every module your project needs and generates one or more bundles(webpack会在内部构建一个 依赖图(dependency graph),此依赖图会映射项目所需的每个模块,并生成一个或多个 bundle)
Перед официальным стартом совместите вышеперечисленноеwebpack
Анализируется описание официального сайта, и основной процесс нашей работы по упаковке выглядит следующим образом:
- Во-первых, нам нужно прочитать содержимое входного файла (то есть содержимое index.js)
- Во-вторых, проанализируйте входной файл, рекурсивно прочитайте содержимое файлов, от которых зависит модуль, и создайте граф зависимостей.
- Наконец, в соответствии с графом зависимостей, сгенерируйте окончательный код, который сможет запустить браузер.
1. Обработать один модуль (в качестве примера возьмем запись)
1.1 Получить содержимое модуля
Поскольку мы хотим прочитать содержимое файла, нам нужно использоватьnode.js
основной модульfs
, давайте сначала посмотрим на то, что мы читаем:
// bundle.js
const fs = require('fs')
const getModuleInfo = file => {
const body = fs.readFileSync(file, 'utf-8')
console.log(body)
}
getModuleInfo('./src/index.js')
Мы определяем методgetModuleInfo
, в этом методе мы читаем содержимое файла, распечатываем его, и результат вывода выглядит следующим образом:
index.js
Все содержимое вывода находится в виде строки, и мы можем использовать регулярные выражения или некоторые другие методы для извлеченияimport
так же какexport
Содержимое входного файла и соответствующее имя файла пути используются для анализа содержимого входного файла и получения полезной информации. но еслиimport
а такжеexport
Контента много, это будет очень хлопотный процесс, тут воспользуемся помощьюbabel
Предоставляет функцию для завершения анализа входного файла
1.2 Содержание модуля анализа
мы устанавливаем@babel/parser
, номер версии, установленной во время демонстрации,^7.9.6
Функция этого модуля babel заключается в преобразовании содержимого кода нашего файла js в форму объектов js.Эта форма объектов js называется抽象语法树(Abstract Syntax Tree, 以下简称AST)
// bundle.js
const fs = require('fs')
const parser = require('@babel/parser')
const getModuleInfo = file => {
const body = fs.readFileSync(file, 'utf-8')
const ast = parser.parse(body, {
// 表示我们要解析的是es6模块
sourceType: 'module'
})
console.log(ast)
console.log(ast.program.body)
}
getModuleInfo('./src/index.js')
использовать@babel/parser
изparse
метод, называемый преобразованием входного файлаAST
, мы распечатываемast
, обратите внимание, что содержимое файла находится вast.program.body
, как показано на следующем рисунке:
Node
узлы, мы можем видеть, что каждый узел имеетtype
свойств, где первые два изtype
собственностьImportDeclaration
, что соответствует двум записям в нашем файле записейimport
заявление, и каждыйtype
собственностьImportDeclaration
узел, которыйsource.value
Свойства — это относительные пути, по которым этот модуль импортируется, так что мы получаем важную информацию в файле записи, полезную для упаковки.
Далее нам нужно обработать полученный ast и вернуть структурированные данные для последующего использования.
1.3 Содержимое модуля обработки
правильноast.program.body
Сбор и обработка части данных по сути является обходом массива и обработкой данных в цикле.Здесь также вводится модуль babel.@babel/traverse
для завершения этой работы.
Установить@babel/traverse
, номер версии, установленной во время демонстрации,^7.9.6
const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const getModuleInfo = file => {
const body = fs.readFileSync(file, 'utf-8')
const ast = parser.parse(body, {
sourceType: 'module'
})
const deps = {}
traverse(ast, {
ImportDeclaration({ node }) {
const dirname = path.dirname(file);
const absPath = './' + path.join(dirname, node.source.value)
deps[node.source.value] = absPath
}
})
console.log(deps)
}
getModuleInfo('./src/index.js')
создать объектdeps
, используемый для сбора зависимостей, введенных самим модулем, используйтеtraverse
траверсast
, нам просто нужноImportDeclaration
узел для обработки, обратите внимание, что обработка, которую мы выполняем, фактически преобразует относительный путь в абсолютный путь, здесь я используюMac
система, если онаwindows
системы, обратите внимание на разницу между косыми чертами
После получения зависимостей нам нужноast
Сделайте преобразование синтаксиса, поместитеes6
Синтаксис преобразуется вes5
синтаксис, использованиеbabel
основной модуль@babel/core
так же как@babel/preset-env
Заканчивать
Установить@babel/core @babel/preset-env
, номера версий, установленных во время демонстрации, оба^7.9.6
const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const babel = require('@babel/core')
const getModuleInfo = file => {
const body = fs.readFileSync(file, 'utf-8')
const ast = parser.parse(body, {
sourceType: 'module'
})
const deps = {}
traverse(ast, {
ImportDeclaration({ node }) {
const dirname = path.dirname(file);
const absPath = './' + path.join(dirname, node.source.value)
deps[node.source.value] = absPath
}
})
const { code } = babel.transformFromAst(ast, null, {
presets: ["@babel/preset-env"]
})
const moduleInfo = { file, deps, code }
console.log(moduleInfo)
return moduleInfo
}
getModuleInfo('./src/index.js')
Как показано на рисунке ниже, мы окончательно преобразуем код модуля в информацию в виде объекта, который содержит абсолютный путь к файлу, информацию о модуле, от которого зависит файл, и внутренний процесс модуль.babel
преобразованный код
2. Рекурсивно получить информацию обо всех модулях
Этот процесс заключается в получении依赖图(dependency graph)
Этот процесс начинается с входного модуля и вызывает каждый модуль и его зависимые модули.getModuleInfo
Метод анализируется, и, наконец, возвращается объект, содержащий всю информацию о модуле.
const parseModules = file => {
// 定义依赖图
const depsGraph = {}
// 首先获取入口的信息
const entry = getModuleInfo(file)
const temp = [entry]
for (let i = 0; i < temp.length; i++) {
const item = temp[i]
const deps = item.deps
if (deps) {
// 遍历模块的依赖,递归获取模块信息
for (const key in deps) {
if (deps.hasOwnProperty(key)) {
temp.push(getModuleInfo(deps[key]))
}
}
}
}
temp.forEach(moduleInfo => {
depsGraph[moduleInfo.file] = {
deps: moduleInfo.deps,
code: moduleInfo.code
}
})
console.log(depsGraph)
return depsGraph
}
parseModules('./src/index.js')
Полученный объект depsGraph выглядит следующим образом:
Данные модульного анализа, которые мы в итоге получили, показаны на рисунке выше.Далее нам нужно создать код, который запустит окончательный браузер, на основе полученных здесь данных модульного анализа.
3. Сгенерируйте окончательный код
Прежде чем мы его реализуем, обратите внимание на граф зависимостей, полученный в предыдущем разделе.Вы можете видеть, что окончательный код содержит синтаксис, такой как экспорт и требование.Поэтому, когда мы генерируем окончательный код, мы должны реализовать и реализовать определенные экспорты и требования. иметь дело с
Сначала мы вызываем упомянутый ранее метод parseModules, чтобы получить объект графа зависимостей всего приложения:
const bundle = file => {
const depsGraph = JSON.stringify(parseModules(file))
}
Далее мы должны преобразовать содержимое объекта графа зависимостей в исполняемый код и вывести его в виде строки. Помещаем весь код в самовыполняющуюся функцию, параметром является объект графа зависимостей
const bundle = file => {
const depsGraph = JSON.stringify(parseModules(file))
return `(function(graph){
function require(file) {
var exports = {};
return exports
}
require('${file}')
})(${depsGraph})`
}
Следующее содержимое на самом деле очень простое, то есть мы получаем информацию о коде входного файла, просто выполняем его, используем функцию eval для выполнения и изначально пишем код следующим образом:
const bundle = file => {
const depsGraph = JSON.stringify(parseModules(file))
return `(function(graph){
function require(file) {
var exports = {};
(function(code){
eval(code)
})(graph[file].code)
return exports
}
require('${file}')
})(${depsGraph})`
}
Выше написанное проблематично, нам нужно сделать абсолютное преобразование пути к файлу, иначеgraph[file].code
Он недоступен, определите метод adsRequire для преобразования относительного пути в абсолютный путь
const bundle = file => {
const depsGraph = JSON.stringify(parseModules(file))
return `(function(graph){
function require(file) {
var exports = {};
function absRequire(relPath){
return require(graph[file].deps[relPath])
}
(function(require, exports, code){
eval(code)
})(absRequire, exports, graph[file].code)
return exports
}
require('${file}')
})(${depsGraph})`
}
Далее нам просто нужно выполнить метод пакета и записать сгенерированный контент в файл JavaScript.
const content = bundle('./src/index.js')
// 写入到dist/bundle.js
fs.mkdirSync('./dist')
fs.writeFileSync('./dist/bundle.js', content)
Наконец, мы вводим это в index.html./dist/bundle.js
файл, мы видим, что консоль правильно выводит результат, который мы хотим
4. Полный код bundle.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')
const getModuleInfo = file => {
const body = fs.readFileSync(file, 'utf-8')
console.log(body)
const ast = parser.parse(body, {
sourceType: 'module'
})
// console.log(ast.program.body)
const deps = {}
traverse(ast, {
ImportDeclaration({ node }) {
const dirname = path.dirname(file);
const absPath = './' + path.join(dirname, node.source.value)
deps[node.source.value] = absPath
}
})
const { code } = babel.transformFromAst(ast, null, {
presets: ["@babel/preset-env"]
})
const moduleInfo = { file, deps, code }
return moduleInfo
}
const parseModules = file => {
// 定义依赖图
const depsGraph = {}
// 首先获取入口的信息
const entry = getModuleInfo(file)
const temp = [entry]
for (let i = 0; i < temp.length; i++) {
const item = temp[i]
const deps = item.deps
if (deps) {
// 遍历模块的依赖,递归获取模块信息
for (const key in deps) {
if (deps.hasOwnProperty(key)) {
temp.push(getModuleInfo(deps[key]))
}
}
}
}
temp.forEach(moduleInfo => {
depsGraph[moduleInfo.file] = {
deps: moduleInfo.deps,
code: moduleInfo.code
}
})
// console.log(depsGraph)
return depsGraph
}
// 生成最终可以在浏览器运行的代码
const bundle = file => {
const depsGraph = JSON.stringify(parseModules(file))
return `(function(graph){
function require(file) {
var exports = {};
function absRequire(relPath){
return require(graph[file].deps[relPath])
}
(function(require, exports, code){
eval(code)
})(absRequire, exports, graph[file].code)
return exports
}
require('${file}')
})(${depsGraph})`
}
const build = file => {
const content = bundle(file)
// 写入到dist/bundle.js
fs.mkdirSync('./dist')
fs.writeFileSync('./dist/bundle.js', content)
}
build('./src/index.js')
2. Почеркloader
а такжеplugin
2.1 Как реализовать самостоятельноloader
Загрузчик — это, по сути, функция, которая выполняется, когда мы загружаем некоторые файлы.
2.1.1 Как реализовать синхронизациюloader
Сначала инициализируем проект, структура проекта показана на рисунке:
Содержимое файлов index.js и webpack.config.js следующее:// index.js
console.log('我要学好前端,因为学好前端可以: ')
// webpack.config.js
const path = require('path')
module.exports = {
mode: 'development',
entry: {
main: './src/index.js'
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js'
}
}
Создаем в корневом каталогеsyncLoader.js
, используемый для реализации синхронного загрузчика, обратите внимание, что эта функция должна возвращатьbuffer
илиstring
// syncloader.ja
module.exports = function (source) {
console.log('source>>>>', source)
return source
}
В то же время мыwebpack.config.js
использовать это вloader
, мы используем здесьresolveLoader
элемент конфигурации, указатьloader
Найдите путь к файлу, поэтому мы используемloader
можно указать напрямуюloader
имя
const path = require('path')
module.exports = {
mode: 'development',
entry: {
main: './src/index.js'
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js'
},
resolveLoader: {
// loader路径查找顺序从左往右
modules: ['node_modules', './']
},
module: {
rules: [
{
test: /\.js$/,
use: 'syncLoader'
}
]
}
}
Далее мы запускаем команду упаковки и видим, что командная строка выводит исходное содержимое, то есть содержимое файла функции загрузчика.
Затем модифицируем наш загрузчик:module.exports = function (source) {
source += '升值加薪'
return source
}
Мы снова запускаем команду package, чтобы увидеть упакованный код:
Таким образом, мы реализовали простой загрузчик, который добавляет часть информации в наш файл. мы можем попробоватьloader
распечатать в функцииthis
, и обнаружил, что результатом вывода является очень длинная строка содержимого,this
Мы можем многоеloader
полезная информация, используемая в , так, дляloader
пишите, не используйте стрелочные функции, это изменитthis
указывает на.
Вообще говоря, мы будем использовать официально рекомендованныйloader-utils
пакет для выполнения более сложныхloader
пишу
мы продолжаем установкуloader-utils
, версия есть^2.0.0
Сначала мы преобразуемwebpack.config.js
:
const path = require('path')
module.exports = {
mode: 'development',
entry: {
main: './src/index.js'
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js'
},
resolveLoader: {
// loader路径查找顺序从左往右
modules: ['node_modules', './']
},
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'syncLoader',
options: {
message: '升值加薪'
}
}
}
]
}
}
Обратите внимание, что у нас естьloader
повысилсяoptions
Элемент конфигурации, затем используйте loader-utils в функции загрузчика, чтобы получить содержимое элемента конфигурации, объединив содержимое, мы все еще можем получить тот же результат упаковки, что и раньше.
// syncLoader.js
const loaderUtils = require('loader-utils')
module.exports = function (source) {
const options = loaderUtils.getOptions(this)
console.log(options)
source += options.message
// 可以传递更详细的信息
this.callback(null, source)
}
Таким образом, мы выполнили простую синхронизациюloader
пишу
2.1.2 Как реализовать асинхронныйloader
Очень похоже на способ написания синхронного загрузчика, мы создаем файл asyncLoader.js в корневом каталоге, его содержимое выглядит следующим образом:
const loaderUtils = require('loader-utils')
module.exports = function (source) {
const options = loaderUtils.getOptions(this)
const asyncfunc = this.async()
setTimeout(() => {
source += '走上人生颠覆'
asyncfunc(null, res)
}, 200)
}
Обратите внимание здесьthis.async()
, говоря официальным языкомTells the loader-runner that the loader intends to call back asynchronously. Returns this.callback.
То есть сообщите WebPack, что этот загрузчик работает асинхронно, и возврат согласуется с синхронным использованием.this.callback
Далее мы модифицируем webpack.config.js
const path = require('path')
module.exports = {
mode: 'development',
entry: {
main: './src/index.js'
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js'
},
resolveLoader: {
// loader路径查找顺序从左往右
modules: ['node_modules', './']
},
module: {
rules: [
{
test: /\.js$/,
use: [
{
loader: 'syncLoader',
options: {
message: '走上人生巅峰'
}
},
{
loader: 'asyncLoader'
}
]
}
]
}
}
Учтите, что порядок выполнения загрузчика взят из интернета, поэтому в тексте сначала напишите "поощрение и повышение зарплаты", а потом напишите "выйти на пик жизни"
До сих пор мы кратко представили, как написатьloader
, в реальном проекте можно рассмотреть какую-то общую простую логику, можно написатьloader
для завершения (например, замена интернационализированного текста)
2.2 Как реализовать самостоятельноplugin
plugin
обычно вwebpack
Для выполнения некоторых операций на определенном временном узле упаковки используемplugin
, как правилоnew Plugin()
Данная форма используется, поэтому, прежде всего, должно быть понятно, что,plugin
Должен быть класс.
Инициализируем проект, аналогичный предыдущей реализации загрузчика, и создаем проект в корневом каталогеdemo-webpack-plugin.js
файл, мы сначалаwebpack.config.js
использовать его в
const path = require('path')
const DemoWebpackPlugin = require('./demo-webpack-plugin')
module.exports = {
mode: 'development',
entry: {
main: './src/index.js'
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js'
},
plugins: [
new DemoWebpackPlugin()
]
}
увидеть сноваdemo-webpack-plugin.js
реализация
class DemoWebpackPlugin {
constructor () {
console.log('plugin init')
}
apply (compiler) {
}
}
module.exports = DemoWebpackPlugin
мы вDemoWebpackPlugin
Конструктор печатает сообщение, которое выводится, когда мы выполняем команду упаковки,plugin
В классе необходимо реализоватьapply
метод,webpack
При упаковке он позвонитplugin
изaplly
метод выполненияplugin
логике, этот метод принимаетcompiler
В качестве параметра этоcompiler
даwebpack
Пример
Суть плагина в том, что при выполнении метода apply он может оперировать каждый момент времени node (хуки, то есть хуки жизненного цикла), упакованные webpack на этот раз, и делать какие-то операции на разных временных узлах.
Чтобы узнать о различных хуках жизненного цикла процесса компиляции веб-пакета, вы можете обратиться кCompiler Hooks
Точно так же эти хуки также делятся на синхронные и асинхронные, как показано ниже.compiler hooks
Способ написания, некоторое ключевое содержание могут относиться к примечаниям:
class DemoWebpackPlugin {
constructor () {
console.log('plugin init')
}
// compiler是webpack实例
apply (compiler) {
// 一个新的编译(compilation)创建之后(同步)
// compilation代表每一次执行打包,独立的编译
compiler.hooks.compile.tap('DemoWebpackPlugin', compilation => {
console.log(compilation)
})
// 生成资源到 output 目录之前(异步)
compiler.hooks.emit.tapAsync('DemoWebpackPlugin', (compilation, fn) => {
console.log(compilation)
compilation.assets['index.md'] = {
// 文件内容
source: function () {
return 'this is a demo for plugin'
},
// 文件尺寸
size: function () {
return 25
}
}
fn()
})
}
}
module.exports = DemoWebpackPlugin
наше этоplugin
Функция состоит в том, чтобы автоматически генерироватьmd
Документ, содержание документа очень простое предложение
Вышеупомянутые асинхронные хуки также могут быть написаны следующими двумя способами:
// 第二种写法(promise)
compiler.hooks.emit.tapPromise('DemoWebpackPlugin', (compilation) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve()
}, 1000)
}).then(() => {
console.log(compilation.assets)
compilation.assets['index.md'] = {
// 文件内容
source: function () {
return 'this is a demo for plugin'
},
// 文件尺寸
size: function () {
return 25
}
}
})
})
// 第三种写法(async await)
compiler.hooks.emit.tapPromise('DemoWebpackPlugin', async (compilation) => {
await new Promise((resolve, reject) => {
setTimeout(() => {
resolve()
}, 1000)
})
console.log(compilation.assets)
compilation.assets['index.md'] = {
// 文件内容
source: function () {
return 'this is a demo for plugin'
},
// 文件尺寸
size: function () {
return 25
}
}
})
Окончательный результат тот же, и документ md генерируется каждый раз, когда он упакован.
До сих пор в этой статье были представлены основные принципы упаковки веб-пакетов и способы самостоятельной реализации загрузчика и плагина. Я надеюсь, что содержание этой статьи поможет вам изучить и использовать webpack, спасибо.