предисловие
Что такое CommonJS?
Спецификация модуля commonjs, используемая приложениями node.js.
Каждый файл представляет собой модуль со своей независимой областью видимости, переменными, методами и т. д., для всех остальных модулейНевидимый. Спецификация CommonJS предусматривает: внутри каждого модуля переменная модуля представляет текущий модуль. Эта переменная является объектом, чьим свойством exports (т.е. module.exports) является внешний интерфейс. Загрузка модуля фактически загружает свойство module.exports модуля. Метод require используется для загрузки модулей.
Особенности модулей CommonJS:
Весь код выполняется в области модуля и не загрязняет глобальную область.
Модуль можно загружать несколько раз, но он будет запущен только один раз при первой загрузке, а затем текущий результат будет кэширован, а при последующей загрузке кэшированный результат будет прочитан напрямую. Чтобы модуль снова заработал, необходимо очистить кеш.
Порядок загрузки модулей, порядок их появления в коде.
как использовать?
Предположим, у нас теперь есть файл a.js, мы хотим использовать некоторые методы и переменные a.js в main.js, а рабочая среда — nodejs. Таким образом, мы можем использовать спецификацию CommonJS и иметь методы/переменные экспорта файла. Затем используйте функцию require для введения переменных/функций.
Пример:
// a.js
module.exports = '这是a.js的变量'; // 导出一个变量/方法/对象都可以
// main.js
let str = require('./a'); // 这里如果导入a.js,那么他会自动按照预定顺序帮你添加后缀
console.log(str); // 输出:'这是a.js的变量'
Напишите требуемую функцию вручную
предисловие
Давайте начнем писать упрощенную версию функции require, эта функция require поддерживает следующие функции:
- Импортируйте файл JS, соответствующий спецификации CommonJS.
- Поддержка автоматического добавления суффикса файла (временно поддерживаются файлы JS и JSON)
Начать сейчас!
1. Определите метод req
Сначала мы определяем метод req, который изолирован от глобальной функции require.
Этот метод req принимает параметр с именем ID, который представляет собой путь к загружаемому файлу.
// main.js
function req(id){}
let a = req('./a')
console.log(a)
2. Создайте новый класс модуля
Создайте новый класс модуля, этот модуль будет обрабатывать весь процесс загрузки файла.
function Module(id) {
this.id = id; // 当前模块的文件路径
this.exports = {} // 当前模块导出的结果,默认为空
}
3. Получите абсолютный путь к файлу
Как мы только что представили, функция require поддерживает передачу пути.Этот путь может быть относительным, абсолютным или без файлового суффикса.
мы в МВ классе модуля добавьте файл с именем "_resolveFilename», который используется для анализа пути к файлу, переданного пользователем, для получения абсолютного пути.
// 将一个相对路径 转化成绝对路径
Module._resolveFilename = function (id) {}
Идите вперед и добавьте свойство «расширения», которое является объектом. ключ — расширение файла, значение — метод обработки различных файлов, соответствующих расширению.
Через исходный код отладчика nodejs мы видим, что нативная функция require поддерживает четыре типа файлов:
- js-файл
- JSON-файл
- файл узла
- файл mjs
Из-за недостатка места здесь поддерживаются только два расширения: .js и .json.
Мы добавляем два свойства к объекту расширений соответственно, и каждое значение двух свойств является функцией. Удобно классифицировать и обрабатывать разные типы файлов.
// main.js
Module.extensions['.js'] = function (module) {}
Module.extensions['.json'] = function (module) {}
Затем мы импортируем собственный модуль «путь» и модуль «fs» из nodejs, чтобы мы могли получить абсолютный путь к файлу и файловым операциям.
Давайте обработаем метод Module._resolveFilename, чтобы он работал правильно.
Module._resolveFilename = function (id) {
// 将相对路径转化成绝对路径
let absPath = path.resolve(id);
// 先判断文件是否存在如果存在了就不要增加了
if(fs.existsSync(absPath)){
return absPath;
}
// 去尝试添加文件后缀 .js .json
let extenisons = Object.keys(Module.extensions);
for (let i = 0; i < extenisons.length; i++) {
let ext = extenisons[i];
// 判断路径是否存在
let currentPath = absPath + ext; // 获取拼接后的路径
let exits = fs.existsSync(currentPath); // 判断是否存在
if(exits){
return currentPath
}
}
throw new Error('文件不存在')
}
Здесь мы поддерживаем принятие параметра с именем id, который будет путем, пройденным пользователем.
Сначала мы используемpath.resolve()Получите абсолютный путь к файлу. Затем используйтеfs.existsSyncПроверьте, существует ли файл. Если он не существует, мы пытаемся добавить суффикс файла.
Мы пересекаем объекты расширения файлов, которые теперь поддерживаются и пытаются сплавать путь. Если файл представлен, файл возвращается в путь к файлу. Там нет бросков исключения.
Таким образом, мы можем получить полный путь к файлу в методе req:
function req(id){
// 通过相对路径获取绝对路径
let filename = Module._resolveFilename(id);
}
4. Загрузка модулей - реализация JS
Вот наш основной момент — загрузка модуля common.js.
Первый новый экземпляр модуля. Передайте путь к файлу и верните новый экземпляр модуля.
Затем определите функцию tryModuleLoad, передав наш вновь созданный экземпляр модуля.
function tryModuleLoad(module) { // 尝试加载模块
let ext = path.extname(module.id);
Module.extensions[ext](module)
}
function req(id){
// 通过相对路径获取绝对路径
let filename = Module._resolveFilename(id);
let module = new Module(filename); // new 一个新模块
tryModuleLoad(module);
}
**функция tryModuleLoad **После получения модуля он будет использоватьpath.extname Функция получает расширение файла, а затем передает его различным функциям для обработки в соответствии с разными расширениями.
Далее мы обрабатываем загрузку файла js.
Первый шаг — передать экземпляр объекта модуля.
Используйте атрибут id в объекте модуля, чтобы получить абсолютный путь к файлу. Получив абсолютный путь к файлу, используйте модуль fs для чтения содержимого файла.Кодировка чтения utf8.
Module.extensions['.js'] = function (module) {
// 1) 读取
let script = fs.readFileSync(module.id, 'utf8');
}
Второй шаг — подделать самовыполняющуюся функцию.
Здесь сначала создайте новый массив-оболочку. Элемент 0 массива — это начало самовыполняющейся функции, а последний элемент — конец.
let wrapper = [
'(function (exports, require, module, __dirname, __filename) {\r\n',
'\r\n})'
];
Эта самовыполняющаяся функция должна передавать 5 параметров: объект экспорта, требуемую функцию, объект модуля, путь к каталогу и имя файла с именем файла.
Мы получим содержимое загружаемого файла, склеим его с шаблоном самоисполняющейся функции и соберем в полноценный исполняемый js-текст:
Module.extensions['.js'] = function (module) {
// 1) 读取
let script = fs.readFileSync(module.id, 'utf8');
// 2) 内容拼接
let content = wrapper[0] + script + wrapper[1];
}
Шаг 3. Создайте среду выполнения «песочницы»
Здесь мы собираемся использовать модуль «vm» в nodejs. Этот модуль может создать виртуальную машину nodejs и предоставить независимую рабочую среду песочницы.
Подробнее см.:Официальное введение модуля vm
Мы используем модуль vmrunInThisContextфункция, он может создать песочницу с глобальными глобальными свойствами. Использование заключается в передаче текстового содержимого js. Мы передаем текстовое содержимое, которое мы только что объединили, и возвращаем функцию fn:
const vm = require('vm');
Module.extensions['.js'] = function (module) {
// 1) 读取
let script = fs.readFileSync(module.id, 'utf8');
// 2) 内容拼接
let content = wrapper[0] + script + wrapper[1];
// 3)创建沙盒环境,返回js函数
let fn = vm.runInThisContext(content);
}
Шаг 4: Запустите среду песочницы, чтобы получить объект экспорта.
Поскольку нам нужен указанный выше путь к каталогу, давайте сначала получим путь к каталогу. Здесь используется модуль путиdirname метод.
Затем мы используем метод вызова, передаем параметры и немедленно выполняем его.
Первый параметр метода вызова — это объект this внутри функции, а остальные параметры — это параметры, требуемые функцией.
Module.extensions['.js'] = function (module) {
// 1) 读取
let script = fs.readFileSync(module.id, 'utf8');
// 2) 增加函数 还是一个字符串
let content = wrapper[0] + script + wrapper[1];
// 3) 让这个字符串函数执行 (node里api)
let fn = vm.runInThisContext(content); // 这里就会返回一个js函数
let __dirname = path.dirname(module.id);
// 让函数执行
fn.call(module.exports, module.exports, req, module, __dirname, module.id)
}
Таким образом, мы передаем объектный модуль, то внутреннее значение будет экспортировано для зависания к свойствам модуля экспорта.
Шаг 5: Верните экспортированное значение
Поскольку наша функция обработки является нечистой функцией, можно напрямую вернуть объект экспорта экземпляра модуля.
function req(id){ // 没有异步的api方法
// 通过相对路径获取绝对路径
let filename = Module._resolveFilename(id);
tryModuleLoad(module); // module.exports = {}
return module.exports;
}
Таким образом, мы реализовали простую функцию require.
let str = req('./a');
// str = req('./a');
console.log(str);
// a.js
module.exports = "这是a.js文件"
5. Загрузка модулей - реализация файлов JSON
Реализация файла json относительно проста. Используйте fs, чтобы прочитать содержимое файла json, а затем используйте JSON.parse, чтобы преобразовать его в объект js.
Module.extensions['.json'] = function (module) {
let script = fs.readFileSync(module.id, 'utf8');
module.exports = JSON.parse(script)
}
6. Оптимизация
В начале статьи мы писали: commonjs будет кэшировать модули, которые мы хотим загрузить. Когда мы читаем его снова, мы идем к кешу, чтобы прочитать наш модуль, вместо того, чтобы снова вызывать модули fs и vm, чтобы получить экспортированный контент.
Мы создаем новое свойство _cache для объекта Module. Это свойство является объектом, ключ — именем файла, а значение — кэшем содержимого, экспортируемым файлом.
Когда мы загружаем модуль, сначала переходим к атрибуту _cache, чтобы узнать, был ли он закэширован. Если есть, верните кэшированный контент напрямую. Если нет, попробуйте получить содержимое экспорта и сохранить объект кеша.
Module._cache = {}
function req(id){
// 通过相对路径获取绝对路径
let filename = Module._resolveFilename(id);
let cache = Module._cache[filename];
if(cache){ // 如果有缓存,直接将模块的结果返回
return cache.exports
}
let module = new Module(filename); // 创建了一个模块实例
Module._cache[filename] = module // 输入进缓存对象内
// 加载相关模块 (就是给这个模块的exports赋值)
tryModuleLoad(module); // module.exports = {}
return module.exports;
}
полная реализация
const path = require('path');
const fs = require('fs');
const vm = require('vm');
function Module(id) {
this.id = id; // 当前模块的id名
this.exports = {}; // 默认是空对象 导出的结果
}
Module.extensions = {};
// 如果文件是js 的话 后期用这个函数来处理
Module.extensions['.js'] = function (module) {
// 1) 读取
let script = fs.readFileSync(module.id, 'utf8');
// 2) 增加函数 还是一个字符串
let content = wrapper[0] + script + wrapper[1];
// 3) 让这个字符串函数执行 (node里api)
let fn = vm.runInThisContext(content); // 这里就会返回一个js函数
let __dirname = path.dirname(module.id);
// 让函数执行
fn.call(module.exports, module.exports, req, module, __dirname, module.id)
}
// 如果文件是json
Module.extensions['.json'] = function (module) {
let script = fs.readFileSync(module.id, 'utf8');
module.exports = JSON.parse(script)
}
// 将一个相对路径 转化成绝对路径
Module._resolveFilename = function (id) {
// 将相对路径转化成绝对路径
let absPath = path.resolve(id);
// 先判断文件是否存在如果存在
if(fs.existsSync(absPath)){
return absPath;
}
// 去尝试添加文件后缀 .js .json
let extenisons = Object.keys(Module.extensions);
for (let i = 0; i < extenisons.length; i++) {
let ext = extenisons[i];
// 判断路径是否存在
let currentPath = absPath + ext; // 获取拼接后的路径
let exits = fs.existsSync(currentPath); // 判断是否存在
if(exits){
return currentPath
}
}
throw new Error('文件不存在')
}
let wrapper = [
'(function (exports, require, module, __dirname, __filename) {\r\n',
'\r\n})'
];
// 模块独立 相互没关系
function tryModuleLoad(module) { // 尝试加载模块
let ext = path.extname(module.id);
Module.extensions[ext](module)
}
Module._cache = {}
function req(id){ // 没有异步的api方法
// 通过相对路径获取绝对路径
let filename = Module._resolveFilename(id);
let cache = Module._cache[filename];
if(cache){ // 如果有缓存直接将模块的结果返回
return cache.exports
}
let module = new Module(filename); // 创建了一个模块
Module._cache[filename] = module;
// 加载相关模块 (就是给这个模块的exports赋值)
tryModuleLoad(module); // module.exports = {}
return module.exports;
}
let str = req('./a');
console.log(str);
конец резюме
Таким образом, мы вручную реализовали упрощенную версию функции require CommonJS.
Давайте рассмотрим процесс реализации require:
- Получите абсолютный путь к загружаемому файлу. попробуйте добавить суффикс без суффикса
- Попытка прочитать экспорт из кеша. Если кэшировано, вернуть кэшированное содержимое. Нет, следующий шаг
- Создайте новый экземпляр модуля и введите его в объект кеша.
- попробуй загрузить модуль
- Классифицировано по типу файла
- Если это файл js, прочитайте содержимое файла, соедините самовыполняющийся функциональный текст, используйте модуль vm для создания экземпляра песочницы для загрузки функционального текста, получения экспортированного содержимого и возврата содержимого.
- Если это файл json, прочитайте содержимое файла, используйте функцию JSON.parse, чтобы преобразовать его в объект js и вернуть содержимое.
- Получите возвращаемое значение экспорта.