青训营笔记创作活动——node.js | 青训营

62 阅读33分钟

Node.js

Node.js可以解析JS代码。

Javascript是脚本语言,而脚本语言需要解析器。

写在html中的js,浏览器就是js的解析器,其主要作用是操作dom元素。

而独立的js,Node.js就是js的解析器,其主要作用是操作文件或者搭建服务器等。

下载安装:nodejs.org/en

查看文档:www.npmjs.com/

vscode终端查看:node -v ; npm -v ;

1、json-server基本使用。

全局安装json-server:npm install json-server -g ;

解决问题:json-server -v ; 以管理员身份运行vscode —— 终端输入Set-ExecutionPolicy RemoteSigned

json-server:基于一个json文件就可以创建很多的后端模拟接口。

json-server test.json --watch   // 启动后端服务器监听json文件 注意 要切换到json文件对应文件夹
http://localhost:3000/users     // 第一级的每一个键就是一个接口 可以直接在浏览器中键入地址查看
test.json:
{
    "users":[
        {
            "id":1,
            "username":"wxm",
            "password":"123456"
        },
        {
            "id":2,
            "username":"hx",
            "password":"123520"
        }
    ]
}

2、Node基本使用。

Chrome浏览器使用V8引擎解析javascript,浏览器也可以解析BOM和DOM;但是Node.js使用V8只能解析原生的js,而不能解析BOM和DOM。

node  .\node\1-hello.js  // 一定要注意node后面的js文件相对于当前终端目录的相对路径喔

使用时直接编写原生js文件,然后在终端输入上述对应命令即可运行js文件。

3、CommonJS模块化。

当在同一个项目中同时引入多个js文件时,可能会存在依赖关系(一个js文件需要依赖另一个js文件如果被依赖的js文件没有先引入则会报错)、命名空间(多个js文件包含某一同名函数则后面的js文件会覆盖前面的js文件)以及代码组织(某一函数只允许该js文件使用但是被其他js文件使用)等问题,这时我们就需要引入模块化这一概念,其包含定义、暴露接口以及引用等概念。

CommonJS规范:我们可以把公共的功能抽离成一个单独的js文件,默认情况下该js文件中的属性和方法外界是无法访问的。如果要让外界访问该js文件中的属性或者方法,可以在该模块内使用module.exports或者exports来暴露属性或者方法,然后在需要引用的文件使用require来引用接口。

a.jsfunction test()
{
    console.log("test-aaa")
}
module.exports = test
b.jsfunction test()
{
    console.log("test-bbb")
}
module.exports = test
c.jsfunction test()
{
    console.log("test-ccc")
}
module.exports = test
index.jsvar a = require('./a')
var b = require('./b')
var c = require('./c')
​
a()
b()
c()

注意暴露接口规范:

//该方法挂在多个会被覆盖
module.exports = test
module.exports = upper
​
//该方法可以通过对象形式挂在多个 test,等价于test:test,
module.exports = 
{
    test,
    upper
}
​
// 该方法可以挂多个
exports.test = test
exports.upper = upper

注意:哪被用哪暴露,在哪用在哪引。

4、包工具npm和yarn。

npm:npm是一个包管理工具,其可以从npm仓库下载第三方模块。安装node.js后默认含有npm!

npm init  // 填写信息后会生成package.json文件 其会记录下载的包信息 局部安装才有packaga.json文件喔
npm install 包名 // 其会生成package-lock.json文件 其会锁定我们下载的具体版本 以及其所依赖的版本
npm install 包名@版本号  //指定版本
npm unistall 包名  //卸载
npm update 包名  //更新
npm install  //安装项目的全部依赖
install - i   --save - -S    --save-dev - -D   
-g(全局安装)  --save(dependencies开发依赖)  --save-dev(devdependencies上线依赖)
"dependencies": {"md5":"^2.1.0"}  ^ 表示如果直接npm install将会安md5 2.*.*最新版本
​
"dependencies": {"md5":"~2.1.0"}  ~ 表示如果直接npm install将会安装md5 2.1.*最新版本
​
"dependencies": {"md5": "*"}  * 表示如果直接npm install将会安装md5最新版本

全局安装nrm:nrm是npm的镜像源管理工具,有时候国外资源太慢,这个时候可以使用nrm来快速在npm源间切换。

npm i -g nrm  //全局安装nrm
npm install -g nrm open@8.4.2 -save  //解决nrm安装问题
nrm -V  //查看nrm版本 注意是大V而不是小v
nrm ls //查看可选的源
nrm use taobao //切换到淘宝源
nrm test //测试速度

全局安装yarn:yarn也是包管理工具,速度超快且超级安全,其缓存了每一个下载过的包,再次使用时无需重复下载。

npm i -g yarn //全局安装yarn
yarn -v  //查看版本
yarn init  //开始新项目 填写信息后会生成package.json文件 其会记录下载的包信息 局部安装才有喔
yarn add md5  //安装md5包
yarn add [package]@[version] //指定安装版本
yarn upgrade [package]@[version] //升级依赖包
yarn remove [package] //移除依赖包
yarn install //安装项目的全部依赖

5、ES模块化。

esmodule.jsconst esmodule = {
    getName(){
        return "esmodule"
    }
}
export default esmodule
index.jsimport esmodule from "./esmodule.js"
console.log(esmodule.getName())
package.json
​
"type":"module"   //默认是CommonJS 

注意ES中的模块化和CommonJS中的模块化的区别,在项目中只允许选择其中一种,而不能混用。

export default esmodule
import esmodule from "./esmodule.js"
export{ esmodule......}
import {esmodule......} from "./esmodule.js"

6、http内置模块。

模块分为内置模块、第三方模块、自定义模块。

// http模块是node.js中的内置模块 当安装完node.js后即有 我们只需要在需要使用其时require即可var http = require("http")
// 创建服务器 并监听3000端口 浏览器访问localhost:3000
http.createServer((req,res)=>{
    //接受浏览器传的参数 返回渲染的内容
    //req接受浏览器传的参数
    res.writeHead(200,{"Content-Type":"text/html;charset=utf-8"})
    //res返回渲染的内容
    res.write(`
        <html>
            <i>hello world</i>
            <b>大家好</b>
        </html>
    `)
    //end表示服务器响应完毕
    res.end()
}).listen(3000,()=>{
    //只要服务器创建成功就执行
    console.log("server start")
})

上述代码只要浏览器访问地址前面是localhost:3000即可执行,如果需要具体区分不同访问地址响应不同内容则可如下。

var http = require("http")
// 创建服务器 并监听3000端口 浏览器访问localhost:3000
http.createServer((req,res)=>{
    //接受浏览器传的参数 返回渲染的内容 每请求一次就响应一次
    //req接受浏览器传的参数
    if(req.url=="/favicon.ico")
        return
    //res返回渲染的内容
    res.writeHead(renderStatus(req.url),{"Content-Type":"text/html;charset=utf-8"})
    res.write(renderHTML(req.url))
    //end表示服务器响应完毕
    res.end()
}).listen(3000,()=>{
    //只要服务器创建成功就执行
    console.log("server start")
})
​
function renderStatus(url)
{
    var arr = ["/home","/list"]
    return arr.includes(url)?200:404
}
​
function renderHTML(url)
{
    switch(url)
    {
        case "/home":
            return `
                <html>
                    <div>Home页面</div>
                </html>
            `
        case "/list":
            return `
                <html>
                    <div>List页面</div>
                </html>
            `
        default:
            return `
                <html>
                    <div>404页面</div>
                </html>
            `
    }
}

一般前端浏览器发送ajax请求时会涉及到跨域问题,那么后端在创建服务器并响应数据时,可以在响应头中设置cors头或者使用jsonp动态创建script,从而解决跨域问题。

"access-control-allow-origin":"*"

http创建服务器,其相对于浏览器是服务器,但是其相对于其他服务器,其可以作为客户端,然后请求其他服务器数据,再将数据返回给浏览器,相当于是一个中间层。

var https = require("https")  // https协议则使用https http协议则使用http
httpget((data)=>{res.end(data)})
function httpget(cb){
    var data = ""
    https.get(`https://i.maoyan.com/api/mmdb/movie/v3/list/hot.json?ct=%E5%B9%BF%E5%B7%9E&ci=20&channelId=4`,res=>{
        // 收集数据  得到一部分就收集一次
        res.on("data",(chunk)=>{
            data+=chunk
        })
        // 响应数据
        res.on("end",()=>{
            //console.log(data)
            cb(data)
        })
    })
}

get请求是get函数,而post请求是request函数。

var https = require("https")  // https协议则使用https http协议则使用http
httppost((data)=>{res.end(data)})
function httppost(cb){
    var data = ""
    //https://m.xiaomiyoupin.com/mtop/market/search/placeHolder
    var options = {
        hostname:"m.xiaomiyoupin.com",   //域名
        port:"443",  // http端口80 https端口443
        path:"/mtop/market/search/placeHolder", //路径
        method:"POST",
        headers:{
            "Content-Type":"application/json"
        }
    }
    var req = https.request(options,(res)=>{
        res.on("data",chunk=>{
            data+=chunk
        })
        res.on("end",()=>{
            cb(data)
        })
    })
    req.write(JSON.stringify([{},{"baseParam":{"ypClient":1}}]))
    req.end()
}

http模块还可以进行爬虫,爬取页面并解析页面,且获取想要的内容,并按照指定格式保存起来。

//使用cheerio模块解析html页面
npm i cheerio 

7、url内置模块。

nodemon是一个小工具,其能满足一旦浏览器发生改变,则自动重启服务器。

npm i -g nodemon
npm i -g node-dev
nodemon .\3-url.js
node-dev .\3-url.js

nodemon和node-dev可以替换node命令实时检测浏览器和服务器。

var urlobj = url.parse(req.url)  // 解析url对象
var pathname = urlobj.pathname  // 只解析前面地址部分
var query = urlobj.query  // 只解析地址查询参数
console.log(urlobj)
console.log(pathname)
console.log(query)
console.log(url.parse(req.url,true).query.a)  // 解析的地址查询参数是对象形式

解析字符串:(旧)

var url = require('url')
const urlString = 'https://www.baidu.com:443/ad/index.html?id=8&name=mouse#tag=110' 
const parsedStr = url.parse(urlString)
console.log(parsedStr)

解析字符串:(新)

const myURL = new URL(req.url,'http://127.0.0.1:3000')  //解析地址前面部分
console.log(myURL)
var pathname = myURL.pathname
const myURL = new URL(req.url,'http://127.0.0.1:3000')  //解析查询参数
console.log(myURL)
console.log(myURL.searchParams)
for(var [key,value] of myURL.searchParams)
   console.log(key,value)

拼接字符串:(旧)

const urlObject = {
    protocol: 'https:',
    slashes: true,
    auth: null,
    host: 'www.baidu.com:443',
    port: '443',
    hostname: 'www.baidu.com',
    hash: '#tag=110',
    search: '?id=8&name=mouse',
    query: {id:'8',name:'mouse'},
    pathname: '/ad/index.html',
    path: '/ad/index.html?id=8&name=mouse'
}
const parsedObj = url.format(urlObject)
console.log(parsedObj)

替换拼接字符串:(旧)

var a = url.resolve('/one/two/three','four')  //替换最后一个斜杠后面部分的内容
var b = url.resolve('http://example.com/','/one')
var c = url.resolve('http://example.com/one','/two') //替换域名后面的部分
console.log(a+","+b+","+c) /one/two/four,http://example.com/one,http://example.com/two

替换拼接字符串:(新)

var d = new URL('/one','http://example.com/aaaa/bbbb/')
console.log(d.href)   http://example.com/one
var e = new URL('https://a:b@测试?abc#foo')
console.log(url.format(e,{unicode:true,auth:false,fragment:false,search:false}))
​
unicode:true 保持原来的编码  测试
auth:false 去除用户名密码   a:b
fragment:false 去除#后面的内容  #foo
search:false 去除查询字符串 ?abc

