Модульное прошлое и настоящее «Front-end Engineering Four Steps» (Часть 1)

Архитектура внешний интерфейс JavaScript
Модульное прошлое и настоящее «Front-end Engineering Four Steps» (Часть 1)

Отказ от ответственности: эта статья является первой подписанной статьей Nuggets, и ее перепечатка без разрешения запрещена.

написать впереди

В контексте все более сложного и разнообразного веб-бизнеса часто упоминается концепция фронтенд-инжиниринга. «Расскажите, как вы понимаете веб-инженерию?» Я полагаю, что многие новички часто будут сталкиваться с ними во время собеседований, и большинство людей сразу подумает о Webpack, думая, что инженерия — это то, чем занимается Webpack. Конечно, я не могу сказать, что нет. , если быть точным, Webpack — это просто инструмент, созданный в контексте инженерии.

Целью проектирования является высокая производительность, стабильность, доступность, ремонтопригодность и эффективное сотрудничество.Пока операции выполняются с этими перспективами в качестве цели, это можно назвать частью проектирования. На самом деле инженерия — это идея в программной инженерии. Нынешний интерфейсный инжиниринг можно разделить на четыре аспекта:模块化、组件化、规范化、自动化.

Название четырех частей инженерии, в этой статье мы берем模块化Чтобы сначала проанализировать концепцию фронтенд-инжиниринга, кодировать слова непросто, поэтому, пожалуйста, ставьте лайк и читайте позже, и выработайте привычку!

что такое модульный

Модульность на самом деле относится к решению сложной проблемы, когда自顶向下逐层把系统划分成若干模块процесс, каждый модуль выполняет определенную подфункцию (единая ответственность), все модули собираются определенным образом, чтобы стать целым, чтобы выполнять функции, необходимые для всей системы, не имеет значения, если вы не поймите это, затем посмотрите вниз.

Зачем нужна модульность

В первые дни, когда веб-страница только появилась, страницы и стили были очень простыми, было очень мало элементов взаимодействия и дизайна, страница не зависела от большого количества файлов, и было очень мало логического кода, как и у статических страниц. В то время фронтенд назывался веб-дизайном.

вместе сWebРазвитие технологий, различные взаимодействия и новые технологии делают веб-страницы все более и более богатыми.Постепенно наши фронтенд-инженеры вышли на сцену, и в то же время объем кода наших фронтенд-студентов быстро увеличился, сложность постепенно увеличивалась, и все больше и больше Много бизнес-логики и взаимодействия реализуется на веб-уровне.Когда кода слишком много, возникает ряд проблем, таких как различные конфликты имен, избыточность кода и усиление зависимостей между файлами. выйти, и даже сделать его трудно поддерживать на более позднем этапе.

По этим вопросам уже есть большой практический опыт в других back-end языках, таких как java и php, то есть модуляризация, потому что небольшой и хорошо организованный код намного легче понять и поддерживать, чем огромный код, поэтому интерфейс также открыт.Модульный процесс.

Модульный JS

Говоря о модульности JS, определенно меньше Commonjs, AMD, CMD, UMD, ESM, я думаю, что многие из новых одноклассников, даже некоторые счастливые люди, часто касаются их, если вы знаете эти понятия, не знаете, что они делают. что связь между ними не понимает основная реализация, то смотрите эту статью.

Доминирующий JS среди ранней фронтенд-тройки мушкетеров не является модульным языком программирования, и в спецификации отсутствует понятие модулей (т.е. модулей), поэтому реализация модулей весьма хлопотна. особенности языка JS для имитации реализации модульности.

Ранние модульные решения JS

Обычная функция

Сначала рассмотрим реализацию функции, потому что функция в JS имеет независимую область видимости, и любой код может быть помещен в функцию, ее нужно вызывать только там, где она должна использоваться, например, следующий код:

function fn1(){
  //...
}
function fn2(){
  //...
}
function fn3() {
  fn1()
  fn2()
}

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

Пространства имен

Как и в случае с обычными функциями, описанными выше, многие переменные и функции объявляются непосредственно в глобальной области видимости, и легко вызвать конфликт имен, поэтому предлагается режим пространства имен (namespace).

Поскольку объект может иметь атрибуты, а его атрибуты могут быть как данными, так и методами, он просто хорошо удовлетворяет потребности, а доступ к атрибутам объекта осуществляется через имя объекта, что эквивалентно настройке пространства имен.

Давайте посмотрим, как написать модуль как объект и поместить все члены модуля в этот объект:

var myModule = {
  name: "isboyjc",
  getName: function (){
    console.log(this.name)
  }
}

// 使用
myModule.getName()

Очевидно, что это осуществимо, но вскоре мы обнаружили его недостатки: будут раскрыты все внутренние свойства объекта, а внутреннее состояние можно будет изменить внешне, следующим образом:

myModule.name = "哈哈哈"
myModule.getName() // 哈哈哈

Функция немедленного выполнения (IIFE)

Хотя режим пространства имен в определенной степени решает проблему загрязнения переменных глобального пространства имен, он не может решить проблему изоляции кода и данных.Примерно в 2003 году аббревиатура функции была выполнена сразу.IIFEПоявился, он фактически использует характеристики закрытия функций для реализации частных данных и общих методов, а именно:

var myModule = (function() {
  var name = 'isboyjc'
  
  function getName() {
    console.log(name)
  }
  
  return { getName } 
})()

Так что мы можем пройтиmyModule.getName()получитьname, и осознатьnameПриватизация собственности, то есть внешние вызовы не могут быть сделаны:

myModule.getName() // isboyjc
myModule.name // undefined

Что, если наш модуль должен зависеть от других модулей? В это время используется введение зависимостей, то есть параметров функции:

// otherModule.js模块文件
var otherModule = (function(){
  return {
    a: 1,
    b: 2
  }
})()

// myModule.js模块文件 - 依赖 otherModule 模块
var myModule = (function(other) {
  var name = 'isboyjc'
  
  function getName() {
    console.log(name)
    console.log(other.a, other.b)
  }
  
  return { getName } 
})(otherModule)

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

внедрение зависимости

В процессе модульной разработки также есть такие схемы, как зависимости определения шаблона и зависимости определения аннотации, я не думаю, что они имеют сильный обучающий характер, поэтому я не буду повторять их. Ниже мы поговорим о внедрении зависимостей (DI), говоря об этом, мы должны упомянуть один из трех основных фреймворков.Angular, который родился в 2009 году, и одной из его основных функций является внедрение зависимостей.

Предположим, у нас есть два примитивных модуляfnAа такжеfnB:

// 模块fnA
let fnA = function(){
  return {name: '我是fnA'}
}

// 模块fnB
let fnB = function(){
  return {name: '我是fnB'}
}

мы пишем функциюfnC, чтобы использовать два вышеуказанных модуля, мы можем сделать это следующим образом:

let fnC = function(){
  let a = fnA()
  let b = fnB()
  console.log(a, b)
}

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

let fnC = function(fnA, fnB){
  let a = fnA()
  let b = fnB()
  console.log(a, b)
}

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

  • Регистрация зависимостей может быть достигнута
  • Инжектор зависимостей должен иметь возможность получать зависимости (функции и т. д.) и возвращать нам функцию, которая может получить все ресурсы после успешного внедрения.
  • Инжектор зависимостей должен иметь возможность сохранять объем передаточной функции.
  • Передаваемая функция может получать пользовательские аргументы, а не только описанные зависимости

Просто реализуем регистратор зависимостей, создаем новыйinjectorобъект, он независим, так что он может иметь одну и ту же функцию в различных частях нашего приложения.

let injector = {
  dependencies: {},
  register: function(key, value) {
    this.dependencies[key] = value;
  },
  resolve: function(deps, func, scope) {
    var args = [];
    for(var i = 0; i < deps.length, d = deps[i]; i++) {
      if(this.dependencies[d]) {
        // 存在此依赖
        args.push(this.dependencies[d]);
      } else {
        // 不存在
        throw new Error('不存在依赖:' + d);
      }
    }
    return function() {
      func.apply(scope || {}, args.concat(Array.prototype.slice.call(arguments, 0)));
    }   
  }
}

Как видите, этот объект очень простой, всего три свойства:dependenciesиспользуется для сохранения зависимостей,registerиспользуется для добавления зависимостей, последнийresolveИспользуется для внедрения зависимостей.

resolveТо, что нужно сделать функции, простое, сначала проверьтеdepsмассив, затем вdependenciesВид объекта ищет зависимости, которые добавляются кargsв массиве, scopeЕсли параметр существует, укажите его область действия и используйте его параметр в возвращаемой функции..applyПереданный метод мы передали обратноfuncПерезвоните.

Давайте посмотрим на использование:

// 添加
injector.register('fnA', fnA)
injector.register('fnB', fnB)

// 注入
(injector.resolve(['fnA', 'fnB'], function(fnA, fnB){
  let a = fnA()
  let b = fnB()
  console.log(a, b)
}))()

При вызове мы также можем передать дополнительные параметры:

(injector.resolve(['fnA', 'fnB'], function(fnA, fnB, str){
  let a = fnA()
  let b = fnB()
  console.log(a, b, str)
}))('isboyjc')

Из этого мы реализуем простую инъекцию зависимостей.Внедрение зависимостей вещь не новая, она уже давно существует в других языках, это паттерн проектирования, его еще можно сказать стиль.

