Краткий анализ принципа работы механизма упаковки Webpack4

внешний интерфейс
Краткий анализ принципа работы механизма упаковки Webpack4

Введение

Webpack — это инструмент для упаковки и модуляции JavaScript.В webpack все файлы являются модулями, файлы конвертируются загрузчиком, хуки внедряются через плагин, и, наконец, выводится файл, состоящий из нескольких модулей. webpack фокусируется на создании модульных проектов.

Простая модель упаковки

Начнем с простого: когда конфигурация webpack имеет только один выход, мы не рассматриваем ситуацию с подпакетом, фактически мы получаем только файл bundle.js, который содержит все используемые нами js-модули, которые могут быть непосредственно загружены и выполнены. Затем я могу проанализировать его идеи упаковки, вероятно, есть следующие 4 шага:

  1. Используйте babel для преобразования кода и синтаксического анализа, а также создайте карту зависимых модулей для одного файла.
  2. Начать рекурсивный анализ с записи и сгенерировать граф зависимостей всего проекта
  3. Упакуйте каждый упомянутый модуль в немедленно выполняемую функцию
  4. Запишите окончательный файл пакета в bundle.js

Карта зависимых модулей для одного файла

У нас будет доступ к этим пакетам:

  • @babel/parser: отвечает за разбор кода в абстрактные синтаксические деревья.
  • @babel/traverse: инструменты для обхода абстрактных синтаксических деревьев, мы можем анализировать определенные узлы в синтаксическом дереве, а затем выполнять некоторые операции, напримерImportDeclarationПолучите модуль, представленный импортом,FunctionDeclarationполучить функцию
  • @babel/core: преобразование кода, например, кода ES6 в режим ES5.

Из функций этих модулей можно сделать вывод, как получить зависимые модули одного файла и обратиться к Ast->обход Ast->вызов ImportDeclaration. код показывает, как показано ниже:

// exportDependencies.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 exportDependencies = (filename)=>{
    const content = fs.readFileSync(filename,'utf-8')
    // 转为Ast
    const ast = parser.parse(content, {
        sourceType : 'module' //babel官方规定必须加这个参数,不然无法识别ES Module
    })

    const dependencies = {}
    //遍历AST抽象语法树
    traverse(ast, {
        //调用ImportDeclaration获取通过import引入的模块
        ImportDeclaration({node}){
            const dirname = path.dirname(filename)
            const newFile = './' + path.join(dirname, node.source.value)
            //保存所依赖的模块
            dependencies[node.source.value] = newFile
        }
    })
    //通过@babel/core和@babel/preset-env进行代码的转换
    const {code} = babel.transformFromAst(ast, null, {
        presets: ["@babel/preset-env"]
    })
    return{
        filename,//该文件名
        dependencies,//该文件所依赖的模块集合(键值对存储)
        code//转换后的代码
    }
}
module.exports = exportDependencies

Пример можно запустить:

//info.js
const a = 1
export a
// index.js
import info from './info.js'
console.log(info)

//testExport.js
const exportDependencies = require('./exportDependencies')
console.log(exportDependencies('./src/index.js'))

Вывод консоли выглядит следующим образом:

GitHub
Глядя на картинку, можно понять, от чего зависит вывод~

Карта зависимых модулей для одного файла

Имея базу для получения зависимостей одного файла, мы можем дополнительно получить на этой основе карту зависимостей модулей всего проекта. Сначала начать расчет с записи, получить entryMap, затем пройти через entryMap.dependencies, вынуть ее значение (то есть путь зависимого модуля), а затем получить карту зависимостей зависимого модуля и так далее рекурсивно. , код выглядит следующим образом:

const exportDependencies = require('./exportDependencies')

//entry为入口文件路径
const exportGraph = (entry)=>{
    const entryModule = exportDependencies(entry)
    const graphArray = [entryModule]
    for(let i = 0; i < graphArray.length; i++){
        const item = graphArray[i];
        //拿到文件所依赖的模块集合,dependencies的值参考exportDependencies
        const { dependencies } = item;
        for(let j in dependencies){
            graphArray.push(
                exportDependencies(dependencies[j])
            )//关键代码,目的是将入口模块及其所有相关的模块放入数组
        }
    }
    //接下来生成图谱
    const graph = {}
    graphArray.forEach(item => {
        graph[item.filename] = {
            dependencies: item.dependencies,
            code: item.code
        }
    })
    //可以看出,graph其实是 文件路径名:文件内容 的集合
    return graph
}
module.exports = exportGraph

