Рукописный простой Webpack

JavaScript

Лу Синь сказал: «Когда мы пользуемся чем-то, мы должны правильно понимать, как это работает».


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метод для работы, а для синтаксического анализа кода требуются следующие два шага:

  1. Используйте абстрактное синтаксическое дерево AST для анализа исходного кода
  2. Нужно несколько пакетов, чтобы помочь
@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Процесс операции выглядит следующим образом:

  1. Получить параметры конфигурации
  2. Создайте экземпляр компилятора и запустите компиляцию с помощью метода запуска.
  3. В соответствии с входным файлом создайте зависимости и рекурсивно получите зависимые модули всех модулей
  4. Используйте загрузчик для разбора соответствующих модулей
  5. Получите шаблон, поместите проанализированные данные в разные шаблоны
  6. выходной файл по указанному пути

Примечание: я просто шучу, мне нужно учитьсяwebpack, точказдесь

ps : я скоро заканчиваю учебу, а потом я безработный.Если есть какая-либо компания, которой не хватает страниц, пожалуйста, свяжитесь со мной.Я также могу вырезать картинки.Я очень прочный.

Электронная почта: will3virgo@163.com