В процессе ранней модульной эволюции еще много решений, поэтому я не буду писать их по одному. Модульные решения, которые мы называем, не являются независимыми друг от друга.Каждое решение может учиться друг у друга, как внедрение зависимостей.IIFE, существует хорошее модульное решение для решения практических задач, точно так же, как решение некоторых проблем, возникающих при внедрении зависимостей выше.

С ростом спроса на модули во фронтенд-разработке в сообществе постепенно появились отличные модульные решения, признанные большинством людей, и они постепенно превратились в общие модульные спецификации сообщества, которые не только решают проблему внедрения зависимостей. проблемы также имеют много уникальных модульных функций, и появление ES6 позже также означает приземление официальной (на уровне языка) модульной спецификации ESM.

Эволюция модульной спецификации JS

Спецификация CommonJS

Введение

API, определенный стандартом JS, предназначен только для создания приложений на основе браузера и не определяет стандартную библиотеку для более широких приложений.

а такжеCommonJSСпецификация в основном предлагается для того, чтобы компенсировать дефект, заключающийся в том, что JS не имеет стандарта. Она предложена сообществом. Конечная цель — предоставить аналогичныйPythonилиRubyили JavaСтандартная библиотека языка не только на этапе написания скриптов.

готов использоватьCommonJS APIНаписанное приложение может не только использовать JS для разработки клиентских приложений, но также писать серверные JS-приложения, инструменты командной строки, настольные приложения с графическим интерфейсом и т. д.

В 2009 году американские программистыRyan DahlкCommonJsоснованный на нормеnode.jsПроект, использующий язык JS для программирования на стороне сервера, закладывающий основу для внешнего интерфейса, с тех пор nodejs сталCommonJsместоимение.

CommonJSВ спецификации указано, что каждый файл является независимым модулем со своей областью видимости. Переменные, функции и классы модуля являются закрытыми. Если вы хотите вызвать его извне, вы должны использоватьmodule.exportsАктивно выставляется, хотя ссылка в другом файле используется напрямуюrequire(path)Вы можете следующим образом:

// num.js
var a = 1
var b = 2
var add = function (){
  return a + b
}

// 导出
module.exports.a = a
module.exports.b = b
module.exports.add = add

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

var num = require('./num.js')

console.log(num.a) // 1
console.log(num.b) // 2
console.log(num.add(a,b)) // 3

requireКоманда отвечает за чтение и выполнение JS-файла и возврат модуля.exportsобъект, выдает ошибку, если не найден.

Также сказано выше,CommonJSСпецификация относится к серверу, то есть относится только кNodeJS, на самом деле простоNodeПредоставляет конструктор внутриModule, все модули являются конструкторамиModuleнапример, следующим образом:

function Module(id, parent) {
  this.id = id
  this.exports = {}
  this.parent = parent
  // ...
}

Внутри каждого модуля находитсяmoduleНапример, объект будет иметь следующие свойства:

  • module.idИдентификатор модуля, обычно имя файла модуля с абсолютным путем.
  • module.filenameИмя файла модуля с абсолютным путем
  • module.loadedВозвращает логическое значение, указывающее, завершилась ли загрузка модуля.
  • module.parentВозвращает объект, представляющий модуль, вызвавший этот модуль
  • module.childrenВозвращает массив других модулей, используемых этим модулем
  • module.exportsУказывает значение, выводимое модулем наружу

В основномCommonJSОсобенности спецификации заключаются в следующем:

  • Весь код выполняется в области модуля и не загрязняет глобальную область.
  • Модуль можно загружать несколько раз, но он будет запущен только один раз при первой загрузке, а затем текущий результат будет кэширован.После загрузки кэшированный результат будет прочитан напрямую.Чтобы модуль снова запустился, кеш надо чистить.
  • Порядок загрузки модулей, порядок их появления в коде

Сказав так много, давайте реализуем простой.

основная реализация

Нечего сказать, код сначала уважителен, несколько десятков строк кода просты, и я покажу вам commonJS.

Во-первых, мы создаемtest.js, напишите следующий код:

module.exports = {
 a:1,
 b:2,
 c(){
   return 3
 }
}

Кто-нибудь спросит:module.exportsОчевидно, это нативное, разве его нельзя реализовать вручную? Тогда смотри.

создать новыйcommonJS.jsФайл, весь код такой, прочитайте еще раз комментарии, а потом вкратце представьте и все будет ОК.

let path = require('path');
let fs = require('fs');
let vm = require('vm');

let n = 0

// 构造函数Module
function Module(filename){
  this.id = n++; // 唯一ID
  this.filename = filename; // 文件的绝对路径
  this.exports = {}; // 模块对应的导出结果
}

// 存放可解析的文件模块扩展名
Module._extensions = ['.js'];
// 缓存
Module._cache = {};
// 拼凑成闭包的数组
Module.wrapper = ['(function(exports,require,module){','\r\n})'];