更多详情请参考官方文档:nodejs.org/dist/latest…

8、querystring内置模块。

querystring可以实现字符串与对象的互相转换。

// 将字符串转换为对象
​
var str = "name=wxm&age=100&location=guangzhou"
var querystring = require("querystring")
var obj = querystring.parse(str)
console.log(obj)
// 将对象转换为字符串
​
var querystring = require("querystring")
var qo = {
    x:3,
    y:4
}
var parsed = querystring.stringify(qo)
console.log(parsed)

querystring还可以实现字符串的加解密,从而防止sql注入。

var str1 = 'id=3&city=北京&url=https://www.baidu.com'
var escaped = querystring.escape(str1)
console.log(escaped)
var escape1 = "id%3D3%26city%3D%E5%8C%97%E4%BA%AC%26url%3Dhttps%3A%2F%2Fwww.baidu.com"
var str2 = querystring.unescape(escape1)
console.log(str2)

9、event内置模块。

const EventEmitter = require("events")
​
const event = new EventEmitter()
​
//监听函数
event.on("play",(data)=>{
    console.log("事件触发了",data)
})
​
setTimeout(()=>{
    //触发函数
    event.emit("play","111111111")
},2000)

event主要用于解决异步问题。

10、fs内置模块。

fs内置模块主要用于文件操作。

const fs = require("fs")
//mkdir创建新目录
fs.mkdir("./avatar",(err)=>{
    console.log(err)
    if(err && err.code === "EEXIST")
        console.log("目录已经存在")
})
const fs = require("fs")
//rename重命名
fs.rename("./avatar","./avatar2",(err)=>{
    if(err && err.code === "ENOENT")
        console.log("目录不存在")
})
const fs = require("fs")
//rmdir删除目录
fs.rmdir("./avatar2",err=>{
    if(err && err.code === "ENOENT")
        console.log("目录不存在")
})
//文件可以没有 但是目录必须要有 文件没有会重新创建 每次执行writefile则前面的会被覆盖
const fs = require("fs")
//writeFile写入文件
fs.writeFile("./avatar/a.txt","hello world",err=>{
    console.log(err)
})
const fs = require("fs")
//appendFile追加文件
fs.appendFile("./avatar/a.txt","你好",err=>{
    console.log(err)
})
const fs =require("fs")
//readFile读取文件
fs.readFile("./avatar/a.txt",(err,data)=>{
    if(!err)
        console.log(data.toString("utf-8"))
})
const fs =require("fs")
//error-first
fs.readFile("./avatar/a.txt","utf-8",(err,data)=>{
    if(!err)
        console.log(data)
})
const fs = require("fs")
//unlink删除文件
fs.unlink("./avatar/a.txt",err=>{
    if(err && err.code === "ENOENT")
        console.log("文件不存在")
})
const fs = require("fs")
//stat查看文件目录信息
fs.stat("./avatar",(err,data)=>{
    //判断是否是文件
    console.log(data.isFile())
    //判断是否是目录
    console.log(data.isDirectory() 
    )
})
const fs = require("fs")
//readdir读取目录
fs.readdir("./avatar",(err,data)=>{
    if(!err)
    {
        console.log(data)
    }
})
// 实现删除同一个目录下多个文件
const fs = require("fs")
fs.readdir("./avatar1/c",(err,data)=>{
    console.log(data)
    data.forEach(item=>{
        //unlinkSync同步删除 如果异步删除可能会出现还没删完文件就删除目录的情况
        fs.unlinkSync(`./avatar1/c/${item}`)
    })
    fs.rmdir("./avatar1/c",err=>{
        console.log(err)
    })
})
一般不推荐同步删除 一般都使用异步操作
//使用promises实现异步删除
const fs = require("fs").promises
fs.readdir("./avatar/c").then(async (data)=>{
    console.log(data)
    let arr = []
    data.forEach(item=>{
        arr.push(fs.unlink(`./avatar/c/${item}`))
    })
    await Promise.all(arr) //所有异步均执行完
    await fs.rmdir("./avatar/c")
})

看来还是要学好Promise和async await!

11、stream内置模块。

stream适合大规模数据复制。

//可读流
const fs =require("fs")
const rs = fs.createReadStream("./1.txt","utf-8")
rs.on("data",(chunk)=>{
    console.log("chunk-",chunk)
})
rs.on("end",()=>{
    console.log("end")
})
rs.on("error",(err)=>{
    console.log(err)
})
//可写流
const fs  = require("fs")
const ws = fs.createWriteStream("./2.txt","utf-8")
ws.write("11111111111111111111111111")
ws.write("22222222222222222222222222222")
ws.write("33333333333333333333333333333333333333")
ws.end()
//通过pipe管道控制读写速度
const fs = require("fs")
const readStream = fs.createReadStream("./1.txt")
const writeStream = fs.createWriteStream("./2.txt")
readStream.pipe(writeStream)

12、zlib内置模块。

zlib内置模块一般是压缩文件从而提高传输速度,前端浏览器向后端服务器请求数据,服务器直接返回相应文件,但是这样得到的文件由于体积较大故导致在网络上传输速度较慢,于是延伸出zlib压缩从而减小文件体积,即服务器将要传输过来的文件压缩再发送,然后浏览器收到后解压缩再解析。

const http = require("http")
const fs = require("fs")
const zlib = require("zlib")
const gzip = zlib.createGzip();
http.createServer((req,res)=>{
    //res 可写流
    const readStream = fs.createReadStream("./index.txt")
    res.writeHead(200,{"Content-Type":"application/x-javascript;charset=utf-8","Content-Encoding":"gzip"})
    readStream.pipe(gzip).pipe(res)
}).listen(3000,()=>{
    console.log("server start")
})

13、crypto内置模块。

crypto内置模块是为了提供通用的加密和哈希算法。

const crypto = require("crypto")
const hash = crypto.createHash("md5")
hash.update("hello world")
console.log(hash.digest("hex"))

14、path内置模块。

const path = require("path")
//__dirname表示当前执行nodemon命令的路径
const pathname = path.join(__dirname,"/static",myURL.pathname)

15、mime内置模块。

//安装:npm i mime
const mime = require("mime")
const type = mime.getType(myURL.pathname.split(".")[1]) //得到文件后缀名对应的类型

16、路由。

将最原始的switch语句封装成一个路由匹配route函数。

const fs = require("fs")
function route(res,pathname)
{
    switch(pathname)
    {
        case "/login":
            res.writeHead(200,{"Content-Type":"text/html;charset=utf-8"})
            res.write(fs.readFileSync("./static/login.html"),"utf-8")
            break;
        case "/home":
            res.writeHead(200,{"Content-Type":"text/html;charset=utf-8"})
            res.write(fs.readFileSync("./static/home.html"),"utf-8")
            break;
        default:
            res.writeHead(404,{"Content-Type":"text/html;charset=utf-8"})
            res.write(fs.readFileSync("./static/404.html"),"utf-8")
    }
}
module.exports = route
​
route(res,myUrl.pathname)

将最原始的switch语句封装成一个路由匹配router对象。(回调函数仍可进一步被封装为渲染页面render函数)

const fs = require('fs')
const router = {
    "/login": (res)=>{
        res.writeHead(200,{"Content-Type":"text/html;charset=utf-8"})
        res.write(fs.readFileSync("./static/login.html"),"utf-8")
    },
    "/home": (res)=>{
        res.writeHead(200,{"Content-Type":"text/html;charset=utf-8"})
        res.write(fs.readFileSync("./static/home.html"),"utf-8")
    },
    "/login": (res)=>{
        res.writeHead(200,{"Content-Type":"text/html;charset=utf-8"})
        res.write(fs.readFileSync("./static/login.html"),"utf-8")
    },
    "/404": (res)=>{
        res.writeHead(404,{"Content-Type":"text/html;charset=utf-8"})
        res.write(fs.readFileSync("./static/404.html"),"utf-8")
    }
}
module.exports=router
​
try {
   router[myUrl.pathname](res)
}
catch {
   router["/404"](res)
}

将原始的http封装成一个服务器启动start函数。

const http = require("http")
const route = require("./route.js")
const router = require("./router.js")
const fs = require("fs")
function start(){
    http.createServer((req,res)=>{
        const myUrl = new URL(req.url,"http://127.0.0.1")
        console.log(myUrl.pathname)
        // route(res,myUrl.pathname)
        try {
            router[myUrl.pathname](res)
        }
        catch {
            router["/404"](res)
        }
        res.end()
    }).listen(3000,()=>{
        console.log("server start")
    })
}
module.exports = start
​
server()

当一个页面存在多个接口,如果将路由和接口都混在一起,那么代码写起来将十分混乱,所以一般情况下是将路由和接口分开编写,然后需要时再合并即可。

首先编写api接口文件。

function render(res, data, type="")  //data是数据 type是数据类型 默认是json
{
    res.writeHead(200,{"Content-Type":`${type?type:"application/json"};charset=utf-8`})
    res.write(data)
    res.end()
}
const apiRouter ={
    "/api/login": (res)=>{
        render(res,`{ok:1}`)
    },
}
module.exports = apiRouter

然后使用assign函数将router和api合并为一个大对象Router。

const Router = {}
Object.assign(Router,router)
Object.assign(Router,api)

最后将server中的router换成Router。

try {
    Router[myUrl.pathname](res)
}
catch {
    Router["/404"](res)
}

为了使得代码耦合性更低,可以将上述assign进行封装为use,然后在启动服务器前进行合并。

// 将文件合在Router中
function use(obj){
    Object.assign(Router,obj)
}
​
server.use(router)
server.use(api)
server.start()

假如返回的html页面中引入了css和js该如何呢?如果还是按照原来的格式写,那么服务器读取html时,其会再次请求css/login.css,但是却发现其目录找不到!注意这个逻辑,其不是在当前编辑代码的目录下读取css,而是在服务器目录下读取喔!

<link rel="stylesheet" href="css/login.css">
p://localhost:3000/css/login.css net::ERR_ABORTED 404 (Not Found)

那么又该如何解决呢?在404部分首先判断是否是文件,如果是则获取文件完整绝对路径,如果路径存在则截取后缀名得到文件类型并且根据类型进行响应。

//静态资源管理
function readStatiFile(req,res)
{
    //获取路径 推荐使用绝对路径寻找
    const myURL = new URL(req.url,"http://127.0.0.1:3000")
    //__dirname表示当前执行nodemon命令的路径
    // console.log(__dirname,myURL.pathname)
    console.log(path.join(__dirname,"/static",myURL.pathname))
    const pathname = path.join(__dirname,"/static",myURL.pathname)
    //处理localhost:3000/
    //if(myURL.pathname==='/') return false //一般在router中将/渲染成home
    //判断是否存在
    if(fs.existsSync(pathname))
    {
        //处理返回
        const type = mime.getType(myURL.pathname.split(".")[1])
        res.writeHead(200,{"Content-Type":`${type};charset=utf-8`})
        res.write(fs.readFileSync(pathname),"utf-8")
        return true
    }
    else{
        return false
    }
}

通过上述处理,也可以将static转换为服务器资源咯!因为你直接在url中输入login.html,其会转换到404部分,此时由于其是文件,故会进行相应的响应。如此便可实现直接通过url获取static目录下的静态资源!

17、express。

官网:www.expressjs.com.cn/

安装:npm i express。

expresss是基于Node.js中的内置http模块进一步封装出来的专门用来创建web服务器的web开发框架。

const express = require("express")
const app = express()
// 第一个参数表示路径
app.get("/",(req,res)=>{
    res.write("hello world")
    res.end()
})
app.listen(3000,()=>{
    console.log("server start")
})

express将wirteHead、write和end封装成了send,其可以自适应解析内容。

