前言:在写Vue代码的时候,经常会看到import、export、require等关键字,这次特地学习了解一下,等用到的时候避免出问题。
1. 模块化的概念
- 模块化就是将程序划分成一个个小的模块;
- 每个模块中有自己的逻辑代码,变量有自己的作用域;
- 其它模块可以使用自己暴露的变量、函数、对象等;
- 也可以通过某种方式,导入其它模块中的变量、函数、对象等;
按照这种结构划分开发程序的过程,就是模块化开发的过程;
网页开发早期,js仅作为一种脚本语言,并不复杂,只需要将js代码写到
<script>
标签中即可,没有必要放到多个文件中来编写。但是现在,js越来越复杂了:
- ajax的出现,前后端开发分离,后端返回数据后,前端需要通过js进行页面渲染;
- SPA(单页面应用)的出现,使前端页面变的很复杂,包括路由、状态管理等复杂的需求,需要js实现;
- Node的实现,js编写复杂的后端程序,没有模块化是硬伤;
所以,模块化是前端技术发展的必要产物。但是js本身,直到ES6(2015)才推出了自己的模块化方案。在此之前,为了让js支持模块化,出现了很多不同的模块化规范:AMD、 CMD、 CommonJS等。
既然称之为模块化,那么模块需要支持导出和导入,需要有相应的方法或者关键字配合实现,下面就是通过模块的导出和导入进行学习。
2. CommonJS 和 Node
2.1 CommonJS介绍
CommonJS是一个规范,最初是在浏览器以外的地方使用,被命名为ServerJS,后来为了提现它的广泛性,改成了CommonJS,平时也会简称为CJS。
- Node 是CommonJS在服务器端一个具有代表性的实现;
- Browserify是CommonJS在浏览器中的一种实现;
- webpack打包工具具备CommonJS的支持和转换;
所以,Node中对CommonJS进行了支持和实现,让我们在开发node的过程中可以方便的进行模块化开发:
- Node中每个js文件都是单独的模块;
- 这个模块中包括CommonJS规范的核心变量
exports
、module.exports
、require
;
可以使用这些关键字来进行模块化开发:
- exports 和 module.exports用于对模块中的内容进行导出;
- require函数用于导入其它模块(自定义模块、系统模块、第三方库模块);
2.2 使用exports导出
test.js文件:
// 每一个js文件就是一个模块
const name = "张三";
// exports默认是空对象
console.log(exports);
// 定义一个方法
function callName(name){
console.log(name);
}
// 导出
exports.name = name;
exports.callName = callName;
main.js文件:
// 可以直接给一个变量赋值,使用的时候需要test.name,这个test就是test.js中的exports对象
// const test = require('./test.js');
// 也可以部分导出
const {name, callName} = require('./test.js');
console.log(test);
console.log(name);
callName("李四"); // 调用方法
在每个文件中都有一个exports对象
,在其它文件导入某个文件时,其实就是拿到该对象的内存地址,如下图所示:
把上面的代码修改并验证一下:
test.js:
// 每一个js文件就是一个模块
const name = "张三";
// exports默认是空对象
console.log(exports);
function callName(name){
exports.name = name;
}
// 导出
exports.name = name;
exports.callName = callName;
main.js:
const test = require('./test.js');
test.callName("李四");
console.log(test.name);
输出:
李四
可以发现,在main.js文件中调用callName方法,修改了test.js文件中的exports.name的值,最后在main.js中输出的结果为:李四,说明test对象确实是对exports对象的浅拷贝(引用赋值)
。
但是,如果使用const {name, callName} = require('./test.js');
这种方式导出,main.js中修改name的值,不会影响到test.js中的name,因为这种只是导入了name的值,上面是导入了exports这个对象。
2.3 使用module.exports导出
上面学习了exports,看样子能完全满足我们日常开发了,为什么还会有module.exports呢?
通过维基百科中对CommonJS规范的解析:
- CommonJS中是没有module.exports的概念的;
- 但是为了实现模块的导出,Node中使用的是Module类,每一个模块都是Module类的一个实例,也就是一个js文件就是一个Module类实例;
- 所以在Node中真正用于导出的其实不是exports,而是module.exports;
- 因为module才是导出的真正实现者;
把一个文件当成一个对象的时候,Node底层就会进行new module
,实际上是做了这么一步操作:module.exports = exports,所以上面的test = exports = module.exports。
把上面的代码修改,再验证一下:
test.js:
// 每一个js文件就是一个模块
const name = "张三";
// exports默认是空对象
console.log(exports);
function callName(name){
module.exports.name = name;
}
// 导出
exports.name = name;
exports.callName = callName;
main.js:
const test = require('./test.js');
test.callName("李四");
console.log(test.name);
输出:
李四
通过上面的代码验证,上面test = exports = module.exports是成立的。
2.3 require函数的细节
require是一个函数,用来引入一个文件(模块)中导出的对象。
require的加载过程是同步的,所以必须等到引入的文件(模块)加载完成之后,才会继续执行其它代码,会产生阻塞现象,因为引入一个文件,则该文件内部的所有代码都会被执行一次。
2.3.1 支持动态导入
动态导入就是可以在js语句中,使用require语法,如下所示:
let lists = ["./index.js", "./config.js"]
lists.forEach((url) => require(url)) // 动态导入
if (lists.length) {
require(lists[0]) // 动态导入
}
2.3.2 require(x)的查找规则:
x是一个核心模块,比如path、http:
直接返回核心模块,停止查找;x是以./ 或 ../ 开头的
a. 将x当做一个文件在对应的目录下查找,如果没有写明后缀名,则按照:x->x.js->x.json->x.node 进行查找;
b. 没有找到对应的文件,将x作为一个目录,查找目录下边的index文件,按照x/index.js->x/index.json->x/index.node进行查找;直接是一个x,并且x不是核心模块
例如我在main.js中编写了require('test’),它会逐级查找上一层目录下的node_modules。
如果都没有找到,则报错:not found
。
2.4 模块的加载过程
- 模块在第一次被引入的时候,模块的js代码会被运行一次;
- 模块被多次引入时,会进行缓存,只执行一次(每个模块对象module都有一个属性:loaded用来标记是否已经加载过)
- 如果有循环引入,那加载顺序是什么?
顺序为:图结果的深度优先算法
3. ES Module
3.1 介绍
ES Module 是ES6推出的,即ES 2015。并且自动采用严格模式:use strict
。
但是ES Module和CommonJS的模块化有一些不同:
- 使用import 和 export关键字,不是模块也不是函数;
- 采用编译器的静态分析,也加入了动态引用的方式;
ES Module模块采用export
和import
关键字来实现模块化:
- export负责将模块内的内容导出;
- import负责从其它模块导入内容;
3.2 使用ES Modules
在浏览器使用ES Modules时,要在script标签上加上type="module"
,且要在服务器上运行,不支持本地运行的file协议(触发CORS)。
index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script src="./index.js" type="module"></script>
</body>
</html>
index.js:
console.log('hello EsModules');
输出:
hello EsModules
3.3 使用export和import
index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script src="./index.js" type="module"></script>
</body>
</html>
index.js 导入:
console.log('hello EsModules');
// 常见的导入方式
// 方式一: import {} from '路径'
// 注意此处的{}不是对象,导入时后边必须要加.js,脚手架里和webpack会自动加
// import { name, age, sayhello } from './modules/foo.js'
// 方式二:导出的变量可以起别名
// import { name as Fname, age as Fage, sayhello as FsayHello } from './modules/foo.js'
// 2.1 导出时已经起了别名的,接收要使用别名接受,可以给别名再起别名
// import {Fname as FooName,Fage as FooAge,FsayHello as FooSayHello} from './modules/foo.js'
// 方式三:import * as foo from '路径'
import * as foo from './modules/foo.js'
console.log(foo.name);
console.log(foo.age);
foo.sayHello('彭先生')
foo.js 导出:
const name = 'pengsir'
const age = 18
const sayHello = function (name) {
console.log('姓名' + name);
}
// 1.导出方式
// 方式一:
// export const name = 'pengsir'
// export const age = 18
// export const sayHello = function (name) {
// console.log('姓名' + name);
// }
// 方式二: 常用!!!!!!
// {} 这里不是类 就和 if(){} 的大括号一样
// {放置要导出变量的引用列表}
export {
name,
age,
sayHello
}
// 方式三:{} 导出时,可以给变量起别名
// export {
// name as Fname,
// age as Fage,
// sayHello as FsayHello
// }
输出结果:
hello EsModules
pengsir
18
姓名彭先生
3.4 export default
上面是在导出时都指定了名字,所以导入时也需要知道具体的名字。在某些情况下很不方便,所以还有另外一种导出方式:export default
:
- 默认导出时,不需要指定名字;
- 导入时很方便,可以自己指定名字;
bar.js 导出 :
// 方式四:默认导出
export default function format() {
console.log('对某一个东西,进行格式化!');
}
index.js导入:
// 方式四: 演示 export default如何导入
import utils from './modules/foo.js'
utils() // 实际是调用 format
输出:
对某一个东西,进行格式化!
但是,一个文件只能有一个默认导出:export default。
3.5 import 函数
通过import加载的模块,是不能放到逻辑代码中的,只能放到最上面,比如:
let flag = true
if (flag) {
// 错误用法,语法错误,不能在逻辑在逻辑代码中使用 import 关键字
import format from './modules/foo.js'
}
为什么会出现这个情况呢?
- ES Module在被js引擎解析时,需要知道它的依赖关系;
- 由于js代码这时没有运行,所以无法在进行类似于if判断中根据代码执行情况进行导入;
解决办法:使用import() 函数,或者require()
// 方式五:import() 函数
// 注意:上边使用import时是作为关键字使用,现在是作为函数使用,
// 该函数为异步函数,返回值为promise
let flag = true
if (flag) {
import('./modules/foo.js').then(res => {
console.log('then里边的回调');
console.log(res);
}, err => {
console.log(err);
})
}
3.6 异步的import
使用 type="module"
时,加载该模块是异步加载
的,就相当于给script加了一个async属性。
示例:
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script src="./index.js" type="module"></script>
<script src="./normal.js"></script>
</body>
</html>
index.js
console.log('hello EsModules');
normal.js
console.log('我是普通的js文件');
由此可见我们的 ES Module 是异步的。
3.7 ES Module的加载过程
ES Module导出的数据是实时变化的:
- 如果在bar.js中导出一个变量A,在index.js中导入该变量,如果1秒之后bar.js中的变量A的值被修改,index.js中导入的变量也会被修改;
- 但是在index.js中不能修改导入的该变量,除非该变量是一个对象类型,因为ES Module在底层实现的时候,每次导出的变量发生变化,都会在模块环境记录中创建一个最新的该变量,类似const name = name,底层使用const定义的,所以导入后的变量内存地址不能发生变化,但是对象类型的值可以;
3.8 统一export
项目中会有很多工具函数,在不同的分拣中,如果要引入的话,需要找到对应的文件来引入,可以给这些工具库弄一个统一的出口,然后直接导入这个出口文件即可:
/**
* 工具的统一出口
*/
// 1.导出方式一:挨个导入再挨个导出
// import { sub, add } from './math.js'
// import { timeFormat } from './format.js'
// export { sub, add, timeFormat }
// 2.导出方式二:直接导出指定的
// export { sub, add } from './math.js'
// export { timeFormat } from './format.js'
// 3.导出方式三: 直接导出所有的
export * from './math.js'
export * from './format.js'
4. 总结
CommonJS和 ES Module的区别:
- CommonJS可以动态加载语句,代码发生在运行时;ES Module是静态的,只能声明在文件最顶部,代码发生在编译时;
- Es Module混合导出,单个导出,默认导出,完全互不影响;
- Es Module导出是引用值,并且值都是可读的,不能修改;CommonJs导出值是拷贝,可以修改;