Лу Синь сказал: «Когда мы пользуемся чем-то, мы должны правильно понимать, как это работает».
1. Что такое вебпак
2. Напишите простой Webpack
1. Взгляните на блок-схему Webpack
Конечно, я не могу реализовать все функции, из-за ограниченных возможностей я выбираю только несколько важных реализаций.
2. Подготовка
Создайте два проекта, один для проектаjuejin-webpack
, инструмент для упаковки, написанный для нас, названныйxydpack
1)juejin-webpack
Содержимое основного входного файла проекта и содержимое конфигурации упаковки:
// webpack.config.js
const path = require('path')
const root = path.join(__dirname, './')
const config = {
mode : 'development',
entry : path.join(root, 'src/app.js'),
output : {
path : path.join(root, 'dist'),
filename : 'bundle.js'
}
}
module.exports = config
// app.js
/*
// moduleA.js
let name = 'xuyede'
module.exports = name
*/
const name = require('./js/moduleA.js')
const oH1 = document.createElement('h1')
oH1.innerHTML = 'Hello ' + name
document.body.appendChild(oH1)
2) Для облегчения отладки нам нужно поставить свойxydpack
Мешокlink
в локальный, а затем импортировать вjuejin-webpack
, конкретные операции следующие
// 1. 在xydpack项目的 package.json文件中加上 bin属性, 并配置对应的命令和执行文件
{
"name": "xydpack",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"bin": {
"xydpack" : "./bin/xydpack.js"
}
}
// 2. 在xydpack项目中添加相应路径的xydpack.js文件, 并在顶部加上该文件的运行方式
#! /usr/bin/env node
console.log('this is xydpack')
// 3. 在 xydpack项目的命令行上输入 npm link
// 4. 在 juejin-webpack项目的命令行上输入 npm link xydpack
// 5. 在 juejin-webpack项目的命令行上输入 npx xydpack后, 会输出 this is xydpack 就成功了
3. Напишите xydpack.js
Из блок-схемы первого шага мы видим, чтоwebpack
Первым шагом в упаковке файла является получение содержимого файла конфигурации упаковки, а затем создание экземпляра файла.Compiler
класс, тогда проходитеrun
включить компиляцию, чтобы я мог поставитьxydpack.js
превратиться в
#! /usr/bin/env node
const path = require('path')
const Compiler = require('../lib/compiler.js')
const config = require(path.resolve('webpack.config.js'))
const compiler = new Compiler(config)
compiler.run()
тогда иди пишиcompiler.js
Содержание
пс: пишитеxydpack
сквозьjuejin-webpack
используется в проектеnpx xydpack
отлаживать
4. Напишите компилятор.js
1. Compiler
Из приведенного выше звонка мы можем знать, что,Compiler
является классом и имеетrun
способ включения компиляции
class Compiler {
constructor (config) {
this.config = config
}
run () {}
}
module.exports = Compiler
2. buildModule
В блок-схеме естьbuildModule
Метод для реализации зависимости строительного модуля и получения пути к основной записи, поэтому мы также добавляем этот метод
const path = require('path')
class Compiler {
constructor (config) {
this.config = config
this.modules = {}
this.entryPath = ''
this.root = process.cwd()
}
buildModule (modulePath, isEntry) {
// modulePath : 模块路径 (绝对路径)
// isEntry : 是否是主入口
}
run () {
const { entry } = this.config
this.buildModule(path.resolve(this.root, entry), true)
}
}
module.exports = Compiler
существуетbuildModule
В методе нам нужно начать с главного входа, получить путь модуля и соответствующий блок кода и поместить код в блок кода.require
метод изменен на__webpack_require__
метод
const path = require('path')
const fs = require('fs')
class Compiler {
constructor (config) { //... }
getSource (modulePath) {
const content = fs.readFileSync(modulePath, 'utf-8')
return content
}
buildModule (modulePath, isEntry) {
// 模块的源代码
let source = this.getSource(modulePath)
// 模块的路径
let moduleName = './' + path.relative(this.root, modulePath).replace(/\\/g, '/')
if (isEntry) this.entryPath = moduleName
}
run () {
const { entry } = this.config
this.buildModule(path.resolve(this.root, entry), true)
}
}
module.exports = Compiler
3. parse
После получения исходного кода модуля вам необходимо разобрать, заменить исходный код и получить зависимости модуля, поэтому добавьтеparse
метод для работы, а для синтаксического анализа кода требуются следующие два шага:
- Используйте абстрактное синтаксическое дерево AST для анализа исходного кода
- Нужно несколько пакетов, чтобы помочь
@babel/parser -> 把源码生成AST
@babel/traverse -> 遍历AST的结点
@babel/types -> 替换AST的内容
@babel/generator -> 根据AST生成新的源码
Уведомление :@babel/traverse
а также@babel/generator
даES6
пакет, нужно использоватьdefault
экспорт
const path = require('path')
const fs = require('fs')
const parser = require('@babel/parser')
const t = require('@babel/types')
const traverse = require('@babel/traverse').default
const generator = require('@babel/generator').default
class Compiler {
constructor (config) { //... }
getSource (modulePath) { //... }
parse (source, dirname) {
// 生成AST
let ast = parser.parse(source)
// 遍历AST结点
traverse(ast, {
})
// 生成新的代码
let sourceCode = generator(ast).code
}
buildModule (modulePath, isEntry) {
let source = this.getSource(modulePath)
let moduleName = './' + path.relative(this.root, modulePath).replace(/\\/g, '/')
if (isEntry) this.entryPath = moduleName
this.parse(source, path.dirname(moduleName))
}
run () {
const { entry } = this.config
this.buildModule(path.resolve(this.root, entry), true)
}
}
module.exports = Compiler
затем получитьast
Что это, каждый может пойтиAST ExplorerПосмотреть код, разобранный наast
Каково после.
Когда есть оператор вызова функции, напримерrequire()/ document.createElement()/ document.body.appendChild()
, там будетCallExpression
Свойства сохраняют эту информацию, поэтому следующее, что нужно сделать, это:
- Вызов функции, который необходимо изменить в коде,
require
, так что надо судить - Путь к ссылочному модулю плюс основной модуль
path
имя каталога
const path = require('path')
const fs = require('fs')
const parser = require('@babel/parser')
const t = require('@babel/types')
const traverse = require('@babel/traverse').default
const generator = require('@babel/generator').default
class Compiler {
constructor (config) { //... }
getSource (modulePath) { //... }
parse (source, dirname) {
// 生成AST
let ast = parser.parse(source)
// 模块依赖项列表
let dependencies = []
// 遍历AST结点
traverse(ast, {
CallExpression (p) {
const node = p.node
if (node.callee.name === 'require') {
// 函数名替换
node.callee.name = '__webpack_require__'
// 路径替换
let modulePath = node.arguments[0].value
if (!path.extname(modulePath)) {
// require('./js/moduleA')
throw new Error(`没有找到文件 : ${modulePath} , 检查是否加上正确的文件后缀`)
}
modulePath = './' + path.join(dirname, modulePath).replace(/\\/g, '/')
node.arguments = [t.stringLiteral(modulePath)]
// 保存模块依赖项
dependencies.push(modulePath)
}
}
})
// 生成新的代码
let sourceCode = generator(ast).code
return {
sourceCode, dependencies
}
}
buildModule (modulePath, isEntry) {
let source = this.getSource(modulePath)
let moduleName = './' + path.relative(this.root, modulePath).replace(/\\/g, '/')
if (isEntry) this.entryPath = moduleName
let { sourceCode, dependencies } = this.parse(source, path.dirname(moduleName))
}
run () {
const { entry } = this.config
this.buildModule(path.resolve(this.root, entry), true)
}
}
module.exports = Compiler
Получите все зависимости модуля рекурсивно и сохраните все пути и зависимые модули
const path = require('path')
const fs = require('fs')
const parser = require('@babel/parser')
const t = require('@babel/types')
const traverse = require('@babel/traverse').default
const generator = require('@babel/generator').default
class Compiler {
constructor (config) { //... }
getSource (modulePath) { //... }
parse (source, dirname) { //... }
buildModule (modulePath, isEntry) {
let source = this.getSource(modulePath)
let moduleName = './' + path.relative(this.root, modulePath).replace(/\\/g, '/')
if (isEntry) this.entryPath = moduleName
let { sourceCode, dependencies } = this.parse(source, path.dirname(moduleName))
this.modules[moduleName] = JSON.stringify(sourceCode)
dependencies.forEach(d => this.buildModule(path.join(this.root, d)), false)
}
run () {
const { entry } = this.config
this.buildModule(path.resolve(this.root, entry), true)
}
}
module.exports = Compiler
4. emit
После получения всех зависимостей модулей и главная запись следующий шаг - вставить данные в шаблон и записывать его в элемент конфигурацииoutput.path
Потому что нужен шаблон, поэтому позаимствоватьwebpack
шаблон, используяEJS
Генерировать шаблоны, не понимаюEJS
точказдесь, содержимое шаблона:
// lib/template.ejs
(function (modules) {
var installedModules = {};
function __webpack_require__(moduleId) {
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
module.l = true;
return module.exports;
}
return __webpack_require__(__webpack_require__.s = "<%-entryPath%>");
})
({
<%for (const key in modules) {%>
"<%-key%>":
(function (module, exports, __webpack_require__) {
eval(<%-modules[key]%>);
}),
<%}%>
});
Ниже мы пишемemit
функция
const path = require('path')
const fs = require('fs')
const parser = require('@babel/parser')
const t = require('@babel/types')
const traverse = require('@babel/traverse').default
const generator = require('@babel/generator').default
const ejs = require('ejs')
class Compiler {
constructor (config) { //... }
getSource (modulePath) { //... }
parse (source, dirname) { //... }
buildModule (modulePath, isEntry) { //... }
emit () {
const { modules, entryPath } = this
const outputPath = path.resolve(this.root, this.config.output.path)
const filePath = path.resolve(outputPath, this.config.output.filename)
if (!fs.readdirSync(outputPath)) {
fs.mkdirSync(outputPath)
}
ejs.renderFile(path.join(__dirname, 'template.ejs'), { modules, entryPath })
.then(code => {
fs.writeFileSync(filePath, code)
})
}
run () {
const { entry } = this.config
this.buildModule(path.resolve(this.root, entry), true)
this.emit()
}
}
module.exports = Compiler
Если вы напишете это, вjuejin-webpack
вклад в проектnpx xydpack
создастdist
каталог, в котором естьbundle.js
файл, который можно запустить в браузере,демо
3. Добавить загрузчик
После второго прохода я просто повернул код, что кажется бессмысленным~
Итак, мы собираемся добавитьloader
, правильноloader
незнакомая точказдесь, потому что это рукопись, поэтому мыloader
напиши это сам
Примечание: потому что эта штука довольно простая, так что играть можно только со стилемloader
, остальные не воспроизвести, поэтому я только демонстрирую и пишу стильloader
1. Загрузчик стилей
я привык использоватьstylus
писать стили, так стили пишутstylus-loader
а такжеstyle-loader
Сначала добавьте в элемент конфигурацииloader
, затем вapp.js
введен вinit.styl
// webpack.config.js
const path = require('path')
const root = path.join(__dirname, './')
const config = {
mode : 'development',
entry : path.join(root, 'src/app.js'),
output : {
path : path.join(root, 'dist'),
filename : 'bundle.js'
},
module : {
rules : [
{
test : /\.styl(us)?$/,
use : [
path.join(root, 'loaders', 'style-loader.js'),
path.join(root, 'loaders', 'stylus-loader.js')
]
}
]
}
}
module.exports = config
-----------------------------------------------------------------------------------------
// app.js
const name = require('./js/moduleA.js')
require('./style/init.styl')
const oH1 = document.createElement('h1')
oH1.innerHTML = 'Hello ' + name
document.body.appendChild(oH1)
создать один в корневом каталогеloaders
каталог для записи нашегоloader
// stylus-loader
const stylus = require('stylus')
function loader (source) {
let css = ''
stylus.render(source, (err, data) => {
if (!err) {
css = data
} else {
throw new Error(error)
}
})
return css
}
module.exports = loader
-----------------------------------------------------------------------------------------
// style-loader
function loader (source) {
let script = `
let style = document.createElement('style')
style.innerHTML = ${JSON.stringify(source)}
document.body.appendChild(style)
`
return script
}
module.exports = loader
loader
Он работает, когда файл читается, поэтому изменитеcompiler.js
, существуетgetSource
функция плюс соответствующая операция
const path = require('path')
const fs = require('fs')
const parser = require('@babel/parser')
const t = require('@babel/types')
const traverse = require('@babel/traverse').default
const generator = require('@babel/generator').default
const ejs = require('ejs')
class Compiler {
constructor (config) { //... }
getSource (modulePath) {
try {
let rules = this.config.module.rules
let content = fs.readFileSync(modulePath, 'utf-8')
for (let i = 0; i < rules.length; i ++) {
let { test, use } = rules[i]
let len = use.length - 1
if (test.test(modulePath)) {
// 递归处理所有loader
function loopLoader () {
let loader = require(use[len--])
content = loader(content)
if (len >= 0) {
loopLoader()
}
}
loopLoader()
}
}
return content
} catch (error) {
throw new Error(`获取数据错误 : ${modulePath}`)
}
}
parse (source, dirname) { //... }
buildModule (modulePath, isEntry) { //... }
emit () { //... }
run () { //... }
}
module.exports = Compiler
затем бегиnpx xydpack
Упаковка, добавит такой кусок кода
"./src/style/init.styl":
(function (module, exports, __webpack_require__) {
eval("let style = document.createElement('style');\nstyle.innerHTML = \"* {\\n padding: 0;\\n margin: 0;\\n}\\nbody {\\n color: #f40;\\n}\\n\";\ndocument.head.appendChild(style);");
}),
Тогда просто запустите его,демо
*2.Загрузчик скрипта
сценарийloader
, первая мысльbabel-loader
, мы пишемbabel-loader
, но нужно использоватьwebpack
Для упаковки измените файл конфигурации на
// webpack.config.js
resolveLoader : {
modules : ['node_modules', path.join(root, 'loaders')]
},
module : {
rules : [
{
test : /\.js$/,
use : {
loader : 'babel-loader',
options : {
presets : [
'@babel/preset-env'
]
}
}
}
]
}
использоватьbabel
Требуются три пакета:@babel/core | @babel/preset-env | loader-utils
После установки напишитеbabel-loader
const babel = require('@babel/core')
const loaderUtils = require('loader-utils')
function loader (source) {
let options = loaderUtils.getOptions(this)
let cb = this.async();
babel.transform(source, {
...options,
sourceMap : true,
filename : this.resourcePath.split('/').pop(),
}, (err, result) => {
// 错误, 返回的值, sourceMap的内容
cb(err, result.code, result.map)
})
}
module.exports = loader
затем используйтеwebpack
Просто упакуйте это
4. Резюме
Здесь мы можем примерно предположитьwebpack
Процесс операции выглядит следующим образом:
- Получить параметры конфигурации
- Создайте экземпляр компилятора и запустите компиляцию с помощью метода запуска.
- В соответствии с входным файлом создайте зависимости и рекурсивно получите зависимые модули всех модулей
- Используйте загрузчик для разбора соответствующих модулей
- Получите шаблон, поместите проанализированные данные в разные шаблоны
- выходной файл по указанному пути
Примечание: я просто шучу, мне нужно учитьсяwebpack
, точказдесь
ps : я скоро заканчиваю учебу, а потом я безработный.Если есть какая-либо компания, которой не хватает страниц, пожалуйста, свяжитесь со мной.Я также могу вырезать картинки.Я очень прочный.
Электронная почта: will3virgo@163.com