require 模块化
1. 前言
本节课将会引导大家学习了解:
- 什么是模块化
- require/exports 怎么实现模块化
- import/export 怎么实现模块化
学习完本节课程后,应该具有:
- 使用 require/exports 实现模块化和加载不同依赖库的能力
- 用 import/export 实现模块化的能力
2. 模块化
模块化概念由来已久,随着前端 JavaScript 脚本代码越来越复杂、庞大,加之 Node.js 出现,支持 JavaScript 成为可用的服务端语言。JavaScript 适应模块化的呼声日益强大。
2.1 什么是模块化编程
模块化编程,就是将实现一个特定功能的代码,分解成若干个独立的、可替换的、具有预定功能的模块。通过调用不同的模块组合来实现不同的实际功能。
2.2 举例
在很多项目中,我们都能看见一个属于该项目自身的工具库,通常都叫 util.js
或者 utils.js
。里面有很多不同的工具,例如:
- 利用正则校验金额的方法
- 利用正则校验手机号的方法
- 利用正则校验邮箱地址的方法
- 接口响应的格式规范方法
这种将一大堆工具放到某一个文件里集中管理,同时也供任何项目代码调用的思想,也是 模块化编程 思想的一种体现。
2.3 模块化编程有什么优势
模块化编程有以下优势:
- 有利于完成设计:较大较复杂的问题不经过拆分很难找到实现功能的根本路径,将其利用模块化思想拆分成若干个小问题,我们就可以从抽象的模块功能而非复杂功能本身理解。使开发人员能专注于解决每一个小问题,而非被复杂的大问题困扰。
- 提高开发效率:可以使有经验有能力的开发人员可以专注于核心模块开发,使得一个功能可以多人同时完成。
- 有利于问题排查:代码出现异常时,能通过由后往前的代码截断来观察实际产生异常的代码段,再由相关人员修复。
- 易维护:需求出现微调时,可以将微调涉及的模块挑出来调整,不需要修改其他函数,保证稳定性。
- 可复用:抽象出来的方法,可以不需要任何修改就可以用于另一个需求的开发中。
下面我们来介绍 2 种目前主流的模块化实现方案:require/exports 和 import/export。
3. require/exports
3.1 require/exports 是什么
require/exports 是用作代码模块化的,采用的是 CommonJS 模块规范,也是 Node.js 10 目前仍主要支持的一种模块化方案。
JavaScript 在 ES6 发布了另一种模块化规范 ------ import/export。
这是目前主流的两种模块化方案。
3.2 代码示例
3.2.1 引用
js
const fs = require('fs');
3.2.2 导出
第一种:module.exports
js
module.exports = {
exportFn: function() {
console.log('this is the export function');
}
}
第二种:exports
js
function exportFn() {
console.log('this is the export function');
}
exports.exportFn = exportFn;
导出有两种写法,那么用 module.exports
和 exports
有什么区别呢?
先来看看能不能打印出来:
js
console.log(module.exports);
console.log(exports);
shell
{}
{}
这么看感觉这两个东西很像,再多打印一个判断条件观察下:
js
console.log(module.exports === exports);
shell
true
由于在 JavaScript 中,对象类型是 引用类型 ,参数实际指向的是存放变量的 内存地址 。操作符===
只会对 内存地址 一致的变量返回true
。
再来看看赋值后的表现:
重新定义 module.exports
js
module.exports = {
fn: function() {
console.log('this is a export function');
}
}
console.log(module.exports);
console.log(exports);
console.log(module.exports === exports);
shell
{ fn: [Function: fn] }
{}
false
插值到 module.exports
js
module.exports.fn = function() {
console.log('this is a export function');
}
console.log(module.exports);
console.log(exports);
console.log(module.exports === exports);
shell
{ fn: [Function] }
{ fn: [Function] }
true
exports
js
function fn() {
console.log('this is a export function');
}
exports.fn = fn;
console.log(module.exports);
console.log(exports);
console.log(module.exports === exports);
shell
{ fn: [Function] }
{ fn: [Function] }
true
重新定义 module.exports
和 exports
混合使用
js
function fn() {
console.log('this is a export function');
}
exports.fn = fn;
module.exports = {
fn1: function() {
console.log('this is a export function 1');
}
}
console.log(module.exports);
console.log(exports);
console.log(module.exports === exports);
shell
{ fn1: [Function] }
{ fn: [Function] }
false
插值到 module.exports
和 exports
混合使用
js
function fn() {
console.log('this is a export function');
}
exports.fn = fn;
module.exports.fn1 = function() {
console.log('this is a export function 1');
}
console.log(module.exports);
console.log(exports);
console.log(module.exports === exports);
shell
{ fn: [Function: fn], fn1: [Function] }
{ fn: [Function: fn], fn1: [Function] }
true
我们可以总结出以下规则:
exports
是module.exports
的引用,它们指向相同的 内存地址。- 使用 重新定义
module.exports
导出时,使用了新开辟的 内存空间 ,使得旧的引用exports
失效,使用exports
导出的模块 和在此之前使用 插值到module.exports
导出的模块也会失效。
建议不要使用 重新定义 module.exports
的方式来导出,因为这样会丢失 exports
的正确引用,导致部分引用丢失的问题。
Tips:
module.exports
和exports
没有本质区别,exports
是 Node.js 为了方便导出而默认定义的引用。但是以module.exports
为准,因为他所用的 内存空间 为实际导出模块的 内存空间。
3.3 调用时机
require 是运行时调用,属于动态加载,所以可以用在代码的任何一个地方,如:
js
if (1 + 1 == 2) {
const fs = require('fs');
}
3.4 工作机制
require 实际上就是在赋值,或者说浅复制(引用类型只复制内存地址,而不新开辟内存存值)。
由于 require 实际上是在赋值,所以就算我们不需要使用该模块所有的导出函数,我们都需要加载整个模块的函数,再提取出需要的函数,如:
js
const {readFileSync, writeFileSync} = require('fs');
这时,先会加载整个 fs 模块,生成 _fs 对象,再在这个对象中取出 readFileSync
和 writeFileSync
函数。
4. import/export
上文提到 import/export
在 ES6 成为模块化标准,这证明了 require/exports
只是 Node.js 私有的方法。
而在 Node.js 9.0 及以上版本,Node.js 官方支持了用一种比较微妙的方法使用 import/export
。
但是在笔者现在撰写文档的时空中,Node.js 发布的最新开发版为 V 13.11.0 import/export
的支持仍在实验阶段。但 import/export
有可能成为若干版本后的 Node.js 使用的模块化工具,在这里也讲解一下 import/export
的内容。
4.1 import/export 怎么使用
由于这是实验阶段特性,本段内容有可能在日后的重大更新中失真
在 Node.js V 13.11.0 中,如果需要使用 import/export
作为模块化工具,则文件后缀名要改成 .mjs
来标识这是符合 ES 标准的模块。
4.2 代码示例
4.2.1 引入
js
// 引入默认模块
import fs from 'fs';
// 引入命名模块
import { readFileSync, writeFileSync } from 'fs';
// 引入所有模块
import * as fs from 'fs';
Tips:根据 ES6 的规定,
import
必须在文件开头引入,它前面不可以有任何逻辑代码。
但由于和 require
相比,失去了灵活性。所以 import
提供了 import()
函数来支持 动态加载。
import()
函数返回一个 promise 对象。
js
if (1 + 1 == 2) {
import('fs')
.then(function(fs) {
const str = fs.readFileSync('./none_js.txt', 'utf8');
console.log(str);
})
}
4.2.2 导出
default
导出默认模块
js
export default {
name: 'none_js',
explainNode: function() {
console.log('Node.js is a backend runtime of javaScript');
}
}
导出命名模块
js
export function fn1() {
console.log('this is another function');
}
js
function fn2() {
// todo...
}
export { fn2 }
导出默认模块时如果没有命名,引入时能自定义一个名称。
4.3 调用时机
和 require
不同,import
是编译时调用,属于静态加载,如果引入的模块不存在,将在编译时报错。
理论上 import
只能用在文件头部,如:
js
import fs from 'fs';
但 import
提供 import()
函数,来支持动态加载,如:
js
if (true) {
import('fs')
.then(function(fs) {
// todo...
})
}
4.4 工作机制
import 实际上是在做解构操作,所以我们不需要使用该模块所有的导出函数,我们就不需要加载整个模块的函数,如:
js
import { readFileSync, writeFileSync } from 'fs';
这时,会只从 fs 模块中 readFileSync
和 writeFileSync
函数,其他函数不加载。
和 require
比较而言,import
对性能更加友好。
5. 小结
本节课程我们主要学习了 模块化编程 、require/exports 、import/export。
重点如下:
-
重点1
模块化编程在复杂系统中十分重要,其优点在于:有利于完成设计 、提高开发效率 、有利于问题排查 、易维护 、可复用。
-
重点2
require/exports
属于动态加载,require
用于引入,exports
用于导出,是module.exports
的引用,最终以module.exports
实际指向的对象为准。 -
重点3
import/export
属于静态加载,是 ES6 提出的标准,Node.js 正在实验其落地的可能性,理论上,其性能要强于require/exports
。