1. Пишите впереди
В нашем ежедневном процессе разработки мы все чаще используемwebpackЭто своего рода инструмент сборки, но для его использования мы больше застряли в какой-то простой конфигурации, такой как загрузчик, конфигурация плагина. Мы редко используем webpack для сборки проекта с нуля (чаще пользуемся cli) и еще меньше понимаем его внутренние принципы упаковки. Почему он может преобразовать файл в модуль и почему он может упаковать все модули в один файл? Как выглядит упакованный файл (может быть, многие редко смотрят упакованный файл сборки)? Мы мало что знаем об этом, но когда скорость нашей сборки становится все медленнее и медленнее, мы хотим ее оптимизировать, но, поскольку мы слишком мало знаем о веб-пакете, мы не можем начать. И по мере того, как в интервью все чаще спрашивают об основных принципах веб-пакета, веб-пакет становится для нас все более и более помехой. Но для многих людей, глядя на исходный код веб-пакета, у многих может быть большая голова и нет возможности начать. Если вы посмотрите на объяснения исходного кода других людей, вы попадете в различные концепции, что такое механизм событий, внутренние хуки, архитектура плагина Tapable и дизайн хука. Все это непонятно. Наоборот, я думаю, что если отталкиваться от результатов, смотреть, как выглядит конечный запакованный файл webpack, а затем реализовывать такой же простой упаковщик, то это позволит нам обойти множество сложных вещей и лучше понять его принципы. В настоящее время, если мы посмотрим на исходный код или статьи других людей, мы можем получить вдвое больший результат с половиной усилий.
Во-вторых, упаковщик модулей
2.1 Что такое сборщик модулей
Смотрим на официальном сайтеwebpackОпределение: webpack — это современное приложение JavaScript.сборщик статических модулей. Когда webpack обрабатывает приложение, он рекурсивно строит граф зависимостей, содержащий каждый модуль, необходимый приложению, а затем объединяет все эти модули в один или несколько пакетов. Более популярно понимание: каждый файл представляет собой модуль, и файл будет представлять содержимое других файлов. В конечном итоге мы хотим добиться использования определенного файла в качестве записи: упаковать все его зависимые файлы в один файл, который являетсясборщик модулей.
2.2 Файлы, упакованные с помощью веб-пакета
Мы знаем, что упаковщик модуля упакует несколько файлов в один файл, поэтому, как выглядит упакованный файл, мы должны знать это, прежде чем сможем реализовать его конкретно, поэтому мы смотрим на эффект следующей упаковки веб-пакета.
Пример: Предположим, у нас есть следующие файлы в одной папке:
Файл: index.js
let action = require("./action.js").action; // 引入aciton.js
let name = require("./name.js").name; // 引入name.js
let message = `${name} is ${action}`;
console.log(message);
index.jsпредставлен в файлеaction.jsа такжеname.js.
Файл: action.js
let action = "making webpack";
exports.action = action;
файл: имя.js
let familyName = require("./family-name.js").name;
exports.name = `${familyName} 阿尔伯特`;
документname.jsповторно введенfamily-name.jsдокумент.Файл: имя-семейства.js
exports.name = "haiyingsitan";
Затем мы используем webpack для упаковки и удаления упакованных комментариев, чтобы получить следующий код:
(() => {
var __webpack_modules__ = ({
"./action.js": ((__unused_webpack_module, exports) => {
let action = "making webpack";
exports.action = action;
}),
"./family-name.js": ((__unused_webpack_module, exports) => {
exports.name = "haiyingsitan";
}),
"./name.js": ((__unused_webpack_module, exports, __webpack_require__) => {
let familyName = __webpack_require__( /*! ./family-name.js */ "./family-name.js").name;
exports.name = `${familyName} 阿尔伯特`;
})
});
var __webpack_module_cache__ = {};
function __webpack_require__(moduleId) {
if (__webpack_module_cache__[moduleId]) {
return __webpack_module_cache__[moduleId].exports;
}
var module = __webpack_module_cache__[moduleId] = {
exports: {}
};
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
return module.exports;
}
(() => {
let action = __webpack_require__( "./action.js").action;
let name = __webpack_require__( "./name.js").name;
let message = `${name} is ${action}`;
console.log(message);
})();
})();
Приведенный выше код все еще выглядит немного сложным, давайте упростим его еще больше:
(() => {
// 获取所有的依赖
var modules = {
"./action.js": (module, exports) => {
let action = "making webpack";
exports.action = action;
},
// ... 其他代码
};
// require对应的模块函数执行
function __webpack_require__(moduleId) {
// 其他实现
return module.exports;
}
// 入口函数立即执行
let entryFn = () => {
let action = __webpack_require__("./action.js").action;
let name = __webpack_require__("./name.js").name;
let message = `${name} is ${action}`;
console.log(message);
};
entryFn();
})();
Мы можем обнаружить, что после окончательной упаковки файла это немедленно выполняемая функция. Эта функция состоит из трех частей:
- коллекция модулей
Этот набор модулей представляет собой набор всех модулей с путем в качестве значения ключа и содержимым модуля в качестве значения. Когда нам нужно использовать модуль, мы можем получить его непосредственно из этой коллекции модулей.
Зачем вам эта коллекция модулей? Представьте, если бы мы столкнулись
require("./action.js"), то этоaction.jsКакому модулю он соответствует? Следовательно, мы должны иметь возможность получить все модули и различить их (используя идентификатор модуля или имя модуля), а затем мы можем получить его непосредственно из этой коллекции модулей через идентификатор модуля или имя модуля.
var modules = {
"./action.js": (module, exports) => {
let action = "making webpack";
exports.action = action;
},
};
- выполнение функции модуля
Каждый модуль соответствует функции, когда встречается
require(xxx)Когда он фактически выполняет введенную функцию модуля.
var __webpack_module_cache__ = {};
function __webpack_require__(moduleId) {
if (__webpack_module_cache__[moduleId]) {
return __webpack_module_cache__[moduleId].exports;
}
var module = __webpack_module_cache__[moduleId] = {
exports: {}
};
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
return module.exports;
}
- Входной файл выполняется немедленно (выполняет функцию модуля) Все мы знаем, что в упаковке модуля должен быть входной файл, и этот файл нужно запускать сразу, чтобы получить все зависимости. По сути, входной файл тоже является модулем, и функция, соответствующая этому модулю, может выполняться сразу.
let entryFn = () => {
let action = __webpack_require__("./action.js").action;
let name = __webpack_require__("./name.js").name;
let message = `${name} is ${action}`;
console.log(message);
};
entryFn();
Что ж, пока мы в основном знаем, как выглядят файлы, сгенерированные после упаковки модуля webpack. Если мы хотим добиться одной и той же функции, нам нужно только реализовать ее одновременно: сбор модулей, выполнение модуля и немедленное выполнение функции входа.Одним из наиболее важных является сбор и выполнение модулей.
В-третьих, конкретная реализация
Из приведенного выше анализа мы можем узнать, что то, чего мы хотим достичь, в основном включает две части:
- Сгенерируйте большой набор модулей из всех файлов проекта
- Модуль выполняет функцию. Когда встречается импортированный модуль, выполняется соответствующая функция.
Далее мы будем реализовывать эти две части по отдельности.
3.1 Реализация набора модулей
3.1.1 Содержимое файла упаковки
Мы можем посмотреть конкретное содержимое каждого модуля после упаковки webpack:
var modules = {
"./action.js": (module, exports) => {
// 文件内容
let action = "making webpack";
exports.action = action;
},
};
Мы можем обнаружить, что каждый модуль на самом деле является функциональной оболочкой на внешнем уровне. Зачем помещать содержимое файла в функцию, это потому, что мы все знаем, что одной из самых важных особенностей модуляризации является изоляция среды, и каждый модуль не влияет друг на друга. Только представьте, если содержимое файла не изолировано и не упаковано напрямую, то переменные, определенные между модулями, обязательно будут влиять друг на друга в одной и той же области видимости. Функции часто используются для формирования единой области видимости для изоляции переменных. Поэтому сначала запаковываем все файлы.
модуль index.js
function(require,exports){
let action = require("./action.js").action;
let name = require("./name.js").name;
let message = `${name} is ${action}`;
console.log(message);
}
модуль action.js
function(require,exports){
let action = "making webpack";
exports.action = action;
}
модуль name.js
function(require,exports){
let familyName = require("./family-name.js").name;
exports.name = `${familyName} 阿尔伯特`;
}
модуль family-name.js.
function(require,exports){
exports.name = "haiyingsitan";
}
Затем, чтобы отличить или получить эти модули, нам нужно дать каждому модулю идентификатор модуля или имя модуля, здесь мы напрямую используем путь к файлу в качестве идентификатора каждого модуля. Наконец, эти модули сгруппированы в коллекцию. Следующим образом:
const modules = {
"./index.js":function(require,exports){
let action = require("./action.js").action;
let name = require("./name.js").name;
let message = `${name} is ${action}`;
console.log(message);
},
"./action.js": function (require, exports) {
let action = "making webpack";
exports.action = action;
},
"./name.js": function (require, exports) {
let familyName = require("./family-name.js").name;
exports.name = `${familyName} 阿尔伯特`;
},
"./family-name.js": function (require, exports) {
exports.name = "haiyingsitan";
}
};
То есть то, чего мы в конечном итоге хотим достичь, — это такая коллекция.
На данный момент мы пытаемся реализовать следующие функции:
- Оболочка содержимого каждого файла
- Каждый модуль принимает путь как идентификатор модуля.
- Объедините все модули вместе, чтобы сформировать набор
Давайте посмотрим на конкретную реализацию следующим образом:
const fs = require("fs");
let modules = {};
const fileToModule = function (path) {
const fileContent = fs.readFileSync(path).toString();
return {
id: path, // 这里以路径作为模块id
code: `function(require,exports){ // 这里加壳了
${fileContent.toString()};
}`,
};
};
let result = fileToModule("./index.js");
modules[result["id"]] = result.code;
console.log("modules=",modules);
Результат:
modules= {
'./index.js':'function(require,exports){\n let action = require("./action.js").action;\r\nlet name = require("./name.js").name;\r\nlet message = `${name} is ${action}`;\r\nconsole.log(message);\r\n;\n }'
}
Как видно из вышеизложенного, мы успешно преобразовали входной файл в модуль, дали ему имя и добавили его в объект модуля. Но мы обнаружили, что наши файлы на самом деле зависят от./action.jsа также./name.js, однако мы не можем получить содержимое их модуля. Поэтому нам необходимо разобраться сrequireимпортный модуль. То есть найти все зависимости в текущем модуле, а затем разрешить эти зависимости в коллекции модулей.
3.1.2 Получить все зависимости текущего модуля
Следующий шаг — найти все зависимости в модуле.
// const action = require("./action.js")
function getDependencies(fileContent) {
let reg = /require\(['"](.+?)['"]\)/g;
let result = null;
let dependencies = [];
while ((result = reg.exec(fileContent))) {
dependencies.push(result[1]);
}
return dependencies;
}
Здесь мы используем обычное суждение, пока оноrequire("")илиrequire('')Этот формат рассматривается как введение модуля (такой вид обработки немного проблематичен, мы пока его проигнорируем и подождем до следующей оптимизации). Затем поместите все импорты в массив, чтобы получить все зависимости текущего модуля.
Мы используем эту функцию для просмотра зависимостей входного файла:
const fileContent = fs.readFileSync(path).toString();
let result = getDependencies(fileContent);
console.log(result) // ["./action.js","./name.js"]
Мы можем успешно получить все зависимости входного файла, а затем нам нужно дополнительно проанализировать зависимости этих входных файлов.
Поэтому, когда мы конвертируем файл в модуль, лучше всего отображать всю информацию о зависимостях модуля для удобства обработки. Поэтому давайте изменимfileToModuleэта функция.
const fileToModule = function (path) {
const fileContent = fs.readFileSync(path).toString();
return {
id:path,
dependencies:getDependencies(fileContent), // 新增模块信息
code:`(require,exports) => {
${fileContent.toString()};
}`
}
};
Ну пока что мы можем получить зависимости каждого модуля, и при этом каждую зависимость мы можем преобразовать в объект, тогда следующим шагом будет поместить все объекты в большой объект, чтобы получить все модули в коллекции проекта .
3.1.3 Сделать все модули коллекцией
function createGraph(filename) {
let module = fileToModule(filename);
let queue = [module];
for (let module of queue) {
const dirname = path.dirname(moduleId);
module.dependencies.forEach((relativePath) => {
const absolutePath = path.join(dirname, relativePath);
const child = fileToModule(absolutePath);
queue.push(child);
});
}
// 上面得到的是一个数组。转化成对象
let modules = {}
queue.forEach((item) => {
modules[item.id] = item.code;
})
return modules;
}
console.log(createGraph("./index.js"));
createGraphЭто получение всех зависимостей по очереди в соответствии с входным файлом и добавление их в массив очереди каждый раз, когда они получены.Поскольку let of используется для обхода, let of продолжит обход вновь добавленных элементов без необходимости для цикла for необходимо обработать.
Давайте взглянем на набор модулей, который в итоге получает входной файл:
{
'./index.js': 'function(require,exports){ let action = require("./action.js").action;\r\nlet name = require("./name.js").name;\r\nlet message = `${name} is ${action}`;\r\nconsole.log(message);\r\n;\n}',
'action.js': 'function(require,exports){let action = "making webpack";\r\nexports.action = action;;\n }',
'name.js': 'function(require,exports){let familyName = require("./family-name.js").name;\r\nexports.name = `${familyName} 阿尔伯特`;;\n}',
'family-name.js': 'function(require,exports){exports.name = "haiyingsitan";;\n }'
}
3.2 Функции исполнительных модулей
Мы получили всю информацию о модуле в указанном выше объекте модуля, затем мы выполняем функцию, соответствующую входному файлу.exec.Из приведенного выше рисунка видно, что когда мы выполняем функцию, соответствующую входному файлу
exec(index.js), он находит:
- существующая зависимость
./action.js, так что звонитеexec("./action.js"). В настоящее время других зависимостей нет, поэтому возвращайте значение напрямую. Эта линия заканчивается. - существующая зависимость
./name.js, так что звонитеexec("./name.js"). Зависит снова./family-name.js, так что звонитеexec("./family-name.js"). В это время других зависимостей нет, и возвращается возвращаемое значение. Эта линия заканчивается.
Мы можем обнаружить, что на самом деле это рекурсивный процесс, постоянно ищущий зависимости, а затем выполняющий соответствующую функцию. Таким образом, мы можем грубо написать следующую функцию:
const exec = function(moduleId){
const fn = modules[moduleId]; // 获取到每个id对应的函数
let exports = {};
const require = function(filename){
const dirname = path.dirname(moduleId);
const absolutePath = path.join(dirname, filename);
return exec(absolutePath);
}
fn(require, exports);
return exports
}
Примечание: вышеmodules[moduleId]Если то, что мы получили из нашей предыдущей структуры данных, на самом деле является строкой, но нам нужно, чтобы она выполнялась как функция. Поэтому для удобства просмотра немного модифицируем здесь код модуля прямой передачи файлов.
const fileToModule = function (path) {
console.log("path:",path)
const fileContent = fs.readFileSync(path).toString();
return {
id: path,
dependencies: getDependencies(fileContent),
code: function(require,exports) {
eval(fileContent.toString()) // 看这里 里面的内容用eval来执行。外面是函数声明,不是一个字符串了。
},
};
};
Нам не удобно выполнять строку, поэтому мы рассматриваем объявление кода как функцию, а функция — это содержимое модуля.evalвыполнить. Но это не нужно, когда мы записываем в файл, это здесь для удобства просмотра.
3.3 Записать упакованный файл в указанный файл
Хорошо, до сих пор мы внедрили самые важные части пакета модуля: коллекция модулей и функции выполнения модулей. Последний код выглядит следующим образом:
const fs = require("fs");
const path = require("path");
// 将文件转化成模块对象
const fileToModule = function (path) {
const fileContent = fs.readFileSync(path).toString();
return {
id: path,
dependencies: getDependencies(fileContent),
code: function (require, exports) {
eval(fileContent.toString());
},
};
};
// 获取模块的所有依赖
function getDependencies(fileContent) {
let reg = /require\(['"](.+?)['"]\)/g;
let result = null;
let dependencies = [];
while ((result = reg.exec(fileContent))) {
dependencies.push(result[1]);
}
return dependencies;
}
// 将所有模块以及他们的依赖转化成模块对象
function createGraph(filename) {
let module = fileToModule(filename);
let queue = [module];
for (let module of queue) {
const dirname = path.dirname(moduleId);
module.dependencies.forEach((relativePath) => {
const absolutePath = path.join(dirname, relativePath);
const child = fileToModule(absolutePath);
queue.push(child);
});
}
let modules = {};
queue.forEach((item) => {
modules[item.id] = item.code;
});
return modules;
}
let modules = createGraph("./index.js");
// 执行模块函数
const exec = function (moduleId) {
const fn = modules[moduleId];
let exports = {};
const require = function (filename) {
const dirname = path.dirname(moduleId);
const absolutePath = path.join(dirname, filename);
return exec(absolutePath);
};
fn(require, exports);
return exports;
};
Давайте начнем упаковать из файла ввода и посмотреть, сможем ли мы получить тот же результат, что и WebPack.
exec("./index.js"); //输出: haiyingsitan 阿尔伯特 is making webpack
Мы можем обнаружить, что успешно получен тот же результат, что и для веб-пакета, и успешно выполнена упаковка модулей. Следующее, чего мы хотим добиться, это упаковать наш модуль и сгенерировать его в файл.
function createBundle(modules){
let __modules = "";
for (let attr in modules) {
__modules += `"${attr}":${modules[attr]},`;
}
const result = `(function(){
const modules = {${__modules}};
const exec = function (moduleId) {
const fn = modules[moduleId];
let exports = {};
const require = function (filename) {
const dirname = path.dirname(moduleId);
const absolutePath = path.join(dirname, filename);
return exec(absolutePath);
};
fn(require, exports);
return exports;
};
exec("./index.js");
})()`;
fs.writeFileSync("./dist/bundle3.js", result);
}
createBundleФункция используется для записи упакованного файла в отдельный файл. Мы можем посмотреть на файлы, сгенерированные после упаковки, следующим образом:
(function () {
// 模块集合
const modules = {
"./index.js": function (require, exports) {
let action = require("./action.js").action;
let name = require("./name.js").name;
let message = `${name} is ${action}`;
console.log(message);;
},
"action.js": function (require, exports) {
let action = "making webpack";
exports.action = action;;
},
"name.js": function (require, exports) {
let familyName = require("./family-name.js").name;
exports.name = `${familyName} 阿尔伯特`;;
},
"family-name.js": function (require, exports) {
exports.name = "haiyingsitan";;
},
};
const exec = function (moduleId) {
const fn = modules[moduleId];
let exports = {};
const require = function (filename) {
const dirname = path.dirname(moduleId);
const absolutePath = path.join(dirname, filename);
return exec(absolutePath);
};
fn(require, exports);
return exports;
};
//入口函数执行
exec("./index.js");
})()
Мы видим, что упакованный файл в основном такой же, как и файл, упакованный webpack. (Примечание. Поскольку в настоящее время поддерживается только импорт пользовательских модулей, встроенный путь не может быть импортирован, поэтому, если вы хотите проверить, может ли упакованный файл выполняться нормально, вручную добавьте введение пути вверху файла. файл).
4. Дальнейшая оптимизация
4.1 Проблемы с использованием регулярного сопоставления требуют
Пока удалось добиться пакетной генерации модулей, но тут еще есть некоторые проблемы, я впереди2.2.1 Получить все зависимости текущего модуляКак упоминалось в реализации, мы используем/require\(['"](.+?)['"]\)/gэто регулярное выражение для соответствияrequireвступление. Однако, если в файле есть содержимое, соответствующее этому обычному, но не используемое для импорта. Например:
const str = `require('随便写的')`;
const str = /require\(['"](.+?)['"]\)/g;
console.log(re.exec(str))// 这里也能够正确匹配
Мы обнаружили, что приведенная выше строка также может правильно соответствовать нашей обычной, но эта строка не являетсяrequireПредставлять. Но это будет рассматриваться как импорт, что приведет к ошибке. Некоторые люди могут сказать, что мы можем написать лучшее регулярное выражение, чтобы различать больше случаев, но независимо от того, насколько хорошо регулярное выражение не может быть совместимо со всеми случаями, есть ли способ правильно отличить импортированные требования от других требований? Мы можем обратиться к тому, как webpack может правильно его идентифицировать. Ключ в том, чтобы использоватьbabel.
4.2 Знакомство с Бабелем
оbabelВы можете найти другие статьи, чтобы увидеть принцип. Здесь мы просто помним, что ядром Babel являетсяРазобрать (разобрать),трансформировать,генерироватьЭти три шага показаны на рисунке ниже.
Разбирая код в абстрактное синтаксическое дерево (AST), мы можем затем работать с нужными нам узлами, преобразовывать в новый AST, а затем генерировать новый код. Вам может показаться, что это сложно, но мы не затрагиваем основные принципы babel, а просто применяем его функцию преобразования, так что нет необходимости углубляться в это. мы можемAST ExploreПосмотрите, как преобразовать код в AST в . В качестве примера возьмем следующий код:
let action = require("./action.js");
мы можем узнать, что,babelПосле преобразования приведенного выше кода в ast мы можем получить точноеrequireТип этого узлаCallExpression, узлаnameдляrequire, значение параметра равно./action.js, так что он может правильно отличить указанныйrequireи как значение или переменнаяrequire. Следовательно, нам нужно изменить реализацию этой функции, чтобы получить зависимости:
до исправления:
function getDependencies(fileContent) {
let reg = /require\(['"](.+?)['"]\)/g;
let result = null;
let dependencies = [];
while ((result = reg.exec(fileContent))) {
dependencies.push(result[1]);
}
return dependencies;
}
После модификации:
function getDependencies(filePath) {
let result = null;
let dependencies = [];
const fileContent = fs.readFileSync(filePath).toString();
// parse
const ast = parse(fileContent, { sourceType: "CommonJs" });
// transform
traverse(ast, {
enter: (item) => {
if (
item.node.type === "CallExpression" &&
item.node.callee.name === "require"
) {
const dirname = path.dirname(filePath);
dependencies.push(path.join(dirname, item.node.arguments[0].value));
console.log("dependencies", dependencies);
}
},
});
return dependencies;
}
После того, как разбираться Бабела, то мы ищем, стоит ли тип каждого узлаCallExpression, а имя узлаrequire, если оба удовлетворены, значит, это require является функцией для введения модулей. Затем мы можем сохранить его параметры в массиве как зависимость.
4.3 Решить проблему взаимозависимости между модулями
Мы знаем, что модули могут ссылаться друг на друга, например, модуль name.js представляет модуль family-name.js. В модуле family.js представлен модуль name.js. Как показано ниже:
модуль name.js
let familyName = require("./family-name.js").name; // 引入了family-name.js模块
exports.name = `${familyName} 阿尔伯特`;
модуль family-name.js
const name1 = require("./name.js"); // 引入了family-name.js模块
exports.name = "haiyingsitan";
Это принесет проблемы. Так как мы2.1.3 Сгруппируйте все модули в набордля создания объекта модуля. Используйте for of для обхода коллекции модулей. Если есть зависимость, преобразуйте ее в модуль и добавьте в коллекцию модулей. Из-за взаимных зависимостей модуль будет удален в начале.family.jsдобавить в модуль, а потом поставитьname.jsк объекту модуля, затемname.jsзависит отfamily.jsнужно повторитьfamily.jsДобавляются модули, что приводит к бесконечному циклу коллекции модулей.
function createGraph(filename) {
let module = fileToModule(filename);
let queue = [module];
for (let module of queue) {
const dirname = path.dirname(module.id);
module.dependencies.forEach((relativePath) => {
const absolutePath = path.join(dirname, relativePath);
// 看这里,会不断地创建依赖
const child = fileToModule(absolutePath);
queue.push(child);
});
}
// 上面得到的是一个数组。转化成对象
let modules = {}
queue.forEach((item) => {
modules[item.id] = item.code;
})
return modules;
}
Как показано на следующем рисунке: В конце концов, коллекция модулей будет по-прежнему содержать повторяющиеся модули.name.jsа такжеfamily.js, в результате чего цикл никогда не завершается.
Мы можем обнаружить, что: на самом деле, корень этой проблемы заключается в постоянном добавлении повторяющихся модулей в набор модулей, поэтому мы можем судить, является ли это повторяющимся модулем, прежде чем добавлять его, и если да, не добавляйте его к нему, поэтому чтобы избежать непрерывного зацикливания. Реализация выглядит следующим образом:
function createGraph(filename) {
let module = fileToModule(filename);
let queue = [module];
for (let module of queue) {
const dirname = path.dirname(module.id);
module.dependencies.forEach((relativePath) => {
const absolutePath = path.join(dirname, relativePath);
// 看这里看这里 判断一下模块集合中是否已经存在这个模块
const result = queue.every((item) => {
return item.id !== absolutePath;
});
if (result) {
// 不存在,直接添加
const child = fileToModule(absolutePath);
queue.push(child);
} else {
// 存在终止本次循环
return false;
}
});
}
let modules = {};
queue.forEach((item) => {
modules[item.id] = item.code;
});
return modules;
}
V. Резюме
Что ж, пока нам удалось реализовать простой сборщик веб-пакетов. Окончательный код выглядит следующим образом:
const fs = require("fs");
const path = require("path");
const {parse} = require("@babel/parser");
const traverse = require("@babel/traverse").default;
// 1.加壳
const fileToModule = function (path) {
const fileContent = fs.readFileSync(path).toString();
return {
id: path,
dependencies: getDependencies(path),
code: `function (require, exports) {
${fileContent};
}`,
};
};
// 2.获取依赖
function getDependencies(filePath) {
let result = null;
let dependencies = [];
const fileContent = fs.readFileSync(filePath).toString();
// parse
const ast = parse(fileContent, { sourceType: "CommonJs" });
// transform
traverse(ast, {
enter: (item) => {
if (
item.node.type === "CallExpression" &&
item.node.callee.name === "require"
) {
const dirname = path.dirname(filePath);
dependencies.push(path.join(dirname, item.node.arguments[0].value));
console.log("dependencies", dependencies);
}
},
});
return dependencies;
}
// 3. 将所有依赖形成一个集合
function createGraph(filename) {
let module = fileToModule(filename);
let queue = [module];
for (let module of queue) {
const dirname = path.dirname(module.id);
module.dependencies.forEach((relativePath) => {
const absolutePath = path.join(dirname, relativePath);
console.log("queue:",queue);
console.log("absolutePath:",absolutePath);
const result = queue.every((item) => {
return item.id !== absolutePath;
});
if (result) {
const child = fileToModule(absolutePath);
queue.push(child);
} else {
return false;
}
});
}
let modules = {};
queue.forEach((item) => {
modules[item.id] = item.code;
});
return modules;
}
let modules = createGraph("./index.js");
// 4. 执行模块
const exec = function (moduleId) {
const fn = modules[moduleId];
let exports = {};
const require = function (filename) {
const dirname = path.dirname(moduleId);
const absolutePath = path.join(dirname, filename);
return exec(absolutePath);
};
fn(require, exports);
return exports;
};
// exec("./index.js");
// 5. 写入文件
function createBundle(modules){
let __modules = "";
for (let attr in modules) {
__modules += `"${attr}":${modules[attr]},`;
}
const result = `(function(){
const modules = {${__modules}};
const exec = function (moduleId) {
const fn = modules[moduleId];
let exports = {};
const require = function (filename) {
const dirname = path.dirname(moduleId);
const absolutePath = path.join(dirname, filename);
return exec(absolutePath);
};
fn(require, exports);
return exports;
};
exec("./index.js");
})()`;
fs.writeFileSync("./dist/bundle3.js", result);
}
createBundle(modules);
Мы можем обнаружить, что окончательный процесс реализации на самом деле таков:
- Упаковщик для преобразования файлов в модули
- Получить зависимости каждого модуля
- Объедините все модули в одну большую коллекцию модулей.
- Выполнить функцию модуля
- записать в файл
Благодаря приведенному выше анализу гораздо проще и интереснее реализовать простой сборщик модулей webpack шаг за шагом с нуля. Конечно, наша текущая функция упаковщика определенно не идеальна, например, в настоящее время мы не поддерживаем встроенный импорт, преобразование синтаксиса ES6 и импорт CSS. Но эти функции можно реализовать поэтапно. Что действительно важно, так это то, что мы уже не до конца понимаем принцип работы сборщика наподобие вебпака (ведь мы реализовали ту же функцию, что и он), и если мы хотим копнуть глубже, то просто добавляем в него функции. . Закончили цветение.