一、模块化介绍
1 模块化由来
问题
- 多人协作造成变量命名冲突问题 - 代码混乱不好维护
基于以上问题的出现,有了模块化的解决方案。
结果
- 可以把复杂的代码拆分成小的模块,方便管理代码和维护 - 每个模块直接的内容都是相互独立的,互不影响
2 模块化历史
2.1 最早期模块化方式
1 单例模式
如果两个开发者都有同一个变量a,可以用以下这种方式加以区分。
var name1 = {
a: 1
}
var name2 = {
a: 2
}
但是这种方式也并没有完全解决问题,毕竟name1 和name2 也需要不同命名,并且这种方式调用起来不是很方便
2 自执行函数
function(){
var a = 1
}()
function(){
var a = 1
}()
每个函数有自己的作用域,所以以上两个函数内部的a变量不会出现冲突。但是这种解决方式也不雅观
2.2 已过时模块化方式
1 AMD模块规范
AMD——异步模块加载规范,就是模块加载过程中即使require的模块还没有获取到,也不会影响后面代码的执行。
RequireJS——AMD规范的实现。其实也可以说AMD是RequireJS在推广过程中对模块定义的规范化产出。
示例如下:
//独立模块定义
define({
a: function() {}
b: function() {}
});
// 非独立模块定义
define(['f1', 'f2'], function(f1, f2){
a: function() {}
b: function() {}
});
// 模块引用
require(['m1', 'm2'], function(m1, m2){
m1.a();
m2.b();
})
2 CMD模块规范
CMD——通用模块规范,由国内的玉伯提出。
SeaJS——CMD的实现,其实也可以说CMD是SeaJS在推广过程中对模块定义的规范化产出。
用法示例:
define(function(require, exports, module){
//依赖模块a
var a = require('./a');
//调用模块a的方法
a.method();
})
与AMD规范的主要区别在于定义模块和依赖引入的部分。AMD需要在声明模块的时候指定所有的依赖,通过形参传递依赖到模块内容中。CMD模块更接近于Node对CommonJS规范(后面会着重讲)的定义,CMD支持动态引入,require、exports和module通过形参传递给模块,在需要依赖模块时,随时调用require( )引入即可。与AMD相比,CMD推崇依赖就近,AMD推崇依赖前置。
3 UMD通用模块规范
所谓的兼容模式是将几种常见模块定义方式都兼容处理。
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined'
? module.exports = factory() // Node , CommonJS
: typeof define === 'function' && define.amd
? define(factory) //AMD CMD
: (global.CodeMirror = factory()); //模块挂载到全局
}(this, (function () {
...
})
接下来我们将介绍目前最主流的前端模块化的方案。
二 node中的模块
Node 应用由模块组成,采用 CommonJS 模块规范。
每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。
2.1 CommonJs简单介绍
CommonJS规范规定,每个模块内部,module变量代表当前模块。这个变量是一个对象,它的exports属性(即module.exports)是对外的接口。加载某个模块,其实是加载该模块的module.exports属性。
// 被引用模块文件
var x = 5;
var addX = function (value) {
return value + x;
};
module.exports.x = x;
module.exports.addX = addX;
// 加载模块文件
var example = require('./example.js');
console.log(example.x); // 5
console.log(example.addX(1)); // 6
2.2 module对象
2.2.1 模块实现
Node内部提供一个Module构建函数。所有模块都是Module的实例。每个模块内部,都有一个module对象,代表当前模块。
function Module(id, parent){
this.id = id;
this.exports = {};
this.parent = parent;
if(parent && parent.children) {
parent.children.push(this);
}
this.filename = null;
this.loaded = false;
this.children = [];
}
- module.id 模块的识别符,通常是带有绝对路径的模块文件名。
- module.filename 模块的文件名,带有绝对路径。
- module.loaded 返回一个布尔值,表示模块是否已经完成加载。
- module.parent 返回一个对象,表示调用该模块的模块。
- module.children 返回一个数组,表示该模块要用到的其他模块。
- module.exports 表示模块对外输出的值。
2.2.2 module.exports与exports
module.exports属性表示当前模块对外输出的接口,其他文件加载该模块,实际上就是读取module.exports变量。
为了方便,Node为每个模块提供一个exports变量,指向module.exports。这等同在每个模块头部,有一行这样的命令。var exports = module.exports;
如果你觉得,exports与module.exports之间的区别很难分清,一个简单的处理方法,就是放弃使用exports,只使用module.exports。
2.3 require命令
Node使用CommonJS模块规范,内置的require命令用于加载模块文件。
require命令的基本功能是,读入并执行一个JavaScript文件,然后返回该模块的exports对象。如果没有发现指定模块,会报错。
2.3.1 node中模块分类
1.核心模块/内置模块 (fs http path等)
2.第三方模块需要安装
3.自定义模块需要通过绝对路径或者相对路径进行引入
2.3.2 模块分类
在Node模块的引入过程中,一般要经过一下三个步骤
- 路径分析
- 文件定位
- 编译执行
核心模块会省略文件定位和编译执行这两步,并且在路径分析中会优先判断,加载速度比一般模块更快。
文件模块——就是外部引入的模块如node_modules里通过npm安装的模块,或者我们项目工程里自己写的一个js文件或者json文件。文件模块引入过程以上三个步骤都要经历。
2.3.3 路径分析
不论核心模块还是文件模块都需要经历路径分析这一步。
node支持如下几种形式的模块标识符
//核心模块
require('http')
----------------------------
//文件模块
//以.开头的相对路径,(可以不带扩展名)
require('./a.js')
//以..开头的相对路径,(可以不带扩展名)
require('../b.js')
//以/开始的绝对路径,(可以不带扩展名)
require('/c.js')
//外部模块名称
require('express')
//外部模块某一个文件
require('codemirror/addon/merge/merge.js');
那么对于这个都是字符串的引入方式,Node 会优先去内存中查找匹配核心模块,如果匹配成功便不会再继续查找
(1)比如require http 模块的时候,会优先从核心模块里去成功匹配
如果核心模块没有匹配成功,便归类为文件模块
(2)以.、..和/开头的标识符,require都会根据当前文件路径将这个相对路径或者绝对路径转化为真实路径,也就是我们平时最常见的一种路径解析
(3)非路径形式的文件模块 如上面的'express' 和'codemirror/addon/merge/merge.js',这种模块是一种特殊的文件模块,一般称为自定义模块。
自定义模块的查找最费时,因为对于自定义模块有一个模块路径,Node会根据这个模块路径依次递归查找。
模块路径——Node的模块路径是一个数组,模块路径存放在module.paths属性上。模块路径的生成规则如下:
- 当前路文件下的node_modules目录
- 父目录下的node_modules目录
- 父目录的父目录下的node_modules目录
- 沿路径向上逐级递归,直到根目录下的node_modules目录
2.3.4 文件定位
扩展名分析
我们在使用require的时候有时候会省略扩展名,那么Node怎么定位到具体的文件呢?
这种情况下,Node会依次按照.js、.json、.node的次序一次匹配。(.node是C++扩展文件编译之后生成的文件)
若扩展名匹配失败,则会将其当成一个包来处理,我这里直接理解为npm包
包处理
对于包Node会首先在当前包目录下查找package.json(CommonJS包规范)通过JSON.parse( )解析出包描述对象,根据main属性指定的入口文件名进行下一步定位。
如果文件缺少扩展名,将根据扩展名分析规则定位。
若main指定文件名错误或者压根没有package.json,Node会将包目录下的index当做默认文件名。
再依次匹配index.js、index.json、index.node。
若以上步骤都没有定位成功将,进入下一个模块路径——父目录下的node_modules目录下查找,直到查找到根目录下的node_modules,若都没有定位到,将抛出查找失败的异常。
2.3.5 模块编译
.js文件——通过fs模块同步读取文件后编译执行
.node文件——用C/C++编写的扩展文件,通过dlopen( )方法加载最后编译生成的文件。
.json——通过fs模块同步读取文件后,用JSON.parse( ) 解析返回结果。
其余扩展名文件。它们都是被当做.js文件载入。
2.4 CommonJS模块加载机制
网上很多地方都在说:CommonJS模块的加载机制是,输入的是被输出的值的拷贝。这句话是错误的。
以下面这段代码为例:
// index.js
const { ss } = require('./lib');
const lib = require('./lib');
console.log('ss', ss);
console.log('lib', lib);
setTimeout(()=>{
console.log('ss', ss);
console.log('lib', lib);
},3000);
// lib.js
module.exports.ss = 'ss1';
setTimeout(()=>{
module.exports.ss = 'ss2';
console.log('module.exports', module.exports);
},2000);
//执行结果
ss ss1
lib { ss: 'ss1' }
lib module.exports { ss: 'ss2' }
ss ss1
lib { ss: 'ss2' }
从执行结果可以看出
commonjs 导出的是module.exports这个对象,导出值给这个对象添加新的属性会影响导入值。
const { ss } = require('./lib'); 相当于 const { ss } = {ss:'ss1'}; 解构赋值,相当于const ss = 'ss1';所以导出对象修改ss不能使导入对象ss也变成2。
三 ESModule
ES6在语言规格层面上实现了模块功能,是编译时加载,完全可以取代CommonJS和AMD规范,可以成为浏览器和服务器通用的模块解决方案.
3.1 ES6模块使用——export
// 导出变量
export var name = 'pengpeng';
// 导出一个函数
export function foo(x, y){}
// 推荐常用导出方式
// person.js
const name = 'dingman';
const age = '18';
const addr = '卡尔斯特森林';
export { name, age, addr };
// as 用法
const s = 1;
export {
s as t,
s as m,
}
3.2 ES6模块使用——import
// 一般用法
import { name, age } from './person.js';
// As用法
import { name as personName } from './person.js';
//整体加载
import * as person from './person.js';
console.log(person.name);
console.log(person.age);
3.3 ES6模块使用——export default
其实export default,在项目里用的非常多,一般一个Vue组件或者React组件我们都是使用export default命令,需要注意的是使用export default命令时,import是不需要加{}的。而不使用export default时,import是必须加{},示例如下:
//person.js
export function getName() {
...
}
//my_module
import {getName} from './person.js';
-----------------对比---------------------
//person.js
export default function getName(){
...
}
//my_module
import getName from './person.js';
export default其实是导出一个叫做default的变量,所以其后面不能跟变量声明语句。
值得注意的是我们可以同时使用export 和export default
//person.js
export name = 'dingman';
export default function getName(){
...
}
//my_module
import getName, { name } from './person.js';
3.4 ES6模块与CommonJS模块加载区别
ES6模块的设计思想,是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。所以说ES6是编译时加载,不同于CommonJS的运行时加载(实际加载的是一整个对象),ES6模块不是对象,而是通过export命令显式指定输出的代码,输入时也采用静态命令的形式:
//ES6模块
import { basename, dirname, parse } from 'path';
//CommonJS模块
let { basename, dirname, parse } = require('path');
以上这种写法与CommonJS的模块加载有什么不同?
当require path模块时,其实 CommonJS会将path模块运行一遍,并返回一个对象,并将这个对象缓存起来,这个对象包含path这个模块的所有API。以后无论多少次加载这个模块都是取这个缓存的值,也就是第一次运行的结果,除非手动清除。
ES6会从path模块只加载3个方法,其他不会加载,这就是编译时加载。ES6可以在编译时就完成模块加载,当ES6遇到import时,不会像CommonJS一样去执行模块,而是生成一个动态的只读引用,当真正需要的时候再到模块里去取值,所以ES6模块是动态引用,并且不会缓存值。
四 总结
以上介绍了模块化的一些知识,欢迎大家批评指正!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。