文章目录
模块与模块化
-
理解
模块就是向外暴露的一些具有特定功能的js程序。一般就是一个js文件。这个文件里面包含了一些数据和对数据的操作,最后在通过指定的方式将他们暴露出去供外部使用
-
为什么要用模块?有什么好处
当js代码更多更复杂时,把它拆分成多个模块,不仅可以简化js的编写,也可以实现对js代码复用,提高了js的运行效率
-
模块化
模块是用来形容js文件的
模块化是用来形容项目或项目的编码方式的
模块化的编码方式:在进行项目的开发时,是以一个模块一个模块的形式进行编写的
组件与组件化
-
理解
组件就是一个界面的局部功能,这个组件包含了这个功能中的所有资源(html、css、js、img等)
-
为什么要用组件?有什么好处?
当一个界面的功能很复杂时,把它拆分成多个组件,不仅可以简化项目的编码,还提高的编码的复用,提高的js的运行效率
-
组件化
组件是用来形容界面或局部的功能界面
组件化是用来项目的:在项目开发时,是一个组件一个组件的进行开发,最后在将这些界面组成一个完整的功能界面
工程化
-
理解
工程化是一种思想,而不是技术。就是将一个项目看做一个工程进行分析、组织和构建。
可以简单的认为模块化和组件化都是工程化的表现形式
-
工程化的三个阶段
- 技术选型
- 构建阶段
- 模块化和组件化开发
-
为什么工程化开发?
是项目结构更加清晰、分工明确、提高开发效率
module的语法
-
ES6之前的模块加载方案:
浏览器方面的模块化:AMD和CMD
服务器方面的模块化:CommonJS(Node是CommonJS一个具有代表性的实现)
ES6提出的模块加载方案:(服务器和浏览器通用)
ES6的模块:编译时就能确定模块的依赖关系,以及输入和输出的变量。
CommonJS 和 AMD 模块:只能在运行时确定这些东西
ES6的模块思想:尽量静态化,在编译时就可以确定模块的依赖关系,以及输入和输出的变量。
ES6的加载实质:从模块中引入指定的方法,而不是整个文件作为一个模块进行加载
这种加载也称编译时加载或静态加载,即ES6在编译时就可以完成模块的加载
CommonJS加载实质:将一个文件整体加载为一个模块,并生成一个对象,然后通过这个对象来获取里面的方法
这种加载也称为运行时加载,因为只有运行时才可以得到这个对象
CommonJS
Node中模块的引入和导出就是CommonJS的一个具体代表
- 导出模块:exports或module.exports
- 导入模块:require
require导入模块的细节:https://blog.csdn.net/qq_43952245/article/details/106602068
缺点:是同步加载模块
只有等到对应的模块加载完毕,当前模块中的内容才能被运行;
AMD和CMD
CommonJS加载模块是同步的,但是在服务器不会有什么问题,因为服务器加载的js文件都是本地文件,加载速度非常快;
如果将它应用于浏览器呢?
浏览器加载js文件需要先从服务器将文件下载下来,之后在加载运行;那么采用同步的就意味着后续的js代码都无法正常运行,即使是一些简单的DOM操作;
所以在浏览器中,我们通常不使用CommonJS规范:在早期为了可以在浏览器中使用模块化,通常会采用AMD或CMD:
-
ADM(即Asynchronous Module Definition 异步模块定义)
AMD实现的比较常用的库是require.js和curl.js;这里我们以require.js为例讲解:
https://mp.weixin.qq.com/s/6Cc5RMw3pAHEUM58KLB-MQ
- 下载require.js
- 引入
- 加载模块:
require(['foo'], function(foo) {... })
使用require开始加载执行模块的代码 - 定义模块:如果一个模块不依赖其他,那么直接使用define(function)即可
-
CMD(即Common Module Definition通用模块定义)
SeaJS就是CMD的优秀实现方案
-
下载SeaJS
-
引入
// index.js文件中:引入foo模块 define(function(require, exports, module) { const foo = require('./modules/foo'); }) // bar.js 暴露指定内容 define(function(require, exports, module) { const name = 'lilei'; const age = 20; const sayHello = function(name) { console.log("你好 " + name); } module.exports = { name, age, sayHello } }) // foo.js 引入bar模块,并使用里面的内容 define(function(require, exports, module) { const bar = require('./bar'); console.log(bar.name); console.log(bar.age); bar.sayHello("韩梅梅"); })
-
ES6和CommonJS的不同
ES6:
- 静态加载,在编译时就完成了模块的加载
- 从模块中引入指定的方法或属性,是一种静态定义,在代码静态解析阶段就会生成
- export输出的接口与模块内变量的对应关系是动态的,通过该接口可以获取模块内部实时的值
- import命令是异步加载(有一个独立的模块依赖的解析阶段)
- 顶层的this指向undefined
CommonJS:
- 动态加载,只有运行时,才可以获得加载的模块对象
- 加载的是一个对象(即module.exports属性),然后通过对象来使用里面的方法(该对象只有在脚本运行时才会生成)
- CommonJS 模块输出的是值的缓存,不存在动态更新
- require()是同步加载模块
- 顶层的this指向当前模块
CommonJS加载原理
CommonJS的一个模块就是一个脚本文件。使用require命令第一次加载该脚本时,就会执行整个脚本,然后在内存中生成一个对象
{
id: '...', // 模块名
exports: { ... }, // 模块输出的各个接口。以后需要用到这个模块的时候,就会到exports属性上面取值
loaded: true, // 表示该模块的脚本是否执行完毕
...
}
CommonJS 模块无论加载多少次,都只会在第一次加载时运行一次,以后再加载,就返回第一次运行的结果,除非手动清除系统缓存。
ES6加载模块:ES6 模块是动态引用
使用
import
从一个模块加载变量(即import foo from 'foo'
),那些变量不会被缓存,而是成为一个指向被加载模块的引用
export命令
用于规定模块的对外接口
注意:export命令就是来规定模块的对外接口,因此export暴露的接口名必须和模块内的变量建立一一对应的关系
// profile.js 方法一
export var firstName = 'Michael';
export function multiply(x, y) {
return x * y;
};
// profile.js 方法二
export { firstName};
错误写法:
// 报错
export 1; // 直接输出1,1只是一个值,不是接口
// 报错
var m = 1;
export m; // 通过变量 m 直接输出1, 1只是一个值,不是接口
正确写法:
/**
规定了对外的接口m,其他脚本可以通过这个接口,取到值1
实质是:在接口名与模块内部变量之间,建立了一一对应的关系。
**/
// 写法一
export var m = 1; // 模块内部变量 m <=> 接口名m
// 写法二
var m = 1;
export {m}; // 模块内部变量 m <=> 接口名 m 暴露出一个对象作为接口
// 写法三
var n = 1;
export {n as m}; // 模块内部变量 n <=> 接口名 m
通常情况下,export
输出的变量就是本来的名字,但是可以使用as
关键字重命名。(接口名默认是输出变量的名字)
function v1() { ... }
function v2() { ... }
export {
v1 as streamV1,
v2 as streamV2,
v2 as streamLatestVersion //重命名后,v2可以用不同的名字输出两次。
};
import命令
引入其他模块提供的功能
使用
export
命令定义了模块的对外接口以后,其他 JS 文件就可以通过import
命令加载这个模块。
// circle.js
export function area(radius) {
return Math.PI * radius * radius;
}
export function circumference(radius) {
return 2 * Math.PI * radius;
}
逐一加载:
// main.js
import { area, circumference } from './circle';
console.log('圆面积:' + area(4));
console.log('圆周长:' + circumference(14));
整体加载:
import * as circle from './circle';
console.log('圆面积:' + circle.area(4));
console.log('圆周长:' + circle.circumference(14));
export 和 export default
export
:
- 一个模块内可以有多个
- import引入时需加
{}
export default
:
- 一个模块内最多只可以有一个
- import引入时不加
{}
因为export default
命令其实只是输出一个叫做default
的变量,所以它后面不能跟变量声明语句。
// 正确
export var a = 1;
// 正确
var a = 1;
export default a; //将后面的值,赋给default变量
// 错误
export default var a = 1;
// 正确
export default 42; // 将2赋值给default
// 报错
export 42;
import()
import
命令会被 JavaScript 引擎静态分析,先于模块内的其他语句执行
import
和export
命令只能在模块的顶层,不能在代码块之中(比如,在if
代码块之中,或在函数之中)否则会报句法错误,而不是执行时错误
优点:有利于编译器提高效率。
缺点:导致无法在运行时加载模块,在语法上,条件加载就不可能实现
如果
import
命令要取代 Node 的require
方法,这就形成了一个障碍。因为require
是运行时加载模块,import
命令无法取代require
的动态加载功能。
解决办法:
引入import()
函数,支持动态加载模块。
import(specifier)
:import
函数的参数specifier
,指定所要加载的模块的位置。import
命令能够接受什么参数,import()
函数就能接受什么参数,两者区别主要是后者为动态加载。
-
import()
返回一个 Promise 对象 -
import()
函数可以用在任何地方不仅仅是模块,非模块的脚本也可以使用
-
它是运行时执行
什么时候运行到这一句,就会加载指定的模块
-
import()
函数与所加载的模块没有静态连接关系import()
类似于 Node 的require
方法,区别主要是前者是异步加载,后者是同步加载。
import()使用场合
-
按需加载:
import()
可以在需要的时候,再加载某个模块。import()
方法放在click
事件的监听函数之中,只有用户点击了按钮,才会加载这个模块。button.addEventListener('click', event => { import('./dialogBox.js') .then(dialogBox => { dialogBox.open(); }) .catch(error => { /* Error handling */ }) });
-
条件加载:
import()
可以放在if
代码块,根据不同的情况,加载不同的模块。 -
动态的模块路径:
import()
允许模块路径动态生成。
module的加载实现
浏览器加载
传统方法:HTML 网页中,浏览器通过<script>
标签加载 JavaScript 脚本。
缺点:
浏览器是同步加载 JavaScript 脚本,即渲染引擎遇到
<script>
标签就会停下来,等到执行完脚本,再继续向下渲染。如果是外部脚本,还必须加入脚本下载的时间。如果脚本体积很大,下载和执行的时间就会很长,因此造成浏览器堵塞
解决办法:
浏览器允许脚本异步加载,下面就是两种异步加载的语法。
<script src="path/to/myModule.js" defer></script> <script src="path/to/myModule.js" async></script>
defer
是“渲染完再执行”,async
是“下载完就执行”。另外,如果有多个defer
脚本,会按照它们在页面出现的顺序加载,而多个async
脚本是不能保证加载顺序的。defer中指的“页面的正常渲染结束”指得是:DOM 结构完全生成,以及其他脚本执行完成
浏览器加载ES6模块
加载规则:
也使用
<script>
标签,但是要加入type="module"
属性。
<script type="module" src="./foo.js"></script>
浏览器对于带有
type="module"
的<script>
,都是异步加载,不会造成堵塞浏览器,即等到整个页面渲染完,再执行模块脚本,等同于打开了<script>
标签的defer
属性。如果网页有多个
<script type="module">
,它们会按照在页面出现的顺序依次执行。一旦使用了async属性,
node加载ES6模块
JavaScript 现在有两种模块。一种是 ES6 模块,简称 ESM;另一种是 CommonJS 模块,简称 CJS。
服务器端的模块加载机制:CommonJS 和 ES6的module
CommonJS 模块是 Node.js 专用的,与 ES6 模块不兼容
语法上:
CommonJS 模块使用
require()
和module.exports
ES6 模块使用
import
和export
。
要想ES6的module在node中使用,就必须为ES6中的module和CommonJS规定好各自的加载方案
-
Node.js 要求 ES6 模块采用
.mjs
后缀文件名只要脚本文件里面使用
import
或者export
命令,那么就必须采用.mjs
后缀名如果不希望将后缀名改成
.mjs
,可以在项目的package.json
文件中,指定type
字段为module
。一旦设置了以后,该目录里面的 JS 脚本,就被解释用 ES6 模块。{ "type": "module" }
-
如果这时还要使用 CommonJS 模块,那么需要将 CommonJS 脚本的后缀名都改成
.cjs
。如果没有
type
字段,或者type
字段为commonjs
,则.js
脚本会被解释成 CommonJS 模块。
总结:
>`.mjs`文件总是以 ES6 模块加载,`.cjs`文件总是以 CommonJS 模块加载,`.js`文件的加载取决于`package.json`里面`type`字段的设置。