const express = require("express")
const app = express()
// 第一个参数表示路径
app.get("/",(req,res)=>{
    res.send("hello world")
})
app.listen(3000,()=>{
    console.log("server start")
})

其地址可以匹配完全字符串、正则字符串以及占位符。

// 第一个参数表示路径
app.get("/",(req,res)=>{
    res.send("hello world")
})
//正则表达式
app.get("/ab?cd",(req,res)=>{
    res.send("abcdefghijklmn")
})
//:占位符
app.get("/wxm/:id",(req,res)=>{
    res.send("wxmmmmmmmmmm")
})

回调函数,也可以叫做中间件,如果其需要执行的逻辑操作较多,那么代码将会太过冗余,所以可以将其根据逻辑分为多个回调函数,然后根据next进行传递,调用next才能执行下一个回调函数,否则不执行。注意,如果先调用send那么就直接返回了,再调用next也没用。

app.get("/",(req,res,next)=>{
    console.log("验证token")
    const isValid = true
    if(isValid)  //成功下一个
    {
        next()
    }else{    //失败则错误
        res.send("error")
    }
},(req,res)=>{
    res.send("hello world")
})

可以使用函数数组的形式使得其看上去更加逻辑清晰。

const fun1 = (req,res,next)=>{
    console.log("验证token")
    const isValid = true
    if(isValid)  //成功下一个
    {
        next()
    }else{    //失败则错误
        res.send("error")
    }
}
const fun2 = (req,res)=>{
    res.send("hello world")
}
// 第一个参数表示路径
app.get("/",[fun1,fun2])

如果第一个函数中有一个计算结果,那么如何将其传递给第二个函数呢?即两个函数如何通信呢?可以在第一个函数中给res加上一个属性并把值挂载在该属性上,然后在第二个函数中通过获取属性来得到对应的值。

app如何获取get请求参数和post请求参数呢?

get:直接使用req.query获取    
console.log(req.query)
​
post:直接使用req.body获取  但是需要配置内置中间件来获取post请求的参数
//配置解析post参数的内置中间件 要在路由前面配置
app.use(express.urlencoded({extended:false})) // post参数 - name=wxm&pwd=123
app.use(express.json()) // post参数 - {"name":"wxm","pwd":"123"}

app如何托管静态文件呢?一般内置中间件和外置中间件最好放在前面,避免未得到应用。

//配置静态资源  可以直接访问public中的静态资源 且访问路径中不需要输入public
app.use(express.static("public"))

服务端渲染:前端写好静态页面和动态效果,然后把前端代码给后端,后端去除假数据并使用从数据库取出的真数据动态填充模板再返回页面给前端。(后端组装页面)

前后端分离:前端写好页面和效果,然后向后端发送请求获取数据,再来动态填充页面。(前端组装页面)

模板引擎,可以在输入url地址时,直接渲染一个html页面。

npm i ejs
//配置模板引擎
app.set("views","./views")
app.set("view engine","ejs")
router.get("/login",(req,res)=>{
    console.log(req.query)
    res.render("login")
})
http://localhost:3000/login   =>    views->login.ejs->写的是login.html

其中ejs和html不同的地方在于,ejs可以直接渲染后端的数据,即ejs是一套简单的模板语言,其可以利用js代码生成html页面。

router.get("/login",(req,res)=>{
    console.log(req.query)
    res.render("login",{title:"1111111111"}) //找到views中的login.ejs
})
<h1> 推荐 - <%=title%> </h1>    // <%=变量名%>   %=要在一起
<%  %> 里面的内容会被解析为js 外面的内容会被解析为html

注意/有和无的区别!

res.redirect("/home")
res.render("login",{error:"用户名或者密码错误"})

express生成器可以快速生成一个应用的骨架。

npm i -g express-generator   //全局安装express生成器
express myApp   //生成一个myApp项目 默认是jade模板引擎
express myApp --view=ejs  //生成一个myApp项目 使用ejs模板引擎
cd myApp //进入package.json所在目录
npm i  //下载依赖 生成node_modules
npm start  //启动项目 将package.json中的start中的node改成nodemon

应用框架含义:

public 静态资源
views  模板引擎
routes 路由匹配
app.js 服务器啊
bin    启动文件

18、中间件。

应用级中间件:使用app.use注册应用级别的中间件。

const express = require("express")
const app = express()
const IndexRouter = require("./router/IndexRouter.js")
//应用级别中间件
app.use(function(req,res,next){
    console.log("验证token")
    next()
})
//应用级别中间件
app.use("/",IndexRouter)  //  /相当于一级目录 IndexRouter相当于二级目录
app.listen(3000,()=>{
    console.log("server start")
})

路由级中间件:使用express.Router()注册路由级中间件。

const express = require("express")
const router = express.Router()
//路由级别中间件
router.get("/home",(req,res)=>{
    res.send("home")
})
router.get("/login",(req,res)=>{
    res.send("login")
})
module.exports = router

可以使用应用级别中间件链接到路由级中间件,从而使得代码更加清晰,将相关的路由放在一起。

app.use("/home",HomeRouter)
app.use("/login",LoginRouter)

错误级中间件:使用status(404)来实现错误级中间件,当前面均匹配不了则执行此处,send默认是200。

//错误级中间件 当前面均匹配不了故执行该处
app.use((req,res)=>{
    res.status(404).send("丢了")
})

19、MongoDB。

mongodb是非关系型数据库,轻量、高效、自由,可以更好的持久化的存储数据。

官网:www.mongodb.com/

手册:www.mongodb.com/docs/manual…

教程:blog.csdn.net/qq_43779149…。(自己写的牛逼吧)

解决:blog.csdn.net/qq_43779149…。(打麻将心态工作)

mongod.exe是服务端,其为客户端提供服务,而mongo.exe就是客户端,由于MongDB6.0之后无mongo.exe,故下载了mongosh.exe。

MongoDB:
​
mongod   服务端
mongosh  客户端
​
cmd(以管理员身份打开):
​
net start mongodb  启动服务端
mongosh            启动客户端
...                输入命令
exit               退出
net stop mongodb   关闭服务端

不同版本操作可能不一样,具体操作名称应该查看官网对应版本喔!

文档:www.mongodb.com/docs/manual…

命令行数据库操作:
​
show dbs    // 查看数据库   空数据库无法被显示
db            // 查看当前使用的数据库
db.version()     // 查看当前db版本
​
use database        // 创建数据库并切换
db.dropDatabase()     // 删除数据库
​
db.createCollection("users")    // 创建集合
db.getCollectionNames()          // 获取该数据库中有哪些集合
db.name.drop()                    // 删除名字为name的集合// 注意:下方的{}里面可以传入条件 如果没有则表示选中所有
​
db.name.insertOne({})               // 插入一个文档
// db.m.insertOne({username:"wxmm",age:22})
db.name.insertMany([{},{},{}])       // 插入多个文档
// db.m.insertMany([{username:"wxm",age:80},{name:"www",myage:222}])
db.name.find()                        // 查找文档内容
​
db.name.deleteOne({})                  //删除满足要求的一个
// db.m.deleteOne({name:"www"})
db.name.deleteMany({})                 //删除满足要求的多个
​
db.name.updateOne({查询条件},{$set:{更新内容}})   //更新满足要求的一个
db.m.updateOne({username:"wxm"},{$set:{age:20}})  
db.name.updateMany({查询条件},{$set:{更新内容}})   //更新满足要求的多个
db.m.updateMany({age:20},{$set:{age:22}})  
​
// set表示设置 inc表示增加或减少  正数增加 负数减少
​
db.m.updateOne({username:"wxm"},{$inc:{age:10}})  
db.m.updateOne({username:"wxm"},{$inc:{age:-8}})  
​
// 查询是最复杂的喔 !!!
​
db.name.find()                  // 查找所有
db.name.find({age:22})          // 查询满足条件的 即age=22的
db.name.find({age:{$gt:20}})    // 查询大于某一数值的
db.name.find({age:{$lt:20}})    // 查询小于某一数值的
db.name.find({age:{$gte:20}})   // 查询大于等于某一数值的
db.name.find({age:{$lte:20}})   // 查询小于等于某一数值的
db.name.find({age:{$gte:20,$lte:30}})  // 查询大于某一数值小于某一数值的范围内的数值// 字符串匹配可以使用正则表达式
​
db.name.find({username:/w/})   // 查询包含w的username
db.name.find({username:/m$/})   // 查询以m为结尾的username
​
db.name.find({},{username:1}) // 第一个是查询条件/二个是查询指定列/想要显示就设置为1否则为0/可多个
​
db.name.find({}).sort({age:1})  // 按照年龄正序排序
db.name.find({}).sort({age:-1}) // 按照年龄倒序排序
​
db.name.find().skip(1).limit(3)  // 跳过几条 限制几条 可用于分页显示
db.name.find().skip((pagenum-1)*pagesize).limit(pagesize)   // 分页显示
​
db.name.find({$or:[{age:22},{username:"wxm"}]})   // 满足条件之一均被选中
​
db.name.find().count()   // 满足要求的数量
​
最后:可以组合使用!!!

Node.js与Mongodb:

 npm i mongoose

config/db.config.js:

// 连接数据库const mongoose = require("mongoose")
​
mongoose.connect("mongodb://127.0.0.1:27017/wxm_project")   
// mongodb 本机域名127.0.0.1 端口号27017 数据库名字 wxm_project// 插入集合和数据 数据库wxm_project会自动创建

bin/www.js:

require("../config/db.config.js")  //引入后会被自动执行

model/UserModel.js:

const mongoose =  require("mongoose")  // 模块引入多次是同一个实例const Schema = mongoose.Schema   // 限制const UserType = {    // 限制模型field类型
    username : String,
    password : String,
    age : Number
}
 
const UserModel = mongoose.model("user",new Schema(UserType))  // 创建一个叫做user的模型 user模型将会对应users集合 对应限制为UserTypemodule.exports = UserModel

views/index.ejs:

register.onclick = ()=>{
        console.log(username.value,password.value,age.value)
        fetch("/api/user/add",{
          method:"POST",
          body:JSON.stringify({
            username:username.value,
            password:password.value,
            age:age.value
          }),
          headers:{
            "Content-Type":"application/json"
          }
        }).then((res)=>res.json()).then(res=>{
          console.log(res)
        })
}
​
updateb.onclick = ()=>{
        console.log(username.value,password.value,age.value)
        fetch("/api/user/update/645b6cfa859fc71836ea8a32",{
          method:"POST",
          body:JSON.stringify({
            username:"修改的名字",
            password:"修改的密码",
            age:1
          }),
          headers:{
            "Content-Type":"application/json"
          }
        }).then((res)=>res.json()).then(res=>{
          console.log(res)
        })
}
​
deleteb.onclick = ()=>{
        console.log(username.value,password.value,age.value)
fetch("/api/user/delete/645b6cfa859fc71836ea8a32").then((res)=>res.json()).then(res=>{
          console.log(res)
        })
}
​
fetch("/api/user/list").then(res=>res.json()).then(res=>{
        console.log(res)
        var tbody = document.querySelector("tbody")
        tbody.innerHTML = res.map(item=>`
          <tr>
            <td>${item._id}</td>
            <td>${item.username}</td>
            <td>${item.password}</td>
            <td>${item.age}</td>
          </tr>
        `).join("")
})

routes/users.js:

router.post("/user/add",(req,res)=>{
  console.log(req.body)
  // 1、创建一个模型 其对应一个数据库
  const {username,password,age} = req.body
  UserModel.create({
    username,password,age
  }).then(data=>{
    console.log(data)
    res.send({ok:1})
  })
})
​
router.post("/user/update/:id",(req,res)=>{
  console.log(req.body,req.params.id)  //req.params.id就是id
  // 1、创建一个模型 其对应一个数据库
  const {username,password,age} = req.body
  UserModel.updateOne({_id:req.params.id},{
    username,password,age
  }).then(data=>{
    console.log(data)
    res.send({ok:1})
  })
})
​
router.get("/user/delete/:id",(req,res)=>{
  UserModel.deleteOne({_id:req.params.id}).then(data=>{
    console.log(data)
    res.send({ok:1})
  })
})
​
router.get("/user/list",(req,res)=>{
  UserModel.find().then(data=>{
    res.send(data)
  })
})