// 没写扩展名,默认添加扩展名
Module._resolveFilename = function (p) {
  p = path.join(__dirname, p);
  if(!/\.\w+$/.test(p)){
    //如果没写扩展名,尝试添加扩展名
    for(let i = 0; i < Module._extensions.length; i++){
      //拼接出一个路径
      let filePath = p + Module._extensions[i];
      // 判断文件是否存在
      try{
        fs.accessSync(filePath);
        return filePath;
      }catch (e) {
        throw new Error('module not found')
      }
    }
  }else {
    return p
  }
}

// 加载模块本身
Module.prototype.load = function () {
  // 解析文件后缀名 isboyjc.js -> .js
  let extname = path.extname(this.filename);
  // 调用对应后缀文件加载方法
  Module._extensions[extname](this);
};

// 后缀名为js的加载方法
Module._extensions['.js'] = function (module) {
  // 读文件
  let content = fs.readFileSync(module.filename, 'utf8');
  // 形成闭包函数字符串
  let script = Module.wrapper[0] + content + Module.wrapper[1];
  // 创建沙箱环境,运行并返回结果
  let fn = vm.runInThisContext(script);
  // 执行闭包函数,将被闭包函数包裹的加载内容
  fn.call(module, module.exports, req, module)
};

// 仿require方法, 实现加载模块
function req(path) {
  // 根据输入的路径 转换绝对路径
  let filename = Module._resolveFilename(path);
  // 查看缓存是否存在,存在直接返回缓存
  if(Module._cache[filename]){
      return Module._cache[filename].exports;
  }
  // 通过文件名创建一个Module实例
  let module = new Module(filename);
  // 加载文件,执行对应加载方法
  module.load();
  // 入缓存
  Module._cache[filename] = module;
  return module.exports
}

let str = req('./test');
console.log(str);

Как и выше, это всего 80 строк кода с комментариями.

Сначала пишем конструкторModuleidуникальный идентификатор,filenameАбсолютный путь для сохранения файла,exportsСохраните результат экспорта, соответствующий модулю.

Мы также добавили в модуль несколько статических свойств, где_extensionsСохраните расширение анализируемого модуля и используйте расширение в качестве ключа позже, чтобы добавить его метод анализа._cacheзаключается в кэшировании загруженных модулей,wrapperпредставляет собой массив, содержащий два строковых элемента, и две строки вместе представляют собой строку функции, которая служит массивом функций, которые мы соберем вместе позже.

Во-вторых, был добавлен статический метод_resolveFilenameДля синтаксического анализа полного пути к файлу также существует более простой метод прототипа.load, используемый для загрузки модулей.

Обычно, когда мы используем узел для загрузки модулей, мы используемrequireметод, в то время как наш почерк используетreqметод, этот метод передает путь к файлу (суффикс можно опустить), в методе мы сначала вызываем конструктор модуля_resolveFilenameМетод анализирует входящий путь в абсолютный путьfilename, затем проверьте_cacheСуществует ли объект вfilenameПуть — это значение ключа, если он есть, читайте кэш напрямую.

Если кеша нет, создайте новый экземпляр модуля, а затем вызовитеloadметод загрузки модуля.

самое главное этоloadпроцесс,loadРазобрать сначалаfilenameString, получить суффикс имени файла, позвонив_extensionsМетод, соответствующий названию суффикса, загружает соответствующий файл, в коде имеемModule._extensions['.js']Добавлен соответствующий метод разбора, то есть разбор файлов с суффиксом js.

Документ в текстеtest.js, суффикс которого.js, что точно соответствует, вызовите метод и передайте это (т. е. экземпляр модуля).

глаза приходятModule._extensions['.js']Способ, на самом деле, тоже простой, сначала черезfilenameПрочитайте содержимое файла, а затем начните собирать метод, который представляет собой следующую строку кода:

let script = Module.wrapper[0] + content + Module.wrapper[1];

Строковый скрипт, составленный из этой строки кода, на самом деле является методом, но строковым методом, как показано ниже:

'function(exports,require,module){ test.js文件内容 }'

Далее вы не понимаетеvm.runInThisContextМетод, вот краткое введение:

vm.runInThisContext(code)Будет создана отдельная среда песочницы для выполнения кода параметра.codeСкомпилируйте, запустите и верните результат. Код, выполняемый этим методом, не имеет доступа к локальной области, но имеет доступ к глобальному объекту Global.

Если ты этого не понимаешь, то все знаютevalБар! На самом деле это иevalАналогично, вот пример:

var vm = require('vm');
var str = '111';
 
//在runInThisContext创建的沙箱环境中执行
var vmRes = vm.runInThisContext('str = "vm222";');
console.log('vmRes: ', vmRes); // vmRes:  vm222
console.log('str: ', str); // str:  111
 
//在eval中执行
var evalRes = eval('str = "eval222";');
console.log('evalRes: ', evalRes); // evalRes:  eval222
console.log('str: ', str); // str:  eval222

