js本身的问题:
不具有模块化的语法规则,在语言层面没有命名空间。
JavaScript 编程过程中很多时候,我们都在修改变量,在一个复杂的项目开发过程中,如何管理函数和变量作用域,显得尤为重要。
function m1(){
//...
}
function m2(){
//...
}
- 通用模块将所有函数方法暴露给全局作用域,造成命名冲突。
- 多个
script
标签在解析过程中,按照从上到下的顺序解析,如果有依赖规则,必须按照执行顺序,被依赖者先执行,依赖者后执行。
模块化的作用:
- 避免命名冲突
- 依赖管理
- 提供可维护和可复用的代码
- 对象写法:函数m1()和m2(),都封装在module1对象里。使用的时候,就是调用这个对象的属性。module1.m1();但是,这样的写法会暴露所有模块成员,内部状态可以被外部改写。比如,外部代码可以直接改变内部计数器的值。
module1._count = 5
;
var module1 = new Object({
_count : 0,
m1 : function (){
//...
},
m2 : function (){
//...
}
});
- 命名空间:顶级的应用命名空间被挂载到唯一一个全局对象上的对象(在浏览器中是 window,在 Node.js 应用中是 global)
var MYNAMESPACE = MYNAMESPACE || {};
MYNAMESPACE.person = function(name) {
this.name = name;
};
MYNAMESPACE.person.prototype.getName = function() {
return this.name;
};
// 使用方法
var p = new MYNAMESPACE.person("doc");
p.getName();
// 嵌套的命名空间
var myMasterNS = myMasterNS || {};
myMasterNS.mySubNS = myMasterNS.mySubNS || {};
myMasterNS.mySubNS.someFunction = function(){
//插入逻辑
};
- 闭包:匿名自执行函数,外部代码无法直接改变内部计数器的值。
module1._count = 5
;
var module1 = (function(){
var _count = 0;
var m1 = function(){
//...
};
var m2 = function(){
//...
};
return {
m1 : m1,
m2 : m2
};
})();
- 继承模块:
var module1 = (function (mod){
mod.m3 = function () {
//...
};
return mod;
})(module1);
// 模块可能为空
var module1 = ( function (mod){
//...
return mod;
})(window.module1 || {});
解决方案:
模块化 CJS、AMD、CMD、UMD、ESM
统一模块规范
commonjs:
var MySalute = "Hello";
module.exports = MySalute;
// world.js
var MySalute = require("./salute");
var Result = MySalute + " world!";
console.log(Result);
- node环境
CommonJS规范
所有代码都运行在模块作用域,不会污染全局作用域。
模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。
模块加载的顺序,按照其在代码中出现的顺序。
var math = require('math');
var math = require('math');
math.add(2,3); // 5
- 浏览器环境
Uncaught ReferenceError: require is not defined
浏览器不兼容CommonJS的根本原因,在于缺少四个Node.js环境的变量:
module
exports
require
global
浏览器加载 CommonJS 模块的原理与实现browserify
第二行math.add(2, 3),在第一行require('math')之后运行,因此必须等math.js加载完成。也就是说,如果加载时间很长,整个应用就会停在那里等。这对服务器端不是一个问题,因为所有的模块都存放在本地硬盘,可以同步加载完成,等待时间就是硬盘的读取时间。但是,对于浏览器,这却是一个大问题,因为模块都放在服务器端,等待时间取决于网速的快慢,可能要等很长时间,浏览器处于"假死"状态。因此,浏览器端的模块,不能采用"同步加载"(synchronous),只能采用"异步加载"(asynchronous)。这就是AMD规范诞生的背景。
AMD:
以浏览器第一的原则发展,异步加载模块。主要有两个Javascript库实现了AMD规范:require.js和curl.js。
- require.js
js 文件越来越大,需要同时加载多个js文件。
<script src="1.js"></script>
<script src="2.js"></script>
<script src="3.js"></script>
<script src="4.js"></script>
<script src="5.js"></script>
<script src="6.js"></script>
问题:
- 加载的时候,浏览器会停止网页渲染,加载文件越多,网页失去响应的时间就会越长
- 其次,由于js文件之间存在依赖关系,因此必须严格保证加载顺序(比如上例的1.js要在2.js的前面),依赖性最大的模块一定要放到最后加载,当依赖关系很复杂的时候,代码的编写和维护都会变得困难。
require.js的诞生,就是为了解决这两个问题:
(1)实现js文件的异步加载,避免网页失去响应;
(2)管理模块之间的依赖性,便于代码的编写和维护。
require([module], callback);
require(['math'], function (math) {
math.add(2, 3);
});
require.js的加载:
<script src="js/require.js" defer async="true" ></script>
// IE不支持这个属性,只支持defer,所以把defer也写上。
// 加载requirejs文件,也可能造成网页失去响应。
// 解决办法有两个,一个是把它放在网页底部加载,
// 另一个是写成上面这样
// 加载我们自己的代码了。假定我们自己的代码文件是main.js,也放在js目录下面。
<script src="js/require.js" data-main="js/main"></script>
CMD:lazyload
依赖就近
define(function(require, exports, module) {
var clock = require('clock');
clock.start();
});
AMD和CMD最大的区别是对依赖模块的执行时机处理不同,二者皆为异步加载模块。
AMD依赖前置,js可以方便知道依赖模块是谁,立即加载;而CMD就近依赖,需要使用把模块变为字符串解析一遍才知道依赖了那些模块,这也是很多人诟病CMD的一点,牺牲性能来带来开发的便利性,实际上解析模块用的时间短到可以忽略。
// CMD
define(function(require, exports, module) {
var a = require('./a')
a.doSomething() // 此处略去 100 行
var b = require('./b') // 依赖可以就近书写
b.doSomething() //
...
})
// AMD
define(['./a', './b'], function(a, b) { // 依赖必须一开始就写好
a.doSomething() // 此处略去 100 行
b.doSomething()
...
})
UMD:
UMD先判断是否支持Node.js的模块(exports)是否存在,存在则使用Node.js模块模式。在判断是否支持AMD(define是否存在),存在则使用AMD方式加载模块。
(function (window, factory) {
if (typeof exports === 'object') {
module.exports = factory();
} else if (typeof define === 'function' && define.amd) {
define(factory);
} else {
window.eventUtil = factory();
}
})(this, function () {
//module ...
});
ES6 Module
<script src="path/to/myModule.js" defer></script>
<script src="path/to/myModule.js" async></script>
// defer与async的区别是:
// defer要等到整个页面在内存中正常渲染结束(DOM 结构完全生成,以及其他脚本执行完成),才会执行;
// async一旦下载完,渲染引擎就会中断渲染,执行这个脚本以后,再继续渲染。
// 一句话,defer是“渲染完再执行”,async是“下载完就执行”。
// 另外,如果有多个defer脚本,会按照它们在页面出现的顺序加载,
// 而多个async脚本是不能保证加载顺序的。
浏览器:
type
属性设为module
,浏览器知道这是一个 ES6 模块。
浏览器对于带有type="module"
的<script>
,都是异步加载,不会造成堵塞浏览器,即等到整个页面渲染完,再执行模块脚本,等同于打开了<script>
标签的defer
属性。
<script type="module" src="./foo.js"></script>
如果网页有多个<script type="module">
,它们会按照在页面出现的顺序依次执行。
<script>
标签的async
属性也可以打开,这时只要加载完成,渲染引擎就会中断渲染立即执行。执行完成后,再恢复渲染。
<script type="module" src="./foo.js" async></script>
一旦使用了async属性,<script type="module">
就不会按照在页面出现的顺序执行,而是只要该模块加载完成,就执行该模块。
ES6 模块也允许内嵌在网页中,语法行为与加载外部脚本完全一致。
<script type="module">
import utils from "./utils.js";
// other code
</script>
node:
Node.js 要求 ES6 模块采用.mjs
后缀文件名。也就是说,只要脚本文件里面使用import
或者export
命令,那么就必须采用.mjs
后缀名。Node.js 遇到.mjs
文件,就认为它是ES6
模块,默认启用严格模式,不必在每个模块文件顶部指定"use strict"
。
如果不希望将后缀名改成.mjs
,可以在项目的package.json
文件中,指定type
字段为module
。
如果这时还要使用 CommonJS
模块,那么需要将 CommonJS
脚本的后缀名都改成.cjs
。
CommonJS和ES6的区别:
CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
CommonJS 模块的require()是同步加载模块,ES6 模块的import命令是异步加载,有一个独立的模块依赖的解析阶段。
CommonJs
// lib.js
var counter = 3;
function incCounter() {
counter++;
}
module.exports = {
counter: counter,
incCounter: incCounter,
};
// main.js
var mod = require('./lib');
console.log(mod.counter); // 3
mod.incCounter();
console.log(mod.counter); // 3
// lib.js
var counter = 3;
function incCounter() {
counter++;
}
module.exports = {
get counter() {
return counter
},
incCounter: incCounter,
};
ES6
// lib.js
export let counter = 3;
export function incCounter() {
counter++;
}
// main.js
import { counter, incCounter } from './lib';
console.log(counter); // 3
incCounter();
console.log(counter); // 4