日期:2024年12月3日

模块化

随着学习的深入,代码数量的增多我们所编写的程序复杂度越来越高。此时如果我们依然将所有的代码编写到同一个文件中,代码将变得非常难以维护。模块化是解决这种问题的关键。

首先我们先来解决第一个问题 —— 什么是模块?模块简单理解其实就是一个代码片段,本来写在一起的JS代码,我们按照不同的功能将它拆分为一个一个小的代码片段,这个代码片段就是一个模块。简单来说,就是化整为零。

接着我们来一起看看模块化会给我们带来哪些好处,模块化后不再是所有代码写在一起,而是按功能做了不同的区分,当我们维护代码时可以比较快捷的找到那些要修改的代码。再者,模块化后,代码被拆分后更容易被复用,同样一个模块可以在不同的项目中使用,大大的降低了我们的开发成本。之前我们学习的jQuery便可以理解为是一种模块。

但是jQuery这种模块化的方式存在有许多的不足。jQuery是通过script标签引入的形式来完成模块化的,引入后实际效果是向全局作用域中添加了一个变量$(或jQuery)这样很容易出现模块间互相覆盖的情况。并且当我们使用了较多的模块时,有一些模块是相互依赖的,必须先引入某个组件再引入某个组件,模块才能够正常工作。比如jQuery和jQueryUI,就必须先引入jQuery,如果引入顺序出错将导致jQueryUI无法使用。这还仅仅是两个组件,而实际开发中的依赖关系往往更加复杂,像是a依赖b,b依赖c,c依赖d这种关系,必须按照d、c、b、a的顺序进行引入,有一个顺序错误就会导致其他的一起失效。所以通过script标签实现的模块化是非常的不靠谱的。

CommonJS

一直到2015年,JavaScript中一直都没有一个内置的模块化系统。但是随着JavaScript项目越来越复杂,模块化的需求早已迫在眉睫。于是大神门开始着手自定义一个模块化系统,CommonJS便是其中的佼佼者。同时它也是Node.js中默认使用的模块化标准。

模块就是一个js文件,在模块内部任何变量或其他对象都是私有的,不会暴露给外部模块。在CommonJS模块化规范中,在模块内部定义了一个module对象,module对象内部存储了当前模块的基本信息,同时module对象中有一个属性名为exports,exports用来指定需要向外部暴露的内容。只需要将需要暴露的内容设置为exports或exports的属性,其他模块即可通过require来获取这些暴露的内容。

const a = 10
const b = 20
const obj = { name: "孙悟空" }

module.exports = a

// const a = require("./m.js")
const a = 10
const b = 20
const obj = { name: "孙悟空" }

module.exports.a = a
module.exports.b = b

// const {a, b} = require("./m.js")
const a = 10
const b = 20
const obj = { name: "孙悟空" }

module.exports = {
    a,
    b,
    obj
}

// const {a, b, obj} = require("./m.js")

默认情况下,Node.js会将以下内容视为CommonJS模块:

  1. 使用.cjs为扩展名的文件
  2. 当前的package.json的type属性为commonjs时,扩展名为.js的文件
  3. 当前的package.json不包含type属性时,扩展名为.js的文件
  4. 文件的扩展名是mjs、cjs、json、node、js以外的值时(type不是module时)

require()是同步加载模块的方法,所以无法用来加载ES6的模块。当我们需要在CommonJS中加载ES模块时,需要通过import()方法来加载。

import("./m2.mjs").then(m2 => {
    console.log(m2)
})

文件作为模块

当我们加载一个自定义的文件模块时,模块的路径必须以/、./../开头。如果不以这些开头,node会认为你要加载的是核心模块或node_modules中的模块。

当我们要加载的模块是一个文件模块时,CommonJS规范会先去寻找该文件,比如:require("./m1")时,会首先寻找名为m1的文件。如果这个文件没有找到,它会自动为文件添加扩展名然后再查询。扩展名的顺序为:js、json和node。还是上边的例子,如果没有找到m1,则会按照顺序搜索m1.js、m1.json、m1.node哪个先找到则返回哪个,如果都没有找到则报错。

文件夹作为模块

当我们使用一个文件夹作为模块时,文件夹中必须有一个模块的主文件。如果文件夹中含有package.json文件且文件中设置main属性,则main属性指定的文件会成为主文件,导入模块时就是导入该文件。如果没有package.json,则node会按照index.jsindex.node的顺序寻找主文件。

node_modules

如果我们加载的模块没有以/、./../开头,且要加载的模块不是核心模块,node会自动去node_modules目录下去加载模块。node会先去当前目录下的node_modules下去寻找模块,找到则使用,没找到则继续去上一层目录中寻找,以此类推,知道找到根目录下的node_modules为止。

