Node.js
定义
nodejs
是前端工程化的基础,是开源的,基于谷歌v8引擎
构建的js运行环境,运行开发者使用js编写的服务器。
三大模块
文件相关
fs模块
封装了与本机
文件系统
进行交互的方法与属性fs.readFile()
1
fs.readFile(path[, options], callback)
参数1:必选参数,字符串,表示
文件路径
。参数2:可选参数,表示以什么
编码格式
来读取文件。参数3:必选参数,文件读取完成后,通过
回调函数
拿到读取的结果,形如(err,dataStr)=>{}
读取成功
err
为null,否则为错误对象
示例:
1
2
3
4fs.readFile('/path/to/file.txt', 'utf8', (err, data) => {
if (err) throw err;
console.log(data);//data是文件内容的buffer数据流
});fs.writeFile()
1
fs.writeFile(file, data[, options], callback)
- 参数1:必选参数,需要指定一个
文件路径
的字符串,表示文件的存放路径
- 参数2:必选参数,表示要写入的
内容
。 - 参数3:可选参数,表示以什么
格式
写入文件内容,默认值是utf-8
。 - 参数4:必选参数,文件写入完成后的
回调函数
。
注意:这个方法只能用来创建文件,不能用来创建路径
path模块
path 模块提供了用于
处理文件路径
的方法,帮助我们在不同操作系统之间处理和标准化
路径字符串__dirname:返回当
前js文件
所在目录的绝对路径
。path.join()
path.join()
方法则简单地将所有给定的路径片段
连接在一起,并规范化
生成的路径。它不会尝试将路径转换为绝对路径,也不会考虑当前工作目录。1
2
3const path = require('path');
const filePath = path.join('/folder', 'subfolder', 'file.txt');
console.log(filePath); // 输出:'/folder/subfolder/file.txt'path.resolve()
path.resolve()
方法会将传入的路径片段
解析为绝对路径
。它从右向左处理参数,直到构造出一个绝对路径
为止。如果所有给定的路径片段
都不是绝对路径
,则会使用当前工作目录(process.cwd())
作为基础来构建绝对路径
。无论输入是什么,path.resolve()
总是返回一个绝对路径。path.parse()
被用来解析路径
1
2
3const pathObj = path.parse('/folder/subfolder/file.txt');
// 输出:{ root: '/', dir: '/folder/subfolder', base: 'file.txt', ext: '.txt', name: 'file' }
console.log(pathObj);
网络相关
http模块
对标浏览器中的XMLHttptRequest
模块,http
模块用于创建 HTTP 服务器或客户端
1 | const http = require('http'); |
req
req
是请求对象,它包含了与客户端相关的数据和属性,req.url
:是客户端请求的URL地址
req.method
:是客户端的请求方法
res
res是响应的结果,或者说是
响应报文
res.statusCode
:设置响应状态码res.setHeader()
:设置响应报文的响应头1
res.setHeader('Content-Type', 'text/plain;charset=utf-8')
res.end()
:结束此次请求与响应,并返回数据,当调用res.end()
方法,并向客户端发送中文内容
的时候,会出现乱码问题,此时,需要手动
设置内容的编码格式
。即res.setHeader('Content-Type', 'text/plain;charset=utf-8')
客户端/服务器
在网络节点中,负责消费资源
的电脑,叫做客户端;负责对外提供网络资源
的电脑,叫做服务器。
总结
这三个模块的组合使用
非常常见,尤其是在构建需要读写文件
并提供网络服务
的应用程序中。例如,你可能需要从文件系统读取配置文件,然后使用这些配置来启动一个 HTTP 服务器,或者接收 HTTP 请求并将请求的数据写入文件系统。
Nodejs与浏览器的区别与联系
- 浏览器依靠内核中的
V8引擎
执行js代码,node.js基于谷歌V8引擎
进行封装
- 都支持
ECMAscript
基础语法(ES语法) - Node.js有独立的
api
,没有DOM
,BOM
模块化
模块化发展流程
把功能封装为一个一个
函数
,把函数和相关的变量放到一个js文件
中,需要使用某个函数的时候就引入js文件
。引入的函数会被挂载到全局对象
上,存在全局变量污染
的问题。1
2
3
4
5
6
7
8// math.js
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
// 在HTML文件中通过script标签引入math.js后,add和subtract会挂载到全局对象上所以就把
相关的函数
和变量
封装到一个对象
中,这样即便存在同名的函数
也没关系,但是这样不安全,因为对象的属性可以被随意修改
。1
2
3
4
5
6
7
8
9
10// math.js
var MathLib = {
add: function(a, b) {
return a + b;
},
subtract: function(a, b) {
return a - b;
}
};
// 在HTML文件中引入math.js后,MathLib.add 和 MathLib.subtract不会直接挂载到全局对象上使用
立即执行函数
私有化变量
和函数
,并使用return
暴露函数。这种写法的,被引入的函数和相关变量不再挂载到全局对象,必须要求原模块代码先于拓展模块代码被引入。1
2
3
4
5
6
7
8
9
10
11
12
13
14// math.js
var MathLib = (function() {
// 私有变量和函数
var privateVar = 'secret';
function privateFn() {
console.log('This is private:',privateVar);
}
// 返回公开接口
return {
privateFn
};
})();为了解决
先后顺序
问题,再传参的时候判断一下如果传入的是undefined
则转为传入{}可以看出模块化实现有多种方式,为了统一,我们必须制定出一个
好用统一
的模块化标准
,比如commonjs
和esm
,他们被打包后都会转换成立即执行函数
风格的模块化代码。
模块分类
内置模块
加载的时候直接写包名,比如上述介绍的
三大模块
第三方模块
加载的时候直接写包名,第三方模块又叫做包,是基于内置模块封装出来的
自定义模块
加载
的时候需要写路径,可以省略.js后缀名,后缀补全规则1
2
3
4
5按照确切的文件名进行加载
补全.js扩展名进行加载
补全.json扩展名进行加载
补全.node扩展名进行加载
加载失败,终端报错
模块作用域
模块内定义的变量和函数
无法被外部访问
模块加载机制
模块在第一次加载后会被缓存
。这也意味着多次调用require()
不会导致模块内的代码被执行多次
内置模块
加载优先级最高
自定义模块
加载时
必须
指定以./
或../
开头的路径标识符,否则则node
会把它当作内置模块
或第三方模块
进行加载。第三方模块
如果传递给
require()
的模块标识符
不是一个内置模块,也没有以./
或../
开头,则Node.js
会从当前模块的父目录
开始,尝试从/node_modules
文件夹中加载第三方模块
。如果没有找到对应的第三方模块,则再移动到上一层父目录中
,进行加载,直到文件系统的根目录
。把
目录
作为模块(没有指明文件名)在被加载的目录下,查找
package.json
的文件,并寻找main
属性,作为require()
加载的入口
如果目录里没有package.json
文件,或者main
入口不存在或无法解析,则Node.js将会式图加载该目录下的index.js
文件。
如果以上两步都失败了,则Node.js 会在终端打印错误消息,报告模块的缺失: Error: Cannotfind module ‘xxx’
两种模块化标准
CommonJS
是nodejs
默认支持的模块化语法。
导入
使用 require
函数来导入模块,导入自定义模块写相对路径
,导入第三方模块
或者内置模块
使用模块名
1 | const myModule = require('./myModule'); |
或者
1 | const http = require('http'); |
通过require
导入模块的方式都是同步的,也就是说会阻塞后续的代码的执行。
导出
通常使用module.exports
,只能导出一个对象。
1 | module.exports = { |
module对象
在每个
.js
自定义模块中都有一个module
对象,它里面存储了和当前模块有关的信息在自定义模块中,可以使用
module.exports
对象,将模块内的成员共享出去,默认为{}
module.exports
可以直接写成exports
,它们起初
指向同一个空对象
1
2
3
4
5
6function add(x,y){
return x+y
}
//module.exports.add = add 如果使用这种导出方式,module.exports和exports指向的对象都包含add方法
//module.exports = {add} 如果使用这种导出方式,module.exports指向的对象都包含add方法,但是exports指向的还是空对象
//exports = {add} 如果使用这种导出方式,module.exports指向的对象还是空对象,exports指向的是包含add方法的对象require()
模块时,得到的永远是module.exports
指向的对象在js文件中使用了
module
,就可以认为这个js文件是commonjs
模块
ESM
ES模块功能主要由两个命令构成:
export
:用于规定模块的对外接口
import
:用于输入
其他模块提供的功能
import
在编译阶段,import
会提升
到整个模块的头部,首先执行
1 | foo(); |
多次重复执行同样的导入,只会执行一次
1 | import 'lodash'; |
export
一个模块就是一个独立的文件,该文件内部的所有变量,外部无法获取
。如果你希望外部能够读取模块内部的某个变量,就必须使用export
关键字输出
该变量。简单的来说,export
就是用来暴露模块内部私有的变量的。
命名导出/导入
命名导出
1
2export const PI = 3.14159;
export const add = function(a,b){return a+b}//math.js或者写成
1
2
3const PI = 3.14159;
const add = function(a,b){return a+b}
export {PI,add}但是不能写成
1
2
3
4const PI = 3.14159;
const add = function(a,b){return a+b}
export PI //报错
export add //报错命名导入
1
2
3
4//import后面接着from关键字,from指定模块文件的位置,可以是相对路径,也可以是绝对路径
import { PI, add } from './math.js';
console.log(PI); // 输出: 3.14159
console.log(add(1, 2)); // 输出: 3如果想要给
输入变量
起别名,通过as
关键字1
import { lastName as surname } from './profile.js';
输入的变量都是
只读
的,不允许修改,但是如果是对象
,允许修改属性
1
2
3
4import {a} from './xxx.js'
a.foo = 'hello'; // 合法操作
a = {}; // Syntax Error : 'a' is read-only;
默认导出/导入
如果不需要知道变量名
或函数
就完成导入,就要用到export default
命令,为模块指定默认输出
默认导入
1
import obj from '模块名/路径' //obj这个名字是自定义的,可以随便取名
默认导出
1
2
3
4
5const baseURL = 'http://hmajax.itheima. net '
const getArraySum = arr => arr.reduce((sum,val) => sum += val,0)
export default {
baseURL,getArraysum
}或者直接导出一个函数
1
2
3export default function () {
console.log('foo');
}
动态加载
允许您仅在需要时
动态加载模块,而不必预先加载所有模块,这存在明显的性能优势
这个新功能允许您将import()
作为函数
调用,将模块的路径
作为参数。 它返回一个 promise
,可以在then
方法中拿到该模块的导出。
1 | import('/modules/myModule.mjs') |
根据模块是使用默认导出
还是命名导出
,module
对象的内容会有所不同。
如果模块使用了默认导出(export default
),那么动态导入的结果module
将是一个带有 default
属性的对象。这个属性的值就是模块中默认导出的内容
。例如:
1 | // myModule.mjs |
在这种情况下,动态导入后拿到的 module
对象看起来像这样:
1 | { |
你可以通过 module.default
来访问默认导出的内容。
如果模块使用了命名导出
(export
),那么动态导入的结果将直接包含
这些命名的属性。例如
1 | // myModule.mjs |
在这种情况下,动态导入后的 module
对象看起来像这样:
1 | { |
可以直接通过 module.PI
和 module.add
来访问这些命名导出
的内容。
如果你的模块同时
使用了默认导出
和命名导出
,那么动态导入的结果将会同时包含这两类内容。例如:
1 | // myModule.mjs |
在这种情况下,动态导入后的 module
对象将包括 default
属性以及其他命名导出的属性:
1 | { |
注意
- 一个模块内可以
同时
使用命名导出
和默认导出
,但是如果没有默认导出
,也不能使用默认导入
- 不能尝试对
默认导入
使用对象解构
,会被当成按需(命名)导入
切换方法
nodejs
默认支持commonjs
模块化语法,但是也可以切换为ESM
语法,在运行模块所在文件夹新建package.json
文件,并设置{ "type" : "module" }
这样就能使用ESM
语法
包
定义
将模块
和其他资料
聚合成一个文件夹
(通常是js文件
和package.json
文件,用来记录包的清单信息),本质就是个文件夹,一个第三方模块。
package.json
package.json
文件是每个 npm 包的核心配置文件
,它包含了关于包的元数据信息,如名称、版本、作者等,以及定义了项目的依赖关系
、开发依赖关系、脚本命令等。它列出项目的直接依赖项
,并指定这些依赖项的版本范围
(例如 ^1.2.3
或 ~1.2.3
)。这意味着安装时(npm i
),npm 可以根据这些范围,从注册表中获取满足条件的最新版本
。由于它只指定了版本范围
而不是具体的版本号,因此允许在一定范围内自动更新依赖项,这可以确保你总是用上最新的修复和改进
,但也可能引入不兼容
的变化。
package-lock.json
package-lock.json
文件是在 npm 5 引入的一个锁定文件,它记录了所有安装过的依赖及其子依赖的确切版本
。不同于 package.json
中的版本范围,package-lock.json
锁定了依赖树中每一个包的具体版本号。这样可以确保无论何时何地重新安装依赖
,都能得到完全相同
的依赖环境,避免因为不同时间点安装不同版本的依赖而引起的潜在问题,从而提供更为稳定的开发环境。
规范的包结构
- 包必须以
单独的目录
而存在 - 包的
顶级目录
下要必须包含package.json
这个包管理配置文件 package.json
中必须包含name
,version
,main
这三个属性,分别代表包的名字
、版本号
,包的入口
。
NPM
定义
npm(node packages manager)是node.js
的标准软件包管理器
,下载好node
这个就能用了,通常用来在项目中下载,引入其他已发布的包,类似java
中的maven
。
常用指令
npm init -y
给一个包初始化
package.json
文件,配置清单信息(记录这个包引入的其他包的信息)只能在
英文
目录下运行,不能包含空格,不能包含中文npm i
安装所有依赖,包括开发依赖dependencies 和 生产依赖devDependencies
npm i 软件包名 -D
下载软件包到本地开发环境(devDependencies),软件包名和
-D
的顺序不重要,下方同理npm i 软件包名 -s
下载安装包到本地的生产环境(dependencies),不添加任何符号,比如
npm i 软件包名
将默认下载到开发依赖中。npm i 软件包名 -g
下载软件包到全局。全局安装的软件包不会被添加到任何
package.json
文件中,因为它们不属于特定的项目,全局安装的软件包会被放置在系统的全局 Node.js 安装目录
下(C:\Users\35194\AppData\Roaming\npm\node_modules)npm i 包1 包2….
一次性安装多个包(用空格隔开)
npm uninstall 包名
卸载包,package.json文件中内容也会改变
推荐的包
nrm
一个方便切换下载源
的工具包,选择合适的下载源,能显著提高我们下载包的速度。
安装指令:
1 | npm i -g nrm |
不借助nrm
切换下载源:
1 | npm config get registry //获取下包的服务器地址 |
明显比较麻烦,需要记住指令
和下载源的网址
。
使用nrm
:
1 | nrm ls //获取所有可用镜像源 |
i5ting_toc
一个可以把md文档
转换成html
的包
安装:
1 | npm install -g i5ting toc |
使用:
1 | i5ting_toc -f 要转换的md文件路径 -o |
nodemon
安装:
1 | npm install -g nodemon //安装到全局 |
或者
1 | npm install --save-dev nodemon //安装到本地开发环境 |
安装完成后,你可以直接用 nodemon
来代替 node
命令来启动你的应用程序。例如,如果你的应用入口文件是 app.js
,你可以这样做:
1 | nodemon app.js |
当你修改app.js
文件并保存时,nodemon
会检测到这个文件变化,并自动重新运行这个文件。
mysql
mysql模块是托管于npm
上的第三方模块。它提供了在 Node.js项目中连接和操作
MySQL数据库的能力。
express
是什么
express
是一个极简且灵活的基于Node.js 的Web 应用框架
,它为构建 Web 应用和 API 提供了一组强大的功能。Express 通过提供路由、中间件、模板引擎等功能简化了
HTTP 服务器的创建过程,并支持快速开发可扩展
的应用程序。
安装
1 | npm i express |
导入
1 | // 引入 express 模块 |
根据语法写业务函数
1 | app.get("/api/complex",(req,res)=>{ |
req
req.query:获取
url
中的查询参数,默认是个空对象req.body:获取请求的请求体
req.params:
1
2
3
4
5app.get( '/user/:id', (req,res) =>{
//req.params 默认是一个空对象
//里面存放着通过︰动态匹配到的参数值
console.log(req.params)
})可以传入多个动态参数,比如
/user/:id/:name
res
res.send(‘数据’):在
响应体
中携带数据,并返回响应res.sendFile(‘’文件路径’’)
res.sendFile
方法自动处理文件的读取
和发送
,减少了手动读取文件
和设置响应头
的工作,它会根据文件扩展名自动设置
响应头中的 Content-Type 字段。
开启服务
1 | app.listen(8081,()=>{ |
然后执行,node express.js
启动服务器(上述代码都写在express.js文件中),每次修改服务器内容后都需要重新启动
,所以建议使用nodemon express.js
配置静态资源
1 | app.use(express.static('./public')) |
上述代码指定了,服务器的静态资源
根目录,为与express.js
文件同一级别的public
文件
1 | /your-project |
这意味着如果客户端请求 http://127.0.0.1:8081/images/logo.png
,Express
会尝试从 public/images/logo.png
提供该文件。
指定虚拟路径前缀
果你想为静态资源设置一个虚拟路径前缀(例如 /static
),可以这样做:
1 | app.use('/static', express.static('public')); |
这行代码意味着所有的静态资源
都将通过 /static
开头的 URL 路径访问。比如,要获取 public/images/logo.png
文件,客户端应该请求 http://127.0.0.1:8081/static/images/logo.png
。
配置多个静态资源
如果你有多个静态资源目录,可以多次调用 express.static
:
1 | app.use(express.static('public')); |
这样,Express 会按照定义的顺序
查找静态文件。首先会在 public
目录下查找,如果没有找到,再尝试在 assets
目录中查找。
路由
在Express 中,路由
指的是客户端的请求
与服务器处理函数
之间的映射
关系,也可以说是后端路由
。和匹配静态资源类似,在匹配时,会按照路由的声明/书写顺序
进行匹配。
路由模块化
创建路由文件
首先,在你的项目中创建一个
routes
目录,并为每个资源创建单独的路由文件
。例如,如果你有一个用户资源和一个产品资源,可以创建如下结构:1
2
3
4
5/your-project
/routes
user.js
product.js
app.js定义路由
在每个路由文件中,使用
express.Router()
来创建一个新的路由器对象
,并定义与该资源相关的路由。然后导出
这个路由器对象。以用户资源为例:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// routes/user.js
const express = require('express');
const router = express.Router();
// 定义用户的 GET 路由
router.get('/info', (req, res) => {
res.send('GET request to the user route');
});
// 定义用户的 POST 路由
router.post('/info', (req, res) => {
res.send('POST request to the user route');
});
// 导出路由器
module.exports = router;加载路由模块
接下来,在主应用文件(通常是
app.js
)中引入这些路由模块,并使用app.use()
方法挂载它们。你还可以为每个路由模块指定一个基础路径
:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18// app.js
const express = require('express');
const app = express();
const userRouter = require('./routes/user');
const productRouter = require('./routes/product');
// 挂载用户路由,基础路径为 /users
// 这样就能通过 /users/info访问到users对应的资源
app.use('/users', userRouter);
// 挂载产品路由,基础路径为 /products
app.use('/products', productRouter);
// 启动服务器
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
中间件
定义
当一个请求到达Express
的服务器之后,可以连续调用多个中间件
,从而对这次请求进行预处理
1 | // routes/user.js |
和路由的区别
中间件函数
的形参列表中,必须包含next
参数。而路由处理函数
中只包含req
和res
。next()
next()
是实现多个中间件连续调用
的关键,它表示把控制权
转交给下一个中间件或路由
共享
多个中间件之间,
共享
同一份req
和res
。基于这样的特性,我们可以在上游的中间件中,统一为req
或res
对象添加自定义的属性或方法,供下游的中间件
或路由
进行使用。执行顺序
可以使用
app.use()
连续定义多个全局中间件。客户端请求到达服务器之后,会按照中间件定义的先后顺序
依次进行
按作用范围分类
全局中间件
所有请求到达都会经过的中间件,通过调用
app.use(中间件函数)
,即可定义一个全局生效
的中间件局部中间件
在定义路由的时候传入中间件
1
app.get('/ ', mw1,(req,res) =>{res.send(' Home page. ')})
定义
多个
局部中间件1
app.get('/ ', mw1, mw2, (req,res) =>{res.send(' Home page. ')})
或者在一个
单独的模块
中定义,通过router.use(中间件函数)
实现1
2
3
4
5
6
7
8
9// routes/user.js
const express = require('express');
const router = express.Router();
// 自定义中间件,用于所有用户路由
router.use((req, res, next) => {
console.log('Time: ', Date.now());
next();
});
其他分类
应用级别:绑定到app上的
路由级别:绑定路由上的
误差级别中间件:专门用来
捕获
整个项目中发生的异常错误
,从而防止项目异常崩溃的问题。区别于其他中间件,必须放在所有路由之后
,可以观察到,错误级别的中间件有四个参数,(err, req, res, next)
1
2
3
4
5
6
7
8
9app.use((err,req,res,next)=>{
if(err.inner.message = "No authorization token was found"){
res.send({
code:1,
msg:"token未携带"
})
}
next('token未携带')//它最终会被传递到最后一个中间件,通常是日志记录中间件,然后打印到控制台
})内置中间件
express.static()
快速托管
静态资源
的内置中间件,所以说,express.static()
返回一个新的中间件函数
。1
app.use(express.static('public'));
express.json()
这个中间件用于解析
JSON
格式的请求体
。它会将接收到的 JSON 数据解析成JavaScript 对象
,并将其存储在req.body
中,以便后续的路由处理器可以访问。express.urlencoded()
此中间件用于解析
URL 编码
格式的请求体
,通常出现在 HTML 表单提交时。与express.json()
类似,它也会将解析后的数据附加到req.body
。
自定义中间件
定义一个模块,编写中间件函数,然后导出,使用的时候导入,再挂载。
1
2
3
4
5
6function myMiddleware(req, res, next) {
// 执行一些操作...
// 调用 next() 将控制权传递给下一个中间件或路由处理程序
next();
}1
2
3
4
5const express = require('express');
const app = express();
// 定义并使用自定义中间件
app.use(myMiddleware);第三方中间件
下载然后使用,比如
cors
注意
一定要在
注册路由
前注册中间价
,特别是全局中间价
,局部中间件也要先声明
在使用。中间件类别的区分在于
参数的个数
。如果在中间件中发生错误,应该传递给
next(err)
,这样可以让错误处理中间件
有机会处理该错误不要忘记调用
next()
:如果你不调用next()
,请求-响应周期将会停止,导致客户端永远等待响应。