이 기사는 JavaScript의 모듈화에 대한 관련 지식을 제공하는 것이 도움이 되기를 바랍니다.
우리 모두 알고 있듯이 프론트엔드 개발에서 JS의 위상. 잘 익히는 것이 정말 중요합니다.
다음 글에서는 모듈성을 소개합니다.
모듈화와 모듈 개발이란 정확히 무엇인가요?
사실 모듈 개발의 궁극적인 목표는 프로그램을 작은 구조로 나누는 것입니다.
이 구조에 고유한 범위가 있고 다른 구조에 영향을 주지 않는 논리 코드를 직접 작성하세요.
이 구조는 사용을 위해 구조에 노출하려는 변수, 함수, 개체 등을 내보낼 수 있습니다.
어떤 방식으로든 다른 구조의 변수, 함수, 객체 등을 가져올 수도 있습니다.
위에서 언급한 구조는 모듈이며, 이 구조에 따라 프로그램을 개발하는 과정을 나누는 것이 모듈 개발 과정입니다.
웹 개발 초기에 Brendan Eich는 간단한 양식 유효성 검사나 애니메이션 구현을 수행하는 스크립팅 언어로만 JavaScript를 개발했습니다. 그 당시에는 코드가 여전히 매우 작았습니다.
이때는
에 JavaScript 코드만 작성하면 되며 여러 파일에 작성할 필요는 없습니다.
그러나 프론트엔드와 JavaScript의 급속한 발전으로 JavaScript 코드는 점점 더 복잡해졌습니다.
Ajax의 등장과 프론트엔드와 백엔드 개발의 분리는 백엔드 이후의 개발을 의미합니다. -end는 데이터를 반환하므로 프런트 엔드 페이지의 JavaScript 렌더링을 통해 이를 수행해야 합니다.
SPA의 출현으로 프런트 엔드 페이지는 더욱 복잡해졌습니다. 프런트 엔드 라우팅, 상태 관리 등을 포함한 일련의 복잡한 요구 사항을 JavaScript를 통해 구현해야 합니다.
Node 구현을 포함하여 JavaScript로 복잡한 백엔드 프로그램을 작성하는 등 모듈성이 부족하다는 점은 치명적인 결함입니다.
그래서 모듈화는 이미 JavaScript에 매우 시급한 요구 사항입니다. 이것이 바로 ES6(2015)이 자체 모듈형 솔루션을 출시한 이유입니다.
이전에 JavaScript가 모듈화를 지원할 수 있도록 AMD, CMD, CommonJS 등 다양한 모듈화 사양이 등장했습니다.
문제가 없습니다.
IIFE(즉시 함수 호출 표현)를 통해 위 문제를 해결하세요. 함수에는 자체 범위가 있으므로 다른 파일 간에 이름 지정 충돌이 발생하지 않습니다.
// 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
그러나 실제로는 새로운 문제가 발생했습니다.
다른 모듈을 사용할 때 올바르게 사용할 수 있도록 각 모듈에서 반환된 개체의 이름을 기억해야 합니다.
코드 작성이 혼란스럽고 각 파일의 코드를 익명 함수로 묶어야 합니다.
적절한 사양이 없으면 모든 사람과 모든 회사에서 모듈 이름을 임의로 지정하거나 동일한 모듈 이름을 가질 수도 있습니다.
그래서 우리는 모듈화가 이루어졌음에도 불구하고 우리의 구현이 너무 단순하고 표준화되지 않았다는 것을 알게 될 것입니다.
모든 사람이 이 사양에 따라 모듈식 코드를 작성하도록 제한하려면 특정 사양을 공식화해야 합니다. 이 사양에는 핵심 기능이 포함되어야 합니다. 모듈 자체는 노출된 속성을 내보낼 수 있고 모듈은 필요한 속성을 가져올 수 있습니다. 위의 문제를 해결하기 위해 JavaScript 커뮤니티에서는 일련의 유용한 사양이 등장했습니다. 다음으로 몇 가지 대표적인 사양을 알아보겠습니다.
CommonJS는 원래 브라우저 외부에서 사용하도록 제안되었으며 나중에 그 범위를 반영하기 위해 ServerJS로 명명되었습니다. 일반적으로 줄여서 CJS라고도 합니다.
Node는 서버 측 CommonJS의 대표적인 구현입니다.
Browserify는 브라우저에서 CommonJS를 구현한 것입니다.
웹팩 패키징 도구에는 CommonJS에 대한 지원 및 변환 기능이 있습니다.
그래서 CommonJS는 Node에서 지원 및 구현되어 Node 개발 과정에서 쉽게 모듈식 개발을 수행할 수 있습니다.
Node의 모든 js 파일은 별도의 모듈입니다.
이 모듈에는 CommonJS 사양의 핵심 변수인 내보내기, module.exports 및 require가 포함되어 있습니다.
이러한 변수를 사용하여 모듈식 개발을 촉진할 수 있습니다.
모듈화의 핵심은 Node에서 구현되는 내보내기 및 가져오기라고 앞서 언급했습니다.
exports 및 module.exports는 모듈의 콘텐츠 내보내기를 담당할 수 있습니다.
require 기능은 다른 모듈(사용자 정의 모듈, 시스템 모듈, 타사 라이브러리 모듈)에서 콘텐츠를 가져오는 데 도움이 될 수 있습니다.
CommonJS는 Node에서 지원 및 구현되어 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),运行代码,计算值,并且将值填充到内存地址中。
1단계:
2단계 및 3단계:
따라서 위에서 볼 수 있듯이 내보내기 파일의 변수 값을 수정하면 가져오기 파일의 값에 영향을 미칩니다. . 그리고 가져온 파일은 내보낸 파일의 값을 수정하는 것이 제한됩니다.
관련 권장 사항: javascript 학습 튜토리얼
위 내용은 고급 JavaScript 구문 모듈화(권장 컬렉션)의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!