目录
模块化与工程化
Javascript一直没有模块体系,无法将一个大程序拆分成互相依赖的小文件,再用简单的方法拼装起来。这对开发大型的、复杂的项目形成了巨大障碍。
在 ES6 之前,社区制定了一些模块加载方案,最主要的有 CommonJS 和 AMD 两种。前者用于服务器,后者用于浏览器。ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。
注意:从 v13.2 版本开始,Node.js 已经默认打开了 ES6 模块支持。在v12的版本里面,需要在package.json中设置
"type": "module",
Tip:模块化是项目工程化的前提。
ES6模块化
ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。
ES6 模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入。上面代码的实质是从fs模块加载 3 个方法,其他方法不加载。这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。
ES6 的模块自动采用严格模式,不管你有没有在模块头部加上"use strict";
Tip:
export
用来从模块中导出,import
用来从模块中导入。// ES6模块使用示例 import { stat, exists, readFile } from 'fs';
export导出
一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用export关键字输出该变量。export命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系,也就是说外部接口需要用这个接口名来引用。
在编写文档的时候编者的node版本是
v14.16.0
,查看node版本使用node -v
命令。在
src
下创建testModule.js
文件,内部内容如下:let firstName = 'Michael'; let lastName = 'vicky'; // 列表导出 export { firstName, lastName };
运行
testModule.js
文件,使用node src/testModule.js
node src/testModule.js
正常情况如下:
如果出现以下错误:
上图蓝色框中的内容就是解决该错误的方法,在
package.json
中添加"type": "module",
以保证该功能正常运行。再次执行
node src/testModule.js
就可以了。node src/testModule.js
let firstName = 'Michael'; let lastName = 'vicky'; // 列表导出 export { firstName, lastName }; // 重命名导出 export { firstName as first, lastName as last}; // 导出单个属性 export let a = 3; // 导出一个函数 export function multiply(x, y) { return x * y; }; // 默认导出,一个模块只能有一个默认导出,不能使用 var、let 或 const 用于导出默认值 export default。 export default {} export default function foo(){} let b = 1; // 报错,因为没有提供对外的接口。应该export let b = 1; 或者export {b} export b;
需要特别注意的是,export命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系,不能直接导出一个值。
import导入
静态的import 语句用于导入由另一个模块导出的绑定。
export default 命令: 使用import命令的时候,用户需要知道所要加载的变量名或函数名,否则无法加载,但是,用户肯定希望快速上手,未必愿意阅读文档,去了解模块有哪些属性和方法。为了给用户提供方便,让他们不用阅读文档就能加载模块,就要用到export default命令,为模块指定默认输出
export default function foo() { console.log('foo'); }
其他模块加载该模块时,import命令可以为该匿名函数指定任意名字。
import customName from './export-default'; customName(); // 'foo’ export default命令用于指定模块的默认输出。显然,一个模块只能有一个默认输出,因此export default命令只能使用一次。所以,import命令后面才不用加大括号,因为只可能对应一个方法或者对象。
// 导入整个模块内容 import * as person from './testExport.js' // 导入列表内的多个接口 import {firstName,lastName} from './testExport.js' // 重命名导入 import {firstName as name} from './testExport.js' // 运行整个模块而不导入任何值 import './testExport.js'; // 导入使用export default导出的模块 import myDefault from './testExport.js';
测试导出和导入:
在
src
目录下新建testExport.js
和testImport.js
两个文件,testExport.js
文件里用来测试导出,testImport.js
用来测试导入。
testExport.js
内部代码let firstName = 'Michael'; let lastName = 'vicky'; // 列表导出 export { firstName, lastName };
testImport.js
内部代码// 导入列表内的多个接口 import { firstName, lastName } from './testExport.js' // 重命名导入 import { firstName as name } from './testExport.js' console.log(firstName, lastName, name); // 结果为 Michael vicky Michael
执行
node src/testImport.js
命令,输出结果。node src/testImport.js
模块化开发的优点主要有这么几点:
使用模块化开发能解决文件之间的依赖关系。
当你引入很多个JS文件的时候,很有可能会不清楚这些JS文件之间的依赖关系,从而导致加载顺序出错。使用模块化开发之后就能避免这个问题。
使用模块化开发可以避免命名的冲突。
JS本身是没有命名空间的,为了减少命名冲突,经常使用对象或者闭包来减少命名冲突。对象只能减少命名冲突的概率,闭包的过多使用会造成内存泄漏。模块化开发之后,在模块内任何形式的命名都不会和其他模块的命名产生冲突,有效的解决了命名冲突的问题。
使用模块化开发能进行代码的复用。
当我们想要实现某个功能的时候,如果某个模块正好有这个功能,我们就可以直接引用该模块,不必再写多余的代码,这样可以提高代码整体的效率,减少重复冗余的代码。
CJS模块化
CommonJS (CJS) 和 AMD 模块,都只能在运行时确定模块之间的依赖关系,以及输入输出的变量。比如,CommonJS 模块就是对象,输入时必须查找对象属性。
// CommonJS模块 let { stat, exists, readfile } = require('fs'); // 等同于如下代码块 let _fs = require('fs'); let stat = _fs.stat; let exists = _fs.exists; let readfile = _fs.readfile;
上面代码的实质是整体加载fs模块(即加载fs的所有方法),生成一个对象(_fs),然后再从这个对象上面读取 3 个方法。这种加载称为“运行时加载”,因为只有运行时才能得到这个对象,导致完全没办法在编译时做“静态优化”。
CommonJS模块化规范,服务器端工作,NodeJS实现了。
模块对象
Node内部提供一个Module构建函数。所有模块都是Module的实例。每个模块内部,都有一个module对象,代表当前模块。它有以下属性,通过
module.XXX
来使用:
属性 说明 id 模块的识别符,通常是带有绝对路径的模块文件名 filename 模块的文件名,带有绝对路径 loaded 返回一个布尔值,表示模块是否已经完成加载 parent 返回一个对象,表示调用该模块的模块 children 返回一个数组,表示该模块要用到的其他模块 exports 表示模块对外输出的值
exports导出
为了方便,Node为每个模块提供一个exports变量,指向module.exports。使用
module.exports={}
或者exports.XXXX=""
来导出。这等同在每个模块头部,有一行这样的命令:let exports = module.exports;
require导入
require函数是nodejs提供的内置函数,用于加载指定路径的模块或者是指定名称的模块。将加载的模块进行返回,使用如下:
返回一个对象
let path = require('fs');
测试导出和导入:
在
src
下创建testExports.js
和testRequire.js
文件,testExports.js
用来测试导出,testRequire.js
用来测试导入。
testExports.js
文件内容如下:// 导出 module.exports = { firstName: 'Michael', lastName: 'vicky' };
testRequire.js
文件内容如下:// 导入 const { firstName, lastName } = require('./testExports'); console.log(firstName, lastName);
执行
node src/testRequire.js
命令,输出结果。node src/testRequire.js
差异
ES6 模块输出的是值的引用。 ES6 模块加载的不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。ES6 模块输出的是值的引用。
CommonJS 模块输出的是一个值的拷贝。 CommonJS加载的是一个对象,即module.exports属性,该对象只有在脚本运行完才能生成。CommonJS 模块输出的是值的拷贝。
CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
验证CommonJS导出的是值的拷贝:
//testCommonJS.js let firstname = 'ren'; let lastname = 'terry'; setTimeout(() => { firstname = 'zhao'; }, 2000); module.exports = { firstname: firstname, lastname: lastname // 对象属性的简写方式 /* firstname, lastname */ }; //useCommonJS.js let { firstname, lastname } = require('./testCommonJS.js'); console.log(firstname, lastname); setTimeout(() => { console.log(firstname, lastname); }, 4000)
node src/useCommonJS.js ren terry ren terry
验证ES6导出的是值引用:
//testES6.js let firstname = 'ren'; let lastname = 'terry'; setTimeout(() => { firstname = 'zhao'; }, 2000); export { firstname, lastname }; //useES6.js import { firstname, lastname } from './testES6.js' console.log(firstname, lastname); setTimeout(() => { console.log(firstname, lastname); }, 4000);
node src/useES6.js ren terry zhao terry
拓展:(授课不做要求)
path模块
path 模块提供了一些工具函数,用于处理文件与目录的路径,使用如下方法引用:
var path = require(‘path’);
path.basename() 该方法返回一个参数路径的最后一部分
path.dirname() 该方法返回一个 path 的目录名
path.extname() 该方法返回 path 的扩展名,即从 path 的最后一部分中的最后一个 .(句号)字符到字符串结束。
path.isAbsolute() 该方法会判定 path 是否为一个绝对路径。
path.join() 该方法使用平台特定的分隔符把全部给定的 path 片段连接到一起,并规范化生成的路径
path.normalize() 该方法会规范化给定的 path,并解析 '..' 和 '.' 片段
path.delimiter 该属性提供平台特定的路径分隔符
querystring模块
querystring 模块提供了一些实用函数,用于解析与格式化 URL 查询字符串。使用如下方法引用
var querystring = require('querystring');
querystring.stringify(obj[, sep[, eq]]) 将对象转换为查询字符串
obj 要序列化成 URL 查询字符串的对象。
sep 用于界定查询字符串中的键值对的子字符串。默认为 '&'。
eq 用于界定查询字符串中的键与值的子字符串。默认为 '='。
querystring.parse(str[, sep[, eq]]) 将查询字符串转换为对象
url模块
url模块提供了一些实用函数,用于 URL 处理与解析。 可以通过以下方式使用
var url = require('url');
url.parse() 将一个url地址转换为一个对象
url.resolve() 该方法会以一种 Web 浏览器解析超链接的方式把一个目标 URL 解析成相对于一个基础 URL
url.resolve('/one/two/three', 'four'); // '/one/two/four'
url.resolve('Example Domain', '/one'); // 'http://example.com/one'
url.resolve('http://example.com/one', '/two'); // 'http://example.com/two'