Как указано выше, используйтеvm.runInThisContextИсполняемый строковый код не изменяет текущую область видимости, ноevalДа, это все.

мысли возвращаются,vm.runInThisContext(script)Превратите написанный нами строковый метод в исполняемый метод, который затем вызывается и передается в параметрах:

fn.call(module, module.exports, req, module)

из-за использованияcallметод, поэтому первый параметр является преобразованнымscriptТо есть указатель this функции fn становится текущимmoduleНапример, оставшиеся три — это параметры вызова функции.Оглядываясь на то время, когда функция была прописана, форма функции участвовала в сравнении входящих значений при вызове текущей функции:

// 原来函数
fn = function(exports, require, module){ 
  // test.js文件内容 
}

// 调用
fn(module.exports, req, module)

Три параметра:

  • экземпляр модуляexportsобъект
  • метод импорта модуля req
  • сам экземпляр модуля

Увидев это, я думаю, все должны понять начало примераtest.jsпочему мы можем напрямую использоватьmodule.exportsэкспортируется, очевидно потому, что в процессе загрузки мы помещаем весьtestФайл запихивается в анонимный метод загрузки как кусок кода, и при выполнении этого метода загрузки формальные параметры существуютmoduleнапример, чтобы мы могли работать напрямуюmoduleэкземпляр, к которомуexportsАтрибуты заполнены данными! ! ! Итак, очень простой рукописный пример commonJS закончен, у вас есть?

В заключение,Проще говоря, CommonJs — это модульный стандарт сообщества, а Nodejs — реализация модульной спецификации CommonJs., он загружает модуль синхронно, то есть только после того, как импортированный модуль будет загружен, будут выполнены следующие операции.NodeВ серверных приложениях модули обычно существуют локально, загружаются быстро, и проблема синхронизации невелика.Это не подходит для браузеров.Вы только представьте, если все модули загружаются синхронно в большом проекте, то опыт Крайне плохой, поэтому также требуется асинхронное модульное решение, поэтомуAMD规范Так родился.

Технические характеристики AMD

Введение

AMD (определение асинхронного модуля) специально разработано для среды браузера, оно определяет набор стандартов асинхронной загрузки для решения проблемы синхронизации.

Синтаксис следующий:

define(id?: String, dependencies?: String[], factory: Function|Object)
  • idто есть имя модуля, строка, необязательная
  • dependenciesЗадает список модулей, от которых зависит. Это массив и необязательный параметр. Вывод каждого зависимого модуля будет передан в качестве параметра один раз.factoryсередина. Если не указаноdependencies, то его значение по умолчанию равно["require", "exports", "module"]
  • factoryОбертывает конкретную реализацию модуля, которая может быть функцией или объектом. Если это функция, возвращаемое значение является выходным интерфейсом или значением модуля.

Мы просто перечисляем некоторые варианты использования, как показано ниже, мы определяем именованныйmyModuleмодули, которые зависят отjQueryМодуль:

// 定义依赖 myModule,该模块依赖 JQ 模块
define('myModule', ['jquery'], function($) {
  // $ 是 jquery 模块的输出
  $('body').text('isboyjc')
})

// 引入依赖
require(['myModule'], function(myModule) {
  // todo...
})

Анонимный модуль без значения идентификатора, и в этом случае имя файла является его идентифицирующим именем, обычно используемым в качестве модуля запуска:

define(['jquery'], function($) {
  $('body').text('isboyjc')
})

Зависит от нескольких модулей:

define(['jquery', './math.js'], function($, math) {})

Выход модуля:

define(['jquery'], function($) {
  var writeName = function(selector){
    $(selector).text('isboyjc')
  }

  // writeName 是该模块输出的对外接口
  return writeName
})

Зависимости внутренней ссылки модуля:

define(function(require) {
  // 引入依赖
  var $ = require('jquery')
  $('body').text('isboyjc')
})

Каждый должен знатьRequireJS, соблюдениеAMDСтандартная библиотека инструментов для управления модулями на стороне клиента.

это черезdefineметод, который определяет код как модуль, черезrequireметод, который реализует модульную загрузку кода, и его нужно скачивать и импортировать при использовании, то есть мы хотим использовать его в браузереAMDПредставить на странице первым при стандартизацииrequire.jsВот и все.

Можно сказатьRequireJSто естьAMDстандартизированная реализация.

основная реализация

Как вы, возможно, знаете вышеприведенное использование, мы просто реализуем загрузчик модулей спецификации AMD, аналогичныйRequireJS.

Прежде чем писать, давайте распишем использованный пример:

файл ввода index.html:

<!DOCTYPE html>
<html lang="en">
  <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Document</title>
  </head>
  <body>
    <script src="./requireJS.js"></script>
    <script>
      require(['a', 'b'], function (a, b) {
          console.log(b + a)
      });
    </script>
  </body>
