模块化机制

455 阅读8分钟

github 地址: 戳这里

模块化机制

CommonJS规范

应用于服务器端,同步加载

nodejs有一个模块加载系统

每个文件被视为一个独立的模块

module.exports属性可以被赋予一个新的值(例如函数或对象)

module.filename === __filename

module === require.main

require.resolve返回路径

module.exports

module.exports 对象是由模块系统创建, 可以导出一个对象或者函数。

module.exports === exports // true

也就是说exportsmodule.exports是指向同一内存的。而模块导出的时候实际是导出module.exports的值

缓存:

模块在第一次加载后会被缓存,是基于其文件名来缓存的

核心模块:

nodejs/lib目录下,require(),会优先加载核心模块

循环:

当循环调用require()时,一个模块可能在为完成执行被返回

例子:

a.js

console.log('a 开始');
exports.done = false;
const b = require('./b.js');
console.log('在 a 中,b.done = %j', b.done);
exports.done = true;
console.log('a 结束');

b.js

console.log('b 开始');
exports.done = false;
const a = require('./a.js');
console.log('在 b 中,a.done = %j', a.done);
exports.done = true;
console.log('b 结束');

main.js

console.log('main 开始');
const a = require('./a.js');
const b = require('./b.js');
console.log('在 main 中,a.done=%j,b.done=%j', a.done, b.done);

结果:

main 开始
a 开始
b 开始
在 b 中,a.done = false
b 结束
在 a 中,b.done = true
a 结束
在 main 中,a.done=true,b.done=true

a.jsrequire b.js, 之后又在b.jsrequire a.js,此时的 const a = require('./a.js');a是在a.js中require(b.js)语句之前导出的对象

官方解释:

main.js 加载 a.js 时,a.js 又加载 b.js。 此时,b.js 会尝试去加载 a.js。 为了防止无限的循环,会返回一个 a.jsexports 对象的 未完成的副本 给 b.js 模块。 然后 b.js 完成加载,并将 exports 对象提供给 a.js 模块。

文件模块:

如果没有找到确切的模块,文件拓展名会尝试加上.js.json.node

从 node_modules 目录加载

require一个非核心模块,也没有'/''../''./' 开头。则会从当前模块的父目录开始,尝试从它的 /node_modules 目录里加载模块。没有找到就继续父目录

AMD/CMD规范

用于浏览器端,由于要保证效率,所以需要异步加载

AMD规范实现: RequireJS

AMD规范的实现

特点

  1. 预加载(加载某个模块前,其依赖模块需要先加载),在并行加载js文件同时,还会解析执行该模块

  2. 当在head中引用js时,是阻塞的,只也是为什么推荐放在body尾。但引用了RequireJS就能解决这个问题,引入RequireJS异步加载

判断是否支持AMD规范

typeof define === "function" && define.amd

起步:

  1. index.html中引入requireJS

<script data-main="./js/app.js" src="./js/require.js"></script>

data-main指向config文件

// ./js/app.js

require.config({
	baseUrl:'../',
    paths: {
        co: '../node_modules/co/index'
    }
});
require(['./js/a.js'], function (data) {
	console.log(data)
})

// a.js是定义的一个模块,用define定义

define(function () {
	var result = {
		a: 1, 
		b: 2
	}
	alert('haha')
	return result
})
  1. 在引用模块时,可以require(arr, callback),也可以用define(arr, callback),再次导出一个对象,供别的模块使用
  2. 对于第三方模块,需要用shim来加载非AMD模块
require.config({
    shim: {
        "underscore" : {
            exports : "_";
        }
    }
})

原理

RequireJS使用head.appendChild()将每一个依赖加载为一个script标签。

RequireJS等待所有的依赖加载完毕,计算出模块定义函数正确调用顺序,然后依次调用它们。

如: <script type="text/javascript" charset="utf-8" async="" data-requirecontext="_" data-requiremodule="co" src="./js/../node_modules/co/index.js"></script>

标注async属性的脚本是异步脚本,即异步下载脚本时,不会阻塞文档解析,但是一旦下载完成后,立即执行,阻塞文档解析, 不能保证执行顺序。

优先级高于defer

因此需要确认脚本间没有依赖关系

优点

加载快速,尤其遇到多个大文件,因为并行解析,所以同一时间可以解析多个文件。

缺点

并行加载,异步处理,处理顺序不一定,可能会造成一些困扰,甚至为程序埋下大坑。

CMD规范实现:SeaJS

判断:

typeof define === "function" && define.cmd

特点:

  1. 就近加载
  2. 模块导出

既可以用exports对象来导出,也可以用return导出

模块定义:

define(id?, deps?, function(require, exports, module) {

  // 模块代码

});

数组 deps 是模块依赖,带有iddepsdefine不属于CMD规范

异步加载

require.async

使用范例:

// index.html

<script src="./js/sea.js"></script>
<script>
	seajs.config({
		base: "./",
		alias: {
			"c": "js/c.js",
			"d": "js/d.js",
			"jquery": "node_modules/co/index.js"
		}
	});
	seajs.use('c');
</script>

// c.js

define(function (require, exports, module) {
	require.async('d', function (obj) {
		console.log(obj)
	})
	console.log('sucess')
	// success
	// {a:1,b:2}
})

// d.js

define((function (require, exports, module) {
	return {
		a:1,
		b:2
	}
}))

如何工作

SeaJS采用了和Node很相似的CMD规范

Seajs分为模块加载期和执行期,加载期需要将执行期所有用到的模块从服务端同步过来,在再执行期按照代码的逻辑顺序解析执行模块。

动态添加script标签, 在network中能看到js加载,但是Element中没有插入

优点

只有在使用的时候才会解析执行js文件,因此,每个JS文件的执行顺序在代码中是有体现的,是可控的

缺点

执行等待时间会叠加。因为每个文件执行时是同步执行(串行执行),因此时间是所有文件解析执行时间之和,尤其在文件较多较大时,这种缺点尤为明显。

ES6模块化

解决在node中import报错问题

npm install --save-dev babel-cli

npm babel-node your-script.js

或者

js文件命名为.mjs

node --experimental-modules your-script.mjs

模块加载

  1. 运行时加载

如CommonJS规范,在require的时候是全部加载的

  1. 编译时加载(静态加载)

如import,只加载导入的变量或方法,其它未导入的不加载

import

好处

  1. 不再需要UMD格式
  2. 不再需要对象最为命名空间,如Math

使用严格模式

use strict

  • 变量必须声明后再使用
  • 函数的参数不能有同名属性,否则报错
  • 不能使用with语句
  • 不能对只读属性赋值,否则报错
  • 不能使用前缀 0
  • 表示八进制数,否则报错
  • 不能删除不可删除的属性,否则报错
  • 不能删除变量delete prop,会报错,只能删除属性delete global[prop]
  • eval不会在它的外层作用域引入变量
  • eval和arguments不能被重新赋值
  • arguments不会自动反映函数参数的变化
  • 不能使用arguments.callee
  • 不能使用arguments.caller
  • 禁止this指向全局对象
  • 不能使用fn.caller和fn.arguments获取函数调用的堆栈
  • 增加了保留字(比如protected、static和interface)

export

export var a = 1;
export const b = function () {}

或者

var a = 1;
var b = 2;
var c = 3;

export { a, b ,c}

或者

function v1 () {}

export {
    v1 as a
}


错误应用及修正

function f () {}
export f // 报错

export function f () {} // 正确

export default function() {} // 正确, 加载时整体加载

等同于

export {add as default};

注意

export 输出的是与值动态绑定的接口

// b.js

var a = 1

function add () {
	a++
}

export {a, add}

// a.js

import {
	add,
	a
} from './b.js'


console.log(a);
add();
console.log(a)

而CommonJS中输出的是值的缓存

// b.js

var a = 1

function add () {
	a++
}

module.exports = {a, add}

// a.js

var obj = require('./b.js')

console.log(obj.a); // 1
obj.add();
console.log(obj.a) // 1

export只能出现在模块顶层

import

加载的变量是const常量,是只读的。

import命令具有提升效果,意思加载的函数可以再之前执行

import是静态执行,不能使用表达式或者变量

import和export同时应用

export { foo, bar } from 'my_module';

foo, 和bar在当前模块不能用,相当于转发出去了

import和export命令只能在模块的顶层,不能在代码块之中

UMD中判断模块如何加载

  • AMD或者CMD

typeof define === "function"

  • CommonJS规范

typeof module === "object" && typeof module.exports === "object"

es6模块和CommonJS的区别

  1. ES6 模块之中,顶层的this指向undefined;CommonJS 模块的顶层this指向当前模块,这是两者的一个重大差异
  2. CommonJS是运行时加载,ES6模块是编译时加载
  3. ES6模块没有arguments,require,module,exports,__filename,__dirname
  4. 不允许import CommonJS模块,如import {readFile} from 'fs'
  5. 对于循环加载,CommonJS加载有缓存机制(输出值的拷贝),循环加载时是加载已经到处的变量,
// CommonJS

// a.js
exports.done = false;
console.log('begin'); // 在循环加载a.js的时候不再执行,只是取得已经导出的
var b = require('./b.js');
console.log('在 a.js 之中,b.done = %j', b.done);
exports.done = true;
console.log('a.js 执行完毕');

// b.js
exports.done = false;
var a = require('./a.js');
console.log('在 b.js 之中,a.done = %j', a.done);
exports.done = true;
console.log('b.js 执行完毕');

ES6模块循环加载时,那些变量不会被缓存,而是指向被加载模块的引用


// a.mjs

let done = 1
console.log('haha')
let foo = function foo () {
	console.log('foo')
}
export {foo, done};
import './b.mjs'


// b.mjs

import {foo} from './a.mjs'
console.log('b====')
foo();

// b====
// ReferenceError: foo is not defined

解决:写成函数声明,函数具有提升作用

// a.js

let done = 1
console.log('haha')
function foo () {
	console.log('foo')
}
export {foo, done};
import './b.mjs'

AMD和CMD的区别

  1. AMD推崇依赖前置,在定义模块的时候就要声明其依赖的模块 ,CMD推崇就近依赖,只有在用到某个模块的时候再去require
  2. CMD加载完某个依赖模块后并不执行,只是下载而已,在所有依赖模块加载完成后进入主逻辑,遇到require语句的时候才执行对应的模块,这样模块的执行顺序和书写顺序是完全一致的。AMD在加载模块完成后就会执行改模块,所有模块都加载执行完后会进入require的回调函数, 但是主逻辑一定在所有依赖加载完成后才执行。

也就是说,AMD先加载,先执行,CMD先加载,延迟执行