定义
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(),请求-响应周期将会停止,导致客户端永远等待响应。