</html>

Как показано выше, это, вероятно, введениеrequireJS.jsфайл, а затем использовать его для импортаaа такжеbдве зависимости и вернуть их сумму.

Давайте посмотрим на модули самиaа такжеb :

// a.js
define([], function () {
  return 1
})

// b.js
define(['c'], function (c) {
  return 2 + c
})

// c.js
define([], function () {
  return 2
})

можно увидеть,a.jsПеременная 1 возвращается или экспортируется в файл.

а такжеb.jsФайл снова зависит от модуляc,вернуть2 + cа также.

Прошлойc.jsФайл модуля напрямую возвращает переменную 2.

Что мы хотим сделать, так это выполнитьindex.htmlфайл, окончательный результат равен 5.

Далее напишем от руки.На самом деле самое главное это два метода.require & define, или сначала введите код, а затем объясните,requireJS.jsФайлы следующие:

(function () {
  // 缓存
  const cache = {}
  let moudle = null
  const tasks = []
  
  // 创建script标签,用来加载文件模块
  const createNode = function (depend) {
    let script = document.createElement("script");
    script.src = `./${depend}.js`;
    // 嵌入自定义 data-moduleName 属性,后可由dataset获取
    script.setAttribute("data-moduleName", depend);
    let fs = document.getElementsByTagName('script')[0];
    fs.parentNode.insertBefore(script, fs);
    return script;
  }

  // 校验所有依赖是否都已经解析完成
  const hasAlldependencies = function (dependencies) {
    let hasValue = true
    dependencies.forEach(depd => {
      if (!cache.hasOwnProperty(depd)) {
        hasValue = false
      }
    })
    return hasValue
  }

  // 递归执行callback
  const implementCallback = function (callbacks) {
    if (callbacks.length) {
      callbacks.forEach((callback, index) => {
        // 所有依赖解析都已完成
        if (hasAlldependencies(callback.dependencies)) {
          const returnValue = callback.callback(...callback.dependencies.map(it => cache[it]))
          if (callback.name) {
            cache[callback.name] = returnValue
          }
          tasks.splice(index, 1)
          implementCallback(tasks)
        }
      })
    }
  }
   
  // 根据依赖项加载js文件
  const require = function (dependencies, callback) {
    if (!dependencies.length) { // 此文件没有依赖项
      moudle = {
        value: callback()  
      }
    } else { //此文件有依赖项
      moudle = {
        dependencies,
        callback
      }
      tasks.push(moudle)
      dependencies.forEach(function (item) {
        if (!cache[item]) {
          // script表亲加载文件结束
          createNode(item).onload = function () {
            // 获取嵌入属性值,即module名
            let modulename = this.dataset.modulename
            console.log(moudle)
            // 校验module中是否存在value属性
            if (moudle.hasOwnProperty('value')) {
              // 存在,将其module value(模块返回值|导出值)存入缓存
              cache[modulename] = moudle.value
            } else {
              // 不存在
              moudle.name = modulename
              if (hasAlldependencies(moudle.dependencies)) {
                // 所有依赖解析都已完成,执行回调,抛出依赖返回(导出)值
                cache[modulename] = callback(...moudle.dependencies.map(v => cache[v]))
              }
            }
            // 递归执行callback
            implementCallback(tasks)
          }
        }
      })
    }
  }
  window.require = require
  window.define = require
})(window)

Это тоже упрощенная версия, не более 90 строк кода, с комментариями, большинству студентов это должно быть понятно после прочтения. Не вдаваясь в подробности, просто кратко опишу процесс.

передачаrequireилиdefineПервый метод — загрузка js-файлов в соответствии с массивом зависимостей.В отличие от commonJS, AMD основана на браузерах.Для чтения файлов мы можем только динамически создавать теги скрипта, поэтомуcreateNodeТо есть создайте тег скрипта для загрузки файлового модуля.

Событие onload будет запущено после загрузки файла введения скрипта, чтобы мы могли контролировать порядок загрузки зависимостей. Только после загрузки модуля JS его обратный вызов может быть выполнен, но все зависимости JS, которые мы представили, используютdefineметод определен, в то время какdefineМетод также может зависеть от каких-то файловых модулей js, но всегда есть источник, от которого не зависит, поэтому рекурсия пригодится.

Наша цель — выполнить обратный вызов callbck после загрузки модуля, но если A зависит от B, B зависит от C и т. д., и мы хотим выполнить обратный вызов A, мы должны дождаться загрузки B и C, поэтому мы используем стек (массив)tasksЧтобы сохранить обратный вызов обратного вызова, дождитесь загрузки всех зависимостей, а затем выполните их последовательно, точно так же, как луковая модель koa платформы Node. Это делается для того, чтобы функции обратного вызова выполнялись в правильном порядке.

