Node模块化
Node.js是什么
官方定义:Node.js是一个基于V8 JavaScript引擎的JavaScript运行时的环境
- Node.js基于V8引擎来执行 JavaScript代码,但是Node.js中不仅仅有V8
- 我们知道,V8可以嵌入到C++应用程序中,因此无论是Chrome还是Node.js,都是嵌入了V8引擎来执行的JavaScript代码
- 在Chrome浏览器中,除了运行JavaScript代码之外,还要解析、渲染HTML、CSS等相关渲染引擎,同时还需要提供支持浏览器操作的API、浏览器自己的事件循环等
- 在Node.js中我们也需要进行一些额外的操作,比如 文件系统的读写、网络IO、加密、压缩解压文件等操作
- Node.js是JS代码的运行环境,由JS/C++/C语音编写(libuv就是C语音编写)
Node.js应用场景
- 应用一:目前 前端开发的库都是以node包的形式进行管理的
- 应用二:npm/yarn/pnpm工具成为前端开发使用的最多工具
- 应用三:越来越多的公司使用 Node.js作为web服务器开发、中间件、代理服务器
- 应用四:大量项目需要 借助Node.js完成前后端渲染的同构应用
- 应用五:资深前端工程师,使用Node.js编写脚本工具
- 应用六:使用Electron来开发桌面应用程序
Node.js的输入输出
- 我们在用命令行执行JS文件的时候,可以在命令后面敲空格,之后输入内容
node js.js num=10
- JS中可以通过process.argv接收
console.log(process.argv);
[
'F:\\nodejs\\node.exe',
'D:\\Mrzhang\\Study\\前端\\CSS\\code\\js.js',
'num=10',
]
- 输出
console.log()
即可
Node.js中的全局对象
- global:相当于浏览器中window
- process:进程相关的内容:
process.argv
是比较常用的 - console
- 定时器函数
setTimeout
setInterval
setImmediate(function () {})
process.nextTick(function(){})
特殊的全局对象
这些对象实际上是 模块中的变量,只是 每个模块都有,看起来是全局变量
在命令行交互中不可以使用
- 包括:
__dirname/__filename/exports/module/require()
//显示文件所在目录(不包含文件名称)
console.log(__dirname);
//显示文件所在目录(包含文件名称)
console.log(__filename);
认识模块化开发
- 目前的程序代码量是十分庞大的
- 模块化的目的是将庞大程序的代码,拆分成一个个小的结构
- 而这个结构有属于 自己的逻辑代码,有自己的作用域,定义变量的时候,不会影响到其他的结构
- 同时 这个结构的某些变量,函数以及对象等,又希望暴露出去,让其余结构访问
- 其余结构可以通过某种方式,导入 另外结构的变量、函数对象等内容
- 上面所说的 结构就是 模块,按照这种 结构划分开发程序的过程,就是模块化开发的过程
模块化的提出,主要是为了应对前端页面更加复杂的局面
在ES正式提出模块化前,社区提出了模块化的规范CommonJS(依旧再用),AMD、CMD(后面的两者均不在用了)
在ES6的时候,正式提出了标准的模块化ESModule
CommonJS规范和Node关系
CommonJS是一个规范,最开始提出来的时候,主要应用于服务器的
-
Node是CommonJS在服务器端一个具有代表性的实现
-
Browserify是CommonJS在浏览器中的一种实现
-
webpack打包工具具备对CommonJS的支持和转换
-
因为Node对CmmmonJS进行了支持和实现,因此在开发Node过程中可以使用模块化开发
- 在Node中每一个JS文件都是单独的模块
- exports和module.exports可以负责对模块中的内容进行导出
- require函数可以帮助我们 导入其他模块中的内容
-
需要导出的文件
let until_name = "until";
function foo() {
console.log("zhangcheng");
}
function bar() {
console.log("bar");
}
//导出相关变量
exports.until_name = until_name;
exports.foo = foo;
exports.bar = bar;
- 需要导入的文件
//引入变量
const until = require("./until.js");
//可以通过until.的方式访问变量
console.log(until.until_name);
until.foo();
until.bar();
-----------------------------------------
//我们可以借助解构赋值的方式,简化代码
//引入变量
const { until_name, foo, bar } = require("./until.js");
console.log(until_name);
foo();
bar();
exports导出的本质
exports实际上是一个对象,通过require函数,将导入文件中的变量与exports进行了引用赋值
- 以上可以通过代码进行验证,更改
exports.name
的值,并在两个文件中打印,即可观察到
module.exports导出
CommonJS导出,实际上是通过module.exports进行导出的,而module.exports和exports是同一个对象
- 因为module.exports和exports是同一个对象,因此可以写出以下导出代码
let until_name = "until";
function foo() {
console.log("zhangcheng");
}
function bar() {
console.log("bar");
}
//导出相关变量
module.exports.until_name = until_name;
module.exports.foo = foo;
module.exports.bar = bar;
- 而在真实的开发中,我们常写出以下代码
let until_name = "until";
function foo() {
console.log("zhangcheng");
}
function bar() {
console.log("bar");
}
//导出相关变量
module.exports = {
until_name,
foo,
bar,
};
- 若在最后,通过exports进行更改相关变量,则在导入的文件中,不会受到影响
let until_name = "until";
function foo() {
console.log("zhangcheng");
}
function bar() {
console.log("bar");
}
//导出相关变量
module.exports = {
until_name,
foo,
bar,
};
//在导入的时候,until_name为“until”,而不是“hhhh”
exports.until_name = "hhhh"
- 接下来看以下内存图
- 通过
module.exports
进行导出
- 通过
通过 module.exports = {}
进行导出
-
因此通过以上两幅图,可以看出,在内存中
module.exports
和exports
的对应关系 -
那么我们通过维基百科中对 CommonJS规范的解析
- CommonJS中没有module.exports的概念
- 但是为了实现模块的导出,Node中使用的是 Module类,每一个模块都是Module的一个实例,也就是 module
- 所以在Node中真正用于导出的其实不是 exports,而是module.exports
- 因为 module.exports = exports,所以,exports也可以进行导出
require的细节
我们知道 require是一个函数,可以帮助我们引入一个文件(模块)中导出的对象,接下来,我们就要看一下它的查找规则是什么样的
require(X)
- 情况一:X是Node核心模块,比如path、http
//会直接返回核心模块,并停止查找
const http = require("http")
- 情况二:X是 以./或者…/或者根目录开头的
- 第一步:将X当作一个文件在对应的目录下查找
- 如果有后缀名,就按照后缀名的格式查找对应的文件
- 如果没有后缀名,会按照如下顺序
- 直接查找文件X
- 查找X.js文件
- 查找X.json文件
- 查找X.node文件
- 第二步:若没有查找到X对应的文件,将X作为一个目录
- 查找目录下面的index文件
- 查找X/index.js文件
- 查找X/index.json文件
- 查找X/index.node文件
- 查找目录下面的index文件
- 如果都没有找到,那么就会报错
- 第一步:将X当作一个文件在对应的目录下查找
const until = require(./until)
//首先会将until当成文件,查找until.js/until.json/until.node
//若没有查到,就会将until当成目录,查找它下面的index.js/index.json/index.node文件
- 情况三:直接是一个X,该X没有路径,且不是一个核心模块
- 会查找本目录下,以及上级目录下中 node_module目录中的模块
const axios = require("axios")
//会在node_module目录中查找axios
模块加载过程
- 结论一:模块在被第一次引入时,模块中的JS代码会被运行一次
const until = require(./until)
//until.js中的代码会先运行一次
- 模块被多次引入时,会进行缓存,最终只加载一次
- 因为每个模块对象module中有一个loaded属性
- false表示还没有被加载,为true表示已经加载
- 已经加载的模块,不会再次被加载
//打印------
console.log(------);
//运行until.js中的代码
const until = require(./until)
//打印+++++
console.log(+++++);
//下面的引入不会再被加载
const until1 = require(./until)
const until2 = require(./until)
const until3 = require(./until)
- 如果出现循环引入的情况
- 会按照图结构,进行深度优先算法进行加载
CommonJS的缺点
- CommonJS加载模块是同步的
- 这就意味着只有 等到对应的模块加载完毕,当前模块中的内容才能被运行
- 这个再服务器中不会出现问题,因为服务器 加载的js文件都是本地文件,加载速度比较快
- 如果应用到浏览器中
- 浏览器 加载js文件需要先从服务器将文件下载下来,之后 再加载运行
- 如果 引入的某个js文件运行时间过长,就会阻塞后面的js代码无法运行,即使是一些简单的DOM操作
认识ESModule
ES6提出的模块化,前提是浏览器支持
-
与CommonJS不同之处
- 一方面使用了import和export(对应的CommonJS的是require和module.exports)
- 另一方面采用了编译期的静态分析,并且加入了动态引用的方式
-
采用ES Module会默认采用严格模式
- 创建一个HTML文件
- 引入 script标签,在标签中写入type = “module”
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<!--注意,在本地测试的时候,ES Module需要启用live server进行测试,单纯运行文件不行-->
<script src="./main.js" type="module"></script>
<!--until.js暴露变量-->
<script src="./until.js" type="module"></script>
</body>
</html>
- 创建until.js暴露变量
- 通过export进行暴露
- 而此 export并不是对象,只是特殊语法,{标识符}
let until_name = "until";
function foo() {
console.log("zhangcheng");
}
function bar() {
console.log("bar");
}
//导出相关变量
export { until_name, foo, bar };
- 创建 main.js引入 until.js暴露的变量
- 通过 import进行引入,跟的文件名一定要写后缀
import { until_name, foo, bar } from "./until.js";
console.log(until_name);
foo();
bar();
ESModule的导入导出扩展
- 导出的三种方式
//通过export直接导出
export{name,foo}
//导出的时候取别名
export{name as unName}
//直接导出变量
export let name = "zhangcheng"
- 导入的三种方式
//直接导入
import {name} from "./until.js"
//导入的时候取别名
import {name as unName} from "./until.js"
//导入的时候将这个module取别名
import * as foo from "./until.js"
foo.name
export和import结合使用
常见于开源的框架中
- 在 index.js中一般不需要写逻辑代码,仅做模块的导入导出即可
- index.html文件
- 该文件中通过 script标签引入了main.js文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script src="./main.js" type="module"></script>
</body>
</html>
- main.js文件
- 该文件直接引入了index.js文件
import { name, foo } from "./until/index.js";
console.log(name);
console.log(foo());
- index.js文件
- 该文件引入了tool.js和tool2.js文件,同时将变量暴露
//统一引入
import { name } from "./tool.js";
import { foo } from "./tool2.js";
//统一导出
export { name, foo };
------------------还可以做出以下优化
//export和import的结合
export { name } from "./tool.js";
export { foo } from "./tool2.js";
//写成这样也可以
export * from "./tool.js";
export * from "./tool2.js";
- tool.js文件
export let name = "zhangcheng";
- tool2.js文件
export function foo() {
return "foo";
}
default用法
前面用到的都是有名字的导出,default是默认导出
- 默认导出 是不需要指定名字的
- 导入的时候不需要加{},且名字可以自己命名
- 注意:一个文件只有一个默认导出
- 默认导出方式
//默认导出方式一
function foo() {
console.log(123);
}
export default foo;
//默认导出方式二
export default function(){
console.log(123)
}
- 引入方式
import aaa from "./until.js"
aaa()
import函数
当我们需要动态引入文件的时候,需要用到import函数
- 正常使用import引入文件的时候,需要写在代码最顶层
import {name} from "./index.js"
//逻辑代码
- 但是有时候需要按需引入一些文件,这时候就可以用到import函数
- import函数返回的是一个Promise
let flag = true
if(flag){
import("./index.js").then(res=>{
console.log(res)
})
}