总结:模型对应集合!用法大同小异!

20、接口规范RESTful。

服务器上每一种资源都有对应的url地址,如果客户端需要对服务器上的资源进行操作,就需要通过http协议执行相应的动作来操作它,比如get、post、put、delete...。

RESTful架构指的是url地址中只包含名词表示资源,使用http动词表示动作进行资源操作。

GET /blog/Articles        //获取所有文章
POST /blog/Articles       //添加一篇文章
PUT /blog/Articles        //修改一篇文章
DELETE /blog/Articles     //删除一篇文章

views/index.ejs:

register.onclick = ()=>{
        console.log(username.value,password.value,age.value)
        fetch("/api/user",{
          method:"POST",
          body:JSON.stringify({
            username:username.value,
            password:password.value,
            age:age.value
          }),
          headers:{
            "Content-Type":"application/json"
          }
        }).then((res)=>res.json()).then(res=>{
          console.log(res)
        })
      }
​
updateb.onclick = ()=>{
        console.log(username.value,password.value,age.value)
        fetch("/api/user/645b74074ccde0c3bf9a6507",{
          method:"PUT",
          body:JSON.stringify({
            username:"修改的名字",
            password:"修改的密码",
            age:1
          }),
          headers:{
            "Content-Type":"application/json"
          }
        }).then((res)=>res.json()).then(res=>{
          console.log(res)
        })
      }
​
deleteb.onclick = ()=>{
        console.log(username.value,password.value,age.value)
        fetch("/api/user/645b74074ccde0c3bf9a6507",{
          method:"DELETE"
        }).then((res)=>res.json()).then(res=>{
          console.log(res)
        })
      }
​
fetch("/api/user").then(res=>res.json()).then(res=>{
        console.log(res)
        var tbody = document.querySelector("tbody")
        tbody.innerHTML = res.map(item=>`
          <tr>
            <td>${item._id}</td>
            <td>${item.username}</td>
            <td>${item.password}</td>
            <td>${item.age}</td>
          </tr>
        `).join("")
      })

routes/users.js:

//响应前端的post请求 - 增加用户
router.post("/user",(req,res)=>{
  console.log(req.body)
  // 1、创建一个模型 其对应一个数据库
  const {username,password,age} = req.body
  UserModel.create({
    username,password,age
  }).then(data=>{
    console.log(data)
    res.send({ok:1})
  })
})
​
//响应前端的put请求 - 更新内容
router.put("/user/:id",(req,res)=>{
  console.log(req.body,req.params.id)  //req.params.id就是id
  // 1、创建一个模型 其对应一个数据库
  const {username,password,age} = req.body
  UserModel.updateOne({_id:req.params.id},{
    username,password,age
  }).then(data=>{
    console.log(data)
    res.send({ok:1})
  })
})
​
//响应前端的delete请求 - 删除内容
router.delete("/user/:id",(req,res)=>{
  UserModel.deleteOne({_id:req.params.id}).then(data=>{
    console.log(data)
    res.send({ok:1})
  })
})
​
//响应前端的get请求 - 请求内容
router.get("/user",(req,res)=>{
  UserModel.find().then(data=>{
    res.send(data)
  })
})

如此便可使用一个路径/api/user就实现所有增删改查操作。

21、业务分层MVC。

MVC:index.js是服务器入口文件,负责接受客户端请求;router.js是路由配置,负责将请求分发给C层;C层是controller.js,负责处理业务逻辑,把数据model交给视图view;M层是model,负责处理数据,增删改查;V层是views,负责展示页面。(controller.js对应原来的router的方法的回调函数)

router.js:

//响应前端的post请求 - 增加用户
router.post("/user",UserController.addUser)
​
//响应前端的put请求 - 更新内容
router.put("/user/:id",UserController.updateUser)
​
//响应前端的delete请求 - 删除内容
router.delete("/user/:id",UserController.deleteUser)
​
//响应前端的get请求 - 请求内容
router.get("/user",UserController.getUser)

controller.js:

const UserController = {
    addUser : async (req,res)=>{
        console.log(req.body)
        // 1、创建一个模型 其对应一个数据库
        const {username,password,age} = req.body
        // 异步操作 操作数据库
        await UserService.addUser(username,password,age)
        res.send({ok:1})
      },
    updateUser : async (req,res)=>{
        console.log(req.body,req.params.id)  //req.params.id就是id
        // 1、创建一个模型 其对应一个数据库
        const {username,password,age} = req.body
        await UserService.updateUser(req.params.id,username,password,age)
        res.send({ok:1})
      },
    deleteUser: async (req,res)=>{
        await UserService.deleteUser(req.params.id)
        res.send({ok:1})
      },
    getUser: async (req,res)=>{
        const data = await UserService.getUser()
        // 这里要用到数据啊!
        res.send(data)
      }
}

model:

const UserService = {
    addUser:(username,password,age)=>{
        return UserModel.create({
            username,password,age
          })
    },
    updateUser:(_id,username,password,age)=>{
        return UserModel.updateOne({_id},{
            username,password,age
          })
    },
    deleteUser:(_id)=>{
        return UserModel.deleteOne({_id})
    },
    getUser:()=>{
        return UserModel.find()
    }
}

注意:async await用法!

22、登录权限cookie和session。

如果编写一个登录页面,满足只有当点击登录后,其对应的用户名和密码在数据库中查找得到时,才可以跳转到用户页面,反之弹出错误提示,其固然可以实现登录跳转功能,但是绕过登录页面直接进入用户页面也可以,并不能解决本质问题,即不能实现只有管理员才能查看相关信息。

login.onclick = ()=>{
        console.log(username.value,password.value)
        fetch("/api/login",{
          method:"POST",
          body:JSON.stringify({
            username:username.value,
            password:password.value,
          }),
          headers:{
            "Content-Type":"application/json"
          }
        }).then((res)=>res.json()).then(res=>{
          console.log(res)
          //如果拿到数据
          if(res.ok===1)
          {
                location.href = "/"
          }
          else
          {
            alert("用户名密码不匹配")
          }
        })
}

我们可以在前端登录后,存储一个cookie,然后在后端中,判断其cookie是否存在,如此达到校验目的,但是实际上前端cookie是可以伪造的,故后端无法判断,这时就需要session,其session是在后端存储的,这样就实现了客户端cookie和服务端session的完美搭配,cookie相当于钥匙,session相当于房间,cookie存储sessionid。

安装express-session:

npm i express-session

引入session:

//引入session
var session = require("express-session")

注册session:(打开localhost:3000可查看application部分的cookies)

//注册session中间件
app.use(session({
  name:"wxmsystem",
  secret:"asfsfrgrg",  //密钥
  cookie:{
    maxAge:1000*60*60,  //过期时间
    secure:false  //为true表示只有https协议才能访问cookie
  },
  resave:true,  //在过期时间内重新设置session后重新设置过期时间
  saveUninitialized:true //初始给cookie但无效只有登录成功才有效
}))

注册session要在路由配置前,否则可能无效。

点击登录按钮后,其给/api/login发送post请求,然后就会到UserController.login部分,在此处我们后端进行设置session:

login: async (req,res)=>{
      const {username,password} = req.body
      const data = await UserService.login(username,password)
      // 这里要用到数据啊!
      if(data.length===0)
      {
        res.send({ok:0})
      }
      else
      {
        //后端设置session
        req.session.user = data[0]
        res.send({ok:1})
      }
}

设置完后使用res.send给前端返回数据,此时在前端数据处理部分进行相应处理:

//如果拿到数据
if(res.ok===1)
{
    location.href = "/"  //跳转到主页
}
else
{
    alert("用户名密码不匹配")
}

在跳转时,需要的页面渲染部分进行session判断:

/* GET home page. */
router.get('/', function(req, res, next) {
    //判断req.session.user
    if(req.session.user)  //如果存在才渲染页面
    {
      res.render('index', { title: 'Express' });
    }
    else
    {
      res.redirect("/login")
    }
});

但是上述代码有些许问题:

1、上述问题只在根目录进行了session判断,如果一个小时后cookie过期,此时并未刷新页面,再从根页面增删改查用户,仍可进行,显然这是不满足要求的;

解决:(使用登录校验中间件)

//登录校验中间件
app.use((req,res,next)=>{
  //排除login相关的路由和接口
  if(req.url.includes("login"))
  {
    next()
    return
  }
  if(req.session.user)
  {
    next()
  }
  else
  {
    //是接口则返回错误码
    //反之则返回重定向
    req.url.includes("api")?res.status(401).json({ok:0}):res.redirect("/login")
  }
})

增加退出按钮:(是destroy不是destory)

//前端
exit.onclick = ()=>{
        fetch("/api/logout").then((res)=>res.json()).then(res=>{
          console.log(res)
          //前端重定向
          if(res.ok===1)
          {
            location.href="login"
          }
        })
}
​
//后端
logout:(req,res)=>{
      req.session.destroy(()=>{
        res.send({ok:1})
      })
    }

2、在有效期内重新访问后其cookie过期时间并未变;

解决:(登录校验时重新设置session从而使得cookie过期时间重新设置)

//登录校验中间件
app.use((req,res,next)=>{
  //排除login相关的路由和接口
  if(req.url.includes("login"))
  {
    next()
    return
  }
  if(req.session.user)
  {
    //重新设置session
    req.session.mydate=Date.now()
    next()
  }
  else
  {
    //是接口则返回错误码
    //反之则返回重定向
    req.url.includes("api")?res.status(401).json({ok:0}):res.redirect("/login")
  }
})

3、session是保存在内存中,如果每次重启服务器,则session就会丢失;

解决:(将session存储在数据库或者文件中)

npm i connect-mongo  //安装connect-mongo
const MongoStore = require("connect-mongo")   //引入connect-mongo
//注册session中间件
app.use(session({
  name:"wxmsystem",
  secret:"asfsfrgrg",  //密钥
  cookie:{
    maxAge:1000*60*60,  //过期时间
    secure:false  //为true表示只有https协议才能访问cookie
  },
  resave:true,  //在过期时间内访问后重新设置过期时间
  saveUninitialized:true, //初始给cookie但无效只有登录成功才有效
  store: MongoStore.create({   //存储在mongodb中
    mongoUrl: 'mongodb://127.0.0.1:27017/wxm_session',   //重新创建数据库
    ttl: 1000 * 60 * 60 // 过期时间  和上面保持一致即可
})
}))

注意:使用一个额外的数据库存储session的喔!

23、JWT(Json Web Token)。

使用cookie和session存储也存在一些问题:一是假如在机器A上登录,则session存储在机器A上,如果此时需要在机器B上登录,则此时需要将机器A上的session复制到机器B上;二是由于cookie本身携带有效信息且是自动携带,故很容易被其他网站伪造信息从而截取信息。

解决:不使用session存储,转而使用localstorage存储,并且使用加密存储喔。

缺点:内容大占带宽;无法注销;JWT加密签名。

npm i jsonwebtoken
//测试token的加密与验证过程
var jwt = require("jsonwebtoken")
​
var token = jwt.sign({
  data: 'wxm' 
}, '123', { expiresIn: '10s' })  //123是密钥console.log(token)  //分别为header/data/签名
//eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjoid3htIiwiaWF0IjoxNjg0MDcxNzM4LCJleHAiOjE2ODQwODE3Mzh9._egr1-pzKYU5H3nqR03Dxy0RUqrbl95SF0Wl7yk6stc 
​
​
var decoded = jwt.verify(token, '123');  //123是密钥 前后保持一致
console.log(decoded)
//{ data: 'wxm', iat: 1684071943, exp: 1684071953 }