比如,当前项目的目录为:'C:\Users\lilichao\Desktop\Node-Course\myProject\node_modules',则模块查找顺序依次为:

‘C:\Users\lilichao\Desktop\Node-Course\node_modules’,
‘C:\Users\lilichao\Desktop\node_modules’,
‘C:\Users\lilichao\node_modules’,
‘C:\Users\node_modules’,
‘C:\node_modules’

模块的包装

每一个CommonJS模块在执行时,外层都会被套上一个函数:

(function(exports, require, module, __filename, __dirname) {
// 模块代码会被放到这里
});

所以我们之所以能在CommonJS模块中使用exportsrequire并不是因为它们是全局变量。它们实际上以参数的形式传递进模块的。

exports,用来设置模块向外部暴露的内容

require,用来引入模块的方法

module,当前模块的引用

__filename,模块的路径

__dirname,模块所在目录的路径

ES模块化

2015年随着ES6标准的发布,ES的内置模块化系统也应运而生,并且在Node.js中同样也支持ES标准的模块化。说来说去使用模块化无非需要注意两件事导出和引入:

导出

// 导出变量(命名导出)
export let name1, name2, …, nameN; 
export let name1 = …, name2 = …, …, nameN; 
​
// 导出函数(命名导出)
export function functionName(){...}
​
// 导出类(命名导出)
export class ClassName {...}
​
// 导出一组
export { name1, name2, …, nameN };
​
// 重命名导出
export { variable1 as name1, variable2 as name2, …, nameN };
​
// 解构赋值后导出
export const { name1, name2: bar } = o;
​
// 默认导出
export default expression;
export default function (…) { … } // also class, function*
export default function name1(…) { … } // also class, function*
export { name1 as default, … };
​
// 聚合模块
export * from …; // 将其他模块中的全部内容导出(除了default)
export * as name1 from …; // ECMAScript® 2O20 将其他模块中的全部内容以指定别名导出
export { name1, name2, …, nameN } from …; // 将其他模块中的指定内容导出
export { import1 as name1, import2 as name2, …, nameN } from …; // 将其他模块中的指定内容重命名导出
export { default, … } from …; 

引入

// 引入默认导出
import defaultExport from "module-name";
​
// 将所有模块导入到指定命名空间中
import * as name from "module-name";
​
// 引入模块中的指定内容
import { export1 } from "module-name";
import { export1 , export2 } from "module-name";
​
// 以指定别名引入模块中的指定内容
import { export1 as alias1 } from "module-name";
import { export1 , export2 as alias2 , [...] } from "module-name";
​
// 引入默认和其他内容
import defaultExport, { export1 [ , [...] ] } from "module-name";
import defaultExport, * as name from "module-name";
​
// 引入模块
import "module-name";

需要注意的是,Node.js默认并不支持ES模块化,如果需要使用可以采用两种方式。方式一,直接将所有的js文件修改为mjs扩展名。方式二,修改package.json中type属性为module。

特点

  1. ES模块基于文件路径解析
  2. 支持加载内置模块
  3. 相对/绝对路径均可
  4. 没有默认扩展名
  5. 文件夹模块没有主文件
  6. 支持node_modules加载包

核心模块

核心模块是node中的内置模块,这些模块有的可以直接在node中使用,有的直接引入即可使用。核心模块有很多,不能一一介绍,这里只是简单介绍几个模块,以帮助同学理解node。

process

process模块用来表示和控制当前的node进程。

  • process.exit([code]) 结束进程
  • process.nextTick(callback[, …args]) 向tick任务队列中添加任务

path

path模块可以帮助我们获取文件(夹)的路径。

  • path.resolve([…paths]) 生成绝对路径

fs

fs模块可以帮助我们读取磁盘中的文件。

  • fs.readFile() 读取文件
  • fs.appendFile() 创建新文件,或将数据添加到已有文件中
  • fs.mkdir() 创建目录
  • fs.rmdir() 删除目录
  • fs.rm() 删除文件
  • fs.rename() 重命名
  • fs.copyFile() 复制文件
4.9 14 投票数
文章评分
订阅评论
提醒
guest

5 评论
最旧
最新 最多投票
内联反馈
查看所有评论
chenyu
chenyu
2 年 前

老师,是否可以讲下node中的await

修己(初见小潭)
修己(初见小潭)
2 年 前

千呼万唤始出来,犹抱琵琶半遮面.超哥yyds

学习的十三
学习的十三
1 年 前

老师呀,我一个全局变量访问不到是咋回事呢,一个方法内有个值,我设全局变量去接收,然后在方法外访问赋值完的全局变量就是underfind

最后由学习的十三编辑于1 年 前
null
null
1 年 前

超哥你就是我再生父母

5
0
希望看到您的想法,请您发表评论x