js 模块化编程
Javascript 模块化编程
最初 Javascript 不是一种模块化编程语言(es6 开始支持)。为了能够尽可能的实现 js 的模块化,我们会把代码写成这样:
// 1. 最原始:封装函数写法 |
// 2. 对象写法 |
js 模块的基本写法
// 3.立即执行函数(自调用函数)写法 (沙箱模式) |
模块化的标准
让模块拥有更好的通用性
- AMD : Async Module Definition 异步模块定义
依赖前置、提前执行: 在一开始就将所有的依赖项全部加载
- CMD : Common Module Definition 通用模块定义
依赖就近、延迟执行: 在需要的时候才去 require 加载依赖项
commonJS: node.js 同步加载模块,适用于服务端
ES6 标准模块化规范
CMD (Common Module Definition)
同步加载模块 SeaJS
// module add.js |
AMD (Asynchronous Module Definition)
异步加载模块 requireJs 库应用这一规范
// module add.js |
AMD 和 CMD 区别
AMD 和 CMD 最大的区别是对依赖模块的执行时机处理不同,而不是加载的时机或者方式不同,二者皆为异步加载模块。
AMD(requirejs)是将所有文件同时加载、一次性引入、推崇依赖前置、也就是在定义模块时要先声明其依赖的模块、加载完模块后会立马执行该模块(运行时加载),所有模块都加载执行完后会进入 require 的回调函数,执行主逻辑,这样的效果就是依赖模块的执行顺序和书写顺序不一定一致,看网络速度,哪个先下载下来,哪个先执行,但是主逻辑一定在所有依赖加载完成后才执行
CMD(seajs)强调的是一个文件一个模块、可按需引入、推崇依赖就近、加载完某个模块后不会立即执行,只是下载而已,所有依赖模块加载完成后进入主逻辑,遇到 require 语句的时候才执行对应的模块,这样模块的执行顺序和书写顺序是完全一致的
// CMD |
CommonJS 规范
Node 应用由模块组成,采用 CommonJS 模块规范,每个文件就是一个模块,有自己的作用域
在前端浏览器里面并不支持 module.exports
有四个重要的环境变量为模块化的实现提供支持:module、exports、require、global
node 中模块分类
核心模块:由 node 本身提供,不需要单独安装(npm),可直接引入使用
- fs:文件操作模块
- http:网络操作模块
- path:路径操作模块
- url:解析地址的模块
- querystring:解析参数字符串的模块
第三方模块:由社区或个人提供,需要通过 npm 安装后使用,比如:mime 模块/art-template/jquery
自定义模块:由开发人员自己创建,比如:tool.js 、 user.js
模块导入
- 核心模块直接引入使用:
require('fs')
加载文件操作模块
// 引入模块 |
- 第三方模块,需要先使用 npm 进行下载
- 自定义模块,需要加上相对路径
./
或者../
,可以省略.js
后缀,如果文件名是index.js
那么 index.js 也可以省略
// 加载模块 |
- 模块可以被多次导入,但是只会在第一次加载
模块导出
- 在模块的内部,
module
变量代表的就是当前模块,它的exports
属性就是对外的接口,加载某个模块,加载的就是module.exports
属性,这个属性指向一个空的对象
// module.exports 指向的是一个对象,我们给对象增加属性即可 |
module.exports 与 exports
exports 不是 module.exports 的缩写,exports 是单独存在的
exports 和 module.exports 默认指向同一个对象
模块最终导出的一定是 module.exports 中的数据
结论:
直接添加属性两者皆可
赋值对象时,只能使用
module.exports
console.log(module.exports === exports) // ==> true |
nodejs 中 require 加载模块的规则
require(‘mime’) 以 mime 为例
- 如果加载的模块是一个路径,表示加载的自定义模块,根据路径查找对应的 js 文件
- 如果加载的模块是一个名字,不是一个路径,说明加载的是核心模块或者是第三方模块
- 判断是否是核心模块,如果不是核心模块,会在当前目录下查找是否有 node_modules 目录
- 如果有,在 node_modules 目录下查找 mime 这个文件夹,找到 mime 文件夹下的 package.json 文件,找到 main 属性,即模块的入口文件,如果没有 main,默认查找当前目录下的 index.js 文件
- 如果没有找到对应的模块,回去上一层目录,继续查找,一直找到根目录 C: || D: || E:
- 报错: can not find module xxx
ES6 模块化 - import 和 export
Modules 不是对象,import 命令会被 JavaScript 引擎静态分析,在编译时就引入模块代码,而不是在代码运行时加载,所以无法实现条件加载。也正因为这个,使得静态分析成为可能
export default 默认导出一个模块 ( 简单类型 + 复杂类型 )
- 导出 : export default
默认只能导出一个
let str = 'abc' |
导入 : import
导入的名字可以任意
import res from './a.js' |
export 导出多个模块,都放在一个对象里
- 导出 : export
export { num, str } |
- 导入 : import
import { num, str } from './a' |
es6 import() 函数
参数同 import 命令的参数
返回一个 promise 对象
import() 函数可以用在任何地方,不仅仅是模块,非模块的脚本也可以使用。它是运行时执行,也就是说,什么时候运行到这一句,也会加载指定的模块。另外,import() 函数与所加载的模块没有静态连接关系,这点也是与 import
import()类似于 Node 的 require 方法,区别主要是前者是异步加载,后者是同步加载。
应用: 按需加载,条件加载,动态模块路径
import('./myModule.js').then(({ export1, export2 }) => { |
同时加载多个模块
Promise.all([ |
import()也可以用在 async 函数之中。
在 webpack 中使用 import() 动态加载模块时,webpack 默认会将所有 import()的模块都进行单独打包
https://webpack.js.org/api/module-methods/#import-1
CommonJS 模块 和 ES6 模块化区别
CommonJS 模块是运行时加载,ES6 模块是编译时输出接口
运行时加载: CommonJS 模块就是对象;即在输入时是先加载整个模块,生成一个对象,然后再从这个对象上面读取方法,这种加载称为“运行时加载”
编译时加载: ES6 模块不是对象,而是通过 export 命令显式指定输出的代码,import 时采用静态命令的形式。即在 import 时可以指定加载某个输出值,而不是加载整个模块,这种加载称为“编译时加载”
使用 require.js
js 文件加载的时候,浏览器会停止网页渲染,加载文件越多,网页失去响应的时间就会越长;由于 js 文件之间存在依赖关系,因此必须严格保证加载顺序,依赖性最大的模块一定要放到最后加载,当依赖关系很复杂的时候,代码的编写和维护都会变得困难
require.js 的诞生,就是为了解决这两个问题:
(1)实现 js 文件的异步加载,避免网页失去响应;
(2)管理模块之间的依赖性,便于代码的编写和维护。
require.js 的下载和引入
<!-- 引入方式: --> |
加载这个文件,也可能造成网页失去响应。解决办法有两个,一个是把它放在网页底部加载,另一个是写成下面这样:
<!-- async 属性表明这个文件需要异步加载,避免网页失去响应 --> |
加载 require.js 以后,下一步就要加载我们自己的代码了。假定我们自己的代码文件是 main.js,也放在 js 目录下面。那么,只需要写成下面这样就行了:
<!-- data-main 属性的作用是,指定网页程序的主模块。在上例中,就是 js 目录下面的 main.js,这个文件会第一个被 require.js 加载。由于 require.js 默认的文件后缀名是 js ,所以可以把 main.js 简写成 main。--> |
主模块的写法
main.js,我把它称为”主模块”,意思是整个网页的入口代码。所有代码都从这儿开始运行
下面就来看,怎么写 main.js
如果我们的代码不依赖任何其他模块,那么可以直接写入 javascript 代码
// main.js |
但这样的话,就没必要使用 require.js 了。真正常见的情况是,主模块依赖于其他模块,这时就要使用 AMD 规范定义的的 require()函数
// main.js |
每个 AMD 模块的写法
require.js 加载的模块,采用 AMD 规范。也就是说,模块必须按照 AMD 的规定来写
具体来说,就是模块必须采用特定的 define() 函数来定义
如果一个模块不依赖其他模块,那么可以直接定义在 define() 函数之中
// 假定现在有一个 math.js 文件,它定义了一个 math 模块。那么,math.js 就要这样写: |
加载方法如下:
// main.js |
如果这个模块还依赖其他模块,那么 define() 函数的第一个参数,必须是一个数组,指明该模块的依赖性
// 当require() 函数加载上面这个模块的时候,就会先加载 myLib.js 文件 |
模块加载的配置
// 上面代码中主模块的依赖模块是['moduleA', 'moduleB', 'moduleC']。默认情况下,require.js 假定这三个模块与 main.js 在同一个目录,文件名分别为 moduleA.js,moduleB.js 和 moduleC.js,然后自动加载 |
// 上面的代码给出了三个模块的文件名,路径默认与 main.js 在同一个目录(js子目录)。如果这些模块在其他目录,比如js/lib目录,则有两种写法。一种是逐一指定路径。 |
加载非规范的模块
理论上,require.js 加载的模块,必须是按照 AMD 规范、用 define() 函数定义的模块。但是实际上,虽然已经有一部分流行的函数库(比如 jQuery)符合 AMD 规范,更多的库并不符合。那么,require.js 是否能够加载非规范的模块呢?
回答是可以的
这样的模块在用 require()加载之前,要先用 require.config() 方法,定义它们的一些特征
// require.config() 接受一个配置对象,这个对象除了有前面说过的 paths 属性之外,还有一个 shim 属性,专门用来配置不兼容的模块。具体来说,每个模块要定义:(1)exports值(输出的变量名),表明这个模块外部调用时的名称;(2)deps数组,表明该模块的依赖性。 |