JS模块化扫盲
CodeLittlePrince opened this issue · 0 comments
模块化目的
- 避免命名冲突;
- 依赖处理;
分类
- AMD
- CMD
- Commonjs
- ES module
- brower(为了方便,后文用ES6-import-browser替代)
- webpack(为了方便,后文用ES6-import-webpack替代)
- node(为了方便,后文用ES6-import-node替代)
- UMD
AMD
定义
代表库
用法
1.html引入requirejs
和主js
:
<script src="/require.js"></script>
<script src="/index.js"></script>
主js:
// index.js
console.log('index')
// 加载入口模块
requirejs(['a'], function(a) {
console.log('index call')
a.speak()
});
模块a:
// a.js
console.log('a')
define(function(require, factory) {
console.log('a call')
var b = require('b')
var c = require('c')
return {
speak: b.speak,
say: c.say
}
});
模块b:
// b.js
define(function () {
console.log('b')
return {
speak: function () {
console.log('Hello World')
}
};
});
模块c:
// c.js
console.log('c')
define(function () {
console.log('c call')
return {
say: function () {
console.log('Wow')
}
};
});
最终,浏览器Console
会打印出:
index
a
b
b call
c
c call
a call
index call
Hello World
过程
-
script标签加载require.js。
-
script标签加载index.js。
-
index.js中通过requirejs函数定义该文件为入口文件。所有子模块(a、b、c)加载方式其实是在
<head>
里append对应的js,并且带async属性。 -
js按照
index.js=>a.js=>b.js
=>c.js
这样的顺序加载执行。所以,自然的先打印出index、a。又因为,b和c都是a所依赖的模块,所以b、c同时异步加载,谁加载完即先执行谁,因为两文件大小几乎一样,并且本地测试网络延迟可以忽略原因,先require谁就会先执行谁,并且,因为b、c都是最后一项被依赖的模块,所以回调就会被调用,因此打印出b、b call、c、c call。 -
a中的依赖逻辑处理完之后,才轮到a除了require之外的逻辑处理,所以继续打印出a call。
(因为它的实现可以看define的实现,其实是将define回调里的函数变为字符串,然后通过正则cjsRequireRegExp
获取到所有的模块名,加入依赖队列;接着,去请求js资源执行,同样,如果被依赖的js文件还有依赖模块,则加入依赖队列,没有则执行define里的回调函数;另外顺便说下,回调的返回值会被defined对象收集,实现可见localRequire实现,所以,如b=require('b')得到的值即是b.js的define回调的返回值) -
index中的依赖逻辑处理完之后,才轮到index除了require之外的逻辑处理,所以继续打印出index call。
优点
-
提升页面响应速度:js文件异步加载;
-
模块化成本低:只需要引入require.js就可以实现模块化;
缺点
-
代码执行顺序不按书写顺序:从文件a.js可以看出,require的模块执行顺序是在console.log('a call')之前,并不是按照书写的顺序那样;
-
无法按需加载:比如:if(false){ require('a') },这样的写法并不会就不去加载a.js的文件了,照样会加载;另外,像上面说到的,b、c可能因为文件大小或者网络原因,执行的顺序有可能不会像require的顺序一样。
CMD
定义
代表库
用法
1.html引入seajs
和主js
:
<script src="/seajs.js"></script>
<script src="/index.js"></script>
主js:
// index.js
console.log('index')
// 加载入口模块
seajs.use(['a'], function(a) {
console.log('index call')
a.speak()
});
模块a:
// a.js
console.log('a')
define(function(require, factory) {
console.log('a call')
var b = require('b')
var c = require('c')
// var flag = false
// if (flag) {
// var c = require.async('c')
// c.say()
// }
return {
speak: b.speak,
say: c.say
}
});
模块b:
// b.js
define(function () {
console.log('b')
return {
speak: function () {
console.log('Hello World')
}
};
});
模块c:
// c.js
console.log('c')
define(function () {
console.log('c call')
return {
say: function () {
console.log('Wow')
}
};
});
最终,浏览器Console
会打印出:
index
a
b
c
a call
b call
c call
index call
Hello World
过程
-
script标签加载sea.js。
-
script标签加载index.js。
-
index.js中通过seajs.use函数定义该文件为入口文件。所有子模块(a、b、c)加载方式其实是在
<head>
里append对应的js,并且带async属性。不过和requirejs不一样的是,seajs会用完后,把script给移除掉,为了减少内存占用。 -
js按照
index.js=>a.js=>b.js
=>c.js
这样的顺序加载执行。所以,自然的先打印出index、a。又因为,b和c都是a所依赖的模块,所以b、c同时异步加载,谁加载完即先执行谁,因为两文件大小几乎一样,并且本地测试网络延迟可以忽略原因,先require谁就会先执行谁。 -
和requirejs不一样的是,seajs不会加载完就立刻去执行define里的回调,而是等到父模块require的时候才去执行,所以我们看到,打印出的是a、b、c、a call、b call、c call。(上面说到,因为网络原因,c可能先于b加载完,那样的话打印出的就是a、c、b、a call、b call、c call)
-
index中的依赖逻辑处理完之后,才轮到index除了require之外的逻辑处理,所以继续打印出index call。
优点
-
提升页面响应速度:js文件异步加载;
-
模块化成本低:只需要引入require.js就可以实现模块化;
相比requirejs优点
-
按需执行:比如:if(false){ require('c') },这样的写法虽然还是会就去加载c.js的文件,但是不会执行c模块里的define回调,从而提升代码执行性能;
-
按需加载:比如:if(false){ require.async('a') },这样的写法就不去加载c.js的文件了;
缺点
- 代码执行顺序不按书写顺序:从文件a.js可以看出,require的模块执行顺序是在console.log('a call')之前,并不是按照书写的顺序那样;另外,像上面说到的,b、c可能因为文件大小或者网络原因,执行的顺序有可能不会像require的顺序一样。
CommonJS
定义
用法
1.因为commonjs是node的模块化方式,我们我们直接在控制台用:
node index.js
主js:
// index.js
console.log('index')
// 加载入口模块
var a = require('./a')
console.log('index call')
a.speak()
模块a:
// a.js
console.log('a')
var b = require('./b')
var c = require('./c')
// var flag = false
// if (flag) {
// var c = require('./c')
// c.say()
// }
console.log('a call')
module.exports = {
speak: b.speak,
say: c.say
}
模块b:
// b.js
console.log('b')
module.exports = {
speak: function () {
console.log('Hello World')
}
};
模块c:
// c.js
console.log('c')
module.exports = {
say: function () {
console.log('Wow')
}
};
最终,控制台会打印出:
index
a
b
c
a call
index call
Hello World
过程
- node执行入口文件index.js。
- 之后的代码全都按照顺序执行,没有什么弯子。
和AMD、CMD的区别
- 使用场景不同:commonjs用于node,即后端。
- 模块同步加载:因为不需要像浏览器端那样考虑文件请求性能而做成异步加载。
- 代码顺序执行:所有的代码全都按照顺序执行,没有花花肠子,直男。
ES6-import-browser
定义
1.html引入主js
:
<script type="module" src="/ES6-import/index.js"></script>
主js:
// index.js
console.log('index')
// 加载入口模块
import a from './a.js'
console.log('index call')
a.speak()
模块a:
// a.js
console.log('a')
import b from './b.js'
import c from './c.js'
// var flag = true
// if (flag) {
// import('./c.js')
// .then(c => {
// c.default.say()
// });
// }
console.log('a call')
export default {
speak: b.speak,
say: c.say
}
模块b:
// b.js
console.log('b')
module.exports = {
speak: function () {
console.log('Hello World')
}
};
模块c:
// c.js
console.log('c')
module.exports = {
say: function () {
console.log('Wow')
}
};
最终,浏览器Console
会打印出:
b
c
a
a call
index
index call
Hello World
过程
- script标签加载index.js,值得注意的是script标签多了一个type="module"的属性。index.js通过type="module"的方式引入,从而自身也是一个模块化的文件,这点我们可以在index.js文件里用var创建一个变量,比如var x = 7来验证,我们会发现所有文件执行完后,我们在控制台中输入x,输出的是x is not defined。
- index.js import了a,import会被最先执行,所以虽然console.log('index')在import之前,确实等a加载执行完之后才开始被执行。
- a.js加载完后,同样的,虽然console.log('a')在import之前,但得等b、c都加载执行完之后才开始被执行。b、c是异步请求的,但和AMD还有CMD不一样的是,它的执行并不会因为c先于b加载完而先执行c文件。而是按照代码顺序,直到b加载执行完,才轮到c。因此打印出了:b => c => a => a call。
- a执行完后,再回到index开始执行,所以继续打印:index => index call => Hello World
优点
-
提升页面响应速度:js文件异步加载;
-
模块化成本低:原生支持;
-
按需加载:比如:if(false){ import('a.js') },这样的写法就不去加载c.js的文件了;
-
引入模块代码按顺序执行:虽然还是不能解决:虽然console.log('a')在import之前,但执行却晚于这两个模块执行,但是,至少不会因为b、c可能因为文件大小或者网络原因,导致这两个文件执行顺序有有变化。
缺点
- 兼容性:高版本浏览器;(但感谢webpack,可以忽略兼容性)
ES6-import-webpack -- 在webpack中的es6 import
介绍
简单地说,就是因为es6虽然有import的能力了,但是因为兼容性不好,所以,目前市面上,都会选择用webpack来做js的模块化,这样对开发者来说,就可以愉快的使用import了。
1.html引入主js
:
<script src="/ES6-import-webpack/index.js"></script>
主js:
// index.js
console.log('index')
// 加载入口模块
import a from './a.js'
console.log('index call')
a.speak()
模块a:
// a.js
console.log('a')
import b from './b.js'
import c from './c.js'
// var flag = true
// if (flag) {
// import('./c.js')
// .then(c => {
// c.default.say()
// });
// }
console.log('a call')
export default {
speak: b.speak,
say: c.say
}
模块b:
// b.js
console.log('b')
module.exports = {
speak: function () {
console.log('Hello World')
}
};
模块c:
// c.js
console.log('c')
module.exports = {
say: function () {
console.log('Wow')
}
};
使用webpack打包
可以自行全局安装,或者局部安装webpack、webpack-cli。然后以index.js作为入口文件,编辑生成代码。
这里,我就直接生成了,文件为dist/main.js。
我们看下打包后的文件:
(() => {
"use strict";
console.log("b");
const o = {
speak: function () {
console.log("Hello World")
}
};
console.log("c");
const l = {
say: function () {
console.log("Wow")
}
};
console.log("a"), console.log("a call");
const s = {
speak: o.speak,
say: l.say
};
console.log("index"), console.log("index call"), s.speak()
})();
最终,浏览器Console
会打印出:
b
c
a
a call
index
index call
Hello World
其实是和es6 import一模一样的。
过程
- script标签加载index.js,然后顺序执行。
优点
-
兼容性:可认为除杠精浏览器外的所有版本浏览器;
-
零语法学习成本:和es6的模块化方式一模一样,没有语法上的学习成本;
-
按需加载:比如:if(false){ import('a.js') },这样的写法就不去加载c.js的文件了;
-
引入模块代码按顺序执行:虽然还是不能解决:虽然console.log('a')在import之前,但执行却晚于这两个模块执行,但是,至少不会因为b、c可能因为文件大小或者网络原因,导致这两个文件执行顺序有有变化。
缺点
- 提升页面响应速度:js都被加入一个文件中了,在目前http2.0时代,并不见得是一件好事;
- 学习成本:还得学webpack;
ES6-import-node -- 在node中的es6 import
介绍
毕竟node也是JavaScript,浏览器都支持import了,作为后端语言,不支持也不好意思吧。
当然,为了大一统,也是毕竟要做的一件事。
emmm,不过这个import真正被引入进Node其实挺晚的,实在v13才开始,以下是overflow里的引用。
Node.js >= v13
It's very simple in Node.js 13 and above. You need to either:
Save the file with .mjs extension, or
Add { "type": "module" } in the nearest package.json.
You only need to do one of the above to be able to use ECMAScript modules.Node.js <= v12
If you are using Node.js version 8-12, save the file with ES6 modules with .mjs extension and run it like:
node --experimental-modules my-app.mjs
1.我们我们直接在控制台用(如果node版本为13及以上,可以不用参数):
node --experimental-modules index.mjs
主js:
// index.js
console.log('index')
// 加载入口模块
import a from './a.js'
console.log('index call')
a.speak()
模块a:
// a.js
console.log('a')
import b from './b.js'
import c from './c.js'
console.log('a call')
export default {
speak: b.speak,
say: c.say
}
模块b:
// b.js
console.log('b')
export default {
speak: function () {
console.log('Hello World')
}
};
模块c:
// c.js
console.log('c')
export default {
say: function () {
console.log('Wow')
}
};
最终,浏览器Console
会打印出:
b
c
a
a call
index
index call
Hello World
并不像commonjs,这里的结果和浏览器端的表现一模一样。
UMD
介绍
UMD在我看来只是一种打包模式,做的是模块兼容的事情,所以不做具体例子。
使用的场景就是做npm包的时候,因为并不知道会被使用者用什么模块化方式引入,所以兼容了AMD、CommonJS的引入方式。
Demo地址
文中的所有demo地址都在:https://github.com/CodeLittlePrince/js-modules
有兴趣的小伙伴可以下载运行帮助理解。