Я не буду размещать здесь изображение тестового примера, если вам интересно, вы можете запустить его самостоятельно~~

Выход сразу выполняет функцию

Во-первых, когда наш код загружен на страницу, его нужно немедленно выполнить. Таким образом, выходной файл bundle.js по сути представляет собой функцию немедленного выполнения. В основном мы обращаем внимание на следующие моменты:

  • Когда мы пишем модули, мы используем импорт/экспорт, после конвертации он становится require/exports.
  • Мы хотим, чтобы require/exports работали правильно, тогда мы должны определить эти две вещи и добавить их в bundle.js.
  • В графе зависимостей код становится строкой. Для выполнения вы можете использовать eval

Поэтому делаем такие вещи:

  1. Определите функцию require.Суть функции require состоит в том, чтобы выполнить код модуля, а затем смонтировать соответствующие переменные в объект экспорта
  2. Получите карту зависимостей всего проекта, начните с записи и вызовите метод require. Полный код выглядит следующим образом:
const exportGraph = require('./exportGraph')
// 写入文件,可以用fs.writeFileSync等方法,写入到output.path中
const exportBundle = require('./exportBundle')

const exportCode = (entry)=>{
    //要先把对象转换为字符串,不然在下面的模板字符串中会默认调取对象的toString方法,参数变成[Object object]
    const graph = JSON.stringify(exportGraph(entry))
    exportBundle(`
        (function(graph) {
            //require函数的本质是执行一个模块的代码,然后将相应变量挂载到exports对象上
            function require(module) {
                //InnerRequire的本质是拿到依赖包的exports变量
                function InnerRequire(relativePath) {
                    return require(graph[module].dependencies[relativePath]);
                }
                var exports = {};
                (function(require, exports, code) {
                    eval(code);
                })(InnerRequire, exports, graph[module].code);
                return exports;
                //函数返回指向局部变量,形成闭包,exports变量在函数执行后不会被摧毁
            }
            require('${entry}')
        })(${graph})`)
}
module.exports = exportCode

Запишите окончательный файл пакета в bundle.js

Здесь встроенный модуль узла fs используется непосредственно для записи файла. В соответствии с output.path веб-пакета вы можете вывести в соответствующий каталог. Здесь для простоты прямо фиксируется выходной путь, а код выглядит следующим образом:

const fs = require('fs')
const path = require('path')

const exportBundle = (data)=>{
    const directoryPath = path.resolve(__dirname,'dist')
    if (!fs.existsSync(directoryPath)) {
      fs.mkdirSync(directoryPath)
    }
    const filePath =  path.resolve(__dirname, 'dist/bundle.js')
    fs.writeFileSync(filePath, `${data}\n`)
}
const access = async filePath => new Promise((resolve, reject) => {
    fs.access(filePath, (err) => {
        if (err) {
        if (err.code === 'EXIST') {
            resolve(true)
        }
        resolve(false)
        }
        resolve(true)
    })
})
module.exports = exportBundle

На этом простая упаковка завершена. Я опубликую результаты демонстрации, которую я провел. Содержимое файла bundle.js:


        (function(graph) {
            //require函数的本质是执行一个模块的代码,然后将相应变量挂载到exports对象上
            function require(module) {
                //InnerRequire的本质是拿到依赖包的exports变量
                function InnerRequire(relativePath) {
                    return require(graph[module].dependencies[relativePath]);
                }
                var exports = {};
                (function(require, exports, code) {
                    eval(code);
                })(InnerRequire, exports, graph[module].code);
                return exports;//函数返回指向局部变量,形成闭包,exports变量在函数执行后不会被摧毁
            }
            require('./src/index.js')
        })({"./src/index.js":{"dependencies":{"./info.js":"./src/info.js"},"code":"\"use strict\";\n\nvar _info = _interopRequireDefault(require(\"./info.js\"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }\n\nconsole.log(_info[\"default\"]);"},"./src/info.js":{"dependencies":{"./name.js":"./src/name.js"},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n  value: true\n});\nexports[\"default\"] = void 0;\n\nvar _name = require(\"./name.js\");\n\nvar info = \"\".concat(_name.name, \" is beautiful\");\nvar _default = info;\nexports[\"default\"] = _default;"},"./src/name.js":{"dependencies":{},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n  value: true\n});\nexports.name = void 0;\nvar name = 'winty';\nexports.name = name;"}})

