【前端Tip】CommonJS规范和ES Module规范

前言:在写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规范的核心变量exportsmodule.exportsrequire

可以使用这些关键字来进行模块化开发:

  • 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对象,在其它文件导入某个文件时,其实就是拿到该对象的内存地址,如下图所示:

image.png

把上面的代码修改并验证一下:
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)的查找规则:
  1. x是一个核心模块,比如path、http:
    直接返回核心模块,停止查找;

  2. x是以./ 或 ../ 开头的
    a. 将x当做一个文件在对应的目录下查找,如果没有写明后缀名,则按照:x->x.js->x.json->x.node 进行查找;
    b. 没有找到对应的文件,将x作为一个目录,查找目录下边的index文件,按照x/index.js->x/index.json->x/index.node进行查找;

  3. 直接是一个x,并且x不是核心模块
    例如我在main.js中编写了require('test’),它会逐级查找上一层目录下的node_modules。

如果都没有找到,则报错:not found

2.4 模块的加载过程

  • 模块在第一次被引入的时候,模块的js代码会被运行一次;
  • 模块被多次引入时,会进行缓存,只执行一次(每个模块对象module都有一个属性:loaded用来标记是否已经加载过)
  • 如果有循环引入,那加载顺序是什么?

顺序为:图结果的深度优先算法


image.png

3. ES Module

3.1 介绍

ES Module 是ES6推出的,即ES 2015。并且自动采用严格模式:use strict

但是ES Module和CommonJS的模块化有一些不同:

  • 使用import 和 export关键字,不是模块也不是函数;
  • 采用编译器的静态分析,也加入了动态引用的方式;

ES Module模块采用exportimport关键字来实现模块化:

  • 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定义的,所以导入后的变量内存地址不能发生变化,但是对象类型的值可以;
image.png

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导出值是拷贝,可以修改;
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容