定义
nodejs
是前端工程化的基础,是开源的,基于谷歌v8引擎
构建的js运行环境,运行开发者使用js编写的服务器。
三大模块
文件相关
fs模块
封装了与本机文件系统
进行交互的方法与属性
fs.readFile()
1 | fs.readFile(path[, options], callback) |
参数1:必选参数,字符串,表示
文件路径
。参数2:可选参数,表示以什么
编码格式
来读取文件。参数3:必选参数,文件读取完成后,通过
回调函数
拿到读取的结果,形如(err,dataStr)=>{}
读取成功
err
为null,否则为错误对象
示例:
1 | fs.readFile('/path/to/file.txt', 'utf8', (err, data) => { |
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 | // math.js |
将函数挂载到对象
所以就把相关的函数和变量封装到一个对象中,这样即便存在同名的函数
也没关系,但是这样不安全,因为对象的属性可以被随意修改
1 | // math.js |
立即执行函数
使用立即执行函数私有化变量和函数,并使用return
暴露函数。这种写法的,被引入的函数和相关变量不再挂载到全局对象,但必须要求原模块代码先于拓展模块代码被引入。
1 | // math.js |
为了解决先后顺序问题,再传参的时候判断一下如果传入的是
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’
包
定义
将模块
和其他资料
聚合成一个文件夹
(通常是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
锁定了依赖树中每一个包的具体版本号。这样可以确保无论何时何地重新安装依赖
,都能得到完全相同
的依赖环境,避免因为不同时间点安装不同版本的依赖而引起的潜在问题,从而提供更为稳定的开发环境。
总结
npm i
下载包,寻找的是lock文件中指定的具体版本的包package.json
文件中指定的包,只会给出包的范围,只参考这个文件可能下载的还是较新的包- 删除
lock
文件,执行npm i
,又会出现新的lock
文件
场景 | 依赖版本选择依据 | 是否更新 package-lock.json |
---|---|---|
首次安装(npm i )(无 package-lock.json ) | 根据 package.json 的版本范围 | 是,生成 package-lock.json |
常规安装(有 package-lock.json ) | 严格按 package-lock.json | 否 |
手动升级依赖(如 npm install package@x.y.z ) | 强制覆盖为指定版本 | 是,更新 package-lock.json |
使用 npm update | 根据 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
文件,配置清单信息(记录这个包引入的其他包的信息),-y
的意思是:后续所有配置都选择yes
,也就是使用默认配置。只能在
英文
目录下运行,不能包含空格,不能包含中文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
字段。res.end
:res.end
是 Node.js 的http模块的原生方法,Express 继承了它。主要用于快速结束请求-响应周期,通常不携带数据或者只携带非常少量的数据。与res.send不同的是,它不会自动设置响应头(如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
目录中查找。
也许推荐的做法是写成:
1 | app.use('/public',express.static('public')); |
这样就能精确的控制具体在哪个目录下查找静态资源。
路由
在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
17// routes/user.js
// 可以看出,导出的express函数上还挂载了一个Router方法,用来创建一个路由器
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)
,err错误对象是第一个参数,因为它对于异常捕获中间件是非常重要的。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()
,请求-响应周期将会停止,导致客户端永远等待响应。