На этом простая модель упаковки завершена. Вы должны увидеть, как пример переместится на:GitHub.com/счастливая победа TY/…

Обзор процесса упаковки webpack

Работающий процесс веб-пакета представляет собой последовательный процесс, от начала до конца будут последовательно выполняться следующие процессы:

  1. параметры инициализации
  2. Начать компиляцию. Инициализировать объект Compiler с параметрами, полученными на предыдущем шаге, загрузить все настроенные плагины, передать Запустите компиляцию, выполнив метод run объекта
  3. Определить запись Найти все файлы записей на основе записи в конфигурации
  4. Скомпилируйте модуль Начиная с файла записи, вызовите все настроенные загрузчики для компиляции модуля, а затем найдите модули, от которых зависит модуль, а затем повторите этот шаг, пока все файлы, от которых зависит запись, не будут обработаны на этом шаге.
  5. Завершение компиляции модуля После перевода всех модулей с помощью Loader на шаге 4 получается финальное содержимое каждого модуля после компиляции и зависимости между ними
  6. Выходные ресурсы: в соответствии с зависимостями между записью и модулем собрать их в чанки, содержащие несколько модулей, а затем преобразовать каждый чанк в отдельный файл и добавить его в выходной список.Это последний шанс изменить выходное содержимое.
  7. Вывод завершен: после определения содержимого вывода определите путь вывода и имя файла в соответствии с конфигурацией и запишите содержимое файла в файловую систему.

В приведенном выше процессе Webpack будет транслировать определенное событие в определенный момент времени, а плагин будет выполнять определенную логику после прослушивания интересующего события, и плагин может вызвать API, предоставленный Webpack, чтобы изменить текущий результат Webpack. . На самом деле вышеупомянутые семь шагов можно просто свести к трем процессам: инициализация, компиляция и вывод, и этот процесс фактически является расширением базовой модели, упомянутой выше.

Упрощенное сравнение результатов упаковки webpack

Далее давайте посмотрим непосредственно на то, как выглядит упакованный код webpack4, и чем он отличается от нашей упрощенной модели выше (чтобы формат выглядел лучше, я его упростил и представил картинками):

GitHub
Как видите, в webpack есть__webpack_require__а также__webpack_exports__Это выглядит знакомо? Затем внимательно наблюдайте, есть объект «Модуль», ключ — это имя модуля, а значение — блок кода. Выход также является функцией немедленного выполнения, начиная с входа...

__webpack_require__ реализация

Здесь тоже упрощенная картинка, ибо исходников многовато! следующим образом:

GitHub
Видно, что основная логика здесь фактически такая же, как и в упрощенной версии. Здесь moduleId — это путь к модулю, например ./src/commonjs/index.js.

Следует отметить, что только в webpack4optimization.namedModulesЕсли это правда, тогда moduleId будет путем к модулю, в противном случае это будет числовой идентификатор. Для облегчения отладки разработчиков, вdevelopmentПо умолчанию для параметраOptimization.namedModules в режиме установлено значение true.

Суммировать

На самом деле, простая модель все еще очень хорошо изучена. После того, как мы разберемся, мы можем более удобно узнать больше о многократных входных пакетах WebPack (вы должны запустить два раза), вы можете взять его), общедоступных пакетах (поскольку модуль загружается, только одно количество записей. Вы можете узнать, сколько раз это пакет загружен, вы можете вынуть его, чтобы сделать публичные сумки). Конечно, деталей много, нужно уметь терпеливо разбираться. Продолжайте учиться!

использованная литература

наконец

  • Добро пожаловать, чтобы добавить меня в WeChat (winty230), привлечь вас в техническую группу, долгосрочный обмен и обучение...
  • Добро пожаловать, чтобы обратить внимание на «Front-end Q», серьезно изучить интерфейс и быть профессиональным техническим специалистом...

GitHub