Примерно как и выше, осталось еще несколько проверок, потому что код простой, поэтому не буду вдаваться в подробности. Но не думайте, что исходный код requireJS действительно так прост, это не так, настоящий исходный код учитывает слишком много вещей, этот код просто для удобства понимания, если вы заинтересованы в самостоятельном прочтении исходного кода~

Спецификация CMD

Введение

CMDпоявился позже, он рисуетCommonJSа такжеAMDПреимущество спецификации также связано с асинхронной загрузкой модуля браузера.

существуетCMDВ спецификации модуль — это файл,define— глобальная функция, определяющая модуль.

defineприниматьfactoryпараметр,factoryМожет быть функцией, объектом или строкой.

factoryКогда это объект и строка, интерфейс, представляющий модуль, является объектом и строкой, как показано ниже:

// factory 为JSON数据对象
define({'name': 'isboyjc'})

// factory 为字符串模版
define('my name is {{name}}!!!')

factoryКогда это функция, это означает, что это метод построения модуля.Выполняя метод построения, можно получить интерфейс, предоставляемый модулем, то естьfunction(require, exports, module):

  • requireэто метод, который принимает идентификатор модуля в качестве единственного параметра для получения интерфейса, предоставляемого другими модулями.
  • exportsОбъект, используемый для обеспечения интерфейса внешнего модуля
  • moduleэто объект, в котором хранятся некоторые свойства и методы, связанные с текущим модулем

factoryКогда это функция, она выглядит следующим образом:

define(function(require, exports, module) {
  var a = require('./a')
  a.doSomething()
  
  // 依赖就近原则:依赖就近书写,什么时候用到什么时候引入
  var b = require('./b')
  b.doSomething()
})

Давайте посмотрим больше обычаев:

define(function(require, exports, module) {
  // 同步引入
  var a = require('./a')
  
  // 异步引入
  require.async('./b', function (b) {
  })
  
  // 条件引入
  if (status) {
      var c = requie('./c')
  }
  
  // 暴露模块
  exports.aaa = 'hahaha'
})

и вышеCommonJS,AMDаналогичный,CMDдаSeaJSНормализованный вывод определений модулей во время продвижения,а такжеCMDспецификация иSeaJSКогда-то его высоко ценили в Китае не только потому, что он прост и удобен, но и потому, чтоSeaJSАвтор Али玉伯Написанное боссом, таким же китайским автором, как и Vue, можно назвать светом китайцев.

основная реализация

Для SeaJS в соответствии со спецификацией CMD, как и RequireJS в соответствии со спецификацией AMD, это загрузчик модулей на стороне браузера. Они очень похожи, но есть очевидные различия. Лично я считаю, что реализация SeaJS относительно красивее, и это был когда-то популярен во фронтенде.Круг,из-за места,ставить его здесь определенно неуместно.У меня есть возможность представить реализацию SeaJS отдельно.В этой статье мы можем сначала понять разницу между CMD и AMD.

ЦМД и АМД
Технические характеристики уважаемый шедевр
AMD Зависит от фронта requirejs
CMD зависит от ближайшего seajs

CMDВ сравненииAMDСказать,CMDболее уважаемыйas lazy as possible(Ленивая загрузка, насколько это возможно, также известная как ленивая загрузка, то есть загрузка только при необходимости).

Для зависимых модулейAMDвыполняется заранее,CMDЭто отложенное выполнение, и методы выполнения у них разные.AMDВ процессе выполнения все зависимости предварительно выполняются, то есть все они выполняются до запуска собственной логики кода, иCMDеслиrequireВведено, но вся логика не использует эту зависимость или она не будет выполняться до тех пор, пока логика ее не использует, ноRequireJSНачиная с 2.0 его тоже можно поменять на отложенное исполнение (в зависимости от метода записи способ обработки разный), с другой стороныCMDПолагаться на близость восхищаются, иAMDПоложитесь на предварительное уважение.

UMD-спецификация

Введение

UMD (Universal Module Definition), определение универсального модуля, видно из названия, эта штука унифицирована.

Он родился с тенденцией к большому внешнему интерфейсу, и один и тот же модуль кода можно использовать во время выполнения или во время компиляции.CommonJs、CMDЧетноеAMDТо есть один и тот же пакет JavaScript, работающий на стороне браузера, на стороне сервера и даже на стороне приложения, должен следовать одному и тому же методу написания, так как же это реализовано?

основная реализация

Давайте посмотрим на этот кусок кода

((root, factory) => {
  if (typeof define === 'function' && define.amd) {
    // AMD
    define(factory);
  } else if (typeof exports === 'object') {
    // CommonJS
    module.exports = factory();
  } else if (typeof define === 'function' && define.cmd){
		// CMD
    define(function(require, exports, module) {
      module.exports = factory()
    })
  } else {
    // 都不是
    root.umdModule = factory();
  }
})(this, () => {
  console.log('我是UMD')
  // todo...
});