封装成包:(JWT.js)

//测试token的加密与验证过程
var jwt = require("jsonwebtoken")
​
const secret = '233333'  //密钥const JWT = {
    generate(value,expires){  //数据 过期时间
        return jwt.sign(value, secret, { expiresIn: expires })
    },
    verify(token){
        try{
            return jwt.verify(token, secret)
        }
        catch{
            return false
        }
    }
}
​
module.exports = JWT

测试工具:(生成和验证)

const token = JWT.generate({name:"wxm"},"10s")
console.log(token)
const decoded = JWT.verify(token)
console.log(decoded)

此时,我们可以将原来的session和cookie改为jsonwebtoken。

<script src="https://cdn.jsdelivr.net/npm/axios@1.1.2/dist/axios.min.js"></script>

Axios最大的优点是拦截器的设置,其可以统一对请求和响应进行处理。

login.ejs:// 设置拦截器
​
<script>
   axios.interceptors.request.use(function (config) {
      console.log("请求发出前,执行的方法")
      //login在请求发出前不需要获取token
      return config;
   }, function (error) {
      return Promise.reject(error);
   });
   axios.interceptors.response.use(function (response) {
      console.log("请求成功后,第一个调用的方法")
      //login请求成功后获取后端挂载在header上的token并将其存储在localStorage中
      const {authorization} = response.headers
      authorization && localStorage.setItem("token",authorization)
      return response;
   }, function (error) {
      return Promise.reject(error);
   });
</script>
login.ejs// 使用axios发送请求
​
login.onclick = ()=>{
     console.log(username.value,password.value)
     //使用axios发送post请求
     axios.post("/api/login",{
        username:username.value,
        password:password.value,
     }).then(res=>{
        //注意fetch是res而axios是res.data
        console.log(res.data)  
        if(res.data.ok===1)
        {
           location.href = "/"
        }
        else
        {
           alert("用户名密码不匹配")
        }
      })
}
UserController.js://后端响应/api/login设置token并挂载在header的Authorization字段上
​
login: async (req,res)=>{
      const {username,password} = req.body
      const data = await UserService.login(username,password)
      // 这里要用到数据啊!
      if(data.length===0)
      {
        res.send({ok:0})
      }
      else
      {
        //后端设置token
        const token = JWT.generate({
          _id:data[0]._id,
          username:data[0].username
        },"1h")
        //将token返回在header中
        res.header("Authorization",token)
        res.send({ok:1})
      }
}

index部分和login部分略有不同:

index.ejs:// 设置拦截器
​
<script>
   axios.interceptors.request.use(function (config) {
      console.log("请求发出前,执行的方法")
      //增删改查在发送请求前需要获取token
      const token = localStorage.getItem("token")
      //并将获取到的token携带在headers的Authorization部分发送请求
      config.headers.Authorization = `Bearer ${token}`
      return config;
   }, function (error) {
      return Promise.reject(error);
   });
   axios.interceptors.response.use(function (response) {
      console.log("请求成功后,第一个调用的方法")
      //增删改查在请求成功后再次获取token并存储在localStorage中
      const {authorization} = response.headers
      authorization && localStorage.setItem("token",authorization)
      return response;
      }, function (error) {
      //失败则跳转到登录页面
      console.log(error.response.status)
      if(error.response.status===401)
      {
        localStorage.removeItem("token")
        location.href="/login"
      }
        return Promise.reject(error);
    });
</script>
login.ejs// 使用axios发送请求
​
register.onclick = ()=>{
  console.log(username.value,password.value,age.value)
  axios.post("/api/user",{
    username:username.value,
    password:password.value,
    age:age.value
  }).then(res=>{
    console.log(res.data)
  })
}
​
updateb.onclick = ()=>{
  console.log(username.value,password.value,age.value)
  axios.put("/api/user/6461b04d749c40e0aa753834",{
    username:"修改的名字",
    password:"修改的密码",
    age:1
  }).then(res=>{
    console.log(res.data)
  })
}
​
deleteb.onclick = ()=>{
   console.log(username.value,password.value,age.value)
   axios.delete("/api/user/6461b04d749c40e0aa753834").then(res=>{
     console.log(res.data)
   })
}
​
axios.get("/api/user").then(res=>{
   res = res.data
   console.log("res:",res)
   var tbody = document.querySelector("tbody")
   tbody.innerHTML = res.map(item=>`
      <tr>
        <td>${item._id}</td>
        <td>${item.username}</td>
        <td>${item.password}</td>
        <td>${item.age}</td>
       </tr>
    `).join("")
})
​
exit.onclick = ()=>{
   localStorage.removeItem("token")
   location.href = "/login"
}
//token校验
app.use((req,res,next)=>{
  //如果是登录则直接放行
  if(req.url.includes("login"))
  {
    next()
    return
  }
  //获取token
  const token = req.headers["authorization"]?.split(" ")[1]
  //如果有则验证token
  if(token)
  {
    const payload = JWT.verify(token)
    //获取token后需要重新计算过期时间
    if(payload)
    {
      //重新计算token过期时间
      const newToken = JWT.generate({
        _id:payload._id,
        username:payload.username
      },"1h")
      res.header("Authorization",newToken)
      next()
    }
    else
    {
      //否则反馈token过期
      res.status(401).send({errCode:-1,errInfo:"token过期"})
    }
  }
  else
  {
    //没有token则放行
    next()
  }
})

24、文件上传Multer。

//后端无法处理此种格式
<form action="/api/user" method="POST" enctype="multipart/form-data">
   <div>
      <label>用户名:<input type="text" name="username"></label>
   </div>
   <div>
      <label>密码:<input type="password" name="password"></label>
   </div>
   <div>
      <label>年龄:<input type="number" name="age"></label>
   </div>
   <div>
      <label>头像:<input type="file" name="avatar"></label>
   </div>
   <div>
      <input type="submit" value="提交添加用户">
   </div>
</form>

此时需要引入中间件multer,其专门用于处理multipart/form-data格式的数据。

npm i multer
//引入multer
const multer  = require('multer')
//创建upload文件夹用于保存上传资源
const upload = multer({ dest: '/public/uploads/' })
//响应前端的post请求 - 增加用户   form表单的avatar
router.post("/user",upload.single("avatar"),UserController.addUser)

注意:avatar在req.file中获取。

addUser : async (req,res)=>{
   console.log(req.body,req.file)
   // avatar在req.file中获取
   const avatar = `/uploads/${req.file.filename}`
   const {username,password,age} = req.body
   // 异步操作 操作数据库
   await UserService.addUser(username,password,age,avatar)
   res.send({ok:1})
}

此时使用前后端分离的方式来实现文件上传:

register.onclick = ()=>{
   //通过files[0]获取
   console.log(username.value,password.value,age.value,avatar.files[0])
   //创建表单
   const formsdata = new FormData()
   //注意命名与UserModel相同
   formsdata.append("username",username.value)
   formsdata.append("password",password.value)
   formsdata.append("age",age.value)
   formsdata.append("avatar",avatar.files[0])
   //发送axios请求
   axios.post("/api/user",formsdata,{
     headers:{
       "Content-Type":"multipart/form-data"
     }
   }).then(res=>{
     console.log(res.data)
   })
}

25、接口文档APIDOC。

apidoc是一个简单的RESTful API文档生成工具,其从代码注释中提取特定格式的内容生成文档。

npm i -g apidoc

VSCode插件:ApiDoc Snippets。

在路由接口处输入apiDocumentation:
​
/**
 * 
 * @api {method} /path title
 * @apiName apiName
 * @apiGroup group
 * @apiVersion  major.minor.patch
 * 
 * 
 * @apiParam  {String} paramName description
 * 
 * @apiSuccess (200) {type} name description
 * 
 * @apiParamExample  {type} Request-Example:
 * {
 *     property : value
 * }
 * 
 * 
 * @apiSuccessExample {type} Success-Response:
 * {
 *     property : value
 * }
 * 
 * 
 */
将上述模板进行按需更改:
​
/**
 * 
 * @api {post} /api/user user
 * @apiName addUser
 * @apiGroup usergroup
 * @apiVersion  1.0.0
 * 
 * 
 * @apiParam  {String} username 用户名
 * @apiParam  {String} password 密码
 * @apiParam  {Number} age 年龄
 * @apiParam  {File} avatar 头像
 * 
 * @apiSuccess (200) {number} ok 标识成功字段
 * 
 * @apiParamExample  {multipart/form-data} Request-Example:
 * {
 *     username : "wxm",
 *     password : "123",
 *     age : 22,
 *     avatar : File
 * }
 * 
 * 
 * @apiSuccessExample {type} Success-Response:
 * {
 *     ok : 1
 * }
 * 
 * 
 */

生成apidoc文件:

//扫描routes文件夹并在doc文件夹下生成接口文档
apidoc -i .\routes\ -o .\doc

apidoc.json:
​
{
    "name":"后台系统接口文档",
    "version":"1.0.0",
    "description":"关于后台系统的接口文档描述",
    "title":"企业网站定制系统"
}

26、koa。

Koa是由Express原班人马打造的Web框架。

npm i koa

注意:区分express和koa这两个web框架的使用。

//基本使用
const Koa = require("koa")
//区别一:创建对象
const app = new Koa()
//ctx===context 上下文对象
app.use((ctx,next)=>{
    //ctx.response
    //ctx.request
    ctx.response.body = "Hello World"   //response可省略
}).listen(3000)

对比express和koa同步:

const express = require("express")
const app = express()
​
app.use((req,res,next)=>{
    console.log("111111")
    next()
    console.log("333333")
    res.send("Hello World")
})
​
app.use((req,res,next)=>{
    console.log("222222")
})
​
app.listen(3000)
const Koa = require("koa")
const app = new Koa()
​
app.use((ctx,next)=>{
    console.log("111111")
    next()
    console.log("333333")
    ctx.body = "Hello World"
})
​
app.use((ctx,next)=>{
    console.log("222222")
})
​
app.listen(3000)

对比express和koa异步:

const express = require("express")
const app = express()
​
app.use(async (req,res,next)=>{
    console.log("111111")
    await next()
    console.log("444444")
    res.send("Hello World")
})
​
app.use(async (req,res,next)=>{
    console.log("222222")
    // 异步 即需要等待delay结果执行完成才能执行console.log 
    // 那么就需要在delay前加上await 并在整个大函数前面加上async
    await delay(1000)
    console.log("333333")
})
​
function delay(time)
{
    return new Promise((resolve,reject)=>{
        setTimeout(resolve,time)
    })
}
​
app.listen(3000)
​
//express更适合流水线工作 上述的async并不会等待next返回 故只会打印1243//改成下面更适合express的流水线工作思想const express = require("express")
const app = express()
​
app.use(async (req,res,next)=>{
    console.log("111111")
    await next()
})
​
app.use(async (req,res,next)=>{
    console.log("222222")
    // 异步 即需要等待delay结果执行完成才能执行console.log 
    // 那么就需要在delay前加上await 并在整个大函数前面加上async
    await delay(1000)
    console.log("333333")
    console.log("444444")
    res.send("Hello World")
})
​
function delay(time)
{
    return new Promise((resolve,reject)=>{
        setTimeout(resolve,time)
    })
}
​
app.listen(3000)
const Koa = require("koa")
const app = new Koa()
​
app.use(async (ctx,next)=>{
    console.log("111111")
    await next()
    console.log("444444")
    ctx.body = "Hello World"
})
​
app.use(async (ctx,next)=>{
    console.log("222222")
    // 异步 即需要等待delay结果执行完成才能执行console.log 
    // 那么就需要在delay前加上await 并在整个大函数前面加上async
    await delay(1000)
    console.log("333333")
})
​
function delay(time)
{
    return new Promise((resolve,reject)=>{
        setTimeout(resolve,time)
    })
}
​
app.listen(3000)
​
//koa类似调用递归栈 调用时将主动权交给被调用函数 调用结束后被调用函数将主动权交给调用函数
//故上述koa会等待next返回 从而打印1234

27、路由koa-router。

因为koa中没有路由模块,故需要配置其对应的路由模块koa-router。

npm i koa-router

使用fehelper FeHelper:简易版Postman测试,注意测试的时候不要打开原来的url地址,否则无效。

const Koa = require("koa")
const Router = require("koa-router")
const app = new Koa()
const router = new Router()
//增
router.post("/list",(ctx,next)=>{
    ctx.body={
        ok:1,
        info:"add list success"
    }
})
//删
router.delete("/list/:id",(ctx,next)=>{
    ctx.body={
        ok:1,
        info:"delete list success"
    }
})
//改
router.put("/list/:id",(ctx,next)=>{
    ctx.body={
        ok:1,
        info:"put list success"
    }
})
//查
router.get("/list",(ctx,next)=>{
    ctx.body=["1111","2222","3333"]
})
//将路由注册在应用上  allowedMethods给应用添加允许的请求方式
app.use(router.routes()).use(router.allowedMethods())
app.listen(3000)

使用ctx.params获取动态路由参数。

如果存在多个接口,则可以将同一组的路由放在同一个文件中,然后在主路由配置文件中,先注册路由级组件,再注册应用级组件。

//user.jsconst Router = require("koa-router")
const router = new Router()
//增
router.post("/",(ctx,next)=>{
    ctx.body={
        ok:1,
        info:"add user success"
    }
})
//删
router.delete("/:id",(ctx,next)=>{
    ctx.body={
        ok:1,
        info:"delete user success"
    }
})
//改
router.put("/:id",(ctx,next)=>{
    ctx.body={
        ok:1,
        info:"put user success"
    }
})
//查
router.get("/",(ctx,next)=>{
    ctx.body=["1111","2222","3333"]
})
​
​
module.exports = router
//list.jsconst Router = require("koa-router")
const router = new Router()
//增
router.post("/",(ctx,next)=>{
    ctx.body={
        ok:1,
        info:"add list success"
    }
})
//删
router.delete("/:id",(ctx,next)=>{
    ctx.body={
        ok:1,
        info:"delete list success"
    }
})
//改
router.put("/:id",(ctx,next)=>{
    ctx.body={
        ok:1,
        info:"put list success"
    }
})
//查
router.get("/",(ctx,next)=>{
    ctx.body=["1111","2222","3333"]
})
​
​
module.exports = router
//index.jsconst Koa = require("koa")
const app = new Koa()
//每次创建就是一个新的Router
const Router = require("koa-router")  
const router = new Router()
//引入路由
const userRouter = require("./routes/user")
const listRouter = require("./routes/list")
//先注册路由级组件
router.use("/user",userRouter.routes(),userRouter.allowedMethods())
router.use("/list",listRouter.routes(),listRouter.allowedMethods())
//再注册应用级组件
//将路由注册在应用上  allowedMethods给应用添加允许的请求方式
app.use(router.routes()).use(router.allowedMethods())
app.listen(3000)

其中,应用级的路由组件也可以进一步封装。

router.prefix("/api")  // 给路由加上前缀
router.redirect("/","/home")  // 重定向 从/重定向到/home

koa处理静态资源也没有内置模块,需要自己下载koa-static。

npm i koa-static
//引入静态资源
const static = require("koa-static")
const path = require("path")
//设置静态资源
app.use(static(path.join(__dirname,"public")))

然后在浏览器中输入localhost:3000/center.html即可访问public中的静态资源(不需要输入public)。

get请求和post请求参数获取:

get:
​
ctx.query  //解析后的对象 {username:'aaa',password:'123'}
ctx.querystring  //未解析的字符串  username=aaa&password=123
post:
​
npm i koa-bodyparser
const bodyParser = require("koa-bodyparser")
app.use(bodyParser())  //后端使用ctx.request.body获取post参数

koa中如何使用ejs模板呢?

npm i ejs
npm i koa-views
const views = require("koa-views")
//配置模板引擎   views文件夹 ejs后缀名
app.use(views(path.join(__dirname,"views"),{extension:"ejs"}))
//home.js   !!!注意其是异步!!!const Router = require("koa-router")
const router = new Router()
​
router.get("/",async (ctx,next)=>{   //异步 等待模板解析完成后才向前端渲染页面
   await ctx.render("home",{username:"wxm"})  //寻找views下面的home.ejs
})
​
module.exports = router
//home.ejs
​
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <h1>home模板页面</h1>
    <div>欢迎<%= username%>回来</div>
</body>
</html>

28、koa-session-minimal。

cookie:

ctx.cookies.get(name,[options])          //读
ctx.cookies.set(name,value,[options])    //写

session:

npm i koa-session-minimal
const session = require('koa-session-minimal')
//session配置
app.use(session({
    key:"wxmSessionId",
    cookie:{
        maxAge:1000*60*60
    }
}))
//后端设置sessionId
router.post("/login",(ctx,next)=>{
    const {username,password} = ctx.request.body
    console.log(username,password)
    if(username==="wxm"&&password==="123")
    {
        //设置sessionId
        ctx.session.user = {
            username:"wxm"
        }   //当成功登录后发现浏览器中出现cookie
        ctx.body={
            ok:1,
            info:"login user success"
        }
    }
    else
    {
        ctx.body={
            ok:0,
            info:"login user fail"
        }
    }
})
//session判断拦截 其目的是使得访问其他页面的前提条件是必须登录
app.use(async (ctx,next)=>{
    if(ctx.url.includes("login"))  //login则放行
    {
        await next()
        return
    }
    if(ctx.session.user)  //登录成功
    {
        //注意此处需要将其session过期时间进行更新否则一直不过期
        ctx.session.date=Date.now()  //修改session后其过期时间重新计时
        await next()  //放行
    }
    else
    {
        ctx.redirect("/login")  //未登录则重定向到登录
    }
})
//注意!!koa不是线性!!koa在app.use时必须async和await啊!!

29、前后端分离JWT。

JWT特别适合前后端分离。

router.post("/login",(ctx,next)=>{
    const {username,password} = ctx.request.body
    console.log(username,password)
    if(username==="wxm"&&password==="123")
    {
        //设置sessionId
        // ctx.session.user = {
        //     username:"wxm"
        // }
​
        //设置header  //后端生成token并将其挂载在header的Authorization字段上
        //生成token
        const token = JWT.generate({
            _id:"111",
            username:"wxm"
        },"1h")
        //将token存放在header中
        ctx.set("Authorization",token)   
        //登录成功后可以在浏览器的Network部分查看login对应的Headers部分的Authorization
​
        ctx.body={
            ok:1,
            info:"login user success"
        }
    }
    else
    {
        ctx.body={
            ok:0,
            info:"login user fail"
        }
    }
})
//login.ejs的axios拦截器部分
<script src="https://unpkg.com/axios@1.1.2/dist/axios.min.js"></script>
<script>
   axios.interceptors.request.use(function (config) {
       return config;
   }, function (error) {
       return Promise.reject(error);
   });
   axios.interceptors.response.use(function (response) {
       //请求成功后首先获取authorization
       const {authorization} = response.headers
       //如果存在则重新将token存储在localStorage中 这样可以重新计算token过期时间
       authorization && localStorage.setItem("token",authorization)
       return response;
   }, function (error) {
       return Promise.reject(error);
   });
</script>
//home.ejs的axios拦截器部分
<script src="https://cdn.jsdelivr.net/npm/axios@1.1.2/dist/axios.min.js"></script>
<script>
  // 设置拦截器
  axios.interceptors.request.use(function (config) {
     //请求发送前获取本地token 如果有则携带token发送给后端
     const token = localStorage.getItem("token")
     config.headers.Authorization = `Bearer ${token}`
     return config;
  }, function (error) {
     return Promise.reject(error);
  });
  axios.interceptors.response.use(function (response) {
     //请求成功后再次设置token重新计算过期时间
     const {authorization} = response.headers
     authorization && localStorage.setItem("token",authorization)
     return response;
  }, function (error) {
     //请求失败则移除token重定向到login
     if(error.response.status===401)
     {
        localStorage.removeItem("token")
        location.href="/login"
     }
     return Promise.reject(error);
  });
</script>
//index.js设置token判断拦截
//token拦截判断
app.use(async (ctx,next)=>{
    if(ctx.url.includes("login"))  //包含login则放行
    {
        await next()
        return
    }
    const token = ctx.headers["authorization"]?.split(" ")[1] //获取token
    if(token) //如果token存在则解析token 
    {
        //如果解析数据则表明有效 此时重新设置token并计算过期时间 之后再放行
        const payload = JWT.verify(token)  //解析token
        if(payload)  //有效
        {
            //访问页面后重新计算过期时间 防止该页面长期不刷新但是token过期仍可访问
            const newToken = JWT.generate({
                _id:payload.id,
                username:payload.username
            },"1h")
            ctx.set("Authorization",newToken)
            await next()  //放行
        }
        //否则无效即返回错误码
        else  //无效
        {
            ctx.status = 401
            ctx.body = {errCode:-1,errInfo:"token过期"}
        }
    }
    else  //ejs模板等可能没有token则直接放行 因为一般页面在发送请求前会获取token的
    {
        await next()
    }
})

JWT登录验证流程:login页面设置axios拦截器,在发送请求前不做任何额外处理;发送请求则后端对应接口部分生成token并将其挂载在header的Authorization字段中;请求成功后则从header的Authorization字段中获取token并将其存储在localStorage中。其他页面按需设置axios拦截器,在发送请求前获取本地token,如果有则携带token发送给后端;请求成功后则从header的Authorization字段中获取token并将其存储在localStorage中,如此重新设置token从而实现重新计算过期时间的目的;请求失败后则移除token并重定向到login页面。app设置token判断拦截,如果页面url包含login相关,则直接放行,否则尝试获取token,如果token存在则解析token,如果解析数据则表明有效,此时重新设置token并计算过期时间再放行,防止页面长时间不刷新导致token过期但是仍然可以访问,否则解析数据无效则返回错误码以及错误提示信息,反之没有token则直接放行,因为一般有访问限制的页面在发送请求前会获取token的。(注意应用级中间件token的判断拦截在任何页面访问时都会判断)

30、文件上传@koa/multer。

npm i @koa/multer    //使用multer需要依赖@koa/multer
router.use("/upload",uploadRouter.routes(),uploadRouter.allowedMethods())
const Router = require("koa-router")
const router = new Router()
​
const multer = require("@koa/multer")
const upload = multer({dest:"public/uploads"})  //文件存放在public/uploads目录下
​
router.get("/",async (ctx,next)=>{
    await ctx.render("upload")
})
​
router.post("/", upload.single("avatar"), (ctx,next)=>{   //form表单提交文件
    // avatar在ctx.file中获取
    ctx.body={
        ok:1,
        info:"upload success"
    }
})
​
module.exports = router

31、操作Mongodb。

// config/db.config.js// 连接数据库
const mongoose = require("mongoose")
mongoose.connect("mongodb://127.0.0.1:27017/wxm_koa_project")   
// mongodb 本机域名127.0.0.1 端口号27017 数据库名字 wxm_koa_project
// 插入集合和数据 数据库wxm_koa_project会自动创建
// index.js
require("./config/db.config")    //就是这样引入!
// model/UserModel.jsconst mongoose =  require("mongoose")  // 模块引入多次是同一个实例
const Schema = mongoose.Schema   // 限制
const UserType = {    // 限制模型field类型
    username : String,
    password : String,
    age : Number,
    avatar:String
}
const UserModel = mongoose.model("user",new Schema(UserType))  // 创建一个叫做user的模型 user模型将会对应users集合 对应限制为UserType
module.exports = UserModel
// upload.jsconst UserModel = require("../model/UserModel")
const multer = require("@koa/multer")  //引入multer
const upload = multer({dest:"public/uploads"})  //文件存放在uploads文件夹下
router.post("/", upload.single("avatar"), async (ctx,next)=>{
    const {username,password,age} = ctx.request.body
    const avatar = ctx.file?`/uploads/${ctx.file.filename}`:``   //判断是否有图片
    await UserModel.create({
        username,
        password,
        age,
        avatar
    })
    ctx.body={
        ok:1,
        info:"upload success"
    }
})

32、Mysql。

Mysql数据库是关系型数据库的代表!Mongodb数据库是非关系型数据库的代表!关系型数据库一般存放的是表格!非关系型数据库一般存放的是键值对!集成环境:WAMP!数据库:mysql+navicat!

学习能力:忘记语法了可以去查询即可!最重要的是要有印象!随用随查!

多表查询:笛卡尔积内连接条件查询所需行列!( Inner Join )

外键约束:多表关联任意一张表都不可以瞎搞喔!( InnoDB )

下面一起看看node.js配合mysql使用吧!

npm i mysql2
//mysql2基本使用const express = require("express")
const app = express()
//引入mysql
const mysql2 = require("mysql2")
app.get("/",async (req,res)=>{
    //创建连接池进行操作
    const config = getDBConfig()
    const promisePool = mysql2.createPool(config).promise()
    //query中编写sql语句
    var users = await promisePool.query("select * from students")
    //数据在users[0]中喔!
    console.log(users[0])
    res.send({
        ok:1
    })
})
app.listen(3000)
​
//配置连接所需内容
function getDBConfig()
{
    return {
        host:'127.0.0.1',           //域名
        port:3306,                  //端口号
        user:"root",                //用户
        password:"root",            //密码
        database:"wxm_test",        //数据库
        connectionLimit:1           //连接限制
    }
}

如果需要使用前端传来的参数进行数据库操作,则需要在对应sql语句中使用?占位,然后传递一个数据数组:

//假设前端传来字符串参数
var name = "wxm"
//query中编写sql语句
var users = await promisePool.query(`select * from students where name=? and gender=?`,[name,2])

插入成功返回对象:

//插入成功返回对象
var users = await promisePool.query(`INSERT INTO students(id,name,score,gender,class_id) VALUES (2,"fxx",98,2,2)`)
//数据在users[0]中喔!
console.log(users[0])

更新成功返回对象:

//更新成功返回对象
var users = await promisePool.query(`update students set name=? where id=?`,["hx",2])
//数据在users[0]中喔!
console.log(users[0])

删除成功返回对象:

//删除成功返回对象
var users = await promisePool.query(`delete from students where id=?`,[2])
//数据在users[0]中喔!
console.log(users[0])

33、Socket编程。

websocket一般应用于弹幕、实时聊天、实时更新等场景,实现浏览器与服务器的全双工通信。

npm i ws
//chat.html
​
<h1>聊天室</h1>
<script>
  //向ws协议的8080端口号发送连接请求
  var ws = new WebSocket("ws://localhost:8080")
  //监听打开
  ws.onopen = ()=>{
     console.log("连接成功")
  }
  //监听消息
  ws.onmessage = (msgObj)=>{
     console.log(msgObj.data)
  }
  //监听错误
  ws.onerror = ()=>{
     console.log("出错了")
  }
</script>
//ws.jsconst express = require("express")
const app = express()
//配置静态资源
app.use(express.static("public"))
//http响应
app.get("/",(req,res)=>{
    res.send({
        ok:1
    })
})
app.listen(3000)
//websocket响应
const { WebSocketServer } = require("ws")
//wss是创建的服务器
const wss = new WebSocketServer({ port: 8080 });
//ws是当前连接的客户端
wss.on('connection', function connection(ws) {
  //监听错误
  ws.on('error', console.error);
  //监听消息
  ws.on('message', function message(data) {
    console.log('received: %s', data);
  });
  //给浏览器发送消息
  ws.send('欢迎来到聊天室');
});

浏览器url部分输入:localhost:3000/chat.html。

浏览器console部分输入ws.send("1111111")服务端即可收到。

chat流程:首先浏览器输入localhost:3000/chat.html相当于访问静态资源,此时chat.html对应js部分向ws协议的8080端口发送请求,服务器端创建ws服务器wss,其监听连接请求connection,当有连接到来时,ws指向的是对应的当前连接的客户端。服务器部分使用ws.send给浏览器部分发送消息,此时浏览器部分使用onmessage监听到服务器发来的消息,并使用msgObj.data打印出消息内容;浏览器在控制台中使用ws.send给服务器部分发送消息,此时服务器部分也使用onmessage监听到浏览器发送的消息,并使用'received: %s'打印。当同时打开多个浏览器并分别向服务器发送信息也是可以的喔!

当前客户端向其他客户端(包括自己)广播消息:

