CMJ(CommonJS规范)
谈到CMJ我们很容易想到node.js因为node所使用的模块化规范正是CMJ,侧面说明了CMJ是一个适用与服务器的模块化规范。 CMJ有几个主要方法分别为:module、exports、require、global。 在开发中我们用module.exports 来导出模块,用 require来加载模块。 我们来看个简单的用例: math.js
//在math.js中,我们定义了一个变量和一个函数并把它暴露出来
let num = 0;
function add(a, b) {
return a + b;
}
module.exports = { //在这向外暴露出函数与变量
add: add,
num: num
}
index.js
//我们在这里使用require方法引用math.js
let math = require('./math');
//这时我们可以调用到math中的add方法。
math.add(4, 9);
这里要注意的是引用自定义的模块时,参数包含路径,可省略.js,而引用核心模块时,不需要带路径。 因为CommonJS用同步的方式加载模块。在浏览器中我们用同步方式加载模块时用户将无法继续操作网页,所以前端需要一个能异步加载模块的模块化规范。
文件是一个模块,私有。内置两个变量 module require (exports = module.exports)
一个引入一个导出,就构成了通信的基本结构
需要注意的两个问题
- 缓存,require 会缓存一下,所以
// a.js
var name = 'morrain'
var age = 18
exports.name = name
exports.getAge = function(){
return age
}
// b.js
var a = require('a.js')
console.log(a.name) // 'morrain'
a.name = 'rename'
var b = require('a.js')
console.log(b.name) // 'rename'
- 引用拷贝还是值拷贝的问题(CMJ 是值拷贝)
// a.js
var name = 'morrain'
var 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
- 运行时加载 / 编译时加载(多阶段,异步)ESM
AMD
AMD就是一个异步加载模块的模块化规范。AMD的实现就是require.js AMD规范定义了一个函数define,通过define方法定义模块
define(id?, dependencies?, factory);
- id:可选参数,用来定义模块的标识,如果没有提供该参数,模块标识就取脚本文件名(去掉扩展名)。
- dependencies:可选参数,用来传入当前模块依赖的模块名称数组。
- factory:工厂方法,模块初始化要执行的函数或对象,如果是函数,它只被执行一次。如果是对象,此对象应该为模块的输出值。定义模块例子:
define('a', function () {
console.log('a load')
return {
run: function () { console.log('a run') }
}
})
在require.js 中还有require.config和require,我们以加载一个jquery库为例:
require.config({
paths : {
"jquery" : ["http://libs.baidu.com/jquery/2.0.3/jquery"]
}
})
//这时做完上面的操作后我们就可以直接用jquery来引入模块,不用后面那一大串CDN地址了
require(["jquery"],function($){
$(function(){
alert("load finished");
})
})
依赖前置,换句话说,在解析和执行当前模块之前,模块作者必须指明当前模块所依赖的模块。
代码在一旦运行到此处,能立即知晓依赖。而无需遍历整个函数体找到它的依赖,因此性能有所提升,缺点就是开发者必须显式得指明依赖——这会使得开发工作量变大,比如:当你写到函数体内部几百上千行的时候,忽然发现需要增加一个依赖,你不得不回到函数顶端来将这个依赖添加进数组。
define('a', function () {
console.log('a load')
return {
run: function () { console.log('a run') }
}
})
define('b', function () {
console.log('b load')
return {
run: function () { console.log('b run') }
}
})
require(['a', 'b'], function (a, b) {
console.log('main run')
a.run()
b.run()
})
// a load
// b load
// main run
// a run
// b run
//从这个打印结果我们可以看出,ADM在require时就把所有模块都加载了并没有管你有没有用到他,这就是依赖前置。
简单实现
const def = new Map();
// AMD mini impl
const defaultOptions = {
paths: ''
}
// From CDN 加载CDN (System 加载库)
const __import = (url) => {
return new Promise((resove, reject) => {
System.import(url).then(resove, reject)
})
}
// normal script 读取路径
const __load = (url) => {
return new Promise((resolve, reject) => {
const head = document.getElementsByTagName('head')[0];
const node = document.createElement('script');
node.type = 'text/javascript';
node.src = url;
node.async = true;
node.onload = resolve;
node.onerror = reject;
head.appendChild(node)
})
}
// 为啥没写 let const var
// 千万不要在实际使用这种比较 low 的方式 ?
rj = {};
rj.config = (options) => Object.assign(defaultOptions, options);
// 定义模块,触发的时机其实是在 require 的时候,所以 -> 收集
define = (name, deps, factory) => {
// todo 参数的判断,互换
def.set(name, { name, deps, factory });
}
// dep -> a -> a.js -> 'http:xxxx/xx/xx/a.js';
const __getUrl = (dep) => {
const p = location.pathname;
return p.slice(0, p.lastIndexOf('/')) + '/' + dep + '.js';
}
// 其实才是触发加载依赖的地方
require = (deps, factory) => {
return new Promise((resolve, reject) => {
Promise.all(deps.map(dep => {
// 走 CDN
if (defaultOptions.paths[dep]) return __import(defaultOptions.paths[dep]);
return __load(__getUrl(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))
}
CMD
代表技术实现 Seajs CMD例子:
//function有三个参数: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')
var a = require('a')
a.run()
var b = require('b')
b.run()
})
seajs.use('main')
我们可以看到我们的依赖是用require按需要来引入的。这就是依赖后置。而且我们需要调用seajs.use()方法来执行这个模块。
简单实现
const modules = {};
const exports = {};
sj = {};
const toUrl = (dep) => {
const p = location.pathname;
return p.slice(0, p.lastIndexOf('/')) + '/' + dep + '.js';
}
const getDepsFromFn = (fn) => {
let matches = [];
// require('a ')
//1. (?:require\() -> require( -> (?:) 非捕获性分组
//2. (?:['"]) -> require('
//3. ([^'"]+) -> a -> 避免回溯 -> 回溯 状态机
let reg = /(?:require\()(?:['"])([^'"]+)/g; // todo
let r = null;
while((r = reg.exec(fn.toString())) !== null) {
reg.lastIndex
matches.push(r[1])
}
return matches
}
const __load = (url) => {
return new Promise((resolve, reject) => {
const head = document.getElementsByTagName('head')[0];
const node = document.createElement('script');
node.type = 'text/javascript';
node.src = url;
node.async = true;
node.onload = resolve;
node.onerror = reject;
head.appendChild(node)
})
}
// 依赖呢?
// 提取依赖: 1. 正则表达式 2. 状态机
define = (id, factory) => {
const url = toUrl(id);
const deps = getDepsFromFn(factory);
if (!modules[id]) {
modules[id] = { url, id, factory, deps }
}
}
const __exports = (id) => exports[id] || (exports[id] = {});
const __module = this;
// 这里面才是加载模块的地方
const __require = (id) => {
return __load(toUrl(id)).then(() => {
// 加载之后
const { factory, deps } = modules[id];
if (!deps || deps.length === 0) {
factory(__require, __exports(id), __module);
return __exports(id);
}
return sj.use(deps, factory);
})
}
sj.use = (mods, callback) => {
mods = Array.isArray(mods) ? mods : [mods];
return new Promise((resolve, reject) => {
Promise.all(mods.map(mod => {
return __load(toUrl(mod)).then(() => {
const { factory } = modules[mod];
return factory(__require, __exports(mod), __module)
})
})).then(resolve, reject)
}).then(instances => callback && callback(...instances))
}
补充
有时候遇到递归调用,总是觉得非常绕,这时候我们可以试着拆开来看待,把它假设成为一个最基础简单的单过程
假设 模块 A 依赖 模块 B , 模块 B 则 无需依赖
我们简化下模型
假设只加载模块 B
定义 B 模块
define('B', function () {
console.log('B load')
return {
run: function () { console.log('B run') }
}
})
引入 模块 B
require(["B"],function(B){
B.run()
})
实现
require = (deps, factory) => {
return new Promise((resolve, reject) => {
// 循环调用依赖
Promise.all(deps.map(dep => {
// 走 CDN
if (defaultOptions.paths[dep]) return __import(defaultOptions.paths[dep]);
// 本地引用
return __load(__getUrl(dep)).then(() => {
const { deps, factory } = def.get(dep); // 获取 define 时存储的 B 模块
return factory(null); // 执行 B 模块, 返回 B 模块 return 结果
})
})).then(resolve, reject) // 将 ↑ 执行结果放回 resolve 的值为 Promise.all 执行完成后的 factory 范围值 数组
})
.then(instances => factory(...instances)) // 返回结果 执行 require 的 回调函数 factory
}
当 A 模块存在引用 B 模块时
定义 A 模块
define('A', ['B'], function (B) {
console.log('A load')
return {
run: function () { B.run() }
}
})
引入 模块 A 时则要去检测 A 模块需要依赖的模块,当假设每个模块都有自己的依赖时,这个过程就像是一个树状图,不断地调用 各自的 require,每个树节点都去加载自己节点分支模块,符合递归调用的条件。
递归调用需要一个执行条件,条件就是该模块是否需要依赖
return __load(__getUrl(dep)).then(() => {
const { deps, factory } = def.get(dep); // 取出 模块 检查 是否需要依赖
if (deps.length === 0) return factory(null); // 当依赖长度部位0时
return require(deps, factory) // 否则 加载 自身依赖
})
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!