目录
(一)Nodejs简介
1.nodejs是什么
2.nodejs架构
3.nodejs的应用场景
(二)准备工作
1.安装nodejs
2.nodejs版本管理工具
(三)nodejs的使用
1.node的输入
2.node的输出
3.其他的console方法
(四)全局对象
1.常见的全局对象
2.特殊的全局对象
3.global和window的区别
(五)模块化***
1.什么是模块化
2.CommonJS
(1)CommonJS的广泛使用
(2)CommonJS在Node的使用
(3)CommonJS在Node实现的本质
(4)module.exports的本质
(5)require()查找模块的细节
(6)Node模块的加载过程
多次引入的模块加载
循环引入的模块加载顺序
3.ES Module
(1)es module的导入和导出
(2)导出(暴露)的三种方法
(3)导入的三种方法
(4)import函数
import()
import.meta
nodejs是基于v8 JavaScript引擎的运行时环境
node基于v8引擎来执行js文件,但不仅仅只有v8引擎,也有额外的操作,如文件读写、网络IO、加密、压缩解压文件等操作
node是用JS、C、C++语言编写的
编写的js代码会通过v8引擎,再经过nodejs的bindings,将任务放到Libuv的事件循环中
libuv:是使用C语言编写的库,提供了事件循环、文件系统读写、网络IO等内容
在中文网里安装node.js: Node.Js中文网
LTS版本:更加稳定,不会轻易改变版本(项目开发推荐)
current版本:最新版本,拥有新特性,不太稳定
当需要使用到多个版本的node且来回切换时,可以使用版本工具进行快速安装和切换
n或nvm:适用于Mac系统
nvm-windows:适用于windows系统 GitHub - coreybutler/nvm-windows: A node.js version management utility for Windows. Ironically written in Go.
nvm-windows的基本使用:
在github里下载:Releases · coreybutler/nvm-windows · GitHub
nvm list ----列出nvm管理的node版本
nvm install xx版本 ----下载xx版本
nvm use xx版本 ----切换为xx版本
nvm current ----展示当前使用的node版本
在控制台运行js文件:node xxx
输出:console.log()
输入:node xxx 变量 变量...
输入的变量会存储在全局对象process的argv数组里,这个数组存储有node的路径和运行的js文件的路径,以及输入的变量
因此可以通过process.argv获取输入的变量值
argv是什么?
是argument vector的缩写,指传入的具体的参数
console.clear()清空控制台
console.trace()打印函数的调用栈
更多在文档中查看:Console | Node.js v19 API
这些对象在所有模块中都可用:Global objects | Node.js v19 API
global:全局对象
process:process对象提供有关当前Node.js进程的信息和控制 Process | Node.js v19 API
console:对控制台的简单调试
定时器函数:
setTimeout()
setInterval()
setImmediate():callback I/O事件之后的回调立即执行
setImmediate和setTimeout的执行顺序有区别,后续再x
process.nextTick():添加到下一次tick队列中
以下变量可能看起来是全局的,但实际上不是。它们只存在于模块的作用域中
详情见:CommonJS modules | Node.js v19 API
__dirname:当前文件所在的目录结构
__filename:当前目录+文件名称
exports、module、require():在后面的模块化会着重讲到
global对象是js运行时nodejs提供的全局对象,上文提到的process、console、定时器函数等都包含在global对象中;
window对象是浏览器的全局对象,里面也包含了console、定时器函数等;
但在浏览器里global相关的属性是不存在的,在node里window属性也是不存在的
因此使用全局属性globalThis可以解决该问题
globalThis - JavaScript | MDN
通过使用globalThis,你的代码将在 window 和非 window 上下文中工作,而无需编写额外的检查或测试。在大多数环境中,globalThis直接引用该环境的全局对象。但是,在浏览器中,内部使用代理来考虑iframe和跨 window 安全性。实际上,它并不会改变我们编写代码的方式。
在早期js开发中,对于怎么样导入导出没有具体的规范,大家自发的建立起CommonJS、amd/cmd等模块开发规范,直到ES6的ESModule给出了官方的模块化规范
nodejs的模块化是基于CommonJS的规范实现的
模块的导出
exports、module.exports
const name = 'csq'
function sayHello() {
console.log('hello');
}
exports.name = name
exports.sayHello = sayHello
模块的导入
require()命令的基本功能是,读入并执行一个 js 文件,然后返回该模块的 exports 对象。如果没有发现指定模块,会报错。
const a = require('./a')
// 解构赋值
// const {name,sayHello} = require('./a')
console.log(a.name);
a.sayHello()
require()通过引用赋值将另一模块的exports对象绑定在当前模块中
意思是:require获取到的对象和导出的对象指向同一个地址
在导入的模块中修改require获取到的对象,在导出模块的exports对象也会发生改变;反之,在导出模块的exports对象发生改变,那么在导入模块的require获取的对象也会发生改变。
在main.js中:
const a = require('./a')
console.log(a.name); // ‘csq’
// 2s后修改对象属性 对应导出的exports对象也会改变
setTimeout(() => {
a.name = 'zkj'
}, 2000)
在a.js中:
const name = 'csq'
setTimeout(() => {
console.log(exports.name); // ‘zkj’
}, 3000);
exports.name = name
CommonJS中是没有module.exports的概念的,是依靠exports进行导出的。而在Node中为了实现模块的导出,使用的是Module类,每一个模块的实例也就是module,真正用于模块导出的是module.exports。因此exports只是指向了module.exports
如下图,require()导入的a和module.exports、exports都指向同一地址,因此对三者任意一个进行修改,其他的也都会改变
在开发中,更加推荐的导出方法是:
// 推荐导出方法
module.exports = {
name,
sayHello
}
这种方法创建了一个新对象并将module.exports也指向了新的内存,exports对象也就没有了意义
因此,无论如何修改exports内的数据都不会影响到module.exports
情况一:require("内置模块名"):导入Node内置模块
如:path、http、fs等内置模块
情况二:require("路径"):导入自定义的模块
查找顺序:如"./xxx",会先按照以下顺序查找当前目录有无该文件:
1.xxx
2.xxx.js
3.xxx.json
4.xxx.node
当这些文件都没有后,再将xxx作为一个目录,自动补全index文件,查找目录后有无index文件,没有就会报错
情况三:require("外部模块"):当require内不是路径也不是内置模块时,会查找当前路径的node_modules文件夹,再在文件夹内寻找有无该模块;若当前路径没有node_modules文件夹,就向上一级文件夹依次继续寻找
例如require("axios") 需要通过node i axios下载后才能在node_modules内找到
第一次加载某个模块时,Node.js 会缓存该模块。以后再加载该模块,就直接从缓存取出该模块的exports属性返回
const a = require('./a')
// 第一次加载某个模块时,Node.js 会缓存该模块。
// 以后再加载该模块,就直接从缓存取出该模块的 module.exports 属性返回
a.name = 'zkj'
const b = require('./a')
console.log(b.name); // 'zkj'
由以上实例可证明,再次导入该模块,就不会再从该模块中重新加载了
原因:在module对象中有一个属性loaded默认为false,当导入后就会变为true,不会再被加载了
当出现循环引入,如图
Node会按照图的深度优先算法进行遍历
即顺序:main ->aaa->ccc->ddd->eee->bbb
commonJS模块化适用于服务器端,而官方推出的ES Module是在浏览器端实现模块化的一个很好的方案
在浏览器中实现模块化,只需要在引入时 type=module即可
导入:import {xxx,yyy} from "xxx.js"
在main.js中:
// 在浏览器上使用模块化 导入时文件名后缀要写完整
import { name, sayHello } from "./a.js"
console.log(name); // ‘csq’
sayHello() // ‘hello’
注意:在浏览器使用esModule时要加上文件后缀js,否则查找不到对应文件
导出:export {xxx,yyy}
在a.js中:
let name = 'csq'
function sayHello() {
console.log("hello");
}
export { name, sayHello }
注意:在浏览器使用ESModule必须开启一个服务端口打开对应html文件,否则会报错:不能通过本地下载js文件的方式解析,file协议前缀会被跨域策略阻止,必须使用http、https等协议前缀
在MDN上有解释:JavaScript 模块 - JavaScript | MDN
你需要注意本地测试——如果你通过本地加载 HTML 文件(比如一个 file:// 路径的文件),你将会遇到 CORS 错误,因为 JavaScript 模块安全性需要。你需要通过一个服务器来测试。
分别暴露
// 1.分别暴露
export let name = 'csq'
export function sayHello() {
console.log("hello");
}
统一暴露
// 2.统一暴露
export { name, sayHello }
默认暴露
// 3.默认暴露 (只能暴露一个变量、方法或对象等)
export default {
name:'csq',
age:18
}
// 1.导入方式1
import { name, sayHello } from "./a.js"
// 2.导入方式2 全部导入
import * as a from "./a.js"
// 3.默认暴露的导入
import sayHello from "./a.js";
注意:默认暴露的导入名字是自定义的,一般使用与导出相关的名字
import和export命令只能在模块的顶层
import命令会被 JavaScript 引擎静态分析,先于模块内的其他语句执行。若在if语句内写import语句,在编译时是不会被处理的,导致报语句错误,如下:
if(name){
import { age } from "./b.js"; // 报错:导入声明只能在顶层使用
}
ES2020 引入import()函数,支持动态加载模块
import("./xxx.js")函数返回一个promise对象
if (name) {
import('./b.js').then(res => {
console.log(res) // module对象
console.log(res.age) // 18
})
}
import()和require()一样都能实现动态加载模块,区别是:前者是异步加载,后者是同步加载
开发者使用一个模块时,有时需要知道模板本身的一些信息(比如模块的路径)。ES2020 为 import 命令添加了一个元属性import.meta,返回当前模块的元信息。
import.meta只能在模块内部使用,如果在模块外部使用会报错
console.log(import.meta); // 在b.js中
https://hacks.mozilla.org/2018/03/es-modules-a-cartoon-deep-dive/
构建:查找、下载并将所有文件解析为模块记录(Module record)
实例化:对模块记录进行实例化,分配内存空间,然后解析模块的导入和导出,将导出和导入都指向对应的内存地址
运行:运行代码,将实际的值填入对应的内存地址中