js 模块化编程
js 模块化编程
最初 js 不是一种模块化编程语言(es6 开始支持)。为了能够尽可能的实现 js 的模块化,我们会把代码写成这样:
- 最原始: 封装函数写法
function fn1() { |
上面的函数 fn1()
和 fn2()
,组成一个模块。使用的时候,直接调用就行了,这种做法的缺点很明显:”污染”了全局变量,无法保证不与其他模块发生变量名冲突,而且模块成员之间看不出直接关系
- 对象写法
为了解决上面的缺点,可以把模块写成一个对象,所有的模块成员都放到这个对象里面
var module1 = { |
上面的函数 fn1()
和 fn2()
,都封装在 module1
对象里。使用的时候,就是调用这个对象的属性:module1.fn1()
但是,这样的写法会暴露所有模块成员,内部状态可以被外部改写。比如,外部代码可以直接改变内部计数器的值:module1._count = 666
- 立即执行函数(自调用函数)写法 (沙箱模式)
// 使用 立即执行函数,可以达到不暴露私有成员的目的 |
使用上面的写法,外部代码无法读取内部的 _count 变量:console.info(module1._count)
=> undefined
模块化的标准
让模块拥有更好的通用性
AMD : Async Module Definition 异步模块定义:依赖前置、提前执行: 在一开始就将所有的依赖项全部加载
CMD : Common Module Definition 通用模块定义:依赖就近、延迟执行: 在需要的时候才去 require 加载依赖项
commonJS: node.js 同步加载模块,适用于服务端
ES 标准模块化规范
AMD (Asynchronous Module Definition)
异步加载模块 requireJs 库应用这一规范
// module add.js |
CMD (Common Module Definition)
同步加载模块 SeaJS
// module add.js |
AMD 和 CMD 区别
AMD 和 CMD 最大的区别是对依赖模块的执行时机处理不同,而不是加载的时机或者方式不同,二者皆为异步加载模块。
AMD(requirejs)是将所有文件同时加载、一次性引入、推崇依赖前置、也就是在定义模块时要先声明其依赖的模块、加载完模块后会立马执行该模块(运行时加载),所有模块都加载执行完后会进入 require 的回调函数,执行主逻辑,这样的效果就是依赖模块的执行顺序和书写顺序不一定一致,看网络速度,哪个先下载下来,哪个先执行,但是主逻辑一定在所有依赖加载完成后才执行
CMD(seajs)强调的是一个文件一个模块、可按需引入、推崇依赖就近、加载完某个模块后不会立即执行,只是下载而已,所有依赖模块加载完成后进入主逻辑,遇到 require 语句的时候才执行对应的模块,这样模块的执行顺序和书写顺序是完全一致的
// AMD |
CommonJS 规范
Node 应用由模块组成,采用 CommonJS 模块规范,每个文件就是一个模块,有自己的作用域
模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。
在前端浏览器里面并不支持 module.exports
有四个重要的环境变量为模块化的实现提供支持:module、exports、require、global
node 中模块分类
核心模块:由 node 本身提供,不需要单独安装(npm),可直接引入使用
- fs:文件操作模块
- http:网络操作模块
- path:路径操作模块
- url:解析地址的模块
- querystring:解析参数字符串的模块
第三方模块:由社区或个人提供,需要通过 npm 安装后使用,比如:mime
自定义模块:由开发人员自己创建,比如:tool.js、user.js
模块导入
- 核心模块直接引入使用:
require('fs')
加载文件操作模块
// 引入模块 |
- 第三方模块,需要先使用 npm 进行下载
- 自定义模块,需要加上相对路径
./
或者../
,可以省略.js
后缀,如果文件名是index.js
那么 index.js 也可以省略
// 加载模块 |
- 模块可以被多次导入,但是只会在第一次加载
模块导出
- 在模块的内部,
module
变量代表的就是当前模块,它的exports
属性就是对外的接口,加载某个模块,加载的就是module.exports
属性,这个属性指向一个空的对象
// module.exports 指向的是一个对象,我们给对象增加属性即可 |
let m = require('./module.js') |
// 也可以直接给 module.exports 赋值,但是多次导出会覆盖 |
let m = require('./module.js') |
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
ES 模块化 - import 和 export
Modules 不是对象,import 命令会被 JavaScript 引擎静态分析,在编译时就引入模块代码,而不是在代码运行时加载,所以无法实现条件加载。也正因为这个,使得静态分析成为可能
export 导出多个模块,都放在一个对象里
export default 默认只能导出一个,一个模块只允许有一个 export default,否则报错
export default 后面不可以用 var、let、const 可用 export default function(){} function add(){}
命名导出(Named exports)
// 导出 |
默认导出(Default Export)
仅当源模块只有一个导出时,才建议使用此做法
// 导出 |
将默认和命名导出组合在同一模块中是不好的做法,尽管它是规范允许的。
// 导出 |
es import() 函数
参数同 import 命令的参数,返回一个 promise 对象
import() 函数可以用在任何地方,不仅仅是模块,非模块的脚本也可以使用。它是运行时执行,也就是说,什么时候运行到这一句,才会加载指定的模块。另外,import() 函数与所加载的模块没有静态连接关系
import 命令会被 js 引擎静态分析,import 语句放在 if 代码块之中毫无意义,因此会报句法错误,即不能用于条件加载
import() 类似于 Node 的 require 方法,区别主要是前者是异步加载,后者是同步加载
应用: 按需加载,条件加载,动态模块路径
import('./module.js').then(({ export1, export2 }) => { |
同时加载多个模块
Promise.all([ |
import() 也可以用在 async 函数之中。
在 webpack 中使用 import() 动态加载模块时,webpack 默认会将所有 import() 的模块都进行单独打包,https://webpack.js.org/api/module-methods/#import-1
CommonJS 模块 和 ES 模块化区别
CommonJS 模块
// commonJsModule.js |
let m = require('./commonJsModule.js') |
ES 模块
// esModule.js |
import { num,obj, addNum } from './esModule.js' |
CommonJS 模块是运行时加载,ES6 模块是编译时输出接口
CommonJS和 ES 模块都可以对导出对象内部属性的值进行改变
CommonJS 模块输出的是一个值的拷贝,类似浅拷贝
ES 模块输出的 不论是基本类还是引用类型的数据,都是值的引用
ES 模块对导出的数据不可以重新赋值(只读状态),重新赋值会编译报错(即导出的数据指针指向不能变),但可以改变对象的属性,类似 const 声明的变量
CommonJS 模块的require()是同步加载模块,ES6 模块的import命令是异步加载,有一个独立的模块依赖的解析阶段。
CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 Modules不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。
运行时加载: CommonJS 模块就是对象;即在输入时是先加载整个模块,生成一个对象,然后再从这个对象上面读取方法,这种加载称为“运行时加载”
编译时加载: ES6 模块不是对象,而是通过 export 命令显式指定输出的代码,import 时采用静态命令的形式。即在 import 时可以指定加载某个输出值,而不是加载整个模块,这种加载称为“编译时加载”