JS服务端技术—Node.js知识点

【版权声明】未经博主同意,谢绝转载!(请尊重原创,博主保留追究权)
https://www.cnblogs.com/cnb-yuchen/p/18031964
出自【进步*于辰的博客

1、NPM

推荐一篇博文《NPM概述及使用简介》(转发)。

我暂未整理相关阐述,大家可查阅这篇文章。

2、Buffer

推荐一篇博文《02-Node.js—Buffer(缓冲器)》(转发)。
参考笔记三,P49.1。

Buffer是一种类似数组的对象,用于表示固定长度的字节序列,其本质是一段内存空间,且空间由c++申请,每个元素占一个字节。

创建:

  1. Buffer.alloc(size):创建长度为 size 的字节序列;
  2. buffer.allocUnsafe(size):同上,区别是在分配内存时不会清除旧数据(指曾使用过仍保留数据、但目前未使用的内存空间);
  3. 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个元素。

2from(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 读文件

函数:

  1. 异步读取:readFile(path[, options], (err, content) => {})
  2. 同步读取:var content = readFileSync(path[, options])
  3. 流式读取:(1)创建读取流:createReadStream(path[, options]);(2)通过'data''end'事件读取。

注:options是读取配置,如:'utf-8',否则读取结果为二进制序列。

3.2 写文件

函数:

  1. 异步写入:writeFile(path, data[, options], err => {})
  2. 同步写入:writeFileSync(path, data[, options]),返回undefinied
  3. 流式写入:(1)创建写入流:createWriteStream(path[, options]);(2)写入:write(data)
  4. 附加写入:appendFile() / appendFileSync(),参数同上。

注:options是写入配置,如:{flag: 'a'}表示附加写入(暂不理解)。

4、path模块

推荐一篇博文《04-Node.js—path模块》(转发)。
参考笔记三,P51。

函数:

  1. 解析路径:resolve(path),path 前常附加当前目录__dirname
  2. 返回文件后缀:extname(path)

5、express模块

推荐一篇博文《09-Node.js—express框架》(转发)。
参考笔记三,P46、P50、P51。

express模块是基于Node.js的web应用开发框架,主要用于搭建js服务器。

5.1 响应相关函数

  1. 重定向:res.redirect(url)
  2. 响应文件:res.download(path),path 可以是绝对 / 相对路径,此函数是基于fs.readFile()res.end()的封装;
  3. 以json字符串作为响应体:res.json({})
  4. 设置响应体:res.send(),此函数是基于res.end()(http模块)的封装,可响应任何类型,且只保留数据部分,如会将str最外层的''/""省略。
    注意:此函数是设置响应体,也是响应。虽是响应,但请求处理未结束,也由于是响应,故在其后不能再做响应配置,如:res.write()(http模块)、res.set()(见第7项);
  5. 响应文件: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服务器中使用)。

使用步骤:

  1. 创建路由:var rou = express.Router()
  2. 配置路由(与js服务器相同);
  3. 开放接口:module.exports = rou
  4. 引入:ser.use(require(Router路径)),路径必须是相对路径,且必须采用./格式。

注意:

  1. 若使用 Router,必须在 Router 的路由的适当位置调用next()(先在路由回调函数上添加next参数)。因为 Router 是子服务,一般使用 Router 时,主服务中肯定也有路由,如果不调用next(),则不会检索主路由;
  2. PS:Router 没有跨域问题,原因未知。

5.4 解析请求体数据

debug 一下,就可以发现req对象具有三个属性,query(封装请求行数据)、params(封装动态请求数据,暂不清楚)、body(封装请求体数据)。
假设body = {id: 2023},则通过req.body.idreq.body['id']即可获取参数id的值,之所以能获取到,是因为body中数据的格式是js对象。换言之,若请求数据的格式不是js对象,就不一定能解析成功。
这时,可以使用body-parser模块进行辅助解析。

步骤:

  1. (1)若请求数据封装的方式是x-www-form-urlencoded,构造对象:var urlParser = bodyParser.urlencoded({extended: false});(2)若封装方式是json,构造对象:var jsonParser = bodyParser.json()
  2. 将路由的第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、设置响应头时,若有多个值,需使用数组。

5res.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中很多情况需要手动配置。

这个问题出现的场合:

  1. 上面http模块中项目根目录无效导致无法访问静态资源;
  2. js服务器响应html文件,文件内css、js、img等静态资源无效或无法访问。(这就是那位博主所述的情况)

为了让大家充分理解,我逐步说明。。。

首先,若客户端请求的是图像文件,且正常响应,会无效吗?答案是 NO,除非响应有问题。比如:服务器未将图像的所有信息(二进制)响应,那么此图像肯定无法正常显示。(一般不会有这种操作)
然后,若客户端请求的是css、js等文件,同样正常响应,会无效吗?也是 NO。这种情况下即便响应不完整,也不会无效,因为是一并解释。

那为何无效或无法访问?
二种情况:

  1. 客户端处理响应时调用的是text(),而不是html()(以 jq 为例),以字符串的方式处理响应内容,自然无法识别标签(如:<script>),故无效;
  2. 找不到静态资源。

若是第一种情况,调用html()即可。

那为何找不到资源?
走到这一步,客户端已经可以正常处理响应,无论html、css、js还是img,故原因是:

  1. 访问路径有误;
  2. 服务器响应有误。

因为已经可以正常解析响应的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。

“通用设置”指不同模块中业务相同的操作或函数。

  1. 设置状态码:res.statusCode()res.status()
  2. 设置状态码描述:res.statusMessage
  3. 设置响应体:res.write()res.end()res.send()
  4. 设置响应头:res.setHeader('标头', 值)res.set('标头', 值)
  5. 获取请求头:req.headers.refererreq.get('referer')
  6. (就列举这些哈,其他操作或函数不常用或者通过debug就可以知晓,以名达意。)

注:

  1. “或”前操作或函数属http模块,后属express模块,且express模块是基于http模块的封装(express模块兼容http模块);
  2. res.end()res.send()都是设置响应体的末操作,故其后不能再做响应配置。

9、mysql模块

参考笔记三,P50.1。

顾名思义,此模块用于连接MySQL数据库。可能有博友疑惑:“前端怎么能连接数据库?” 是的,JS作为前端渲染技术,无法做到,但使用Node.js可以。

Node.js作为JS服务端技术,即后端,可通过mysql模块实现数据库的连接。

我暂未对此模块进行研究,仅是了解,大家有兴趣可查阅博文《为什么不能在前端连接数据库呢?》(转发)。

最后

本人的核心语言是Java,故有时倾向于以Java的思想进行阐述,这可能会给向前端发展的博友们的阅读带来不适。并且,由于本文相当于是我系统学习Node.js的笔记,也基于我的Java功底,所以有些阐述不会那么详细。

不过,Java作为一种强类型的编程语言,我的阐述会很严谨,所以需要大家在阅读时多一点耐心。

再者,本文中的例子是为了方便大家理解和阐述知识点而简单举出的,旨在阐明知识点,并不一定有实用性,仅是抛砖引玉。


本文持续更新中。。。

posted @ 2023-11-22 04:16  进步·于辰  阅读(26)  评论(0编辑  收藏  举报  来源