目标
- 了解模块化的历史
- 现代模块化规范
- 手写简易实现
要点
模块化的历史
1. 什么是模块?
-
块的内部数据与实现是私有的, 并向外部暴露一些接口(方法)与外部其它模块通信
-
将一个复杂的程序依据一定的规则(规范)封装成几个块(文件), 并进行组合在一起
-
本质上模块就是一种提供对外通信接口,进行代码切分/组合的管理方式。其呈现的方式因不同的模块化方案而不同,基本是以文件粒度区分。
2. 为什么要用模块化?
-
随着前端的发展以及前后端的逐渐分离,也随着前端的能力在纵深都得到增强之后,市场决定了需要更好的代码管理、组织、通信的模式,也就伴随着各种模块化的技术模式。
-
项目越来越大,拆分成多个模块可以降低复杂度,还可以更优雅的代码管理
-
模块化便于多人协同开发,面向过程开发
-
可以实现单一模块的独立发布-和微服务的思想更加match
3. 模块化发展历史
3.1 前期模块化处于混沌时期,组织代码就是靠「经验」基本就是全局function模式 : 将不同的功能封装成不同的全局函数
缺点:污染全局命名空间, 容易引起命名冲突或数据不安全,而且模块成员之间看不出直接关系
function fn () {};
3.2 命名空间 - namespace模式 : 简单对象封装
缺点:数据不安全(外部可以直接修改模块内部的数据)
var student = {
name: 'tom',
getScore: function() {}
}
student.name;
student.getScore();
3.3 IIFE模式:匿名函数自调用(闭包)
- 优点:模块化的解决方案再次提升,利用闭包使得污染的问题得到解决,更加纯粹的内聚。
- 缺点:老司机🛴已经打开了度娘 如果当前这个模块依赖另一个模块怎么办?
// moduleA.js
(function(global) {
var name = 'tom';
function getScore() {};
// 把模块挂载全局变量并暴漏出去
global.moduleA = { name, getScrore };
})(window)
基于 IIFE 还有很多玩儿法,也是现代模块实现的基石 🛠
4. 总的来说,现代模块化机制要解决的问题如下:
- 命名污染,全局污染,变量冲突等基础问题
- 内聚且私有,变量不能被外界污染到
- 怎么引入(依赖)其它模块,怎样暴露出接口给其它模块
- 最烦人的是依赖顺序问题,老司机还记得以前的 Jquery 问题吗😩
- 引入模块可能导致循环引用的问题以及其他一些边界情况
模块化规范
简要说明,详解请找度娘娘
- CommonJS
Node.js采用 CommonJS 模块规范,加载模块是同步的 采用module.exports向外暴漏模块 采用require(xxx)引入模块,xxx为模块名;如果是自定义模块,xxx为模块文件路径
// moduleA.js
let a = 5;
const add = function (value) {
return value + a;
};
module.exports.a = a;
module.exports.add = add;
const moduleA = require('./moduleA.js');
console.log(moduleA.a); // 5
console.log(moduleA.add(1)); // 6
- AMD
- AMD规范是异步加载模块,允许指定回调函数
- AMD 的代表肯定就是大名鼎鼎的 RequireJS
- 模块前置:顾名思义,就是首先引入需要的模块,类似jquery,但是优点是不用像jquery要严格按照引入顺序,否则会报错。 暴漏模块
define(['moduleA'], function(_) {
console.log('moduleA load');
return {
str: function() {
console.log('moduleA run');
// 业务实现...
return _.repeat('>>>>>>>>>>', 20)
}
}
})
引入模块
require(['moduleA'], function(moduleA){
// 使用moduleA 的变量和方法
console.log(moduleA.str())
})
优质程序员:请问 define和 require为什么可以直接使用,有点好奇? ok, 盘它⛏⛏⛏
const def = new Map();
// 定义模块,触发的时机是在 require 的时候,所以先依赖 -> 收集
define = (name, deps, factory) => {
/**
* name 模块名
* deps 依赖
* factory 回调函数
*/
def.set(name, { name, deps, factory });
}
// 触发加载依赖
require = (deps, factory) => {
return new Promise((resolve, reject) => {
Promise.all(deps.map(dep => {
// __load方法此处省略,老司机都知道是新建一个script标签然后放到head中
return __load(dep).then(() => {
const { deps, factory } = def.get(dep);
if (deps.length === 0) return factory(null);
// 递归处理模块中嵌套引入子模块
return require(deps, factory)
})
})).then(resolve, reject)
})
.then(instances => factory(...instances))
}
看到这儿还不明白AMD如何实现的call我上门服务☎
- CMD
CMD规范专门用于浏览器端,模块的加载是异步的,模块使用时才会加载执行。CMD规范整合了CommonJS和AMD规范的特点。在 Sea.js 中,所有 JavaScript 模块都遵循 CMD模块定义规范。
不啰嗦直接上例子,想看啰嗦的可以去找CSDN、博客园、segmentfault...
// sea.js
// 回调函数有三个参数 require, exports, module
define('a', function (require, exports, module) {
console.log('a load')
exports.run = function () { console.log('a run') }
})
define('b', function (require, exports, module) {
console.log('b load')
exports.run = function () { console.log('b run') }
})
define('main', function (require, exports, module) {
console.log('main run')
// cmd本质就是按需加载,也就是网上的说法依赖后置也好,依赖就近也好
// 需要的时候才引入模块
var a = require('a')
a.run()
var b = require('b')
b.run()
})
sj.use('main')
// main run
// a load
// a run
// b load
// b run
> 这个use哪里来的,客官别急
// 使用正则表达式获取 require的所有模块
const getDepsFromFn = (fn) => {
let matches = [];
// require('a ')
//1. (?:require\() -> require( -> (?:) 非捕获性分组
//2. (?:['"]) -> require('
//3. ([^'"]+) -> a -> 避免回溯 -> 回溯 状态机
let reg = /(?:require\()(?:['"])([^'"]+)/g; // RegExp
let r = null;
while((r = reg.exec(fn.toString())) !== null) {
reg.lastIndex
matches.push(r[1])
}
return matches
}
// define 提取依赖
const define = (id, factory) => {
const url = toUrl(id); // url是解析后模块的路径
const deps = getDepsFromFn(factory); // 获取require的所有模块
if (!modules[id]) {
modules[id] = { url, id, factory, deps }
}
}
const __exports = (id) => exports[id] || (exports[id] = {});
const __module = this;
// __require 引入并加载模块
const __require = (id) => {
// 这里的id是依赖的标识 比如require('a') id 就是 a
// _load方法省略,就是在head中新建一个script标签 src引入对应的依赖
return __load(id).then(() => {
// 加载之后
const { factory, deps } = modules[id];
if (!deps || deps.length === 0) {
// define的回调函数,三个参数 require, exports, module
factory(__require, __exports(id), __module);
return __exports(id);
}
return sj.use(deps, factory);
})
}
// 实现__require
sj.use = (modules, callback) => {
mods = Array.isArray(modules) ? modules : [modules];
return new Promise((resolve, reject) => {
Promise.all(modules.map(module => {
// _load方法省略,就是在head中新建一个script标签 src引入对应的依赖
return __load(module).then(() => {
const { factory } = modules[mod];
return factory(__require, __exports(modules), __module)
})
})).then(resolve, reject)
}).then(instances => callback && callback(...instances))
}
同上~看不明白CMD如何实现的call我上门服务☎
- ES6模块化
ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。
// export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。
// add.js
let num = 0;
const add = function (a, b) {
return a + b;
};
export { num, add };
/** 引用模块 **/
import { num, add } from './add';
console.log(add(10 + num))
import export 底层实现等我总结之后再来补充,未来可期 😁😁
补充知识点
CommonJS 模块输出的是一个值的拷贝
- CommonJS
// a.js
let name = 'morrain'
let age = 18
exports.name = name
exports.age = age
exports.setAge = function(a){
age = a
}
// b.js
var a = require('a.js')
console.log(a.age) // 18
a.setAge(19)
console.log(a.age) // 18
a.js 中,exports 被赋值为一个对象(暂称为导出对象),而导出对象的 age 属性源自 age 变量,由于 age 变量是数值类型,属于 js 的基本类型之一,是按值传递的,所以 age 属性得到的只是 age 变量的拷贝值,也就是说从赋值之后开始 age 变量的任何变化都与导出对象的 count 属性毫无关系。
- ES6 ES6 模块输出的是值的引用
// a.js
let name = 'xuSir君'
let age = 18
exports.name = name
exports.getAge = function(){
return age
}
// b.js
var a = import('a.js')
console.log(a.name) // 'xuSir君'
a.name = 'rename'
var b = import('a.js')
console.log(b.name) // 'rename'
与 Commonjs 提供的导出规范不同,ES module 模块导出是值的引用,在原始值改变时 import 的加载值也会随之变化。
欢迎纠错🎈🎈🎈