-
Notifications
You must be signed in to change notification settings - Fork 4.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
ES6 系列之模块加载方案 #108
Comments
其实我比较想知道作者这些文章的主题是怎么想出来的 |
mark 🤞 |
写得非常好,最近看了朴灵深入浅出nodejs,相得益彰 |
大佬,我可以转发到我的公众号吗?会标注作者和来源的 |
楼主,你好,关于CommonJS ,有点疑问想请教你一下: 先看代码: /*************** child.js**********************/
let foo = 1
setTimeout(()=>{
foo=2;
exports.foo= foo
},1000)
exports.foo=foo
/*******************index.js***************************/
var test =require('./child');
console.log(test.foo);// 1
setTimeout(()=>{
console.log(test.foo) // 2
},2000)
把上面的 /*************** child.js**********************/
let foo = 1
setTimeout(()=>{
foo=2;
module.exports={foo}
},1000)
module.exports={foo}
/*******************index.js***************************/
var test =require('./child');
console.log(test.foo);// 1
setTimeout(()=>{
console.log(test.foo) // 1
},2000) 按理来说暴露为一个新对象是应该要被更新的 在此有两个疑问想请教你:
|
博主是不是可以这么理解:要是CDM的require全都写在头部,那就跟AMD加载顺序无异了,CMD只是能够自己定义加载的位置而已 |
这个应该是解构的原因 |
@zikuai 我试了下,感觉解构出来的foo还是引用类型~ 比如: var obj={
a:{
y:1
}
};
var {a}=obj;
console.log(a); // {y: 1}
obj.a.y=3
console.log(a); // {y: 3}
a=5
console.log(a); // 5 这个该怎么解释呢?请大神指点~ |
这里解构拿到的变量a是个对象,是引用类型,所以会跟着变;例子中的foo是个普通变量,参考博主的解释:
|
我怎么感觉和module.export有关系。。。 |
请问下, |
@divasatanica |
@zikuai @wd2010 @snoopy1412 这块大家可以了解下在node.js中模块导出内容时 exports 和 module.exports 之间的区别(exports 就是 module.exports 的引用)。 但是如果你在 child.js 是通过 module.exports 去导出一个全新的对象。比如你的例子给的是 let foo = 1
setTimeout(() => {
foo=2
module.exports = { foo }
}, 1000)
module.exports = { foo } 导出的为一个全新的对象,且 foo 为一个数值类型,那么不管你在 child.js 里面如何改变 foo 的值,在 main.js 都没法感知的,除非你定义的 foo 为一个引用类型。另外你们可能疑惑的一点就是在 setTimeout 里面重新 举2个例子: // child.js
let foo = {
a: 1
}
setTimeout(() => {
foo.a = 2
exports.foo = foo
}, 1000)
// main.js
var test = require('./child')
setTimeout(() => {
console.log(test.foo.a) // 2
}, 2000) 另外一个 // child.js
let foo = 1
setTimeout(() => {
foo = 2
module.exports = { foo }
}, 1000)
// main.js
var test = require('./child')
setTimeout(() => {
console.log(test.foo) // undefined
}, 2000) |
@divasatanica 这里可以理解为webpack最后编译出来的代码有一套自己的模块系统,如果是同一个 chunk 的不同模块当然是同步加载的,如果你在源码当中使用了 webpack 提供的异步加载模块的语法,例如: |
你好
举个例子 let foo = 1;
const obj = {
foo
}
// 等价于
let foo = 1;
const obj = {
foo: 1
} 所以无论怎么更改foo obj.foo的值永远是1 |
具体实践也可以看看这篇文章: |
前言
本篇我们重点介绍以下四种模块加载规范:
最后再延伸讲下 Babel 的编译和 webpack 的打包原理。
require.js
在了解 AMD 规范之前,我们先来看看 require.js 的使用方式。
项目目录为:
index.html
的内容如下:data-main="vender/main"
表示主模块是vender
下的main.js
。main.js
的配置如下:require 的第一个参数表示依赖的模块的路径,第二个参数表示此模块的内容。
由此可以看出,
主模块
依赖add 模块
和square 模块
。我们看下
add 模块
即add.js
的内容:requirejs
为全局添加了define
函数,你只要按照这种约定的方式书写这个模块即可。那如果依赖的模块又依赖了其他模块呢?
我们来看看
主模块
依赖的square 模块
,square 模块
的作用是求出一个数字的平方,比如输入 3 就返回 9,该模块依赖一个乘法模块
,该乘法模块即multiply.js
的代码如下:而
square 模块
就要用到multiply 模块
,其实写法跟 main.js 添加依赖模块一样:require.js 会自动分析依赖关系,将需要加载的模块正确加载。
requirejs 项目 Demo 地址:https://github.com/mqyqingfeng/Blog/tree/master/demos/ES6/module/requirejs
而如果我们在浏览器中打开
index.html
,打印的顺序为:AMD
在上节,我们说了这样一句话:
那这个约定的书写方式是指什么呢?
指的便是 The Asynchronous Module Definition (AMD) 规范。
所以其实 AMD 是 RequireJS 在推广过程中对模块定义的规范化产出。
你去看 AMD 规范 的内容,其主要内容就是定义了 define 函数该如何书写,只要你按照这个规范书写模块和依赖,require.js 就能正确的进行解析。
sea.js
在国内,经常与 AMD 被一起提起的还有 CMD,CMD 又是什么呢?我们从
sea.js
的使用开始说起。文件目录与 requirejs 项目目录相同:
index.html
的内容如下:main.js 的内容如下:
add.js 的内容如下:
square.js 的内容如下:
multiply.js 的内容如下:
跟第一个例子是同样的依赖结构,即 main 依赖 add 和 square,square 又依赖 multiply。
seajs 项目 Demo 地址:https://github.com/mqyqingfeng/Blog/tree/master/demos/ES6/module/seajs
而如果我们在浏览器中打开
index.html
,打印的顺序为:CMD
与 AMD 一样,CMD 其实就是 SeaJS 在推广过程中对模块定义的规范化产出。
你去看 CMD 规范的内容,主要内容就是描述该如何定义模块,如何引入模块,如何导出模块,只要你按照这个规范书写代码,sea.js 就能正确的进行解析。
AMD 与 CMD 的区别
从 sea.js 和 require.js 的例子可以看出:
1.CMD 推崇依赖就近,AMD 推崇依赖前置。看两个项目中的 main.js:
2.对于依赖的模块,AMD 是提前执行,CMD 是延迟执行。看两个项目中的打印顺序:
AMD 是将需要使用的模块先加载完再执行代码,而 CMD 是在 require 的时候才去加载模块文件,加载完再接着执行。
感谢
感谢 require.js 和 sea.js 在推动 JavaScript 模块化发展方面做出的贡献。
CommonJS
AMD 和 CMD 都是用于浏览器端的模块规范,而在服务器端比如 node,采用的则是 CommonJS 规范。
导出模块的方式:
引入模块的方式:
我们将之前的例子改成 CommonJS 规范:
CommonJS 项目 Demo 地址:https://github.com/mqyqingfeng/Blog/tree/master/demos/ES6/module/commonJS
如果我们执行
node main.js
,打印的顺序为:跟 sea.js 的执行结果一致,也是在 require 的时候才去加载模块文件,加载完再接着执行。
CommonJS 与 AMD
引用阮一峰老师的《JavaScript 标准参考教程(alpha)》:
ES6
ECMAScript2015 规定了新的模块加载方案。
导出模块的方式:
引入模块的方式:
我们再将上面的例子改成 ES6 规范:
目录结构与 requirejs 和 seajs 目录结构一致。
注意!浏览器加载 ES6 模块,也使用
<script>
标签,但是要加入type="module"
属性。ES6-Module 项目 Demo 地址:https://github.com/mqyqingfeng/Blog/tree/master/demos/ES6/module/ES6
值得注意的,在 Chrome 中,如果直接打开,会报跨域错误,必须开启服务器,保证文件同源才可以有效果。
为了验证这个效果你可以:
然后进入该目录,执行
在浏览器打开
http://localhost:8080/
即可查看效果。打印的顺序为:
跟 require.js 的执行结果是一致的,也就是将需要使用的模块先加载完再执行代码。
ES6 与 CommonJS
引用阮一峰老师的 《ECMAScript 6 入门》:
第二个差异可以从两个项目的打印结果看出,导致这种差别的原因是:
重点解释第一个差异。
举个例子:
counter.js 模块加载以后,它的内部变化就影响不到输出的 mod.counter 了。这是因为 mod.counter 是一个原始类型的值,会被缓存。
但是如果修改 counter 为一个引用类型的话:
value 是会发生改变的。不过也可以说这是 "值的拷贝",只是对于引用类型而言,值指的其实是引用。
而如果我们将这个例子改成 ES6:
这是因为
Babel
鉴于浏览器支持度的问题,如果要使用 ES6 的语法,一般都会借助 Babel,可对于 import 和 export 而言,只借助 Babel 就可以吗?
让我们看看 Babel 是怎么编译 import 和 export 语法的。
是不是感觉有那么一点奇怪?编译后的语法更像是 CommonJS 规范,再看 import 的编译结果:
你会发现 Babel 只是把 ES6 模块语法转为 CommonJS 模块语法,然而浏览器是不支持这种模块语法的,所以直接跑在浏览器会报错的,如果想要在浏览器中运行,还是需要使用打包工具将代码打包。
webpack
Babel 将 ES6 模块转为 CommonJS 后, webpack 又是怎么做的打包的呢?它该如何将这些文件打包在一起,从而能保证正确的处理依赖,以及能在浏览器中运行呢?
首先为什么浏览器中不支持 CommonJS 语法呢?
这是因为浏览器环境中并没有 module、 exports、 require 等环境变量。
换句话说,webpack 打包后的文件之所以在浏览器中能运行,就是靠模拟了这些变量的行为。
那怎么模拟呢?
我们以 CommonJS 项目中的 square.js 为例,它依赖了 multiply 模块:
webpack 会将其包裹一层,注入这些变量:
那 webpack 又会将 CommonJS 项目的代码打包成什么样呢?我写了一个精简的例子,你可以直接复制到浏览器中查看效果:
最终的执行结果为:
参考
ES6 系列
ES6 系列目录地址:https://github.com/mqyqingfeng/Blog
ES6 系列预计写二十篇左右,旨在加深 ES6 部分知识点的理解,重点讲解块级作用域、标签模板、箭头函数、Symbol、Set、Map 以及 Promise 的模拟实现、模块加载方案、异步处理等内容。
如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。
The text was updated successfully, but these errors were encountered: