到底什么是模块化、模块化开发呢?
上面说提到的结构,就是模块;按照这种结构划分开发程序的过程,就是模块化开发的过程;
无论你多么喜欢JavaScript,以及它现在发展的有多好,它都有很多的缺陷:
Brendan Eich本人也多次承认过JavaScript设计之初的缺陷,但是随着JavaScript的发展以及标准化,存在的缺陷问题基本都得到了完善。
无论是web、移动端、小程序端、服务器端、桌面应用都被广泛的使用;
在网页开发的早期,Brendan Eich开发JavaScript仅仅作为一种脚本语言,做一些简单的表单验证或动画实现等,那个时候代码还是很少的:
但是随着前端和JavaScript的快速发展,JavaScript代码变得越来越复杂了:
所以,模块化已经是JavaScript一个非常迫切的需求:
在这里我将详细讲解JavaScript的模块化,尤其是CommonJS和ES6的模块化。
早期没有模块化带来了很多的问题:比如命名冲突的问题
当然,我们有办法可以解决上面的问题:立即函数调用表达式(IIFE)---- IIFE (Immediately Invoked Function Expression)
但是,我们其实带来了新的问题:
所以,我们会发现,虽然实现了模块化,但是我们的实现过于简单,并且是没有规范的。
我们需要知道CommonJS是一个规范,最初提出来是在浏览器以外的地方使用,并且当时被命名为ServerJS,后来为了体现它的广泛性,修改为CommonJS,平时我们也会简称为CJS。
所以,Node中对CommonJS进行了支持和实现,让我们在开发node的过程中可以方便的进行模块化开发:
前面我们提到过模块化的核心是导出和导入,Node中对其进行了实现:
注意:exports是一个对象,我们可以在这个对象中添加很多个属性,添加的属性会导出;
另外一个文件中可以导入:
上面这行完成了什么操作呢?理解下面这句话,Node中的模块化一目了然
但是Node中我们经常导出东西的时候,又是通过module.exports导出的:module.exports和exports有什么关系或者区别呢?
我们追根溯源,通过维基百科中对CommonJS规范的解析:
CommonJS中是没有module.exports的概念的;
但是为了实现模块的导出,Node中使用的是Module的类,每一个模块都是Module的一个实例,也就是
module;
所以在Node中真正用于导出的其实根本不是exports,而是module.exports;
因为module才是导出的真正实现者;
但是,为什么exports也可以导出呢?
我们现在已经知道,require是一个函数,可以帮助我们引入一个文件(模块)中导出的对象。
那么,require的查找规则是怎么样的呢?
https://nodejs.org/dist/latestv14.x/docs/api/modules.html#modules_all_together
这里我总结比较常见的查找规则:导入格式如下:require(X)
情况一:X是一个Node核心模块,比如path、http—直接返回核心模块,并且停止查找
情况二:X是以 ./ 或 …/ 或 /(根目录)开头的
情况三:直接是一个X(没有路径),并且X不是一个核心模块
如果上面的路径中都没有找到,那么报错:not found
结论一:模块在被第一次引入时,模块中的js代码会被运行一次
结论二:模块被多次引入时,会缓存,最终只加载(运行)一次
结论三:如果有循环引入,那么加载顺序是什么?
如果出现右图模块的引用关系,那么加载顺序是什么呢?
CommonJS加载模块是同步的:
如果将它应用于浏览器呢?
所以在浏览器中,我们通常不使用CommonJS规范:
在早期为了可以在浏览器中使用模块化,通常会采用AMD或CMD:
AMD主要是应用于浏览器的一种模块化规范:
我们提到过,规范只是定义代码的应该如何去编写,只有有了具体的实现才能被应用:AMD实现的比较常用的库是require.js和curl.js;
第一步:下载require.js
第二步:定义HTML的script标签引入require.js和定义入口文件:
data-main属性的作用是在加载完src的文件后会加载执行该文件
require.js的使用
CMD规范也是应用于浏览器的一种模块化规范:
CMD也有自己比较优秀的实现方案: SeaJS
第一步:下载SeaJS
第二步:引入sea.js和使用主入口文件:seajs是指定主入口文件的
JavaScript没有模块化一直是它的痛点,所以才会产生我们前面学习的社区规范:CommonJS、AMD、CMD等,
所以在ES推出自己的模块化系统时,大家也是兴奋异常。
ES Module和CommonJS的模块化有一些不同之处:
ES Module模块采用export和import关键字来实现模块化:
这里我在浏览器中演示ES6的模块化开发:
如果直接在浏览器中运行代码,会报如下错误:
这个在MDN上面有给出解释:
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Modules
你需要注意本地测试 — 如果你通过本地加载Html 文件 (比如一个 file:// 路径的文件), 你将会遇到 CORS 错误,因为Javascript 模块安全性需要。
你需要通过一个服务器来测试。
我这里使用的VSCode,VSCode中有一个插件:Live Server
export关键字将一个模块中的变量、函数、类等导出;
我们希望将其他中内容全部导出,它可以有如下的方式:
方式一:在语句声明的前面直接加上export关键字
方式二:将所有需要导出的标识符,放到export后面的 {}中
方式三:导出时给标识符起一个别名
// 导出方式一:export const name='liu' // 在声明变量,函数,类的时候直接导出 // export const name='liu' // export const age=21 // 导出方式二:export {name,age} // const name='liu' // const age=21 // export { // name, // age // } // 导出方式三:起别名 // const name='liu' // const age=21 // export { // name as fName, // age as fAge // }
import关键字负责从另外一个模块中导入内容
导入内容的方式也有多种:
// 导入方式一:import {name,age} from './foo.js' // import {name,age} from './foo.js' // 注意,这种方式导入的名字必须跟导出的名字一致 // console.log(name) // console.log(age) // 导入方式二:起别名 // import {name as fName,age as fAge} from './foo.js' // console.log(fName) // console.log(fAge) // 导入方式三:import * as 对象名自己起 from './foo.js' // 一次性导入所有到自己命名的对象中 // import * as foo from './foo.js' // console.log(foo.name) // console.log(foo.age)
补充:export和import可以结合使用
为什么要这样做呢?
// 导入导出方式一 // import {timeFormat} from './shijian.js' // import {format} from './time.js' // export { // timeFormat, // format // } // 导入导出方式二 // export {timeFormat} from './shijian.js' // export {format} from './time.js' // 导入导出方式三 // export * from './time.js' // export * from './shijian.js'
前面我们了解的导出功能都是有名字的导出(named exports):
还有一种导出叫做默认导出(default export)
注意:在一个模块中,只能有一个默认导出(default export);
通过import加载一个模块,是不可以在其放到逻辑代码中的,比如:
为什么会出现这个情况呢?
但是某些情况下,我们确确实实希望动态的来加载某一个模块:
import.meta是一个给JavaScript模块暴露特定上下文的元数据属性的对象。
ES Module是如何被浏览器解析并且让模块之间可以相互引用的呢?
https://hacks.mozilla.org/2018/03/es-modules-a-cartoon-deep-dive/
ES Module的解析过程可以划分为三个阶段: