模块化的理解
什么是模块化
模块化,就是把一个大的问题,化解为小的问题在这种思维下提炼出来的工程化解决方案。
核心思想
核心思想就是隔离。要有自己的内部属性,内部方法,以及自己来决定哪些属性与方法能够被其他模块访问。
模块的设计目的是什么?
有了模块,我们可以更方便的使用别人的代码,想要什么功能,就加载什么模块。
什么是 js 模块?
在 es6 之前,JavaScript 没有官方规范,主要采用 CommonJS 和 AMD 这两种规范。
无模块化
函数 -> 命名空间 -> 立即执行函数
CommonJS 规范
nodeJS 采用的就是 CommonJS 规范,运行在 nodeJS 中的规范。
CommonJS 的加载方式称为运行时加载,以下代码的实质是加载整个fs
对象,再从fs
对象上读取这三个方法。
1 | // CommonJS模块 |
require()
在 node 源码中有一个 module.js 文件,这个文件实现了 node 的整个模块加载系统。
1 | // module.js |
我们在 nodeJS 中使用的 require()方法调用的就是 module.require 方法。具体可参考 Node.js 模块加载机制 Require()
注意:node 中使用的**_require()_**
和 RequireJS 是两个东西。
使用
主要有 module、exports、require。
导出:module
module.exports 由 Module 对象创建。如果我们想导出一个对象
1 | // a-common.js(导出) |
exports 是 module.exports 的一个引用,所以我们可以使用
1 | // ✅ |
错误使用,这相当于重新定义了 exports
1 | // ❎ |
导入:require
1 | var o = require('./a-common.js'); |
优缺点
优点:
解决了依赖、全局变量污染的问题
缺点:
因为 nodeJS 是运行在服务端,服务端所有的模块是放在本地的,模块的加载速度就是硬盘的读取速度,是同步的。但是浏览器的模块是放在服务器端,如果一个模块过大,浏览器会处于一种假死状态。
因此,浏览器端的模块不能采用”同步加载”,只能采用”异步加载”。所以 AMD 规范诞生了。
AMD 规范
AMD 是”Asynchronous Module Definition”的缩写,意思是”异步模块定义“。模块的加载不影响后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等模块加载完毕,回调函数才会执行。
define 和 require 就是 require.js 在全局注入的函数。
AMD 规范主要由 require.js 和 curl.js 这两个 JavaScript 库实现。
在网页中嵌入 require.js,就可以进行模块化编程了。
1 | <script data-main="scripts/main" src="scripts/require.js"></script> |
定义模块
使用 define 方法
1 | define('A', ['require', 'exports'], function (require, exports) { |
导入模块
AMD 也是采用 require()语句加载模块,require([module], callback)
1 | require(['math'], function (math) { |
注意,不要与 NodeJS 中的 require()混淆。CommonJS 中的 require 是 NodeJS 源代码 module.js 文件中定义的方法,AMD 中的 require()是由 RequireJs 实现的。
CMD
CMD,全称 Common Module Definition,它整合了 CommonJS 和 AMD 规范的特点。
主要区别为一下两点:
- CMD 可以同步加载模块,也可以异步加载,而 AMD 需要异步加载模块
- CMD 遵循依赖就近原则,而 AMD 遵循依赖前置原则。也就是说 CMD 在使用模块前,引入模块即可,而 AMD 需要把所有的依赖提前放在依赖数组中。
UMD
umd,全称 Universal Module Definition,它允许在环境中同时使用 AMD 规范和 CommonJS 规范。
ES Module
module 可完全取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的解决方案。
ES Module 的设计思想是静态化,也就是说在编译时就确定了模块的依赖关系,而 CommonJS 和 AMD 是在运行时确定,这是 EM 模块和其他模块最显著的差别;第二个差别是 ES6 模块输出的是值的引用,而 CommonJS 输出的是值的拷贝。
ES6 模块的加载方式称为“编译时加载”,在编译时就完成了加载,效率要比 CommonJS 和 AMD 的“运行时加载”效率高。
1 | // ES6模块 |
以上代码只会从fs
中加载这 3 个方法,其他方法不会加载。
引入模块
1 | // test.js |
1 | // index.js |
- import 表示引入一个模块
- test 可理解为要引入模块的名字
- from 表示从哪里引入
- ‘./test’ 为 ‘./test.js’ 的简写,表示将要引入的模块的路径
引入这个模块的同时,'./test'
里的代码也执行了。因为 test.js 没有对外暴露接口,所以 test 为空对象。
对外提供接口
1 | console.log('my name is test.js'); |
使用 export default
对外暴露一个对象。当使用import test from './test'
时,test 就是这个暴露的对象
我们还可以仅通过 export 暴露几个方法和属性,在 test 中添加以下代码,看看 index.js 中的 test 会发生什么变化
1 | // test.js |
结果发现 test 并没有什么变化,因为它仅仅等于export defaule
所暴露的对象
如果想要获取 test.js 对外暴露的所有接口,可通过如下模式获取
1 | import * as test from './test'; |
*
号表示所有,是比较常用的通配符,as 表示别名。* as test
表示将 test.js 对外暴露的所有接口组成的对象,命名为 test。
1 | import test, { age, bar } from './test'; |
这种写法非常常见,test 仍表示为export default
所暴露的对象,{ age, bar }
表示用解构的语法从返回的整个对象中取对应的接口。结果显而易见
在 react,这种使用方法很常见,我们能根据导入语法,就知道 react 内部是如何封装接口的
1 | import { lazy } from 'react' |
还有另一种写法:
1 | export { default as RightMain } from './right-main'; |