Модульность интерфейса — это первый и самый важный шаг в проектировании интерфейса; используете ли вы React, Vue или Nodejs, модульность неотделима. Существует множество модульных спецификаций, и сейчас чаще всего используются спецификации CommonJS и ES6, поэтому давайте более подробно рассмотрим эти две спецификации и различия между ними.
Я участвую в рейтинге популярных авторов Nuggets 2020, с нетерпением жду васдрагоценный голос
Эта статья была впервые опубликована в публичном аккаунте【前端壹读】
, для более интересного контента, пожалуйста, обратите внимание на последние новости официального аккаунта.
CommonJS
Спецификация CommonJS — это способ загрузки модулей синхронно, то есть только при загрузке модуля могут выполняться следующие операции. Поскольку Nodejs в основном используется для программирования на стороне сервера, а файлы модулей обычно уже существуют на локальном жестком диске, загрузка происходит относительно быстро, поэтому спецификация CommonJS для синхронной загрузки модулей более применима.
Обзор
Спецификация CommonJS предусматривает, что каждый файл JS является модулем и имеет свою собственную область видимости; переменные и функции, определенные в модуле, являются закрытыми переменными и невидимы для других файлов.
// number.js
let num = 1
function add(x) {
return num + x
}
В приведенном выше number.js переменная num и функция add являются частными для текущего файла и недоступны для других файлов. В то же время CommonJS предусматривает, что каждый модуль имеетmodule
переменная, представляющая текущий модуль; эта переменная является объектом,exports
свойства (т.е.module.exports
) предоставляет интерфейс для внешнего экспорта модулей.
// number.js
let num = 1
function add(x) {
return num + x
}
module.exports.num = num
module.exports.add = add
Таким образом, определяемые нами приватные переменные могут предоставлять внешний доступ; загрузка определенного модуля означает загрузку модуля.module.exports
Атрибуты.
module
Как указано выше,module
Переменная представляет текущий модуль, давайте напечатаем ее, чтобы увидеть, какая информация в ней содержится:
//temp.js
require('./a.js')
console.log(module)
Module {
id: '.',
path: 'D:\\demo',
exports: {},
parent: null,
filename: 'D:\\demo\\temp.js',
loaded: false,
children: [{
Module {
id: 'D:\\demo\\a.js',
path: 'D:\\demo',
exports: {},
parent: [Circular],
filename: 'D:\\demo\\a.js',
loaded: true,
children: [],
paths: [Array]
}
}],
paths: [
'D:\\demo\\node_modules',
'D:\\projects\\mynodejs\\node_modules',
'D:\\projects\\node_modules',
'D:\\node_modules'
]
}
Мы обнаружили, что он обладает следующими свойствами:
- id: идентификатор модуля, обычно имя файла модуля с абсолютным путем
- имя файла: имя файла модуля с абсолютным путем.
- Загружен: возвращает логическое значение, указывающее, был ли загружен модуль.
- parent: возвращает объект, представляющий модуль, вызвавший этот модуль.
- Children: возвращает массив, представляющий другие модули, используемые этим модулем.
- exports: объект, который модуль экспортирует во внешний мир.
- путь: имя каталога модуля.
- paths: пути поиска модуля.
Если мы вызовем модуль через командную строку, напримерnode temp.js
, то этот модуль является модулем верхнего уровня, егоmodule.parent
имеет значение null; если он вызывается в других модулях, таких какrequire('temp.js')
, то егоmodule.parent
это модуль, который его вызывает.
Но в последней версии Nodejs 14.6module.parent
Устарело, официально рекомендованоrequire.main
илиmodule.children
Вместо этого давайте посмотрим на причины устаревания:
module.parent
Значение — это значение этого модуля, на которое ссылается требуемый. Если это запись текущего запущенного процесса, значение равно null. Если этот модуль импортируется в нестандартном формате JS, таком как REPL, или импортируется путем импорта, значение не определено.
exports
Для удобства экспорта модулей мы также можем передатьexports
переменная, которая указывает наmodule.exports
, так что это эквивалентнорецессивныйдобавил эту строку кода:
var exports = module.exports;
При внешнем экспорте модулей вы можете добавить атрибуты к объекту экспорта.
// number.js
let num = 1
function add(x) {
return num + x
}
exports.num = num
exports.add = add
нужноУведомлениеОднако прямое преобразование невозможно.exports
переменная указывает на значение, потому что это отсекаетexports
а такжеmodule.exports
отношение между
// a.js
exports = 'a'
// main.js
var a = require('./a')
console.log(a)
// {}
Хотя мы проходимexports
Строка экспортируется, но из-за обрезанияexports = module.exports
связь междуmodule.exports
На самом деле он по-прежнему указывает на пустой объект, и конечный результат экспорта также является пустым объектом.
require
Основная функция require — читать и выполнять JS-файлы и возвращать экспортированные модули.module.exports
Объект:
const number = require("./number.js")
console.log(number.num)
number.add()
Если модуль экспортирует функцию, ее нельзя определить вexports
На объекте:
// number.js
module.exports = function () {
console.log("number")
}
// main.js
require("./number.js")()
В дополнение к возможности загружать модуль как вызов функции, require сам по себе как объект имеет следующие свойства:
- разрешить: путь к модулю для разрешения.
- основной:
Module
Объект, представляющий сценарий входа, который загружается при запуске процесса. - extensions: Как работать с расширениями файлов.
- cache: импортированный модуль будет кэшироваться в этом объекте.
кеш модуля
Когда у нас есть несколько раз в проектеrequire
Для одного и того же модуля CommonJS не выполняет файл модуля несколько раз; вместо этого он кеширует модуль при первой загрузке; когда модуль загружается позже, он напрямую считывает модуль из кеша:
//number.js
console.log('run number.js')
module.exports = {
num: 1
}
//main.js
let number1 = require("./number");
let number2 = require("./number");
number2.num = 2
let number3 = require("./number");
console.log(number3)
// run number.js
// { num: 2 }
Нам требуется много раз загрузить числовой модуль, но внутри всего одна распечатка, значение внутренней переменной также изменяется при второй загрузке, а значение внутренней переменной остается последним присваиванием при третьей загрузке, что доказывает, что следующееrequire
читать из кеша.
наверхуrequire
, мы ввели все атрибуты под ним и обнаружили, что есть атрибут cache, который используется для кэширования модулей, сначала напечатаем его:
{
'D:\\demo\\main.js': Module {},
'D:\\demo\\number.js': Module {}
}
Кэш кэширует модули в виде путей, которые мы можем передатьdelete require.cache[modulePath]
Удалим кешированный модуль, перепишем приведенный выше код:
//number.js
console.log('run number.js')
module.exports = {
num: 1
}
//main.js
let number1 = require("./number");
let number2 = require("./number");
number2.num = 2
//删除缓存
delete require.cache['D:\\demo\\number.js']
let number3 = require("./number");
console.log(number3)
// run number.js
// run number.js
// { num: 1 }
Очевидно, что числовой модуль запускается дважды, и при второй загрузке модуля мы очищаем кеш модуля, поэтому числовое значение, прочитанное в третий раз, также является последним; мы также можем передатьObject.keys
Цикл для удаления кеша для всех модулей:
Object.keys(require.cache).forEach(function(key) {
delete require.cache[key];
})
загрузочный механизм
Механизм загрузки CommonJS заключается в том, что вывод модуля — это копия значения, для вывода базовых типов данных — это копия, а для сложных типов данных — поверхностная копия. Рассмотрим пример:
// number.js
let num = 1
function add() {
num++
}
module.exports.num = num
module.exports.add = add
// main.js
var number = require('./number')
//1
console.log(number.num)
number.add()
//1
console.log(number.num)
number.num = 3
//3
console.log(number.num)
Поскольку CommonJSкопия значения, как только модуль выводит значение, изменения внутри модуля не влияют на это значение, поэтому значение в main.jsnumber
сама переменная иnumber.js
Отношения указания нет, хотя мы вызываем модуль внутриadd
функцию для изменения значения, но это не влияет на значение; вместо этого мы можем произвольно отредактировать значение после вывода.
дляrequire
Эта функция, мы также можем понять, что она переводит модуль в самовыполняющуюся функцию для выполнения:
var number = (function(){
let num = 1
function add() {
num++
}
return {
num,
add,
}
})()
//1
console.log(number.num)
number.add()
//1
console.log(number.num)
Для сложных типов данных, поскольку CommonJS выполняет поверхностное копирование, если два скрипта одновременно ссылаются на один и тот же модуль, модификация модуля повлияет на другой модуль:
// obj.js
var obj = {
color: {
list: ['red', 'yellow','blue']
}
}
module.exports = obj
//a.js
var obj = require('./obj')
obj.color.list.push('green')
//{ color: { list: [ 'red', 'yellow', 'blue', 'green' ] } }
console.log(obj)
//b.js
var obj = require('./obj')
//{ color: { list: [ 'red', 'yellow', 'blue', 'green' ] } }
console.log(obj)
//main.js
require('./a')
require('./b')
В приведенном выше коде мы ссылаемся на модуль для модификации и чтения через скрипты a.js и b.js одновременно; следует отметить, что из-за кеша b.js фактически является модулем, читаемым из кеша, когда он загружен.
мы сказали вышеrequire
При загрузке код в модуле выполняется, и модульmodule.exports
Свойства возвращаются как возвращаемые значения, мы обнаружили, что этот процесс загрузки происходит на этапе выполнения кода, и нет возможности определить зависимости модуля до его выполнения.Этот метод загрузки называется运行时加载
; Поскольку среда выполнения CommonJS загружает модули, мы можем даже динамически выбирать модуль для загрузки, судя по оператору:
let num = 10;
if (num > 2) {
var a = require("./a");
} else {
var b = require("./b");
}
var moduleName = 'number.js'
var number = require(`./${moduleName}`)
Но именно из-за этой динамической загрузки невозможно выполнить статическую оптимизацию во время компиляции.
циклическая загрузка
Благодаря наличию механизма кэширования модули CommonJS можно загружать циклически, не беспокоясь о том, что возникнет бесконечный цикл:
//a.js
exports.a = 1;
var b = require("./b");
console.log(b, "a.js");
exports.a = 2;
//b.js
exports.b = 11;
var a = require("./a");
console.log(a, "b.js");
exports.b = 22;
//main.js
const a = require("./a");
const b = require("./b");
console.log(a, "main a");
console.log(b, "main b");
В приведенном выше коде логика кажется очень сложной, a.js загружает b.js, а b.js загружает a.js, но если мы проанализируем их по отдельности, мы обнаружим, что на самом деле все очень просто.
- Загрузите main.js и обнаружите, что модуль загружен; прочитайте и сохраните в кеше
- Выполнить модуль a, экспортировать {a:1}; обнаружить, что модуль b загружен, прочитать и сохранить его в кеше
- Выполните модуль b, экспортируйте {b:11}; снова загрузите модуль a, прочитайте кеш, в это время модуль a экспортирует только {a:1}
- б готовый модуль, производный {б: 22}
- Вернитесь к модулю, выполните его, экспортируйте {A: 2}
- Вернитесь к main.js, снова загрузите модуль b, прочитайте кеш
Итак, окончательный результат печати:
{ a: 1 } b.js
{ b: 22 } a.js
{ a: 2 } main a
{ b: 22 } main b
Особенно обратите внимание на консоль в первом модуле b, так как модуль a в это время был загружен в кэш, но не был выполнен, а модуль a экспортирует только первый.{a:1}
.
Мы обнаружили, что циклическая загрузка относится к выполнению во время загрузки; после циклической загрузки модуля будет выводиться только та его часть, которая была выполнена, а та часть, которая не была выполнена, не будет выводиться.
ES6
Отличается от динамической нагрузки спецификации Commonjs, идею модульной конструкции ES6 - это как можно более статически, так что зависимости между модулями могут быть определены при компиляционном времени. мы вПолный анализ конфигурации Webpack (оптимизация)Только что говорили, используя схему статической загрузки модуля ES6, вы можете добитьсяTree Shaking
для оптимизации кода.
export
Как и CommonJS, спецификация ES6 также определяет файл JS как независимый модуль.Переменные внутри модуля являются частными и не могут быть доступны другим модулям, однако ES6 пропускаетexport
ключевое слово для экспорта переменной, функции или класса:
export let num = 1
export function add(x) {
return num + x
}
export class Person {}
Или мы можем экспортировать объект напрямую, эти два способа эквивалентны:
let num = 1
function add(x) {
return num + x
}
class Person {}
export { num, add, Person }
При экспорте объектов мы также можем использоватьas
Ключевые слова переименовывают экспортируемые переменные:
let num = 1
function add(x) {
return num + x
}
export {
num as number,
num as counter,
add as addCount,
add as addFunction
}
пройти черезas
С одним и тем же именем мы несколько раз экспортировали переменную. должны знать о том,export
Оговаривается, что экспортируемый интерфейс является внешним интерфейсом, который должен устанавливать однозначное соответствие с переменными внутри модуля. Следующие два варианта написания неверны:
// 报错,是个值,没有提供接口
export 1;
// 报错,需要放在大括号中
var m = 1;
export m;
import
использоватьexport
После экспорта внешнего интерфейса модуля другие файлы модуля могут передаватьсяimport
Команда загружает этот интерфейс:
import {
number,
counter,
addCount,
addFunction
} from "./number.js"
Приведенный выше код загружает переменную из модуля Number.js, команда импорта принимает пару фигурных скобок, указывает имя импортируемой переменной из модуля, имя импортируемой переменной должно совпадать с именем переменной внешнего интерфейса импорта модуль.
Как и команда экспорта, мы можем использоватьas
ключевое слово для переименования имен импортируемых переменных:
import {
number as num,
} from "./number.js"
console.log(num)
Помимо указания интерфейса переменной в модуле загрузки, мы можем также использовать общую загрузку, указав объект через (*), все выходные значения загружаются на этот объект:
import * as number from "./number.js"
Команда импорта имеет эффект подъема и будет поднята до головы всего модуля.Сначала выполните:
console.log(num)
import {
number as num,
} from "./number.js"
Приведенный выше код не сообщит об ошибке, потому что импорт будет выполнен первым; в отличие от требования спецификации CommonJS, импорт выполняется статически, поэтому импорт не может быть расположен в области блочного уровня, а также нельзя использовать выражения и переменные, это только во время выполнения.Структура синтаксиса для получения результата:
//报错
let moduleName = './num'
import { num, add } from moduleName;
//报错
//SyntaxError: 'import' and 'export' may only appear at the top level
let num = 10;
if (num > 2) {
import a from "./a";
} else {
import b from "./b";
}
export default
Когда IMPORT импортирует внешний интерфейс экспорта в код, вам нужно знать внешний интерфейс, чтобы получить соответствующее значение, что доставляет больше проблем, иногда нам нужно экспортировать; для этой спецификации ES6export default
для экспорта по умолчанию:
//add.js
export default function (x, y) {
return x + y;
};
//main.js
import add from './add'
console.log(add(2, 4))
из-заexport default
является экспортом по умолчанию, поэтому эту команду можно использовать только один раз в модуле, в то время какexport
Интерфейс экспорта можно экспортировать несколько раз:
//报错
//SyntaxError: Only one default export allowed per module.
//add.js
export default function (x, y) {
return x + y;
};
export default function (x, y) {
return x + y + 1;
};
export default
По сути, это синтаксический сахар, который, по сути, присваивает следующее значениеdefault
переменная, так что вы можете записать значение вexport default
после; но именно потому, что он выводитdefault
переменная, поэтому за ней не может следовать оператор объявления переменной:
//正确
export default 10
//正确
let num = 10
export default num
//报错
export default let num = 10
теперь, когдаexport default
По сути, это синтаксический сахар, который экспортирует переменную по умолчанию, поэтому мы также можем передатьexport
переписать:
//num.js
let num = 10;
export { num as default };
Приведенные выше два кода эквивалентны, и когда мы импортируем, мы также ставимdefault
Переменная переименовывается во что угодно, поэтому следующие два кода импорта также эквивалентны:
import num from './num'
//等效
import { default as num } from './num'
В модулеexport
может иметь более одного,export default
Может быть только один, но оба они могут существовать одновременно:
//num.js
export let num1 = 1
export let num2 = 2
let defaultNum = 3
export default defaultNum
//main.js
import defaultNum, {
num1,
num2
} from './num'
загрузочный механизм
В CommonJS мы сказали, что вывод модуля — это копия значения; в то время как вывод ES6 — это внешний интерфейс, мы перепишем код в CommonJS выше, чтобы понять разницу между ними:
//number.js
let num = 1
function add() {
num++
}
export { num, add }
//main.js
import { num, add } from './number.js'
//1
console.log(num)
add()
//2
console.log(num)
Мы обнаружили, что результат работы в CommonJS совершенно другой, вызов функции в модуле влияет на значение переменной в модуле, именно потому, что модуль ES6 выводит только внешний интерфейс, мы можем понимать этот интерфейс как引用
, фактическое значение все еще находится в модуле; и это引用
еще один只读引用
, будь то примитивный или сложный:
//obj.js
let num = 1
let list = [1,2]
export { num, list }
//main.js
import { num, list } from './obj.js'
//Error: "num" is read-only.
num = 3
//Error: "list" is read-only.
list = [3, 4]
Импорт также кэширует импортированные модули.Повторный импорт для импорта одного и того же модуля будет выполнен только один раз, и демонстрация кода здесь выполняться не будет.
циклическая ссылка
Также существуют циклические ссылки между модулями ES6.Давайте посмотрим на код в CommonJS:
//a.js
export let a1 = 1;
import { b1, b2 } from "./b";
console.log(b1, b2, "a.js");
export let a2 = 11;
//b.js
export let b1 = 2;
import { a1, a2 } from "./a";
console.log(a1, a2, "b.js");
export let b2 = 22;
//main.js
import { a1, a2 } from "./a";
import { b1, b2 } from "./b";
Сначала мы, должно быть, приняли как должное, чтоb.js
Печать равна 1 и не определена, потому чтоa.js
Загружается только первый экспорт, но после печати результатаb.js
Оба они не определены, потому что импорт имеет эффект подъема.
сводка различий
Сравнивая спецификацию CommonJS и спецификацию ES6 выше, мы резюмируем разницу между ними:
- Модули CommonJS загружаются во время выполнения, модули ES6 являются выходными интерфейсами во время компиляции.
- Модули CommonJS выводят копию значения, модули ES6 выводят ссылку на значение
- CommonJS загружает модуль целиком, то есть загружает все методы, ES6 может загружать один из методов отдельно
- CommonJS
this
указать на текущий модульmodule.exports
, в ES6this
указывает на неопределенное - CommonJS по умолчанию использует нестрогий режим, модули ES6 автоматически используют строгий режим.
Для получения дополнительной информации о внешнем интерфейсе, пожалуйста, обратите внимание на общедоступный номер【前端壹读】
.
Если вы думаете, что это хорошо написано, пожалуйста, следуйте за мнойДомашняя страница Наггетс. Для получения дополнительных статей, пожалуйста, посетитеБлог Се Сяофэй