можно увидеть,defineдаAMD/CMDграмматика иexportsтолько вCommonJSЕсли он существует в модуле, вы обнаружите, что он обнаружит текущую среду использования и метод определения модуля при определении модуля. Если он соответствует, он будет использовать свой стандартный синтаксис. Если он не соответствует, он будет монтируется на глобальный объект, мы видим, что входящийthis, который в браузере относится кwindow, что в серверной среде означаетglobal, используя этот метод, чтобы сделать различные модульные определения совместимыми.

На самом деле существует множество норм, сформированных сообществом, предназначенных для модульной разработки JS, но те, о которых мы упоминали выше, используются чаще всего.

Что мы сказали до сих порCommonJS,AMD,CMDЭто просто единая модульная спецификация, признанная сообществом, но она не является официальной (уровень языка JS), поэтому это официальная модульная спецификация JS.

ES Module

июнь 2015 г.,ECMAScript2015Вот что мы сказалиES6Выпущенный JS наконец-то реализует функцию модуля на уровне языкового стандарта, так что зависимости модуля и его входных и выходных переменных можно определить во время компиляции, в отличие отCommonJS,AMDТакие вещи должны определяться во время выполнения (такие инструменты, как FIS, могут только предварительно обрабатывать зависимости, которые, по сути, являются разрешением во время выполнения) и стать распространенным модульным решением для браузеров и серверов.

Поэтому до ES6 в JS не было официального модульного механизма, ES6 реализовывал функцию модуляризации на уровне языковых стандартов, и реализация была достаточно простой, стремилась стать единым модульным решением для браузеров и серверов. Функция в основном состоит из двух команд: экспорта и импорта.Команда экспорта указывает внешний интерфейс модуля, а команда импорта используется для ввода функций других модулей. ES6 также предоставляет команду экспорта по умолчанию. Задает вывод по умолчанию для модуля. Соответствующий оператор импорта не требует фигурных скобок. Это также ближе к цитированию AMD.

Модуль ES6 не является объектом Команда импорта статически анализируется движком JavaScript, а код модуля импортируется во время компиляции. Вместо загрузки во время выполнения кода условная загрузка невозможна. Это также делает возможным статический анализ.

  • export

Что экспорт может экспортировать, так это то, что объект содержит несколько свойств и методов, а экспорт по умолчанию может экспортировать только одну функцию, которая может быть анонимной. Мы можем импортировать с помощью импорта. В то же время мы также можем использовать require напрямую, потому что webpack позволяет работать с сервером.

  • import
import { fn } from './xxx' //    export导出的方式

import fn from 'xx' //    export default方式

Механизм работы модуля ES6 отличается от механизма работы commonjs. Когда движок js статически анализирует скрипт, он создаст ссылку только для чтения после того, как обнаружит инструкцию загрузки модуля. Подождите, пока скрипт действительно не выполнится. Значение получается путем обращения к модулю, в процессе обращения к исполнению значение в модуле меняется, а также будет меняться импортируемое. Модули ES6 импортируются динамически. Значение не кэшируется. Модуль всегда привязан к модулю, в котором он находится.

наконец

На самом деле, грубо говоря, для модульности JS все вышеперечисленные решения решают одни и те же проблемы:

  • Загадочное глобальное переменное загрязнение
  • раздражающие конфликты имен
  • громоздкие файловые зависимости

Различные модульные подходы решают эти проблемы. Первые две проблемы на самом деле очень легко решить, используя замыкания для немедленного выполнения функций, и более сложные, используя компиляцию в песочнице, кэширование вывода и так далее. Сложность заключается в сортировке и загрузке зависимостей файлов. CommonJS использует модуль fs для синхронного чтения файлов на стороне сервера, в то время как в браузере, будь то RequireJs спецификации AMD или SeaJs спецификации CMD, он фактически загружается путем динамического создания тегов сценария, а затем выполняется после зависимостей Это избавляет от необходимости вручную писать теги сценария и обращать внимание на порядок загрузки.

Как модульное решение на уровне стандарта языка, ESM не требует от нас введения дополнительных сторонних пакетов для модуляризации.Помимо проблем совместимости, это, безусловно, лучший выбор и будущая тенденция, которой достаточно, чтобы доказать на Vite.

Прочитав это, у вас появилось более четкое представление о модульности? В этой статье мы в основном говорим о модуляризации JS.Конечно, модуляризация касается не только JS.Далее будет представлено содержание модуляризации CSS.Добро пожаловать, обратите внимание! Если у вас есть какие-либо вопросы, пожалуйста, не стесняйтесь указывать на ошибки и опечатки!

Ссылаться на

Dependency injection in JavaScript

[Рекомендуется к просмотру] Семидневный доклад о модуляризации JavaScript