Cet article vous apporte des connaissances pertinentes sur la modularisation en JavaScript. J'espère qu'il vous sera utile.
Comme nous le savons tous, le statut de js dans le développement front-end. Il est vraiment important de bien l'apprendre.
L'article suivant présente la modularité.
Qu'est-ce que la modularisation et le développement modulaire exactement ?
En fait, le but ultime du développement modulaire est de diviser le programme en petites structures.
Écrivez votre propre code logique dans cette structure, qui a sa propre portée et n'affectera pas les autres structures.
Cette structure peut exporter les variables, fonctions, objets, etc. que vous souhaitez exposer à sa structure pour utilisation.
Vous pouvez également importer des variables, des fonctions, des objets, etc. dans d'autres structures d'une manière ou d'une autre.
La structure mentionnée ci-dessus est un module ; diviser le processus de développement d'un programme selon cette structure est le processus de développement modulaire.
Au début du développement Web, Brendan Eich développait JavaScript uniquement comme langage de script pour effectuer une simple validation de formulaire ou une implémentation d'animation. À cette époque, le code était encore très petit :
. Pour le moment, il nous suffit d'écrire le code JavaScript dans
et il n'est pas nécessaire de l'écrire dans plusieurs fichiers.
Mais avec le développement rapide du front-end et de JavaScript, le code JavaScript est devenu de plus en plus complexe :
L'émergence d'ajax et la séparation du développement front-end et back-end font qu'après le back-end -end renvoie des données, nous devons le faire via le rendu JavaScript des pages frontales.
Avec l'émergence de SPA, les pages frontales sont devenues plus complexes : une série d'exigences complexes, notamment le routage front-end, la gestion de l'état, etc., doivent être implémentées via JavaScript.
Y compris l'implémentation de Node, l'écriture de programmes back-end complexes en JavaScript, le manque de modularité est un défaut fatal.
Donc, la modularisation est déjà un besoin très urgent pour JavaScript. C'est pourquoi ES6 (2015) a lancé sa propre solution modulaire.
Avant cela, afin de permettre à JavaScript de prendre en charge la modularisation, de nombreuses spécifications de modularisation différentes ont émergé : AMD, CMD, CommonJS, etc.
comme des conflits de noms.
Résolvez le problème ci-dessus grâce à l'expression d'invocation de fonction immédiate (IIFE). Étant donné que la fonction a sa propre portée, elle ne provoquera pas de conflits de noms entre les différents fichiers.
// a.js var moduleA = (function() { var name = "llm" var age = 22 var isFlag = true return { name: name, isFlag: isFlag } })()
// b.js var moduleB = (function() { var name = "zh" var isFlag = false return { name: name, isFlag: isFlag } })()
// 使用 moduleA.name moduleB.name
Cependant, nous avons en fait apporté de nouveaux problèmes :
Je dois me souvenir du nom de l'objet renvoyé dans chaque module afin qu'il puisse être utilisé correctement lors de l'utilisation d'autres modules.
Le code est déroutant à écrire et le code de chaque fichier doit être enveloppé dans une fonction anonyme.
En l'absence de spécifications appropriées, tout le monde et chaque entreprise peut nommer les modules arbitrairement, voire avoir le même nom de module.
Nous constaterons donc que bien que la modularisation soit réalisée, notre mise en œuvre est trop simple et non standardisée.
Nous devons formuler certaines spécifications pour contraindre chacun à écrire du code modulaire selon cette spécification. Cette spécification doit inclure des fonctionnalités de base : le module lui-même peut exporter les propriétés exposées et le module peut importer les propriétés dont il a besoin. Afin de résoudre les problèmes ci-dessus, la communauté JavaScript a émergé avec une série de spécifications utiles. Ensuite, nous apprendrons quelques spécifications représentatives.
Nous devons savoir que CommonJS est une spécification. Elle a été initialement proposée pour une utilisation en dehors du navigateur, et a été nommée ServerJS à l'époque, afin de refléter son ampleur, elle a été modifiée en CommonJS. . Nous l'appelons généralement également CJS en abrégé.
Node est une implémentation représentative de CommonJS côté serveur.
Browserify est une implémentation de CommonJS dans le navigateur.
L'outil d'empaquetage Webpack prend en charge et convertit CommonJS.
Ainsi, CommonJS est pris en charge et implémenté dans Node, nous permettant de réaliser facilement un développement modulaire pendant le développement de Node.
Chaque fichier js dans Node est un module distinct.
Ce module comprend les variables principales de la spécification CommonJS : exports, module.exports et require.
Nous pouvons utiliser ces variables pour faciliter le développement modulaire.
Nous avons mentionné plus tôt que le cœur de la modularisation est l'exportation et l'importation, qui sont implémentées dans Node :
exports et module.exports peuvent être responsables de l'exportation du contenu du module. La fonction
require peut nous aider à importer du contenu à partir d'autres modules (modules personnalisés, modules système, modules de bibliothèque tiers).
CommonJS est pris en charge et implémenté dans Node, nous permettant de réaliser facilement un développement modulaire pendant le développement de node :
在Node中每一个js文件都是一个单独的模块。
这个模块中包括CommonJS规范的核心变量:exports、module.exports、require。
exports和module.exports可以负责对模块中的内容进行导出。
require函数可以帮助我们导入其他模块(自定义模块、系统模块、第三方库模块)中的内容。
下面我们将来介绍exports、module.exports、require的使用。
exports是一个对象,我们可以在这个对象中添加很多个属性,添加的属性会导出。
我们也可以通过module.exports直接导出一个对象。
我们通过require()函数导入一个文件。并且该文件导出的变量。
下面来详细介绍一个module.exports。
CommonJS中是没有module.exports的概念的。
但是为了实现模块的导出,Node中使用的是Module的类,每一个模块都是Module的一个实例,也就是module。
所以在Node中真正用于导出的其实根本不是exports,而是module.exports。
因为module才是导出的真正实现者。
并且内部将exports赋值给module.exports。
该方式的导入导出有以下特点:
Node中的文件都运行在一个函数中。可以通过打印console.log(arguments.callee + "")来验证。
导入导出是值的引用,如果导出的是一个基本数据类型值,那么导出文件改变该值,然后导入文件该变量的值也不会变。
// a.js const obj = require("./b.js") console.log(obj) setTimeout(() => { obj.name = "llm" }, 1000)
// b.js const info = { name: "zh", age: 22, foo: function() { console.log("foo函数~") } } setTimeout(() => { console.log(info.name) // llm }, 2000) module.exports = info
他是通过require 函数来导入的,只有在执行js代码才会知道模块的依赖关系。
代码是同步执行的。
模块多次引入,只会加载一次。每个module内部会存在一个loaded来确定是否被加载过。
代码循环引入的时候,深度优先来加载模块。然后再广度优先。
我们现在已经知道,require是一个函数,可以帮助我们引入一个文件(模块)中导出的对象。
那么,require的查找规则是怎么样的呢?
详细查找规则,请访问这里
这里我总结比较常见的查找规则:导入格式如下:require(X)
模块在被第一次引入时,模块中的js代码会被运行一次
模块被多次引入时,会缓存,最终只加载(运行)一次
为什么只会加载运行一次呢?
这是因为每个模块对象module都有一个属性:loaded。为false表示还没有加载,为true表示已经加载。
如果有循环引入,那么加载顺序是什么?
如上图,Node采用的是深度优先算法:main -> aaa -> ccc -> ddd -> eee ->bbb
CommonJS加载模块是同步的:
同步的意味着只有等到对应的模块加载完毕,当前模块中的内容才能被运行。
这个在服务器不会有什么问题,因为服务器加载的js文件都是本地文件,加载速度非常快。
如果将它应用于浏览器呢?
浏览器加载js文件需要先从服务器将文件下载下来,之后再加载运行。
那么采用同步的就意味着后续的js代码都无法正常运行,即使是一些简单的DOM操作。所以在浏览器中,我们通常不使用CommonJS规范。当然在webpack中使用CommonJS是另外一回事。因为它会将我们的代码转成浏览器可以直接执行的代码。
在早期为了可以在浏览器中使用模块化,通常会采用AMD或CMD。但是目前一方面现代的浏览器已经支持ES Modules,另一方面借助于webpack等工具可以实现对CommonJS或者ES Module代码的转换。AMD和CMD已经使用非常少了,所以这里我们进行简单的演练。
AMD主要是应用于浏览器的一种模块化规范:
AMD是Asynchronous Module Definition(异步模块定义)的缩写。它采用的是异步加载模块。
我们提到过,规范只是定义代码的应该如何去编写,只有有了具体的实现才能被应用。
AMD实现的比较常用的库是require.js和curl.js。
require.js的使用
定义HTML的script标签引入require.js和定义入口文件。data-main属性的作用是在加载完src的文件后会加载执行该文件
// index.html <script src="./require.js" data-main="./index.js"></script>
//main.js require.config({ baseUrl: '', // 默认是main.js的文件夹路径 paths: { foo: "./foo" } }) require(["foo"], function(foo) { console.log("main:", foo) })
// foo.js define(function() { const name = "zh" const age = 22 function sum(num1, num2) { return num1 + num2 } return { name, age, sum } })
CMD规范也是应用于浏览器的一种模块化规范:
CMD 是Common Module Definition(通用模块定义)的缩写。它也采用了异步加载模块,但是它将CommonJS的优点吸收了过来。
AMD实现的比较常用的库是SeaJS。
SeaJS的使用
引入sea.js和使用主入口文件。
// index.html <script src="./sea.js"></script> <script> seajs.use("./main.js") </script>
//main.js define(function(require, exports, module) { const foo = require("./foo") console.log("main:", foo) })
// foo.js define(function(require, exports, module) { const name = "zh" const age = 22 function sum(num1, num2) { return num1 + num2 } // exports.name = name // exports.age = age module.exports = { name, age, sum } });
ES Module和CommonJS的模块化有一些不同之处:
一方面它使用了import和export关键字来实现模块化。
另一方面它采用编译期的静态分析,并且也加入了动态引用的方式。
export负责将模块内的内容导出。
import负责从其他模块导入内容。
采用ES Module将自动采用严格模式:use strict。
基本使用
// index.html <script src="./main.js" type="module"></script>
// foo.js let obj = { name: "zh", age: 22 } export default sum
// main.js import foo from './foo.js' console.log(foo)
在html文件加载入口文件的时候,需要指定type为module。
在打开html文件时,需要开启本地服务,而不能直接打开运行在浏览器上。
这个在MDN上面有给出解释:
你需要注意本地测试 — 如果你通过本地加载Html 文件 (比如一个 file:// 路径的文件), 你将会遇到 CORS 错误,因为Javascript 模块安全性需要。
你需要通过一个服务器来测试。
export关键字将一个模块中的变量、函数、类等导出。
我们希望将其他中内容全部导出,它可以有如下的方式:
方式一:在语句声明的前面直接加上export关键字。
export const name = "zh" export const age = 22
方式二:将所有需要导出的标识符,放到export后面的 {} 中。注意:这里的 {}里面不是ES6的对象字面量的增强写法,{}也不是表示一个对象的。所以: export {name: name},是错误的写法。
const name = "zh" const age = 22 function foo() { console.log("foo function") } export { name, age, foo }
方式三:导出时给标识符起一个别名。(基本没用,一般在导入文件中起别名)。然后在导入文件中就只能使用别名来获取。
export { name as fName, age as fAge, foo as fFoo }
import关键字负责从另外一个模块中导入内容。
导入内容的方式也有多种:
方式一:import {标识符列表} from '模块'。注意:这里的{}也不是一个对象,里面只是存放导入的标识符列表内容。
import { name, age } from "./foo.js"
方式二:导入时给标识符起别名。
import { name as fName, age as fAge } from './foo.js'
方式三:通过 * 将模块功能放到一个模块功能对象(a module object)上。然后通过起别名来使用。
import * as foo from './foo.js'
表示导入导出。
import { add, sub } from './math.js' import {otherProperty} from './other.js' export { add, sub, otherProperty }
等价于
// 导入的所有文件会统一被导出 export { add, sub } from './math.js' export {otherProperty} from './other.js'
等价于
export * from './math.js' export * from './other.js'
为什么要这样做呢?
在开发和封装一个功能库时,通常我们希望将暴露的所有接口放到一个文件中。 这样方便指定统一的接口规范,也方便阅读。这个时候,我们就可以使用export和import结合使用。
前面我们学习的导出功能都是有名字的导出(named exports):
在导出export时指定了名字。
在导入import时需要知道具体的名字。
还有一种导出叫做默认导出(default export)
// foo.js const name = "zh" cconst age = 22 export { name, // 或者这样的默认导出 // age as default } export default age
// 导入语句: 导入的默认的导出 import foo, {name} from './foo.js' console.log(foo, name) // 22 zh
默认导出export时可以不需要指定名字。
在导入时不需要使用 {},并且可以自己来指定名字。
它也方便我们和现有的CommonJS等规范相互操作。
注意:在一个模块中,只能有一个默认导出(default export)。
通过import加载一个模块,是不可以在其放到逻辑代码中的,比如:
if(true) { import foo from './foo.js' }
为什么会出现这个情况呢?
这是因为ES Module在被JS引擎解析时,就必须知道它的依赖关系。
由于这个时候js代码没有任何的运行,所以无法在进行类似于if判断中根据代码的执行情况。
但是某些情况下,我们确确实实希望动态的来加载某一个模块:
如果根据不同的条件,动态来选择加载模块的路径。
这个时候我们需要使用 import() 函数来动态加载。import函数返回的结果是一个Promise。
import("./foo.js").then(res => { console.log("res:", res.name) })
es11新增了一个属性。meta属性本身也是一个对象: { url: "当前模块所在的路径" }
console.log(import.meta)
ES Module是如何被浏览器解析并且让模块之间可以相互引用的呢?
ES Module的解析过程可以划分为三个阶段:
阶段一:构建(Construction),根据地址查找js文件,并且下载,将其解析成模块记录(Module Record)。
阶段二:实例化(Instantiation),对模块记录进行实例化,并且分配内存空间,解析模块的导入和导出语句,把模块指向对应的内存地址。
阶段三:运行(Evaluation),运行代码,计算值,并且将值填充到内存地址中。
Phase 1 :
Phase 2 et 3 :
Ainsi, comme le montre ce qui précède, la modification de la valeur de la variable dans le fichier d'exportation affectera la valeur dans le fichier d'importation . Et le fichier importé ne peut pas modifier la valeur du fichier exporté.
Recommandations associées : Tutoriel d'apprentissage Javascript
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!