JS服务端技术—Node.js知识点
【版权声明】未经博主同意,谢绝转载!(请尊重原创,博主保留追究权)
https://www.cnblogs.com/cnb-yuchen/p/18031964
出自【进步*于辰的博客】
1、NPM
推荐一篇博文《NPM概述及使用简介》(转发)。
我暂未整理相关阐述,大家可查阅这篇文章。
2、Buffer
推荐一篇博文《02-Node.js—Buffer(缓冲器)》(转发)。
参考笔记三,P49.1。
Buffer
是一种类似数组的对象,用于表示固定长度的字节序列,其本质是一段内存空间,且空间由c++申请,每个元素占一个字节。
创建:
Buffer.alloc(size)
:创建长度为 size 的字节序列;buffer.allocUnsafe(size)
:同上,区别是在分配内存时不会清除旧数据(指曾使用过仍保留数据、但目前未使用的内存空间);Buffer.from(xx)
:xx 可以是数组、字符串或 Buffer。
说明:
1、由于每个元素占一个字节,故alloc(size)
和allocUnsafe(size)
创建的字节序列共包含 size 个字节。
示例:
var buf = Buffer.alloc(10)
// 打印buf:<Buffer 00 00 00 00 00 00 00 00 00 00>
规定以16进制的格式进行显示,00
(16进制)是0000 0000
(二进制),共10个元素。
2、from(xx)
创建的字节序列所占字节数由 xx 决定。
示例1。(xx
是数组)
var arr = [2, 0, 2, 3]
var buf = Buffer.from(arr)
// 打印buf:<Buffer 02 00 02 03>
数字占一个字节,故长度为4
。
2
(数字,十进制)是02
(16进制)。
示例2.。(xx
是字符串)
var buf = Buffer.from('2023')
// 打印buf:<Buffer 32 30 32 33>
为何buf[0]
是32
?因为此时的2
不是数字,而是字符。
'2'
的 ASCLL 码是50
,转换成16进制就是32
。
示例3。(xx
是字符串)
var buf = Buffer.from('汉字')
// 打印buf:<Buffer e6 b1 89 e5 ad 97>
是不是有点懵?因为Buffer
采用utf-8
编码,一个汉字占3个字节,故用三个元素表示一个汉字。
改一下。
var buf = Buffer.from('汉字')
buf[0] = 97 + 7// 'h'的ASCLL码
buf[1] = 97
buf[2] = 97 + 13
console.log(buf.toString())// 打印:han字
toString()
会将每个元素都转换成对应的字符,这样是不是一目了然了。
再补充一点。
var buf = Buffer.from('汉字')
buf[0] = 97 + 7 + 256// ------------------A
buf[1] = 97
buf[2] = 97 + 13
console.log(buf.toString())// 打印:han字
97 + 7
是'h'
的 ASCLL 码,再+ 256
已经不是'h'
,为何最后还是'h'
?
因为Buffer
规定,一个字符占一个字节。换言之,只会用一个字节来表示字符,如果字符对应的 ASCLL 码超出一个字节(8位)的表示范围(255
),超出的部分会被丢弃。
256
对应的二进制是1 00000000
,即需要两个字节,则第一个字节舍去,剩下0000 0000
,为0
(十进制)。
示例4。(xx 是Buffer
)
var buf1 = Buffer.from([2, 0, 2, 3])
var buf2 = Buffer.from(buf1)
// 打印buf:<Buffer 02 00 02 03>
与示例1相同。
3、fs模块
推荐一篇博文《03-Node.js—fs模块》(转发)。
参考笔记三,P50、P51。
fs模块是Node.js的内置模块,负责与文件系统的交互。
3.1 读文件
函数:
- 异步读取:
readFile(path[, options], (err, content) => {})
; - 同步读取:
var content = readFileSync(path[, options])
; - 流式读取:(1)创建读取流:
createReadStream(path[, options])
;(2)通过'data'
、'end'
事件读取。
注:options
是读取配置,如:'utf-8'
,否则读取结果为二进制序列。
3.2 写文件
函数:
- 异步写入:
writeFile(path, data[, options], err => {})
; - 同步写入:
writeFileSync(path, data[, options])
,返回undefinied
; - 流式写入:(1)创建写入流:
createWriteStream(path[, options])
;(2)写入:write(data)
; - 附加写入:
appendFile() / appendFileSync()
,参数同上。
注:options
是写入配置,如:{flag: 'a'}
表示附加写入(暂不理解)。
4、path模块
推荐一篇博文《04-Node.js—path模块》(转发)。
参考笔记三,P51。
函数:
- 解析路径:
resolve(path)
,path 前常附加当前目录__dirname
; - 返回文件后缀:
extname(path)
;
5、express模块
推荐一篇博文《09-Node.js—express框架》(转发)。
参考笔记三,P46、P50、P51。
express模块是基于Node.js的web应用开发框架,主要用于搭建js服务器。
5.1 响应相关函数
- 重定向:
res.redirect(url)
; - 响应文件:
res.download(path)
,path 可以是绝对 / 相对路径,此函数是基于fs.readFile()
和res.end()
的封装; - 以json字符串作为响应体:
res.json({})
; - 设置响应体:
res.send()
,此函数是基于res.end()
(http模块)的封装,可响应任何类型,且只保留数据部分,如会将str
最外层的''/""
省略。
注意:此函数是设置响应体,也是响应。虽是响应,但请求处理未结束,也由于是响应,故在其后不能再做响应配置,如:res.write()
(http模块)、res.set()
(见第7项); - 响应文件:
res.sendFile(path.resolve(__dirname + path))
。path 必须是绝对路径,故拼接了__dirname
。
5.2 中间件
1、路由中间件。
“路由中间件”表现为具有三个参数(req, res, next
)的函数,用于封装路由公共代码(匹配路由前的操作),故需要置于所有路由之前(即中间件之前的路由不会执行中间件,因为路由匹配至上而下),且必须调用next()
才能执行路由(暂不知next
是什么)。
2、静态资源中间件。
设置项目根目录为目录
:express.static(目录)
。
注意:中间件必须使用ser.use()
引入。
5.3 Router
Router是一个完整的中间件和路由系统,可看作是一个小型的js服务器(当然并不是js服务器,故需要引入到js服务器中使用)。
使用步骤:
- 创建路由:
var rou = express.Router()
; - 配置路由(与js服务器相同);
- 开放接口:
module.exports = rou
; - 引入:
ser.use(require(Router路径))
,路径必须是相对路径,且必须采用./
格式。
注意:
- 若使用 Router,必须在 Router 的路由的适当位置调用
next()
(先在路由回调函数上添加next
参数)。因为 Router 是子服务,一般使用 Router 时,主服务中肯定也有路由,如果不调用next()
,则不会检索主路由; - PS:Router 没有跨域问题,原因未知。
5.4 解析请求体数据
debug 一下,就可以发现req
对象具有三个属性,query
(封装请求行数据)、params
(封装动态请求数据,暂不清楚)、body
(封装请求体数据)。
假设body = {id: 2023}
,则通过req.body.id
或req.body['id']
即可获取参数id
的值,之所以能获取到,是因为body
中数据的格式是js对象
。换言之,若请求数据的格式不是js对象
,就不一定能解析成功。
这时,可以使用body-parser
模块进行辅助解析。
步骤:
- (1)若请求数据封装的方式是
x-www-form-urlencoded
,构造对象:var urlParser = bodyParser.urlencoded({extended: false})
;(2)若封装方式是json
,构造对象:var jsonParser = bodyParser.json()
; - 将路由的第2个参数设置为
urlParser
。
PS:我尝试了很多方法进行模拟测试,ajax、postman、form等等,可不知为何,req
对象中始终没有body
属性(js服务端使用vscode开发),故根本无法测试,所以只能请大家自行测试领悟了。
5.5 综合示例
先言:代码稍微有点长,一是为了尽量多使用express模块的函数,做个示例;二是为了使功能稍微丰满一点。大家阅读的时候,直接跳过与业务相关的代码,留意以上四个知识点相关的部分就OK。
1、主服务代码。
const express = require('express')
const bodyParser = require('body-parser')
const path = require('path')
const ser = express()// 构建服务
// 引入静态资源中间件,
ser.use(express.static('./'))// 设置项目根目录为当前目录
// 引入路由中间件
ser.use((req, res, next) => {
res.set('access-control-allow-origin', 'http://127.0.0.1:5501')// 跨域配置
next()
})
// 引入Router
ser.use(require('./r1'))// Router与当前文件同目录,文件名是 r1.js
var users = [
{
id: 1,
name: '进步',
pass: '2023'
}, {
id: 2,
name: '于辰',
pass: '2021'
}
]
ser.get('/g1', (req, res) => {
var id = req.query.userid
var result = users.find(item => {
if (item.id == id) {
res.json({
id: id,
user: item.name,
pass: item.pass
})
return true
}
})
if(!result)// 未找到
res.send('<h1>此账号异常</h1>')
})
var jsonParser = bodyParser.json()
ser.post('/p1', jsonParser, (req, res) => {
var user = req.body.username
var pass = req.body.password
var result = users.find(item => {
if(item.name == user && item.pass == pass) {
// 账号、密码正确,返回用户界面
res.sendFile(path.resolve(__dirname + '/userinfo.html'))
}
})
if(!result) {
// 账号、密码错误,返回首页
res.download('index.html')
}
})
ser.all('*', (req, res) => {
res.send('<h1>not found route</h1>')
})
// 启动服务,监听8081端口
ser.listen(8081, () => {
console.log('created')
})
2、Router代码
const express = require('express')
const bodyParser = require('body-parser')
let rou = express.Router()// 创建Router
// Router是完整的中间件和路由系统,故也可在此创建路由中间件
// ser.use((req, res, next) => {
// res.set('access-control-allow-origin', 'http://127.0.0.1:5501')
// next()
// })
rou.get('/rou/g1', (req, res) => {
var id = req.query.userid
})
var urlParser = bodyParser.urlencoded({extended: false})
rou.post('/rou/p1', urlParser, (req, res) => {
var user = req.body.username
var pass = req.body.password
})
rou.all('rou/*', (req, res, next) => {
// 路由检索自上而下,若匹配此路由,说明此Router中没有”有效“匹配的路由,
// 则调用 next() ”跳出“此Router,去主服务检索路由
next()
})
module.exports = rou// 开放接口
6、http模块
推荐一篇博文《05-Node.js—http模块》(转发)。
参考笔记三,P50。
http模块是一个Node.js中与HTTP协议对接的模块,用于搭建HTTP服务,或者说用于搭建js服务器。
相关操作:
1、创建http服务:http.createServer((req, res) => {})
。
2、获取请求行数据。
方法一:
// 由于http服务封装的req对象中没有query、params属性,
// 故需要使用url模块将req.url进行构造,从中获取请求行数据
const u = require('url')
var url = u.parse(req.url)
// url中包含query属性,但其中数据的格式是字符串,故下行代码报错,无法获取
var ID = url.query.userid
// 重新构造
var url = u.了parse(req.url, true)
// 这样格式就转化成了js对象
方法二:
// 原理同上,只是换用内置对象URL进行构造,
// 前缀'http...'是任意的。不过,出于业务考虑,应与客户端相同
var url = new URL(req.url, 'http://127.0.0.1:5500')
// URL对象中请求行数据封装在属性searchParams中,而不是query
// searchParams中数据的格式不是js对象,需要调用get()获取
var id = url.searchParams.get('id')
3、获取请求体数据。
// req对象中同样没有body属性,需要使用'data'和'end'事件进行获取
var bodyData
req.on('data', temp => {
// temp的类型是String,故直接拼接
bodyData += temp
})
req.on('end', () => {
// bodyData是String,故如此无法获取,
var id = bodyData.userid
})
我暂且也不知如何解析:String → js对象,大家自行补充了。
PS:我未查阅资料的原因:(1)在上面express
模块的示例中我提起过,不知为何req
对象中没有body
属性,故我无法测试;(2)express
模块是基于http
模块的封装,使用express
模块搭建的js服务器更强大。
4、设置响应头时,若有多个值,需使用数组。
5、res.write(str)
需与res.end(str)
连用。其中,res.write()
用于附加响应。
6、当使用live-server
打开html文件时,项目根目录为当前html文件所在目录。
PS:这一点我还不太理解,至少我测试http://localhost:8081/1.jpg
时仍然访问不到图片(1.jpg
与当前html文件同目录)。无妨,解决办法往下看。。。
7、关于静态资源无法访问问题
关于这个问题,相关概述在博文《05-Node.js—http模块》(转发)的第4.5项,找到其中“我们该如何解决?”那一段可见。
参考笔记三,P49.3。
在学习此模块时,一开始并未注意这个细节,对这句“对路径进行判断”无法理解,我认为只要路径正确怎会访问不到。
因为本人致力于Java,项目根目录都是自动配置,并不知Nodejs中很多情况需要手动配置。
这个问题出现的场合:
- 上面http模块中项目根目录无效导致无法访问静态资源;
- js服务器响应html文件,文件内css、js、img等静态资源无效或无法访问。(这就是那位博主所述的情况)
为了让大家充分理解,我逐步说明。。。
首先,若客户端请求的是图像文件,且正常响应,会无效吗?答案是 NO,除非响应有问题。比如:服务器未将图像的所有信息(二进制)响应,那么此图像肯定无法正常显示。(一般不会有这种操作)
然后,若客户端请求的是css、js等文件,同样正常响应,会无效吗?也是 NO。这种情况下即便响应不完整,也不会无效,因为是一并解释。
那为何无效或无法访问?
二种情况:
- 客户端处理响应时调用的是
text()
,而不是html()
(以 jq 为例),以字符串的方式处理响应内容,自然无法识别标签(如:<script>
),故无效; - 找不到静态资源。
若是第一种情况,调用html()
即可。
那为何找不到资源?
走到这一步,客户端已经可以正常处理响应,无论html、css、js还是img,故原因是:
- 访问路径有误;
- 服务器响应有误。
因为已经可以正常解析响应的html文件,即静态资源标签的src
属性有效。
在解析html文件时,会同时根据
src
属性的路径向服务器发起请求。
因此,这种情况下src
必须是完整路径,如:http://127.0.0.1:8081/1.jpg
,这样才有可能找到文件。
完整路径能找到静态资源吗?
若js服务器由express
模块搭建,只要配置好项目根目录(静态资源中间件),就可以直接找到静态资源。
若js服务器由http
模块搭建,由于无法配置项目根目录,故只能由路由对完整路径进行处理,从而返回静态资源(那位博主说“对路径进行判断”就是这个意思)。
示例:
const httpSer = http.createServer((req, res) => {
if (req.url == '/1.jpg') {
fs.readFile(__dirname + url, (err, data) => {
res.end(data)
})
return
}
var data = fs.readFileSync(__dirname + '/index.html')
res.end(data)
})
OK!Perfect!!http模块中项目根目录无效导致无法访问静态资源的问题也解决了。
8、通用设置
参考笔记三,P49.2/4。
“通用设置”指不同模块中业务相同的操作或函数。
- 设置状态码:
res.statusCode()
或res.status()
; - 设置状态码描述:
res.statusMessage
; - 设置响应体:
res.write()
、res.end()
或res.send()
; - 设置响应头:
res.setHeader('标头', 值)
或res.set('标头', 值)
; - 获取请求头:
req.headers.referer
或req.get('referer')
; - (就列举这些哈,其他操作或函数不常用或者通过
debug
就可以知晓,以名达意。)
注:
- “或”前操作或函数属
http
模块,后属express
模块,且express
模块是基于http
模块的封装(express
模块兼容http
模块); res.end()
与res.send()
都是设置响应体的末操作,故其后不能再做响应配置。
9、mysql模块
参考笔记三,P50.1。
顾名思义,此模块用于连接MySQL数据库。可能有博友疑惑:“前端怎么能连接数据库?” 是的,JS作为前端渲染技术,无法做到,但使用Node.js可以。
Node.js作为JS服务端技术,即后端,可通过mysql
模块实现数据库的连接。
我暂未对此模块进行研究,仅是了解,大家有兴趣可查阅博文《为什么不能在前端连接数据库呢?》(转发)。
最后
本人的核心语言是Java,故有时倾向于以Java的思想进行阐述,这可能会给向前端发展的博友们的阅读带来不适。并且,由于本文相当于是我系统学习Node.js的笔记,也基于我的Java功底,所以有些阐述不会那么详细。
不过,Java作为一种强类型的编程语言,我的阐述会很严谨,所以需要大家在阅读时多一点耐心。
再者,本文中的例子是为了方便大家理解和阐述知识点而简单举出的,旨在阐明知识点,并不一定有实用性,仅是抛砖引玉。
本文持续更新中。。。