const WebSocket = require("ws")
const WebSocketServer = WebSocket.WebSocketServer
ws.on('message', function message(data) {
    wss.clients.forEach(function each(client) {
        if (client.readyState === WebSocket.OPEN) {
          client.send(data, { binary: false });
    }
});

当前客户端向其他客户端(不包括自己)广播消息:

const WebSocket = require("ws")
const WebSocketServer = WebSocket.WebSocketServer
ws.on('message', function message(data) {
    wss.clients.forEach(function each(client) {
        if (client !== ws && client.readyState === WebSocket.OPEN) {
          client.send(data, { binary: false });
        }
    });
});

客户端和服务器端群聊和私聊使用ws实现:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <h1>聊天室</h1>
    //发送文本内容
    <input type="text" id="text"><button id="send">send</button>
    //所有在线用户
    <select id="select">
    </select>
    <script>
        var select = document.querySelector('#select')
        var text = document.querySelector('#text')
        var send = document.querySelector('#send')
        //消息类型
        const WebSocketType = {
            Error:0,      //错误
            GroupList:1,  //名单
            GroupChat:2,  //群聊
            SingleChat:3  //私聊
        }
        function createMessage(type,data,to)
        {
            return JSON.stringify({
                type,  //消息类型
                data,  //消息内容
                to     //目标对象
            })
        }
        // 建立socket连接 带着token 后端验证
        //ws协议的8080端口号
        var ws = new WebSocket(`ws://localhost:8080?token=${localStorage.getItem("token")}`)
        //监听打开
        ws.onopen = ()=>{
            console.log("连接成功")
        }
        //监听消息
        ws.onmessage = (msgObj)=>{
            // console.log(msgObj.data)
            const msgobj = JSON.parse(msgObj.data)
            console.log(msgobj)
            switch(msgobj.type)
            {
                //如果消息错误则移除token并重定向到login
                case WebSocketType.Error:
                    localStorage.removeItem("token")
                    location.href = "/login"
                    break;
                //如果消息类型为在线用户列表则渲染在select部分
                case WebSocketType.GroupList:
                    console.log(JSON.parse(msgobj.data))
                    const onlineList = JSON.parse(msgobj.data)
                    //先清空
                    select.innerHTML = ""
                    select.innerHTML = `<option value="all">all</option>`
                     + onlineList.map(item=>`
                        <option value=${item.username}>${item.username}</option>
                    `).join("")
                    break;
                //如果消息类型为群聊则在控制台显示
                case WebSocketType.GroupChat:
                    var title = msgobj.user?msgobj.user.username:"广播"
                    console.log(title+":"+msgobj.data)
                    break;
                //如果消息类型为私聊则在控制台显示
                case WebSocketType.SingleChat:
                    console.log(msgobj.user.username+":"+msgobj.data)
                    break;
            }
        }
        //监听错误
        ws.onerror = ()=>{
            console.log("出错了")
        }
        //根据下拉框内容判断是群发还是私发
        send.onclick = ()=>{
            if(select.value === "all")
            {
                // console.log("群发")
                ws.send(createMessage(WebSocketType.GroupChat,text.value))
            }
            else
            {
                // console.log("私聊")
                ws.send(createMessage(WebSocketType.SingleChat,text.value,select.value))
            }
        }
    </script>
</body>
</html>
const JWT = require("../util/JWT")
//websocket响应
const WebSocket = require("ws")
const WebSocketServer = WebSocket.WebSocketServer
//wss是创建的服务器
const wss = new WebSocketServer({ port: 8080 });
const WebSocketType = {
    Error:0,      //错误
    GroupList:1,  //名单
    GroupChat:2,  //群聊
    SingleChat:3  //私聊
}
//ws是当前连接的客户端
wss.on('connection', function connection(ws,req) {
  const myURL = new URL(req.url,"http://127.0.0.1:3000")
  //获取并校验token
  const payload = JWT.verify(myURL.searchParams.get("token"))
  if(payload)
  {
    //一登录上来给所有在线用户群聊发消息success
    ws.send(createMessage(WebSocketType.GroupChat,null,"success"))
    //第一次连接成功时加上user字段到ws上
    ws.user = payload
    //群发消息多少在线用户
    sendAll()
  }
  else
  {
    ws.send(createMessage(WebSocketType.Error,null,"fail"))
  }
  ws.on('error', console.error);
  ws.on('message', function message(data) {
    const msgobj = JSON.parse(data)
    switch(msgobj.type)
    {
        case WebSocketType.GroupList:
            //返回在线用户列表              ws.send(createMessage(WebSocketType.GroupList,null,JSON.stringify(Array.from(wss.clients).map(item=>item.user))))
            break;
        case WebSocketType.GroupChat:
            //给所有用户群聊发消息
            wss.clients.forEach(function each(client) {
                if (client.readyState === WebSocket.OPEN) {
                  client.send(createMessage(WebSocketType.GroupChat,ws.user,msgobj.data))
                }
            });
            break;
        case WebSocketType.SingleChat:
            //给目标用户群聊发消息
            wss.clients.forEach(function each(client) {
                if (client.user.username===msgobj.to && client.readyState === WebSocket.OPEN) {
                 client.send(createMessage(WebSocketType.SingleChat,ws.user,msgobj.data))
                }
            });
            break;
    }
  });
  //用户关闭浏览器窗口后则表示退出登录
  ws.on('close',()=>{
    wss.clients.delete(ws.user)
  })
});
function createMessage(type,user,data)
{
    return JSON.stringify({
        type,    //操作类型
        user,   //发送对象
        data    //发送内容
    })
}
function sendAll()
{
    //群发消息
    wss.clients.forEach(function each(client) {
        if (client.readyState === WebSocket.OPEN) {     client.send(createMessage(WebSocketType.GroupList,null,JSON.stringify(Array.from(wss.clients).map(item=>item.user))))
        }
    });
}

服务器和浏览器建立一个发送消息类型标准,各自接收到消息后首先判断消息类型,再根据消息类型相应操作。

socket.io模块:

npm i socket.io

客户端和服务器端群聊和私聊使用socket.io实现:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="/javascripts/socketio.js"></script>
</head>
<body>
    <h1>聊天室</h1>
    <h1>当前用户:
        <b id="user"></b>
    </h1>
    <input type="text" id="text"><button id="send">send</button>
    <select id="select">
    </select>
    <script>
        var select = document.querySelector('#select')
        var text = document.querySelector('#text')
        var send = document.querySelector('#send')
        var user = document.querySelector('#user')
        user.innerHTML = localStorage.getItem("username")
        const WebSocketType = {
            Error:0,      //错误
            GroupList:1,  //名单
            GroupChat:2,  //群聊
            SingleChat:3  //私聊
        }
        function createMessage(data,to)
        {
            return {
                data,
                to
            }
        }
        //引入socketio.js  默认连接localhost
        const socket = io(`ws://localhost:3000?token=${localStorage.getItem("token")}`);          //使用on监听群聊 并在控制台打印
        socket.on(WebSocketType.GroupChat,(msg)=>{
            var title = msg.user?msg.user.username:"广播"
            console.log(title+":"+msg.data)
        })
        //使用on监听错误
        socket.on(WebSocketType.Error,(msg)=>{
            localStorage.removeItem("token")
            location.href = "/login"
        })
        //使用on监听用户列表 并将其渲染为下拉框
        socket.on(WebSocketType.GroupList,(msg)=>{
            const onlineList = msg.data
            //先清空
            select.innerHTML = ""
            select.innerHTML = `<option value="all">all</option>`
                + onlineList.map(item=>`
                <option value=${item.username}>${item.username}</option>
            `).join("")
        })
        //使用on监听私聊 并在控制台打印
        socket.on(WebSocketType.SingleChat,(msg)=>{
            console.log(msg.user.username+":"+msg.data)
        })
        send.onclick = ()=>{
            if(select.value === "all")
            {
                // console.log("群发")
                socket.emit(WebSocketType.GroupChat,createMessage(text.value))
            }
            else
            {
                // console.log("私聊")
            socket.emit(WebSocketType.SingleChat,createMessage(text.value,select.value))
            }
        }
    </script>
</body>
</html>
const JWT = require("../util/JWT")
const WebSocketType = {
    Error:0,      //错误
    GroupList:1,  //名单
    GroupChat:2,  //群聊
    SingleChat:3  //私聊
}
function createMessage(user,data)
{
    return {
        user,  //当前对象
        data   //发送内容
    }
}
//因为需要http的server作为参数故设置为函数形式
function start(server)
{
    const io = require('socket.io')(server);
    //socket表示当前连接的socket.io客户端
    io.on('connection', (socket,req) => { 
        //获取并解析token
        const payload = JWT.verify(socket.handshake.query.token)
        if(payload)
        {
            //挂载当前用户
            socket.user = payload
            //发送欢迎  事件 内容
            socket.emit(WebSocketType.GroupChat,createMessage(null,"欢迎来到聊天室"))
            //给所有用户发送用户列表
            sendAll(io)
        }
        else
        {
            socket.emit(WebSocketType.Error,createMessage(null,"token过期"))
        }
        socket.on(WebSocketType.GroupChat,(msg)=>{
            //给所有人发
            io.sockets.emit(WebSocketType.GroupChat,createMessage(socket.user,msg.data))
            //除了自己不发其他人发
         //io.broadcast.emit(WebSocketType.GroupChat,createMessage(socket.user,msg.data))
        })
        socket.on(WebSocketType.GroupList,(msg)=>{
            console.log(Array.from(io.sockets.sockets).map(item=>item[1].user))
        })
        socket.on(WebSocketType.SingleChat,(msg)=>{
            Array.from(io.sockets.sockets).forEach(item=>{
                if(item[1].user.username===msg.to)
                {
               item[1].emit(WebSocketType.SingleChat,createMessage(socket.user,msg.data))
                }
            })
        })
        socket.on("disconnect",()=>{
            sendAll(io)
        })
     });
}
function sendAll(io)
{  io.sockets.emit(WebSocketType.GroupList,createMessage(null,Array.from(io.sockets.sockets).map(item=>item[1].user).filter(item=>item)))
}
module.exports = start

socket.io相对于ws写法更加简洁,不需要使用switch判断分支,直接使用emit发送以及on监听即可,逻辑相同。

问题:如何在内置模块的众多嵌套结构和定义中获取自己想要的内容以及想要的格式呢?即如何学一个新的包!以及利用该新的包实现一个功能!

34、Mocha单元测试。

单元测试是用来对于一个模块、一个函数或者一个类来进行正确性检验的测试工作。

mocha是javascript的一种单元测试框架。(!可以测试异步代码!)

下面先看看没有mocha时的测试该如何编写:

//sum.js编写求和函数
module.exports = function(...rest){
    var sum = 0
    for(let i of rest)
    {
        sum+=i
    }
    return sum
}
//test.js编写测试函数
var sum = require("./sum")
// console.log(sum(1,2,3))
//内置断言模块assert
var assert = require("assert")
//成功不报错
assert.strictEqual(sum(),0)
assert.strictEqual(sum(1),1)
assert.strictEqual(sum(1,2),3)
//错误就出错 一个错影响后面所有测试
assert.strictEqual(sum(1,2,3),3)

上述node.js内置断言模块assert用于判断第一个参数的结果是否与第二个参数相同,其存在的问题是,如果成功则不报错,相反如果错误就报错,并且影响后续的所有测试。

npm i mocha   //局部安装
{
  "name": "mocha",
  "version": "1.0.0",
  "description": "",
  "main": "test.js",
  "scripts": {
    "test": "mocha"   //将test改为mocha方便后续测试喔
  },
  "author": "",
  "license": "ISC"
}
//推荐将所有测试文件放在test文件夹下方便后续测试!
//使用npm test即可运行测试!

多组测试describe和单个测试it:

describe("描述文字",()=>{   //多组测试可以嵌套
    //回调函数
})
it("描述文字",()=>{         //单个测试
    //回调函数
})

示例如下:(如果测试全对则会在描述前打上绿色对号√;如果某一测试错误也不会阻塞后续测试)

var sum = require("../sum")
var assert = require("assert")
//describe 一组测试 嵌套
//it 一个测试
describe("大组1测试",()=>{
    describe("小组1测试",()=>{
        it("sum()结果应该是0",()=>{
            assert.strictEqual(sum(),0)
        }) 
        it("sum(1)结果应该是1",()=>{
            assert.strictEqual(sum(1),1)
        }) 
        it("sum(1,2)结果应该是3",()=>{
            assert.strictEqual(sum(1,2),3)
        }) 
        it("sum(1,2,3)结果应该是6",()=>{
            assert.strictEqual(sum(1,2,3),6)
        }) 
    })
})

35、chai断言库。

chai断言库包含assert、should、expect。

npm i chai
//chai assert风格
var chai = require('chai')
var assert = chai.assert;
var sum = require("../sum")
describe("大组1测试",()=>{
    describe("小组1测试",()=>{
        it("sum()结果应该是0",()=>{
            assert.equal(sum(),0)
        }) 
        it("sum(1)结果应该是1",()=>{
            assert.equal(sum(1),1)
        }) 
        it("sum(1,2)结果应该是3",()=>{
            assert.equal(sum(1,2),3)
        }) 
        it("sum(1,2,3)结果应该是6",()=>{
            assert.equal(sum(1,2,3),6)
        }) 
    })
})
//chai should风格
var chai = require('chai');
chai.should();
var sum = require("../sum")
describe("大组1测试",()=>{
    describe("小组1测试",()=>{
        it("sum()结果应该是0",()=>{
            sum().should.exist.and.equal(0)
        }) 
        it("sum(1)结果应该是1",()=>{
            sum(1).should.exist.and.equal(1)
        }) 
        it("sum(1,2)结果应该是3",()=>{
            sum(1,2).should.exist.and.equal(3)
        }) 
        it("sum(1,2,3)结果应该是6",()=>{
            sum(1,2,3).should.exist.and.equal(6)
        }) 
    })
})
//chai expect风格
var chai = require('chai');
var expect = chai.expect;
var sum = require("../sum")
describe("大组1测试",()=>{
    describe("小组1测试",()=>{
        it("sum()结果应该是0",()=>{
            expect(sum()).to.equal(0)
        }) 
        it("sum(1)结果应该是1",()=>{
            expect(sum(1)).to.equal(1)
        }) 
        it("sum(1,2)结果应该是3",()=>{
            expect(sum(1,2)).to.equal(3)
        }) 
        it("sum(1,2,3)结果应该是6",()=>{
            expect(sum(1,2,3)).to.equal(6)
        }) 
    })
})

还有很多其他的使用方法,注意测试用例和代码都要保证正确喔~

36、异步测试。

方案一:done回调函数。

const fs = require("fs")
var assert = require("assert")
describe("异步测试",()=>{
    it("异步读取文件",(done)=>{
        fs.readFile("./1.txt","utf8",(err,data)=>{
            if(err)
            {
                done(err)
            }
            else
            {
                assert.strictEqual(data,"hello")
                //done()是回调函数
                done()
            }
        })
    })
})

方案二:async和await。

const fs = require("fs")
var assert = require("assert")
describe("异步测试",()=>{
    it("异步读取文件",async ()=>{
        var data = await fs.promises.readFile("./1.txt","utf8")
        assert.strictEqual(data,"hello")
    })
})

一般读写都会涉及到异步。

使用异步代码测试服务器:

const Koa = require("koa")
const app = new Koa()
app.use((ctx)=>{
    ctx.body="<h1>Hello</h1>"
})
app.listen(3000)
var assert = require("assert")
var axios = require("axios")
describe("测试接口",()=>{
    it("返回html代码片段测试",async ()=>{
        //axios
        var res = await axios.get("http://localhost:3000/")
        //请求的数据在res.data中
        console.log(res.data)
        assert.strictEqual(res.data,"<h1>Hello</h1>")
    })
})

一般测试服务器会引用supertest库:(其不是在服务器启动的时候测试,而是在测试中才启动服务器,并且在编写异步代码的过程中使用expect风格链式调用,同时测试结束后需要关闭服务器)

npm i supertest
const request = require('supertest')
const app = require('../app');
describe('#test koa app', () => {
    //在测试中才启动服务器
    let server = app.listen(3000);
    describe('#test server', () => {
        it('#test GET /', async () => {
            //使用expect风格链式调用
            await request(server)
                .get('/')
                .expect('Content-Type', /text/html/)
                .expect(200, '<h1>Hello</h1>');
        });
        //测试结束后关闭服务器
        after(function () {
            server.close()
        });
    });
});

37、钩子函数。

钩子函数是什么?钩子函数就是在某一阶段给你一些处理其的机会。

describe("大组1测试",()=>{
    describe("小组1测试",()=>{
        it("sum()结果应该是0",()=>{
            assert.strictEqual(sum(),0)
        }) 
        it("sum(1)结果应该是1",()=>{
            assert.strictEqual(sum(1),1)
        }) 
        it("sum(1,2)结果应该是3",()=>{
            assert.strictEqual(sum(1,2),3)
        }) 
        it("sum(1,2,3)结果应该是6",()=>{
            assert.strictEqual(sum(1,2,3),6)
        }) 
        before(function () {
            console.log('before:');
        });
​
        after(function () {
            console.log('after.');
        });
​
        beforeEach(function () {
            console.log('  beforeEach:');
        });
​
        afterEach(function () {
            console.log('  afterEach.');
        });
    })
})
​
大组1测试
    小组1测试
before:
  beforeEach:
      ✔ sum()结果应该是0
  afterEach.
  beforeEach:
      ✔ sum(1)结果应该是1
  afterEach.
  beforeEach:
      ✔ sum(1,2)结果应该是3
  afterEach.
  beforeEach:
      ✔ sum(1,2,3)结果应该是6
  afterEach.
after.
describe("大组1测试",()=>{
    before(function () {
        console.log('before:');
    });
​
    after(function () {
        console.log('after.');
    });
​
    beforeEach(function () {
        console.log('  beforeEach:');
    });
​
    afterEach(function () {
        console.log('  afterEach.');
    });
    describe("小组1测试",()=>{
        it("sum()结果应该是0",()=>{
            assert.strictEqual(sum(),0)
        }) 
        it("sum(1)结果应该是1",()=>{
            assert.strictEqual(sum(1),1)
        }) 
        it("sum(1,2)结果应该是3",()=>{
            assert.strictEqual(sum(1,2),3)
        }) 
        it("sum(1,2,3)结果应该是6",()=>{
            assert.strictEqual(sum(1,2,3),6)
        }) 
    })
})
​
大组1测试
before:
    小组1测试
  beforeEach:
      ✔ sum()结果应该是0
  afterEach.
  beforeEach:
      ✔ sum(1)结果应该是1
  afterEach.
  beforeEach:
      ✔ sum(1,2)结果应该是3
  afterEach.
  beforeEach:
      ✔ sum(1,2,3)结果应该是6
  afterEach.
after.