Nodejs 相关知识
Nodejs是一个js运行环境,可以让js开发后端程序,实现几乎其他后端语言实现的所有功能,能够让js与其他后端语言平起平坐。
nodejs是基于v8引擎,v8是Google发布的开源js引擎,本身就是用于chrome浏览器的js解释部分,现在把v8转移到服务器上,用于做服务器端的软件。
Nodejs超强的高并发能力,能够实现高性能服务器
node环境:
只有v8引擎解析js,没有dom和bom对象。
浏览器环境:
Blink:起到排版作用,对html/css进行解析,确定每个元素位置
V8:解析js
CommonJS规范
我们可以把公共功能抽离成一个单独的js文件作为一个模块,默认情况下这个里面的方法或者属性,外面是没法访问的,如果要外部可以访问模块里的方法或者属性,就必须在模块里面通过exports 或者 module.exports 暴露属性或者方法。
module.exports = {test,a}module.exports = test//可以一个一个的多个导出
exports.test = test
exports.a = aconst a = require('./test.js')
npm
npm init
npm install/i -g
npm install/i --save -dev
npm list -g(列举目录下的安装包)
npm info 包名
npm i/install md5@1(指定安装版本)
npm outdated(检查包是否已过时)
--save:添加在dependencies
-dev/-D:添加在devDependencies
-g:全局安装
依赖版本前的符号:
'^2.1.0' :^表示会安装 2.*.* 的最新版本
'~2.1.0' :^表示会安装 2.1.* 的最新版本
'*' :^表示会安装最新版本
nrm
nrm是npm的镜像源管理工具,国外资源太慢,可以使用这个在npm源间切换,手动切换:
npm config set registry https://registry.npm.taobao.org
全局安装 nrm
npm i -g nrm
使用nrm
执行命令nrm ls 查看可选源 ,其中带有 * 的是当前使用源,上面的输出表明是官方源。
切换nrm
切到taobao源:
npm use taobao
测试速度
测试相应源的响应时间
nrm test
查看当前的仓库
npm config get registry
中国npm镜像
这是一个完整的npmjs.org镜像,你可以用此代替官方版本(只读),同步频率目前为10分钟一次与官方服务同步,之后可以使用 cnpm 下载。
npm i -g cnpm --registry=https://registry.npmmirror.com
yarn
npm install -g yarn
比npm快,yarn 缓存每个下载过的包,所以再次使用无须重复下载,同时利用并行下载以最大化资源利用率,因此安装速度更快。
安全性:执行代码前,yarn 会通过算法检验每个安装包的完整性。
开始新项目:yarn init
添加依赖:yarn add 包名 | yarn add 包名@版本 | yarn add 包名 --dev
升级依赖包:yarn upgrade 包名@版本
移除依赖包:yarn remove 包名
安装全部依赖:yarn install
ES模块化写法
在package.json中加入type选项,即可使用es的模块化写法。
npm init
{"name": "aaa","version": "1.0.0","description": "","main": "index.js","type": "module","scripts": {"test": "echo \"Error: no test specified\" && exit 1"},"author": "jerry","license": "ISC"
}
//暴露出 引入
export default obj => import obj from './1.js'export const demo = 'hello' => import { demo } from './2/js'
内置模块
http模块
创建服务器,node app.js 启动服务器
全局安装 nodemon 自动重启(node-dev也可以)
npm i -g nodemon / npm i -g node-dev
启动服务器:
nodemon app.js
const http = require('http')
console.log(http);const server = http.createServer((req, res) => {res.writeHead(200, { 'content-type': 'application/json;charset=utf-8' });//通过访问不同的url发送不同的数据res.end(JSON.stringify(render(req.url)))
})server.listen(8000, () => {console.log('start');
})
function render(url) {switch (url) {case '/home':return { data: 'home' }case '/about':return {data: 'about'}default:return 'index'}
}
JsonP
解决跨域的办法之一,后端直接返回一个函数,并且执行,前提是在html文件有已经定义好的函数,前后端保持一致。
const http = require('http')
const url = require('url')const server = http.createServer((req, res) => {res.writeHead(200, { 'content-type': 'application/json;charset=utf-8' });//通过访问不同的url发送不同的数据const {pathname,query} = url.parse(req.url,true) //http://localhost:8000/home?callback=test2res.end(`${query.callback}(${JSON.stringify(render(pathname))})`)
})server.listen(8000, () => {console.log('start');
})function render(url) {switch (url) {case '/home':return { data: 'home' }case '/about':return {data: 'about'}default:return 'index'}
}
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head><body></body>
<script>function test(param){console.log(param);}function test2(param){console.log(param);}
</script>
<script src="http://localhost:8000/home?callback=test2"></script>
</html>
cors
添加cors头,解决跨域
const http = require('http')
const url = require('url')const server = http.createServer((req, res) => {res.writeHead(200, { 'content-type': 'application/json;charset=utf-8', 'access-control-allow-origin': '*' });//通过访问不同的url发送不同的数据const { pathname, query } = url.parse(req.url, true) //http://localhost:8000/homeconsole.log(query);res.end(JSON.stringify(render(pathname)))
})server.listen(8000, () => {console.log('start');
})function render(url) {switch (url) {case '/home':return { data: 'home' }case '/about':return {data: 'about'}default:return 'index'}
}
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head><body></body>
<script>fetch('http://localhost:8000/home').then(res=>res.json()).then(res=>{console.log(res);})
</script>
</html>
get
http既可以做服务端,也可以做客户端,可以解决跨域成为代理服务
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head><body></body>
<script>fetch('http://localhost:8000/home').then(res=>res.json()).then(res=>{console.log(res);})
</script>
</html>
const http = require('http')
const https = require('https')
const url = require('url')const server = http.createServer((req, res) => {res.writeHead(200, { 'content-type': 'application/json;charset=utf-8', 'access-control-allow-origin': '*' });//通过访问不同的url发送不同的数据const { pathname, query } = url.parse(req.url, true) //http://localhost:8000/homeswitch (pathname) {case '/home':httpget((data)=>{res.end(data)})case '/about':return {data: 'about'}default:return 'index'}
})server.listen(8000, () => {console.log('start');
})function httpget(callback){let data = ''//https://api.douban.com/v2/movie/in_theatershttps.get('https://i.maoyan.com/api/mmdb/movie/v3/list/hot.json',res=>{res.on('data',chunk=>{data+=chunk})res.on('end',()=>{callback(data)})})
}
post
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head><body></body>
<script>fetch('http://localhost:8000/home').then(res=>res.json()).then(res=>{console.log(res);})
</script>
</html>
const http = require('http')
const https = require('https')
const url = require('url')const server = http.createServer((req, res) => {res.writeHead(200, { 'content-type': 'application/json;charset=utf-8', 'access-control-allow-origin': '*' });//通过访问不同的url发送不同的数据const { pathname, query } = url.parse(req.url, true) //http://localhost:8000/homeswitch (pathname) {case '/home':httppost((data)=>{res.end(data)})case '/about':return {data: 'about'}default:return 'index'}
})server.listen(8000, () => {console.log('start');
})function httppost(callback){let data = ''//'https://m.xiaomiyoupin.com/mtop/market/search/placeHolder'//请求的信息let options={hostname:'m.xiaomiyoupin.com',port:"443",path:"/mtop/market/search/placeHolder",method: "POST",headers:{'Content-Type':"application/json"//'Content-Type':"x-www-form-urlencoded" 这种对应传参req.write("name=zhangsan&age=6")}}let req = https.request(options,res=>{res.on('data',chunk=>{data+=chunk})res.on('end',()=>{callback(data)})})req.write(JSON.stringify([{},{"baseParam":{"ypClient":1}}]))req.end()
}
爬虫
可以直接爬出网页,调用接口,可以拿到网页的html结构数据。然后通过过滤出有效的信息返回给前端。需要工具 cheerio,用法类似于jQuery
npm i --save cheerio
const http = require('http')
const https = require('https')
const url = require('url')
const cheerio = require('cheerio')const server = http.createServer((req, res) => {res.writeHead(200, { 'content-type': 'application/json;charset=utf-8', 'access-control-allow-origin': '*' });//通过访问不同的url发送不同的数据const { pathname, query } = url.parse(req.url, true) //http://localhost:8000/homeswitch (pathname) {case '/home':httpget((data) => {res.end(spider(data))})case '/about':return {data: 'about'}default:return 'index'}
})server.listen(8000, () => {console.log('start');
})function httpget(callback) {let data = ''//https://api.douban.com/v2/movie/in_theatershttps.get('https://i.maoyan.com/?requestCode=32779c7926dc56716d208cc297ee8da1z5u0y', res => {res.on('data', chunk => {data += chunk})res.on('end', () => {callback(data)})})
}function spider(data) {let $ = cheerio.load(data)//通过选择器来定位到元素let $movielist = $(".column.content")let movies = []$movielist.each((index, value) => {movies.push({ title: $(value).find('.title').text(), grade: $(value).find('.grade').text(),actor: $(value).find('.actor').text()})})console.log($movielist);console.log(movies);return JSON.stringify(data)
}
url模块
parse和format(旧版):
parse:在使用查询字符串的方式请求时,可以用来解析带有查询字符串的请求。两个参数:url:传入的url,第二个是否将参数解析为对象格式,默认是false,pathname是对应的url,query是请求的请求参数对象。
format:与parse正好相对立,将url.parse解析的对象转为对应的url地址
const http = require('http')
const url = require('url')const server = http.createServer((req, res) => {res.writeHead(200, { 'content-type': 'application/json;charset=utf-8' });//通过访问不同的url发送不同的数据// {// protocol: null,// slashes: null,// auth: null,// host: null,// port: null,// hostname: null,// hash: null,// search: '?a=1&b=2',// query: [Object: null prototype] { a: '1', b: '2' },// pathname: '/home',// path: '/home?a=1&b=2',// href: '/home?a=1&b=2'// }const {pathname,query} = url.parse(req.url,true) //http://localhost:8000/home?a=1&b=2console.log(url.format(url.parse(req.url,true))); //格式化为url地址res.end(JSON.stringify(render(pathname)))
})server.listen(8000, () => {console.log('start');
})function render(url) {switch (url) {case '/home':return { data: 'home' }case '/about':return {data: 'about'}default:return 'index'}
}
resolve (旧版)
用于拼接url
const url = require('url')
let a = url.resolve('1/2/3/','4') // '1/2/3/4'
let b = url.resolve('1/2/3','4') // '1/2/4'let c = url.resolve('http://baidu.com/','/4') // 'http://baidu.com/4'域名后的所有都被替换
let d = url.resolve('http://baidu.com/1','/4') //'http://baidu.com/4'
URL对象(新)
也是用于获取请求的url地址以及参数
const http = require('http')
const url = require('url')const server = http.createServer((req, res) => {res.writeHead(200, { 'content-type': 'application/json;charset=utf-8' });//通过访问不同的url发送不同的数据const myURL = new URL(req.url,'http://127.0.0.1:3000') //第二个参数必填,合法地址,这里取本地,因为这里请求的是本地console.log(myURL);// {// href: 'http://127.0.0.1:3000/home?a=1&b=2',// origin: 'http://127.0.0.1:3000',// protocol: 'http:',// username: '',// password: '',// host: '127.0.0.1:3000',// hostname: '127.0.0.1',// port: '3000',// pathname: '/home',// search: '?a=1&b=2',// searchParams: URLSearchParams { 'a' => '1', 'b' => '2' },// hash: ''// }const pathname = myURL.pathnameres.end(JSON.stringify(render(pathname)))
})server.listen(8000, () => {console.log('start');
})function render(url) {switch (url) {case '/home':return { data: 'home' }case '/about':return {data: 'about'}default:return 'index'}
}
URLSearchParams类
提供对 URL 查询的读写访问,在全局对象上也可用,WHATWG URLSearchParams 接口和querystring 模块有相似的用途,但querystring模块用途更广,因为它允许自定义的分隔符(& 和 =),此api纯粹是为网址查询字符串设计的。
const myURL = new URL("https://example.org/?abc=123")console.log(myURL.searchParams.get('abc')); //123myURL.searchParams.append('www','xyz')console.log(myURL.href); //"https://example.org/?abc=123&www=xyz"myURL.searchParams.delete('abc')myURL.searchParams.set('a','b')console.log(myURL.href) //"https://example.org/?a=b"const newSearchParams = new URLSearchParams(myURL.searchParams)console.log(newSearchParams); // { 'www' => 'xyz', 'a' => 'b' }newSearchParams.append('a','c')console.log(myURL.href); //https://example.org/?a=bconsole.log(newSearchParams.toString());//www=xyz&a=b&a=c
拼接作用:
let b = new URL('one',"https://example.org/?abc=123")console.log(b.href); //https://example.org/one
详情参考:URL | Node.js v19 API
format(新)
参数:
url:WHATWG 网址对象
options:auth<boolean> 如果序列化的网址字符串包含用户名和密码,则为true,否则为false,默认值:true。
fragment<boolean>:如果序列化的网址字符串包含片段,则为true,否则为false,默认值:true。
search<boolean>:如果序列化的网址字符串包含搜索查询,则为true,否则为false,默认值:true。
unicode<boolean>:如果序列化的网址字符串的主机组件中的unicode字符应该被直接编码而不是Punycode编码,默认值:false。
返回值<string>
返回 WHATWG 网址 对象的网址 String 表示的可自定义的序列化。
网址对象具有 toString方法和href属性,用于返回网址的字符串序列化。但是这些都不能以任何方式自定义。url.format(url,[options])方法允许对输出进行基本的自定义。
const myURL = new URL("https://a:b@测试?abc#foo")console.log(url.format(myURL));//https://a:b@xn--0zwm56d/?abc#foo 编译了unicode码console.log(url.format(myURL,{unicode:true}));//https://a:b@测试/?abc#foo 保持unicode码console.log(url.format(myURL,{unicode:true,auth:false}));//https://测试/?abc#foo 去除了a:bconsole.log(url.format(myURL,{unicode:true,fragment:false}));//https://a:b@测试/?abc 去除了#后面的内容console.log(url.format(myURL,{unicode:true,search:false}));//https://a:b@测试/#foo 去除了?后面的内容到#结束
url.fileURLToPath(url) 和 url.pathToFileURL(path)
url.fileURLToPath(url) 新增于10.12
参数:url:<URL> | <string> 要转换为路径的文件网址字符串或网址对象,必须绝对路径
返回:<string> 完全解析的特定于平台的node.js文件路径。
此函数可确保正确解码宝粉笔编码字符,并确保跨平台有效的绝对路径字符串。
new URL的方式对于文件路径是有问题的,所以采用fileURLToPath。
console.log(url.fileURLToPath('file:///C:/path/')) // /C:/path/console.log(new URL('file:///C:/path/').pathname) // /C:/path/console.log(url.fileURLToPath('file://nas/foo.txt')) // //nas//foo.txt(Windows)console.log(new URL('file://nas/foo.txt').pathname) // /foo.txtconsole.log(url.fileURLToPath('file:///你好.txt')) // /你好.txt(POSIX)console.log(new URL('file:///你好.txt').pathname) // /%E4%BD%A0%E5%A5%BD.txtconsole.log(url.fileURLToPath('file:///hello world')) // /hello world (POSIX)console.log(new URL('file:///hello world').pathname) // /hello%20worldconst _
url.pathToFileURL(path) 新增于10.12
参数:path<string> 要转为文件网址的路径
返回:<URL> 文件网址对象
该函数确保path被绝对解析,并且在转换为文件网址时正确编码网址控制字符。
import {pathToFileURL} from 'url'new URL('/foo#1','file') //错误:file:///foo#1
pathToFileURL('/foo#1') //正确:file:///foo%231(POSIX)new URL('/some/path%.c','file') //错误:file:///some/path%.c
pathToFileURL('/some/path%.c') //正确:file:///some/path%25.c(POSIX)
url.urlToHttpOptions(url)
参数:url<url> 要转换为选项对象的 WHATWG对象。
返回:<Object>选项对象
protocal <string> 使用的协议
hostname <string> 向其发出请求的服务器域名或ip地址
hash <string> 网址的片段部分
search <string> 网址的序列化的查询部分
pathname <string> 网址的路径部分
path <string> 请求的绝对路径。应包括查询字符串(如果有)。当请求路径包含非法字符时抛出异常。目前只有空格被拒绝。
href <string> 序列化的网址
port <number> 远程服务器的端口
auth <string> 基本身份验证
该实用函数按照http.request() 和 https.request() API的预期将网址对象转换为普通选项对象。
const url = require('url')const myURL = new URL("https://a:b@测试?abc#foo")console.log(url.urlToHttpOptions(myURL));// {// protocol: 'https:',// hostname: 'xn--0zwm56d',// hash: '#foo',// search: '?abc',// pathname: '/',// path: '/?abc',// href: 'https://a:b@xn--0zwm56d/?abc#foo',// auth: 'a:b'// }
querystring模块
parse解析
const querystring = require('querystring')let str = 'name=zhangsan&age=12'
let obj = querystring.parse(str)
console.log(obj); //{ name: 'zhangsan', age: '12' }
stringify编码
const querystring = require('querystring')let obj = { name: 'zhangsan', age: '12' }
let str = querystring.stringify(obj)
console.log(str); //name=zhangsan&age=12
escape 转译特殊字符
const querystring = require('querystring')let str = 'name=zhangsan&age=8&url=https://baidu.com'
let escape = querystring.escape(str)
console.log(escape); //name%3Dzhangsan%26age%3D8%26url%3Dhttps%3A%2F%2Fbaidu.com
unescape 解码特殊字符
const querystring = require('querystring')let str = 'name%3Dzhangsan%26age%3D8%26url%3Dhttps%3A%2F%2Fbaidu.com'
let unescape = querystring.unescape(str)
console.log(unescape); //name=zhangsan&age=8&url=https://baidu.com
event 模块
类似于发布订阅,使用event模块将之前的get方法做一下优化,需要引入EventEmitter对象,对event添加监听的方法,在数据处理好后抛出,并发送个给前端。
const http = require('http')
const https = require('https')
const { EventEmitter } = require('events')
const url = require('url')
let event = null
const server = http.createServer((req, res) => {res.writeHead(200, { 'content-type': 'application/json;charset=utf-8', 'access-control-allow-origin': '*' });//通过访问不同的url发送不同的数据const { pathname, query } = url.parse(req.url, true) //http://localhost:8000/homeswitch (pathname) {case '/home':event = new EventEmitter()event.on('play',(data)=>{res.end(data)})httpget()case '/about':return {data: 'about'}default:return 'index'}
})server.listen(8000, () => {console.log('start');
})function httpget(){let data = ''//https://api.douban.com/v2/movie/in_theatershttps.get('https://i.maoyan.com/api/mmdb/movie/v3/list/hot.json',res=>{res.on('data',chunk=>{data+=chunk})res.on('end',()=>{event.emit('play',data)})})
}
fs文件操作模块
包括同步和异步的方法,同步会阻塞进程,需要搭配 try catch 语句来进行捕获错误。
同步方法在异步方法后面添加Sync即可,由于node环境执行的js代码是服务器代码,所以绝大部分需要在服务器运行期反复执行业务逻辑的代码,必须使用异步,否则同步代码在执行时期,服务器停止响应,js只有一个执行线程。
服务器启动时如果需要读取配置文件,或者结束时需要写入到状态文件时,可以使用同步代码,因为这些代码只在启动和结束时执行一次,不影响服务器正常运行时的异步操作。
但是在异步方法中容易产生回调地狱,可以使用promise的写法
const fs = require('fs').promisesfs.mkdir('./avater').then(res=>{console.log(res);
}).catch(err=>{console.log(err);
})
创建目录
const fs = require('fs')
//创建文件夹
fs.mkdir('./avater',err=>{if(err && err.code == "EEXIST"){console.log("文件夹已存在");}
})//同步写法
try {fs.mkdirSync('./avater2')
} catch (error) {console.log(error);
}
修改目录名称
const fs = require('fs')
//修改文件夹名称
fs.rename('./avater','./avater2',err=>{if(err && err.code == "ENOENT"){console.log("当前文件夹不存在");}
})
创建新的文件(写入内容是覆盖之前的内容)
const fs = require('fs')
//创建文件
fs.writeFile('./avater2/a.txt','hello world',err=>{console.log(err);
})
删除文件夹
//删除文件夹
fs.rmdir('./avater2',err=>{console.log(err)
})
追加文件内容
// 追加内容
fs.appendFile('./avater2/a.txt','\n你好',err=>{console.log(err);
})
读取文件内容
//读取文件
fs.readFile("./avater2/a.txt",'utf-8',(err,data)=>{if(!err){console.log(data);}
})
删除文件
//删除文件
fs.unlink("./avater2/a.txt",(err)=>{console.log(err);
})
读取文件夹下的文件
//读取文件夹下的文件
fs.readdir('./avater2', (err, data) => {if (!err) {console.log(data);//[ 'a.txt', 'b.txt', 'c.js' ]}
})
查看文件或文件夹信息
主要用里面的判断文件或文件夹的方法。
//查看文件或文件夹信息
fs.stat('./avater2',(err,data)=>{console.log(data.isDirectory()); //判断是否是文件目录console.log(data.isFile()); //判断是否是文件
})// {
// dev: 16777231,
// mode: 16877,
// nlink: 5,
// uid: 501,
// gid: 20,
// rdev: 0,
// blksize: 4096,
// ino: 12893475,
// size: 160,
// blocks: 0,
// atimeMs: 1692457394626.1108,
// mtimeMs: 1692457394548.2456,
// ctimeMs: 1692457394548.2456,
// birthtimeMs: 1692453342095.4685,
// atime: 2023-08-19T15:03:14.626Z,
// mtime: 2023-08-19T15:03:14.548Z,
// ctime: 2023-08-19T15:03:14.548Z,
// birthtime: 2023-08-19T13:55:42.095Z
// }
stream流模块
stream是node.js提供的又一个仅在服务端可用的模块,目的是支持流这种数据结构。
创建可读流
const fs = require('fs')const rs = fs.createReadStream('./1.txt','utf-8')rs.on('data',chunk=>{console.log(chunk);
})rs.on('end',()=>{console.log('end');
})rs.on('error',(err)=>{console.log(err);
})
创建可写流
const ws = fs.createWriteStream('./2.txt','utf-8')ws.write('111')
ws.write('222')ws.end()
把读取的数据写入新的文件
const fs = require('fs')
const rs = fs.createReadStream('./1.txt')
const ws = fs.createWriteStream('./2.txt')
rs.pipe(ws)
zlib
压缩资源传输
const http = require('http')
const fs = require('fs')
const zlib = require('zlib')
const gzip = zlib.createGzip()const server = http.createServer((req, res) => {const rs = fs.createReadStream('./1.txt')res.writeHead(200, { 'content-type': 'application/x-javascript;charset=utf-8', 'access-control-allow-origin': '*','Content-Encoding':'gzip' }); //告诉浏览器根据什么方式解压缩,如不添加就是流rs.pipe(gzip).pipe(res) // 压缩
})server.listen(8000, () => {console.log('start');
})
crypto
crypto模块的额目的是为了提供通用的加密和哈希算法。用纯js实现起来比较麻烦,并且速度会很慢,所以nodejs通过c/c++实现后,通过crypto这个模块暴露出来,方便快捷。
MD5是一个常用的哈希算法,用于给任意数据一个签名,通常以十六进制的字符串表示,也可以用 base64 的形式展示。
const crypto = require("crypto")const hash = crypto.createHash('md5')hash.update('hello world') //update方法默认字符串编码为utf-8,也可以传入bufferconsole.log(hash.digest('hex')); //十六进制的形式展示
//console.log(hash.digest('base64')); //base64的形式展示
update方法默认字符串编码为utf-8,也可以传入buffer。
如果要计算SHA1,只需要将 md5 改为 sha1,就可以得到结果(md5,sha1,sha256)
const crypto = require("crypto")const hash = crypto.createHash('sha1')hash.update('hello world') //update方法默认字符串编码为utf-8,也可以传入bufferconsole.log(hash.digest('hex')); //十六进制的形式展示
// console.log(hash.digest('base64')); //base64的形式展示
Hmac算法
也是一种hash算法,他可以利用md5或者SHA1等hash算法。不同的是,Hmac还需要一个秘钥。只要秘钥发生变化,生成的签名也就不同。
const crypto = require("crypto")const hash = crypto.createHmac('sha256','secret')hash.update('hello world') //update方法默认字符串编码为utf-8,也可以传入bufferconsole.log(hash.digest('hex')); //十六进制的形式展示
// console.log(hash.digest('base64')); //base64的形式展示
AES是一种常用的对称加密算法,加解密都用同一个秘钥。crypto模块提供了AES支持,但是需要自己封装好函数便于使用。
const crypto = require("crypto")function encrypt(key,iv,data){ //iv是秘钥let dep = crypto.createCipheriv("aes-128-cbc",key,iv)return dep.update(data,'binary','hex')+dep.final('hex') //update参数:原始数据,输入格式为 binary,以十六进制展示
}function decrypt(key,iv,data){let crypted = Buffer.from(data,'hex').toString('binary')//十六进制的对象转为buffer,再转为二进制。let dep = crypto.createDecipheriv('aes-128-cbc',key,iv)return dep.update(crypted,'binary','utf8')+dep.final('utf8')
}
//16 * 8 = 128 必须是8的倍数 key和iv都要遵循
let key = "abcdef1234567890"
let iv = "1234567890abcdef"let data ='jerry'console.log(encrypt(key,iv,data));//895f852b13da04523548c2cfebf25eff
console.log(decrypt(key,iv,encrypt(key,iv,data)));//jerry
路由
可以返回前端html页面,也可以实现api接口
//server.jsconst http = require('http')let Router ={}
function use(obj){Router ={...Router,...obj}
}function start() {const server = http.createServer((req, res) => {let myurl = new URL(req.url, 'http://127.0.0.1')try {Router[myurl.pathname](req,res)} catch (error) {Router['/404'](req,res)}})server.listen('3000', () => {console.log('启动服务器');})
}exports.start = start
exports.use = use
const server = require('./server')
const route= require("./route")
const api = require("./api")server.use({...route,...api}) //合并接口
server.start()
//route.jsconst routes = {'/home':(req,res)=>{res.writeHead(200,{'Content-Type':"text/html;charset=utf-8"})res.write(fs.readFileSync('./home.html'),'utf-8')res.end()},'/index':(req,res)=>{res.writeHead(200,{'Content-Type':"text/html;charset=utf-8"})res.write(fs.readFileSync('./index.html'),'utf-8')res.end()},'/favicon.ico':(req,res)=>{// res.writeHead(200,{'Content-Type':"image/x-icon;charset=utf8"})// res.write(fs.readFileSync('./favicon.ico'))res.end()},'/404':(req,res)=>{res.write('404','utf-8')res.end()}
}
module.exports = routes
//api.jsconst api = {'/api/findStr':(req,res)=>{res.writeHead(200,{'Content-Type':"application/json;charset=utf-8"})res.write(`{"a":"str"}`)res.end()},
}
module.exports = api
路由获取请求参数:
根据服务端的地址,访问 localhost:3000/index
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head>
<button id="login">登录-get</button>
<button id="login2">登录-post</button><body></body>
<script>//get请求login.onclick = function () {let username = 'zhangsan'let passsword = '123456'fetch(`/api/findStr?username=${username}&password=${passsword}`).then(res => res.json()).then(res => {console.log(res)})}//post请求login2.onclick = function () {fetch(`/api/findStrpost`,{method:'post',body:JSON.stringify({username:'zhangsan',passsword:'123456'}),headers:{'Content-Type':'application/json'}}).then(res => res.json()).then(res => {console.log(res)})}</script></html>
参数获取,上述的URL模块即可
const api = {//get请求'/api/findStr':(req,res)=>{const myUrl = new URL(req.url,'http://127.0.0.1')let username = myUrl.searchParams.get('username')let password = myUrl.searchParams.get('password')console.log(username,password);res.writeHead(200,{'Content-Type':"application/json;charset=utf-8"})res.write(`{"a":"str"}`)res.end()},//post请求'/api/findStrpost':(req,res)=>{let post = ""req.on('data',chunk=>{ //收集数据post+=chunk})req.on('end',()=>{console.log(post);res.writeHead(200,{'Content-Type':"application/json;charset=utf-8"})res.write(`{"a":"str"}`)res.end()})},
}
module.exports = api
路由获取静态资源
这里需要一个插件 mime可以能够获取到对应的文件应当的content-type
npm i mime
const mime = require('mime')
mime.getType('txt') // 'text/plain'
mime.getExtension('text/plain') // 'txt'
const fs =require('fs')
const path = require('path')
const mime = require('mime')const routes = {'/home':(req,res)=>{res.writeHead(200,{'Content-Type':"text/html;charset=utf-8"})res.write(fs.readFileSync('./home.html'),'utf-8')res.end()},'/index':(req,res)=>{res.writeHead(200,{'Content-Type':"text/html;charset=utf-8"})res.write(fs.readFileSync('./index.html'),'utf-8')res.end()},'/favicon.ico':(req,res)=>{// res.writeHead(200,{'Content-Type':"image/x-icon;charset=utf8"})// res.write(fs.readFileSync('./favicon.ico'))res.end()},'/404':(req,res)=>{if(readStaticFile(req,res)){ //判断是否存在文件return}res.write('404','utf-8')res.end()}
}
function readStaticFile(req,res){const myURL = new URL(req.url,'http://127.0.0.1:3000')const pathname = path.join(__dirname,myURL.pathname); //绝对路径const type = mime.getType(myURL.pathname.split('.')[1]) //获取文件后缀if(myURL.pathname=='/') return falseif(fs.existsSync(pathname)){res.writeHead(200,{'Content-Type':`${type};charset=utf8`}) //设置响应头res.write(fs.readFileSync(pathname),'utf-8')res.end()return true}else{return false}
}
module.exports = routes
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head>
<link rel="stylesheet" href="./static/css/index.css"><body><div><button id="login">登录-get</button><button id="login2">登录-post</button></div>
</body>
<script>//get请求login.onclick = function () {let username = 'zhangsan'let passsword = '123456'fetch(`/api/findStr?username=${username}&password=${passsword}`).then(res => res.json()).then(res => {console.log(res)})}//post请求login2.onclick = function () {fetch(`/api/findStrpost`, {method: 'post',body: JSON.stringify({username: 'zhangsan',passsword: '123456'}),headers: {'Content-Type': 'application/json'}}).then(res => res.json()).then(res => {console.log(res)})}</script></html>
Express
基于nodejs平台,快速,开放,极简的web开发框架
npm i express --save
路由路径和请求方法一起定义了请求的端点,可以是字符串,字符串模式或者是正则表达式。
基本路由
const express = require('express')const app = express() //创建服务器
app.get('/',(req,res)=>{res.send('123') //res.send可以发送任意格式的内容
})app.listen(3000,()=>{console.log('启动了');
})
以下情况访问 http://localhost:3000/ac 或者 http://localhost:3000/abc 均可请求
app.get('/ab?c',(req,res)=>{
res.send('123') //res.send可以发送任意格式的内容
})
占位符,能够匹配参数,请求:http://localhost:3000/abc/1 格式即可,
app.get('/abc/:id',(req,res)=>{console.log(req.params.id)res.send('456') //res.send可以发送任意格式的内容
})
可匹配 abcd abbcd abbbcd 等,b可以一次或多次。
app.get('/ab+cd',(req,res)=>{res.send('ab+cd')
})
可以在之间写任意内容
app.get('/ab*cd',(req,res)=>{res.send('ab*cd')
})
匹配正则表达式
app.get(/q/,(req,res)=>{ //路径中包含 q 即可res.send('q')
})
在接口的参数中,除了对应的访问路径以外,还可以写多个回调函数来操作处理然后返回给前端数据。其中next函数非常重要,是进入下一个回调函数的开关,一旦使用了res.send之后的 代码都不再执行。
app.get('/home',(req,res,next)=>{//验证tokenconsole.log('验证成功')next()
},(req,res)=>{res.send('home') //res.send可以发送任意格式的内容
})
多个回调函数可以写成数组的形式,并且能够传参数,使用res传递。
const a = function (req,res,next){//验证tokenconsole.log('验证')res.name='zhangsan'next()
}
const b = function (req,res,next){console.log(res.name)res.send('home') //res.send可以发送任意格式的内容
}
app.get('/home',[a,b])
res.send() 支持发送片段以及json
res.json() 只能支持发送json
res.render() 支持发送模版
中间件
express是一个资深功能极简,完全是由路由和中间件构成的一个web开发框架:从本质上说,一个express应用就是在调用各种中间件。
中间件是一个函数,可以访问请求对象,响应对象,以及web应用中处于响应循环流程中的中间件,一般被命名为next的变量。
中间件的功能包括:
执行任何代码 修改请求和响应对象 终结请求-响应循环 调用堆栈中的下一个中间件。
如果当前中间件始终没有 终结请求-响应循环,则必须调用next 方法将控制权交给下一个中间件,否则就会挂起。
Express应用可以使用如下几种中间件:
应用级中间件
应用级中间件绑定到app对象使用 app.use() 和 app.method() (包括app.get app.post等),其中,method是需要处理http请求的方法,例如get,put,post等。在请求接口前先验证token是否有效。
const express = require('express')const app = express()
const a = function (req,res,next){//验证tokenconsole.log('验证')res.name = 123next()
}
app.use(a)
const b = function (req,res,next){console.log(res.name);res.send('home') //res.send可以发送任意格式的内容
}
app.get('/home',[b])
app.get('/ab?c',(req,res)=>{res.send('123') //res.send可以发送任意格式的内容
})
app.get('/abc/:id',(req,res)=>{res.send('456') //res.send可以发送任意格式的内容
})
app.get(/q/,(req,res)=>{res.send('q') //res.send可以发送任意格式的内容
})
app.listen(3000,()=>{console.log('启动了');
})
路由级中间件
类似于接口模块化,可以给对应的模块加上前缀,使用express.Router() 来创建
下面的访问为:http://localhost:3000/index/home 等。
//indexRouter.js
const express = require('express')const router = express.Router()router.get('/',(req,res)=>{res.send('/')
})
router.get('/home',(req,res)=>{res.send('home')
})router.get('/ab?c',(req,res)=>{res.send('123')
})
router.get('/abc/:id',(req,res)=>{res.send('456')
})
router.get(/q/,(req,res)=>{res.send('q')
})module.exports = router
const express = require('express')
const IndexRouter = require('./router/IndexRouter')const app = express()
const a = function (req,res,next){//验证tokenconsole.log('验证')res.name = 123next()
}
app.use(a)
app.use('/index',IndexRouter)
//app.use('/login',LoginRouter)
//app.use('/home',HomeRouter)app.listen(3000,()=>{console.log('启动了');
})
错误处理中间件
在匹配不到接口时报错,放在app.use() 应用中间件的最后。没有任何路径匹配,万能中间件,任何数据都可以返回。使用4个参数,(err,req,res,next)err是参数状态码。
app.use((req,res)=>{res.status(404).send('丢了')
})
内置中间件
express.static是express唯一内置的中间件。它基于serve-static,负责在express应用中替托管静态资源。每个应用可有多个静态目录。
第三方中间件
安装所需功能的node模块,并在应用中加载,可以在应用级加载,也可以在路由级加载。
路由获取前端参数
http://localhost:3000/index/home?a=1&b=2
get请求,通过req.query来获取查询字符串
router.get('/home',(req,res)=>{console.log(req.query);res.send('home')
})
post请求,通过req.body来获取,但是要提前配置解析post请求的中间件,旧版需要下载body-parser,现在已经内置,需要引入配置。
const express = require('express')
const IndexRouter = require('./router/IndexRouter')const app = express()
const a = function (req,res,next){//验证tokenconsole.log('验证')res.name = 123next()
}
//配置post请求的中间件
//form 表单的形式a=1&b=2
app.use(express.urlencoded({extended:false}))
//Json对象形式{a:'1',b:'2'}
app.use(express.json()) app.use(a)
app.use('/index',IndexRouter)app.use((req,res)=>{res.status(404).send('丢了')})
app.listen(3000,()=>{console.log('启动了');
})
router.post('/homePost',(req,res)=>{console.log(req.body); //必须配置中间件res.send('homePost')
})router.post('/homePostJson',(req,res)=>{console.log(req.body); //必须配置中间件res.send('homePost')
})
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head><body><div><button id="login">登录-post</button><button id="login2">登录-postJson</button></div>
</body>
<script>login.onclick = function () {let username = 'zhangsan'let passsword = '123456'fetch(`/index/homePost`, {method: 'post',body:`username=${username}&password=${passsword}`,headers: {'Content-Type': 'applicatin/x-www-form-urlencoded'}}).then(res => res.text()).then(res => {console.log(res)})}login2.onclick = function () {fetch(`/index/homePostJson`, {method: 'POST',body: JSON.stringify({username: 'zhangsan',passsword: '123456'}),headers: {'Content-Type': 'application/json'}}).then(res => res.text()).then(res => {console.log(res)})}</script></html>
利用express托管静态文件
通过express内置的express.static可以方便地托管静态文件,例如图片,css,js文件等。
将静态资源文件所在的目录作为参数传递给express.static中间件就可以提供静态资源文件的访问了。
app.use(express.static('public')) //可以是任意文件夹。
然后就可以访问文件了:
http://localhost:3000/index/images/kitten.jpg
http://localhost:3000/index/js/app.js
http://localhost:3000/index/css/style.css
http://localhost:3000/index/hello.html
所有文件的路径都是相对于存放目录的,因此,存放静态文件的目录不会出现在URL中
只需访问 http://localhost:3000/index.html
同时可以设置多个静态文件
const express = require('express')
const IndexRouter = require('./router/IndexRouter')const app = express()
app.use(express.urlencoded({extended:false})) //配置post请求的中间件 a=1&b=2
app.use(express.json()) //配置post请求的中间件
app.use(express.static('public')) //配置静态资源
app.use(express.static('static'))const a = function (req,res,next){//验证tokenconsole.log('验证')res.name = 123next()
}app.use(a)
app.use('/index',IndexRouter)app.use((req,res)=>{res.status(404).send('丢了')})
app.listen(3000,()=>{console.log('启动了');
})
服务端渲染(模版引擎)
客户端渲染:前后端分离,BSR(前段中组装页面)做好静态页面,动态效果,json模拟,ajax,动态创建页面,真实接口数据,前后联调。
服务端渲染:后端镶嵌模版,后端渲染模版,SSR(Server-side-render 后端把页面组装),做好静态页面,动态效果,把前端代码提供给后端,后端把静态html以及html里的假数据给删掉,通过模版进行动态生成html的内容。
需要ejs 模版引擎工具,在应用中设置,让express渲染模版文件
views,放模版文件的目录,比如:app.set('views','./views')
view engine,模版引擎,比如:app.set('view engine','ejs')
<img src="%E7%AC%94%E8.assets/image-2021.png" alt="not-found" style="zoom:50%"/>
<% %> 流程控制标签(写入js代码)
<%= %> 输出标签(原文输出HTML标签,html片段)
<%- %> 输出标签 HTML会被浏览器解析
<%# %> 注释标签
<%- include('user/show',{user.user}) %> 导入公模版内容
npm i ejs
//服务端
const express = require('express')
const IndexRouter = require('./router/IndexRouter')
const HomeRouter = require('./router/HomeRouter')const app = express()//配置模版引擎
app.set('views',"./views") //配置文件夹views下的ejs文件为渲染路由页面
app.set('view engine',"ejs")app.use(express.urlencoded({extended:false})) //配置post请求的中间件 a=1&b=2
app.use(express.json()) //配置post请求的中间件
app.use(express.static('public')) //配置静态资源const a = function (req,res,next){//验证tokenconsole.log('验证')res.name = 123next()
}app.use(a)
app.use('/index',IndexRouter)
app.use('/home',HomeRouter)app.use((req,res)=>{res.status(404).send('丢了')})
app.listen(3000,()=>{console.log('启动了');
})
配置路由接口
//IndexRouter.js
const express = require('express')const router = express.Router()router.get('/',(req,res)=>{res.render('index',{title:"hello world"})//渲染模版后,自动返回给前端,找到views下的index.ejs
})
router.post('/validate',(req,res)=>{console.log(req.body);res.redirect('/home') //重定向到home页面
})module.exports = router
//HomeRouter.js
const express = require('express')
const router = express.Router()router.get('/',((req,res)=>{res.render('home',{list:[1,2,3],content:`<h1>我是片段</h1>`})
}))module.exports=router
在views文件下创建ejs文件
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head>
<body>index 页面<div>标题:<%=title%></div><form action="/index/validate" method="post"><div>用户名:<input type="text" name="username"></div><div>密码<input type="password" name="password"></div><div><input type="submit" value='登录'></div></form>
</body>
</html>
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head><body>home 页面<ul><% for(let i=0;i<list.length;i++){ %><li><%= list[i]%></li><%}%></ul><div>最新消息:<%- content %></div><%# 注释的写法 不会在页面资源中显示,只存在开发环境中 %> <div><%-include('./about.ejs',{showItem:true}) %></div>
</body>
<script>
</script></html>
有时候我们不需要ejs文件,只需要html文件,那么应该怎么做?
//配置模版引擎
app.set('views',"./views")
app.set('view engine',"html")
app.engine('html',require('ejs').renderFile) //直接支持html渲染
配置好后,在views下写好对应的html文件即可。
Express脚手架
通过express-generator可以快速创建一个应用骨架
npm i -g express-generator
创建项目并下载依赖,启动项目:
express myapp --view=ejsnpm inpm start
此时启动后不能随时重编译,做一下修改package.json,完成脚手架的配置。
"scripts": {"start": "nodemon ./bin/www"},
//一些模块
res.locals.message = err.message; //res.locals类似于上下文,在ejs中这种可以直接获取到里面的内容
app.use(logger('dev')); //记录请求的生成器,控制台可以输出错误
MongoDB
关系型与非关系型数据库
关系型数据库:sql语句增删改查操作,保持事务的一致性,事务机制(回滚),包括mysql,sqlserver,db2,oracle。
非关系型数据库:no sql:not only sql;轻量,高效,自由,包括mongodb,Hbase,redis。
mongoDB的优势:
由于独特的数据处理方式,可以将热点数据加载到内存,故而对查询来讲,会非常快(当然也消耗内存);同时由于采用了BSON的方式存储数据,对JSON格式数据具有非常好的支持性以及友好的表结构修改性,文档式的存储方式,数据有好可见;数据库的分片集群负载具有非常好的扩展性以及非常不错的自动 鼓掌转移。
sql | MongoDB | 说明 |
database | database | 数据库 |
table | collection | 表/集合 |
row | document | 表行/文档 |
column | field | 表列字段/域 |
index | index | 索引 |
table joins | 不支持 | 表连接 |
primary key | primary key | 主键 |
安装数据库
Install MongoDB Community Edition — MongoDB Manual
可自行选择系统匹配的下载
这里使用mac版本,先下载homebrew,然后安装mongodb
//终端安装homebrew
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
//根据提示继续执行
eval "$(/opt/homebrew/bin/brew shellenv)"
brew install mongodb
如果报错:No available formula with the name "mongodb". Did you mean mongosh or monetdb?
首先tap一个仓库
brew tap mongodb/brew
安装社区版
brew install mongodb-community
安装好mongodb以后,启动:
mongosh
在命令行中操作数据库
操作库
查看命令提示 | help db.help() db.test.help() db.test.find().help() |
创建/切换数据库 | use testdb |
查询数据库 | show dbs |
查看当前使用的数据库 | db/db.getName() |
显示当前DB状态 | db.stats() |
查看当前DB版本 | db.version |
产看当前DB的连接机器地址 | db.getMongo() |
删除数据库 | db.dropDatabase() |
操作集合
创建一个集合 | db.createCollection('cillName',{size:222,capped:true,max:5000});最大存储空间为5M,最多5000个文档的集合。 |
得到指定名称的聚集集合 | db.getCollection('account') |
得到当前db的所有聚集集合 | db.getCollectionNames() |
显示当前db所有聚集的状态 | db.printCollectionStats() |
删除集合 | db.collection.drop(); |
操作集合数据
添加集合数据 | db.users.insert({name:'zhangsan',age:25,sex:1}) db.users.insert([{name:'zhangsan',age:25,sex:1},{name:'lisi',age:25,sex:1}]) 在这里插入的数据,数据结构可以完全不一样,也能够插入。 |
修改 | db.users.update(age:25,$set{name:'wanger',false,true}) 相当于sql:update users set name = "wanger" where age = 25 db.users.update({name:'zhangsan'},{$inc:{age:50}},false,true) 相当于sql:update users set age = age + 50 where name = 'zhangsan' 减的话,$inc:{age:-50}即可 db.users.update({name:'zhangsan'},{$inc:{age:50},$set{name:'lisi'}},false,true) 相当于sql:update users set age = age + 50, name = 'zhangssan' where name = 'lisi' |
删除 | db.users.remove({age:32}); db.users.remove({}); 删除所有 db.users.deleteOne({age:32}); db.users.deleteMany({age:32}); |
查询所有数据 | db.userInfo.find() 相当于sql:select * from userInfo |
查询某字段去重后的数据 | db.userInfo.distinct('name') 相当于sql:select distinct name from userInfo |
查询age=22的数据 | db.userInfo.find({age:22}) 相当于sql:select * from userInfo where age = 22 |
查询age>22的数据 | db.userInfo.find({age:{$gt:22}}) 相当于sql:select * from userInfo where age > 22 |
查询age<22的数据 | db.userInfo.find({age:{$lt:22}}) 相当于sql:select * from userInfo where age < 22 |
查询age>=22的数据 | db.userInfo.find({age:{$gte:22}}) 相当于sql:select * from userInfo where age > =22 |
查询age<=22的数据 | db.userInfo.find({age:{$lte:22}}) 相当于sql:select * from userInfo where age <= 22 |
查询age>=22 age<=26的数据 | db.userInfo.find({age:{$gte:26,$lte:22}}) 相当于sql:select * from userInfo where age >= 22 and age <= 26 |
查询name包含 mongo的数据 | db.userInfo.find({name:/mongo/}),写入正则表达式 相当于sql:select * from userInfo where name like 'mongo%' |
查询指定列 name,age数据(想显示哪列,就将字段设置为1,不想显示的就设置为0) | db.userInfo.find({},{name:1,age:1}) 相当于sql:select name,age from userInfo |
查询指定列 name,age数据 age>25 | db.userInfo.find({age:{$gt:25}},{name:1,age:1}); 相当于sql:select name,age from userInfo where age >25 |
按照年龄排序(数组就是多列查询) | 升序:.db.userInfo.find().sort({age:1}) 降序:.db.userInfo.find().sort({age:-1}) |
查询 name:'zhangsa',age:22的数据 | db.userInfo.find({name:'zhangsan',age:22}) sql:select * from userInfo where name='zhangsan' and age=22 |
查询前5条数据 | db.userInfo.find().limit(5) sql:select top 5 * from userInfo |
查询10条以后的数据 | db.userInfo.find().skip(10) sql:select * from userInfo where id not in ( select top 10 * from userInfo ) |
查询5-10之间的数据 | db.userInfo.find().skip(5).limit(5) |
or 与 查询 | db.userInfo.find({$or:[{age:22},{age:25}]}) sql:select * from userInfo whrer age = 22 or age = 25 |
查询第一条数据 | db.userInfo.findOne() / db.userInfo.find().limit(1) sql:select 1 * from userInfo |
查询某个结果集的记录条数 | db.userInfo.find({age:{$gte:25}}).count() sql:select count(*) from userInfo where age >= 20 |
可视化工具进行增删改查
Robomogo Robo3T adminMongo ,可自行下载使用
使用nodejs操作数据库
这里使用生成的express ejs的脚手架,数据库连接,新建数据库文件连接,在启动文件下引入,可以在app.js或者bin下面的www文件,这里在express脚手架中,www文件引入
npm i mongoose
const mongoose =require('mongoose')mongoose.connect('mongodb://127.0.0.1:27017/express_test')
//插入集合数据,数据库会自动创建
#!/usr/bin/env node/*** Module dependencies.*/var app = require('../app');
var debug = require('debug')('myapp:server');
var http = require('http');
require('../config/db.config')
/*** Get port from environment and store in Express.*/var port = normalizePort(process.env.PORT || '3000');
app.set('port', port);/*** Create HTTP server.*/var server = http.createServer(app);/*** Listen on provided port, on all network interfaces.*/server.listen(port);
server.on('error', onError);
server.on('listening', onListening);/*** Normalize a port into a number, string, or false.*/function normalizePort(val) {var port = parseInt(val, 10);if (isNaN(port)) {// named pipereturn val;}if (port >= 0) {// port numberreturn port;}return false;
}/*** Event listener for HTTP server "error" event.*/function onError(error) {if (error.syscall !== 'listen') {throw error;}var bind = typeof port === 'string'? 'Pipe ' + port: 'Port ' + port;// handle specific listen errors with friendly messagesswitch (error.code) {case 'EACCES':console.error(bind + ' requires elevated privileges');process.exit(1);break;case 'EADDRINUSE':console.error(bind + ' is already in use');process.exit(1);break;default:throw error;}
}/*** Event listener for HTTP server "listening" event.*/function onListening() {var addr = server.address();var bind = typeof addr === 'string'? 'pipe ' + addr: 'port ' + addr.port;debug('Listening on ' + bind);
}
创建数据库模型,由于mongodb存储数据比较自由,所以需要添加这样的模型来限制传入的字段,以及字段类型,新建限制模型
const mongoose = require("mongoose")// 限制模型 类似于接口interface限制参数
const type = {username:String,password:String
}
// 模型user将会对应users集合
const UserModel = mongoose.model('user',new mongoose.Schema(type))module.exports=UserModel
在接口文件中引入限制模型,并操作数据库。
var express = require('express');
var router = express.Router();
const UserModel = require('../dbModel/UserModel')
/* GET home page. */
router.get('/', function (req, res, next) { //服务端渲染//创建数据库模型,要限制field类型,一一对应数据库集合let page = 1let limit = 3UserModel.find({},['username']).sort({_id:-1}).skip((page-1)*limit).limit(3).then(result => { //数据查询res.render('index', {list:result});})
});
//注册
router.post('/register', function (req, res, next) {const { username, password } = req.body//创建数据库模型,要限制field类型,一一对应数据库集合UserModel.create({ username, password }).then(result => {res.send('register success');})
});
//修改
router.post('/update/:id', function (req, res, next) {const { username, password } = req.bodyconst { id } = req.params//创建数据库模型,要限制field类型,一一对应数据库集合UserModel.updateOne({ id, username, password }).then(result => { res.send('update success');})
});
// 删除
router.get('/delete', function (req, res, next) {const { id } = req.query//创建数据库模型,要限制field类型,一一对应数据库集合UserModel.deleteOne({ _id:id }).then(result => { //插入数据res.send('delete success')})
});
module.exports = router;
页面请求,index.ejs页面
<!DOCTYPE html>
<html><head><link rel='stylesheet' href='/stylesheets/style.css' /></head><body><div>用户名:<input id="username" type="text"></div><div>密码:<input id="password" type="text"></div><button id="reg">注册</button><button id="update">修改</button><button id="deleteItem">删除</button><br><div><ul><% for (let index = 0; index < list.length; index++) { %><li><%=list[index].username %>-</li><% } %></ul></div></body><script>reg.onclick=()=>{fetch('/register',{method:'post',headers:{"Content-Type":"application/json"},body:JSON.stringify({username:username.value,password:password.value})}).then(res=>res.text()).then(res=>{console.log(res);})}update.onclick=()=>{fetch('/update/64eb4d5df24d574cfca6e518',{method:'post',headers:{"Content-Type":"application/json"},body:JSON.stringify({username:'修改',password:'修改'})}).then(res=>res.text()).then(res=>{console.log(res);})}deleteItem.onclick=()=>{fetch('/delete?id=64eb4df708eff7b8d60cadbf').then(res=>res.text()).then(res=>{console.log(res);})}</script>
</html>
fetch接口
//postfetch('/register',{method:'post',headers:{"Content-Type":"application/json"},body:JSON.stringify({username:username.value,password:password.value})}).then(res=>res.text()).then(res=>{console.log(res);})//get 也可以 /delete/id 的占位符写法fetch('/delete?id=64eb4df708eff7b8d60cadbf').then(res=>res.text()).then(res=>{console.log(res);})
接口规范与业务分层
接口规范
restful架构
服务器上每一种资源,比如一个文件,一张图片,一部电影都有对应的url地址,如果我们的额客户端要对服务器的资源进行操作,就要通过http协议执行形影的动作来操作,比如获取,更新,删除。
简单来说就是url地址中只包含名词表示资源,使用http动词表示动作进行操作资源,下面对比,右边为规范的写法
Get /blog/getArticles ---> Get /blog/Articles //获取Get /blog/addArticles ---> Post /blog/Articles //新增Get /blog/editArticles ---> Put /blog/Articles //编辑Get /rest/api/deleteArticles ---> Delete /blog/Articles/1 //删除
使用方式
GET http://www.test.com/api/user //获取列表POST http://www.test.com/api/user //创建用户PUT http://www.test.com/api/user/{id} //修改用户信息DELETE http://www.test.com/api/user/{id} //删除用户信息
fetch('/api/user',{method:'POST',headers:{"Content-Type":"application/json"},body:JSON.stringify({username:username.value,password:password.value})}).then(res=>res.text()).then(res=>{console.log(res);})fetch('/api/user/id',{method:'PUT',headers:{"Content-Type":"application/json"},body:JSON.stringify({username:'修改',password:'修改'})}).then(res=>res.text()).then(res=>{console.log(res);})fetch('/api/user/id',{method:'DELETE',headers:{"Content-Type":"application/json"},body:JSON.stringify({username:'修改',password:'修改'})}).then(res=>res.text()).then(res=>{console.log(res);})
fetch('/api/user'}).then(res=>res.text()).then(res=>{console.log(res);})
router.get('/user', function (req, res, next) { //服务端渲染//创建数据库模型,要限制field类型,一一对应数据库集合let page = 1let limit = 3UserModel.find({},['username']).sort({_id:-1}).skip((page-1)*limit).limit(3).then(result => { //数据查询res.render('index', {list:result});})
});//注册
router.post('/user', function (req, res, next) {const { username, password } = req.body//创建数据库模型,要限制field类型,一一对应数据库集合UserModel.create({ username, password }).then(result => {res.send('register success');})
});
//修改
router.put('/user/:id', function (req, res, next) {const { username, password } = req.bodyconst { id } = req.params//创建数据库模型,要限制field类型,一一对应数据库集合UserModel.updateOne({ id, username, password }).then(result => { res.send('update success');})
});
// 删除
router.delete('/user/:id', function (req, res, next) {const { id } = req.query//创建数据库模型,要限制field类型,一一对应数据库集合UserModel.deleteOne({ _id:id }).then(result => { //插入数据res.send('delete success')})
});
业务分层
router.js:负责将请求分发给c端
controller.js:c层负责处理业务逻辑(v与m之间的沟通)
views:v层 负责展示页面
model:m层 负责处理数据(增删改查)
创建controllers service
//indexRouter.js
const UserModel = require('../dbModel/UserModel')
var express = require('express');
var router = express.Router();
const IndexController = require('../controllers/indexController')
/* GET home page. */
router.get('/', IndexController.queryUser);
//注册
router.post('/register',IndexController.addUser);
//修改
router.post('/update/:id',IndexController.updateUser );
// 删除
router.get('/delete', IndexController.deleteUser);
module.exports = router;
//indexControllers.js
const UserService = require('../service/UserService')
const IndexController = {addUser: async (req, res, next) => {const { username, password } = req.body//创建数据库模型,要限制field类型,一一对应数据库集合await UserService.addUser(username, password)res.send('register success');},updateUser: async (req, res, next) => {const { username, password } = req.bodyconst { id } = req.params//创建数据库模型,要限制field类型,一一对应数据库集合await UserService.updateUser(id, username, password)res.send('update success');},queryUser: async (req, res, next) => { //服务端渲染//创建数据库模型,要限制field类型,一一对应数据库集合let page = 1let limit = 3let result = await UserService.queryUser(page, limit)res.render('index', { list: result });},deleteUser:async (req, res, next)=>{const { id } = req.query//创建数据库模型,要限制field类型,一一对应数据库集合await UserService.deleteUser(id)res.send('delete success')}}module.exports = IndexController
//UserServiceconst UserModel = require('../dbModel/UserModel')
const UserService = {addUser: (username, password) => {return UserModel.create({ username, password }).then(result => {})},updateUser:(id,username, password)=>{return UserModel.updateOne({ id, username, password }).then(result => {})},queryUser:(page,limit)=>{return UserModel.find({}, ['username']).sort({ _id: -1 }).skip((page - 1) * limit).limit(3)},deleteUser:(id)=>{UserModel.deleteOne({ _id: id }).then(result => { //插入数据})}
}module.exports = UserService
Cookie与session
http是无状态的,也就是说http请求方和响应方之间无法维持状态,都是一次性的,他不知道前后的请求都发生了什么,但有的场景下,我们需要维护状态。比如登录情况下才可以发送业务请求等。
使用express-session插件
npm i express-session
在app.js中配置
var session = require("express-session")app.use(session({ //注册sessionname:'sys', //名称secret:'qwer1234', //服务器生成session的签名cookie:{maxAge:1000*60*60, //过期时间secure:false //为true只有https协议才能访问cookie},resave:true, //刷新sessionsaveUninitialized:true //true一开始会给一个无效的cookie
}))
//设置中间件,session过期校验
app.use((req,res,next)=>{if(req.url.includes("login")){ //登录页面放行next()return}if(req.session.user){next()}else{res.redirect('/login')}
})
在登录时添加session,indexController中配置
loginUser: async (req, res, next) => {const { username, password } = req.body//创建数据库模型,要限制field类型,一一对应数据库集合let arr = await UserService.loginUser(username, password)console.log(arr);if (arr.length == 0) {res.send({ code: 0 })} else {req.session.user = "123" //设置sesion对象res.send({ code: 1 })}},
在index.js接口文件中引用
router.post('/loginUser', IndexController.loginUser);
清除session,session失效后,可以通过前端跳转到login界面
router.get('/logout',(req,res)=>{req.sessiion.destroy(()=>{res.send({ok:1})})
})
刷新重置session
app.use((req,res,next)=>{if(req.url.includes("login")){ //登录页面放行req.session.date = Date.now() //刷新重置sessionnext()return}if(req.session.user){next()}else{res.redirect('/login')}
})
将session存储在数据库,保证每次后端变化前端不用重新获取session,使用中间件connect-mongo
npm i connect-mongo
app.js的配置:
const MongoStore = require("connect-mongo")app.use(session({ //注册sessionname:'sys', //名称secret:'qwer1234', //服务器生成session的签名cookie:{maxAge:1000*60*60, //过期时间secure:false //为true只有https协议才能访问cookie},resave:true, //刷新sessionsaveUninitialized:true, //true一开始会给一个无效的cookiestore:MongoStore.create({mongoUrl:'mongodb://127.0.0.1:27017/sys',ttl:1000*60*10 //过期时间})
}))
缺点:由于session都是存在内存或者数据库中,但是如果用户量越来越多,所占的空间也就会越来越大,那么就是问题了。
JSON Web Token
cookie容易被csrf跨站请求伪造导致安全性问题,可以存储在客户端localstorage,并且实行加密,使用sha256加密。前端请求会自动带上cookie但不会带上token。
缺点:占带宽,正常情况下要比session_id更大,需要消耗更多流量,挤占更多宽带。无法在服务端注销,那么就很难解决劫持问题。性能问题,JWT的卖点之一就是加密签名,由于这个特性,接收方得以验证JWT是否有效且被信任。对于有着严格性能要求的web应用,并不理想,尤其是对于单线程环境。
npm i jsonwebtoken
封装token
const jwt = require("jsonwebtoken")
const secret = 'test' const JWT = {generate(data,expires){return jwt.sign(data,secret,{expiresIn:expires})},verify(token){try {return jwt.verify(token,secret)} catch (error) {return false}}
}module.exports = JWT
在登录接口处引用
loginUser: async (req, res, next) => {const { username, password } = req.body//创建数据库模型,要限制field类型,一一对应数据库集合let arr = await UserService.loginUser(username, password)console.log(arr);const token = JWT.generate({id:arr[0]._id,username:arr[0].username},'10s')res.header('Authorization',token)if (arr.length == 0) {res.send({ code: 0 })} else {req.session.user = "123" //设置sesion对象res.send({ code: 1 })}},
前端使用axios可以在响应拦截器中获取到响应头中的token然后存储在localStorage中,同理在发送请求时,在请求头中添加localstorage中的token;同时后端要时刻刷新token。
axios.post('/loginUser',{username:username.value,password:password.value}).then(res=>{console.log(res);if(res.code==0){alert(res)}else{this.localStorage.setItem('token',res.headers.authorization)location.href = '/'}})
//设置中间件,session过期校验
app.use((req,res,next)=>{if(req.url.includes("login")){next()return}const token = req.headers['authorization']?.split(' ')[1]if(token){const payload = JWT.verify(token)if(payload){const newToken = JWT.generate({_id:payload._id,username:payload.username},'10s')res.header('Authorization',newToken)next()}else{res.status(401).send({msg:'token过期'})}}else{next()}
})
<!DOCTYPE html>
<html>
<head><link rel='stylesheet' href='/stylesheets/style.css' />
</head>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script><body><h1>login页面</h1><div>用户名:<input id="username" type="text"></div><div>密码:<input id="password" type="text"></div><button id="log">登录</button><br>
</body>
<script>axios.interceptors.request.use((config) => {console.log(config);const token = localStorage.getItem('token')config.headers.Authorization = 'Bearer ${token}'return config}, err => {return Promise.reject(err)})axios.interceptors.response.use((response) => {this.localStorage.setItem('token', response.headers.authorization)//响应拦截获取存储tokenreturn response}, err => {return Promise.reject(err)})log.onclick = () => {axios.post('/loginUser', { username: username.value, password: password.value }).then(res => {console.log(res);if (res.code == 0) {alert(res)} else {location.href = '/'}})}
</script></html>
其它接口token失效要清除token并且跳转到登录界面
axios.interceptors.request.use((config) => {const token = localStorage.getItem('token')config.headers.Authorization = `Bearer ${token}`return config}, err => {return Promise.reject(err)})axios.interceptors.response.use((response) => {localStorage.setItem('token', response.headers.authorization)//响应拦截获取存储tokenreturn response}, err => {if (err.response.status == 401) {localStorage.removeItem('token')location.href = '/login'}return Promise.reject(err)})reg.onclick = () => {axios.post('/register', { username: username.value, password: password.value }).then(res => {console.log(res);})}
文件上传
前端通过(multipart/form-data)表单传递给后端,但是后端没办法处理,所以使用multer中间件
npm i multer
前端上传请求 upload.ejs文件
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head>
<body><form action="/register" enctype="multipart/form-data" method="post"><input type="text" name="username"><input type="password" name="password"><input type="file" name="avatar"><input type="submit" value="提交"></form>
</body>
</html>
添加upload路由服务端渲染页面
var express = require('express');
const multer = require('multer');
var router = express.Router();
const upload = multer({dest:'public/uploads/'})
/* GET users listing. */
router.get('/', function(req, res, next) {res.render('upload')
});module.exports = router;
添加路由模块
var uploadRouter = require('./routes/upload');app.use('/upload', uploadRouter);
在注册接口中添加,中间件法则,会依次执行
const UserModel = require('../dbModel/UserModel')
var express = require('express');
var router = express.Router();
const multer = require('multer');
const upload = multer({dest:'public/uploads/'}) //指定前端传来图片文件放在的路径
const IndexController = require('../controllers/indexController')
/* GET home page. */
router.get('/', IndexController.queryUser);
//注册
router.post('/register',upload.single('avatar'),IndexController.addUser);//将头像传入到指定的目录
//修改
router.post('/update/:id',IndexController.updateUser );
// 删除
router.get('/delete', IndexController.deleteUser);
//登录页面
router.get('/login', IndexController.login);
//用户登录
router.post('/loginUser', IndexController.loginUser);module.exports = router;
在controller中获取文件,同时修改model限制模型,穿参到service,并且存入数据库
const mongoose = require("mongoose")// 限制模型 类似于接口interface限制参数
const type = {username:String,password:String,avatar:String //添加头像的字段
}
// 模型user将会对应users集合
const UserModel = mongoose.model('user',new mongoose.Schema(type))module.exports=UserModel
addUser: async (req, res, next) => {const { username, password } = req.bodyconst avatar =req.file? `/uploads/${req.file.filename}`:`/images/default.png` //文件信息 避免不传文件给默认值,避免报错//创建数据库模型,要限制field类型,一一对应数据库集合await UserService.addUser(username, password,avatar)res.send('register success');},
addUser: (username, password,avatar) => {return UserModel.create({ username, password,avatar }).then(result => {})},
前端获取服务端渲染
在service中查询接口中查询avatar的数据,返回给index.ejs页面渲染,通过img标签,src属性来获取存入的图片
controller
queryUser: async (req, res, next) => { //服务端渲染//创建数据库模型,要限制field类型,一一对应数据库集合let page = 1let limit = 3let result = await UserService.queryUser(page, limit)console.log(result);res.render('index', { list: result });},
service
queryUser:(page,limit)=>{return UserModel.find({}, ['username','avatar']).sort({ _id: -1 }).skip((page - 1) * limit).limit(3)},
controller 返回渲染
queryUser: async (req, res, next) => { //服务端渲染//创建数据库模型,要限制field类型,一一对应数据库集合let page = 1let limit = 3let result = await UserService.queryUser(page, limit)console.log(result);res.render('index', { list: result });},
<!DOCTYPE html>
<html><head><link rel='stylesheet' href='/stylesheets/style.css' />
</head><body><h1>index页面</h1><div>用户名:<input id="username" type="text"></div><div>密码:<input id="password" type="text"></div><button id="reg">注册</button><button id="update">修改</button><button id="deleteItem">删除</button><br><div><ul><% for (let index=0; index < list.length; index++) { %><li><%=list[index].username %>- <img src="<%= list[index].avatar%>" alt=""> </li><% } %></ul></div>
</body></html>
在前端数据上使用非form的表单,使用FormData对象:
<!DOCTYPE html>
<html><head><link rel='stylesheet' href='/stylesheets/style.css' />
</head>
<style>img{width: 100px;}
</style>
<body><h1>index页面</h1><div>用户名:<input id="username" type="text"></div><div>密码:<input id="password" type="text"></div><div>头像:<input type="file" id="avatar"></div><button id="reg">注册</button><button id="update">修改</button><button id="deleteItem">删除</button><br><div><ul><% for (let index=0; index < list.length; index++) { %><li><%=list[index].username %>- <img src="<%= list[index].avatar%>" alt=""> </li><% } %></ul></div>
</body>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>axios.interceptors.request.use((config) => {const token = localStorage.getItem('token')config.headers.Authorization = `Bearer ${token}`return config}, err => {return Promise.reject(err)})axios.interceptors.response.use((response) => {localStorage.setItem('token', response.headers.authorization)//响应拦截获取存储tokenreturn response}, err => {if (err.response.status == 401) {localStorage.removeItem('token')location.href = '/login'}return Promise.reject(err)})reg.onclick = () => {const formdata = new FormData()formdata.append('username',username.value)formdata.append('password',password.value)formdata.append('avatar',avatar.files[0])axios.post('/register', formdata,{headers:{"Content-Type":"multipart/form-data"}}).then(res => {console.log(res);})}update.onclick = () => {fetch('/update/64eb4d5df24d574cfca6e518', {method: 'post',headers: {"Content-Type": "application/json"},body: JSON.stringify({ username: '修改', password: '修改' })}).then(res => res.text()).then(res => {console.log(res);})}deleteItem.onclick = () => {fetch('/delete?id=64eb4df708eff7b8d60cadbf').then(res => res.text()).then(res => {console.log(res);})}
</script></html>
如果想要批量上传
//前端
<input type="file" id="avatar" multiple> //后端
//注册
router.post('/photoUpload',upload.array('photos',12),(req,res,next)=>{//req.files是文件数组信息//req.body 文本域数据//pjotos是对应的参数名
});
APIDOC--api文档生成工具
apidoc是一个简单的restful api 文档生成工具,有以下特点:
1.跨平台,linux,windows,macOS等都支持
2.支持语言广泛
3.支持多个不同语言的多个项目生成一份文档
4.输出模版可自定义
5.根据模版生成mock数据
npm i -g apidoc
vscode可以下载插件 APIDoc Snippets
输入apiDocumentation,会生成以下的代码,我们可以按照自己的接口情况修改
/**** @api {post} /register 添加用户* @apiName addUser* @apiGroup usergroup* @apiVersion 1.0.0** @apiParam {String} username 用户名* @apiParam {String} password 密码* @apiParam {File} avatar 头像** @apiSuccess (200) {string} register success** @apiParamExample {multipart/form-data} Request-Example:* {* username : 'test'* password : '123123'* avatar : file对象* }*** @apiSuccessExample {string} * register success*/
//注册
router.post('/register',upload.single('avatar'),IndexController.addUser);
在终端中输入指令,生成doc文件夹
apidoc -i ./routes/ -o ./doc
doc文件夹中打开index.html页面就可以看到接口文档。
配置apidoc
根目录下创建apidoc.json文件,配置以下选项即可
{"name":"后台系统接口文档","version":"1.0.0","description":"接口文档","title":"定制系统"
}
koa2
koa编写web应用,通过组合不同的generator,可以免除重复繁琐的回调函数嵌套,并极大地提升错误处理的效率。koa不再内核方法中绑定任何中间件,仅仅提供了一个轻量优雅的函数库,使编写web应用变得得心应手。防守打法第三方结算单
安装koa2
初始化:npm init
安装:npm i koa
//index.js
const Koa = require('koa')const app = new Koa()app.use((ctx,next)=>{ //ctx执行上下文,中含有request以及responseconsole.log('server start');ctx.response.body='hello world'
})
app.listen(3000)
启动服务:nodemon index.js
ctx.req | Node的request对象 |
ctx.res | Node的response对象 |
ctx.request | Koa的request对象 |
ctx.response | Koa的response对象 |
在访问时,可以不访问request或者response对象,直接访问属性即可简写
ctx.response.body | ctx.body |
ctx.request.path | ctx.path |
...... | ...... |
Koa vs express
通常会说Koa是洋葱模型,是由于在于中间件的设计。express也是类似的,但是express中间件使用了callback实现,如果出现异步问题则会让你在执行顺序上感到困惑,因此如果我们想要做接口耗时同级,错误处理koa的这种中间件模式处理起来更方便些。最后一点响应机制也很重要,koa不是立即响应,使整个中间件处理完再最外层进行了响应,而express是立即响应。
更加轻量
koa不提供内置的中间件;不提供路由,把路由这个库分离出来了。
Context对象
koa增加了一个context对象,作为这次请求的上下文对象(在koa中作为中间件的第一个参数传入)。同时context上也挂载了request和response两个对象。和express类似,这两个额对象都提供了大量的便捷方法辅助开发。
异步流程控制
express采用callback来处理异步,koa1采用generator,koa2采用async/await
generator和async/await使用同步的写法来处理异步,明显好于callback和promise
中间件模型
express基于connect中间件,线性模型
koa采用洋葱模型(对于每个中间件,在完成了一些事情后,可以有丫的将控制权传递给下一个中间件,并且能够等待它完成,网后续的中间件完成处理后,控制权又回到了自己)
const Koa = require('koa')const app = new Koa()app.use(async (ctx,next)=>{ //ctx执行上下文,中含有request以及responseconsole.log('server start');console.log(1111);ctx.response.body='hello world' //类似于express中res.sendawait next()console.log(2222);
})app.use(async (ctx,next)=>{ //ctx执行上下文,中含有request以及responseconsole.log(3333);await test()console.log(444);
})function test(){return new Promise((resolve,reject)=>{resolve('123')})
}app.listen(3000)
以上代码结果为:
1111
3333
444
2222
路由
npm i koa-router
基本用法
const Koa = require('koa')
const Router = require('koa-router')const app = new Koa()
const router = new Router()router.get('/list',(ctx,next)=>{ctx.body = [1,2,3]
}).post('/list/:id',(ctx,next)=>{console.log(ctx.params); //id存储在params中ctx.body = 'add success'
}).put('/list/:id',(ctx,next)=>{ctx.body = 'update success'
}).delete('/list/:id',(ctx,next)=>{ctx.body = 'delete success'
})app.use(router.routes()).use(router.allowedMethods()) //router.allowedMethods()能够给出错误提示app.listen(3000)
分模块引入
//home.js
const Router = require('koa-router')
const router = new Router()router.get('/',(ctx,next)=>{ctx.body = '<h1>HomePage</h1>'
})module.exports = router
//user.js
const Router = require('koa-router')
const router = new Router()router.get('/',(ctx,next)=>{ctx.body = [1,2,3]
}).post('/:id',(ctx,next)=>{console.log(ctx.params); //id存储在params中ctx.body = 'add success'
}).put('/:id',(ctx,next)=>{ctx.body = 'update success'
}).delete('/:id',(ctx,next)=>{ctx.body = 'delete success'
})module.exports = router
//list.js
const Router = require('koa-router')
const router = new Router()router.get('/',(ctx,next)=>{ctx.body = [1,2,3]
}).post('/:id',(ctx,next)=>{console.log(ctx.params); //id存储在params中ctx.body = 'add success'
}).put('/:id',(ctx,next)=>{ctx.body = 'update success'
}).delete('/:id',(ctx,next)=>{ctx.body = 'delete success'
})module.exports = router
//routes文件夹index.js 整合路由
const Router = require('koa-router')
const router = new Router()
//引入路由
const userRouter = require('./user')
const listRouter = require('./list')
const homeRouter = require('./home')
//统一加前缀
// router.prefix("/api")
//注册路由级组件
router.use('/user',userRouter.routes(),userRouter.allowedMethods())
router.use('/list',listRouter.routes(),listRouter.allowedMethods())
router.use('/home',homeRouter.routes(),homeRouter.allowedMethods())
router.redirect('/',"/home") //重定向
module.exports = router
//index.js入口文件
const Koa = require('koa')
const router = require('./routes/index')const app = new Koa()//注册应用级组件
app.use(router.routes()).use(router.allowedMethods()) //router.allowedMethods()能够给出错误提示
app.listen(3000)
托管静态资源
npm i koa-static
const Koa = require('koa')
const router = require('./routes/index')
const static = require("koa-static")
const path = require('path')
const app = new Koa()app.use(static(path.join(__dirname,'public')))
//注册应用级组件
app.use(router.routes()).use(router.allowedMethods()) //router.allowedMethods()能够给出错误提示
app.listen(3000)
在public下创建html文件,并且在http://localhost:3000/center.html直接访问
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head>
<link rel="stylesheet" href="./css/center.css">
<body><div>center</div>
</body>
</html>
获取请求参数
获取get请求数据
获取get请求数据源头是koa中request对象中的query或者querystring方法,query返回是格式化好的参数对象,querystring返回的是请求字符串,ctx有对request的api有直接引入放入方式,所以获取get请求数据有两个途径
1.从上下文中直接获取,请求对象ctx.query,返回如{ a:1,b:2},请求字符串ctx.querystring返回 a=1&b=2。
2.从上下文中的request直接获取,请求对象ctx.request.query,返回如{ a:1,b:2},请求字符串ctx.request.querystring返回 a=1&b=2。
获取post参数
对于post请求的处理,koa-bodyparser中间件可以吧koa2上下文的formData数据解析到ctx.request.body中,不可简写。
npm i koa-bodyparser
const Koa = require('koa')
const router = require('./routes/index')
const static = require("koa-static")
const path = require('path')
const bodyParser = require('koa-bodyparser')
const app = new Koa()app.use(bodyParser()) //获取前端post请求传来的参数
app.use(static(path.join(__dirname,'public')))
//注册应用级组件
app.use(router.routes()).use(router.allowedMethods()) //router.allowedMethods()能够给出错误提示
app.listen(3000)
router.post('/:id',(ctx,next)=>{console.log(ctx.request.body); //获取post传参ctx.body = 'add success'
})
ejs模版
npm i --save ejsnpm i koa-views
const Koa = require('koa')
const router = require('./routes/index')
const static = require("koa-static")
const path = require('path')
const bodyParser = require('koa-bodyparser')
const views = require('koa-views')
const app = new Koa()app.use(bodyParser()) //获取前端传来的参数
app.use(static(path.join(__dirname,'public')))
app.use(views(path.join(__dirname,'views'),{extension:'ejs'})) //配置views为模版引擎
//注册应用级组件
app.use(router.routes()).use(router.allowedMethods()) //router.allowedMethods()能够给出错误提示
app.listen(3000)
//home.js
const Router = require('koa-router')
const router = new Router()router.get('/',async (ctx,next)=>{console.log(123);// ctx.body = '<h1>HomePage</h1>'await ctx.render('home')
})module.exports = router
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head>
<body>homePage模版页面
</body>
</html>
cookie&session
cookie
koa提供了从上下文直接读取,写入cookie的方法
ctx.cookies.get(name,[options]) //读取上下文请求中的cookiectx.cookies.set(name,[options]) //写入上下文中的cookie
session
koa-session-minimal适用于koa2的session中间件,提供存储介质的读写接口
const Koa = require('koa')
const router = require('./routes/index')
const static = require("koa-static")
const path = require('path')
const bodyParser = require('koa-bodyparser')
const views = require('koa-views')
const app = new Koa()
const session = require('koa-session-minimal')app.use(bodyParser()) //获取前端传来的参数
app.use(static(path.join(__dirname,'public')))
app.use(views(path.join(__dirname,'views'),{extension:'ejs'})) //配置views为模版引擎
//注册应用级组件
app.use(session({key:'test',cookie:{maxAge:1000*60*60}
}))
app.use(router.routes()).use(router.allowedMethods()) //router.allowedMethods()能够给出错误提示
app.listen(3000)
router.get('/',async (ctx,next)=>{ctx.session.user = {username:"test"}await ctx.render('home')
})
// session判断拦截
app.use(async (ctx,next)=>{if(ctx.url.includes("/login")){await next()return}if(ctx.session.user){//重置sessionctx.session.mydate = Date.now()await next()}else{ctx.redirect('/login')}
})
koa-jwt
npm i jsonwebtoken
可以使用之前写好的jwt模块
const jwt = require("jsonwebtoken")
const secret = 'test' const JWT = {generate(data,expires){return jwt.sign(data,secret,{expiresIn:expires})},verify(token){try {return jwt.verify(token,secret)} catch (error) {return false}}
}module.exports = JWT
const JWT = require('../util/jwt')router.post('/login',async (ctx , next) => {const { username, password } = req.body//创建数据库模型,要限制field类型,一一对应数据库集合let arr = await UserService.loginUser(username, password)const token = JWT.generate({id:arr[0]._id,username:arr[0].username},'10s')ctx.set('Authorization',token)ctx.body = {ok:1}});
前端请求拦截器以及后端校验刷新token
axios.interceptors.request.use((config) => {const token = localStorage.getItem('token')config.headers.Authorization = `Bearer ${token}`return config}, err => {return Promise.reject(err)})axios.interceptors.response.use((response) => {localStorage.setItem('token', response.headers.authorization)//响应拦截获取存储tokenreturn response}, err => {if (err.response.status == 401) {localStorage.removeItem('token')location.href = '/login'}return Promise.reject(err)})
app.use(async (ctx,next)=>{if(ctx.url.includes('login')){await next()return}const token = ctx.headers["authorization"]?.split("")[1]if(token){const payload = JWT.verify(token)if(payload){const newToken = JWT.generate({_id:payload.id,username:payload.username},'10s')ctx.set("authorization",newToken) //重置tokenawait next()}else{ctx.status = 401ctx.body = {errCode:-1,errInfo:'token过期'}}}else{await next()}
})
文件上传
npm i --save @koa/multer multer
前端传参必须是表单的形式
axios.post('/register', formdata,{headers:{"Content-Type":"multipart/form-data"}}).then(res => {console.log(res);})
const multer = require('@koa/multer');
const upload = multer({dest:'public/uploads/'}) //目标地址//前端传输的文件参数名为avatar
router.post('/upload',upload.single('avatar'),(ctx)=>{console.log(ctx.request.body,ctx.file) //表单信息,文件信息ctx.body = 'upload success'
});
操作MongoDB
npm i mongoose
const mongoose =require('mongoose')mongoose.connect('mongodb://127.0.0.1:27017/koa_test')
//插入集合数据,数据库会自动创建
可以直接使用之前express的配置文件,以及限制模型
const mongoose =require('mongoose')mongoose.connect('mongodb://127.0.0.1:27017/koa_test')
//插入集合数据,数据库会自动创建
const mongoose = require("mongoose")// 限制模型 类似于接口interface限制参数
const type = {username:String,password:String,// avatar:String
}
// 模型user将会对应users集合
const UserModel = mongoose.model('user',new mongoose.Schema(type))module.exports=UserModel
router.post('/',async (ctx,next)=>{console.log(ctx.params); //id存储在params中const {username,password} = ctx.request.bodyawait UserModel.create({username,password})ctx.body = 'add success'
})
MySql(关系型数据库)
免费数据库mysql,与非关系型数据库区别:
主要差异是数据存储的方式。关系型数据天然就是表格格式,因此存储在数据表的行和列中。数据表可以彼此关联写作存储,也很容易提取数据。
与其相反,非关系型数据库不适合存储在数据表的行和列中,而是大块组合在一起,非关系型数据通常存储在数据集中,就像文档,键值对或者图结构,你的数据及其特性是选择数据存储和提取方式的首要影响因素。
优点:
易于维护,都是使用表结构,格式一致。
使用方便,sql语言通用,可用于复杂查询。
复杂操作,支持sql,可用于一个表以及多个表之间非常复杂的查询。
缺点:
读写性能比较差,尤其是海量数据的高效率读写
固定的表结构,灵活度稍欠。
高并发读写需求,传统关系型数据库来说,硬盘I/O是一个很大的瓶颈。
非关系型数据库严格上不是一种数据库,应该是一种数据结构化存储方法的集合,可以是文档或者键值对等
优点:
格式灵活,存储数据的格式可以是key,value形式,文档形式,图片形式等,使用灵活,应用场景广泛,而关系型数据库则只支持基础类型。
速度快,nosql可以使用硬盘或者随机存储器作为载体,而关系型数据库只能用硬盘。
高扩展性,成本低,nosql数据库部署简单,基本都是开源。
缺点:
不提供sql支持,无事务处理,数据结构相对复杂,复杂查询方面稍欠。
sql语句
插入,插入的数据的类型需要严格按照表中数据的数据类型
INSERT INTO students(id,name,score,gender) VALUES (null,"test",100,1)
更新
UPDATE student SET name = "test",score = 22 WHERE id=2
删除
DELETE FROM student WHERE id=2
查询
//查询所有
SELECT * FROM student WHERE 1//查询所有数据某个字段
SELECT id,name,score,gender FROM student WHERE 1//条件查询
SELECT * FROM student WHERE id=1//模糊查询
SELECT * FROM student WHERE name like "&k%"//排序
SELECT id,name,gender,score FROM student ORDER BY score
SELECT id,name,gender,score FROM student ORDER BY score DESC //降序//分页查询
SELECT id,name,gender,score FROM student LIMIT 50 OFFSET 0//记录条数
SELECT COUNT(*) FROM student
SELECT COUNT(*)totalNumber FROM student
//多表查询(多表查询成为笛卡尔查询,使用时要非常小心由于结果是目标表的行数乘积,各有100条,返回10000条)
SELECt * FROM students,class;//要使用表名,列名这样的方式来引用列和设置别名,这样就避免了结果集的列名重复问题。
SELECT students.id sid,students.name,students.gender,students.score,class.id cid,class.name cname FROM students,class//联表查询
SELECT s.id,s.name sname,s.gender,s.score,c.name cname FROM students s INNER JOIN class c ON s.class_id = c.id//如果students的数据匹配不到,也会被查出来,对应的数据会为null
SELECT s.id,s.name sname,s.gender,s.score,c.name cname FROM students s LEFT JOIN class c ON s.class_id = c.id//如果class的数据匹配不到,也会被查出来,对应的数据会为null
SELECT s.id,s.name sname,s.gender,s.score,c.name cname FROM students s RIGHT JOIN class c ON s.class_id = c.id//如果class或students的数据匹配不到,也都会被查出来,对应的数据会为null
SELECT s.id,s.name sname,s.gender,s.score,c.name cname FROM students s FULL JOIN class c ON s.class_id = c.id
外键约束
InnoDB支持事务,MyISAM不支持事务。这是mysql将默认存储引擎从MyISAM
变为InnoDB的重要原因之一。
InnoDB支持外键,MyISAM不支持。对一个包含外键的InnoDB表转为MyISAM会失败。
cascade
在父表上update/delete时,同步update/delete子表
的记录
set null
在父表上update/delete时,将子表上匹配记录的列设为null(要注意子表的外键列不能为null)
no action
如果子表中有匹配的记录,则不允许父表对应候选键进行update/delete操作
restrict
同no action,都是立即检查外键约束。
nodejs 操作数据库
npm i mysql2
const express = require('express')
const msql2 = require('mysql2')
const app = express()app.get('/',(req,res)=>{const config = getDBconfig()const promisePool = mysql2.createPool(config).promise()let name = "test"let gender = "1"let id = "1"//let result = await promisePool.query(`select * from students`)//let result = await promisePool.query(`select * from students where name=? and gender=?`,[name,gender] )//let result = await promisePool.query(`insert into student (name,gender) values (?,?)`,[name,gender])//let result = await promisePool.query(`update students set name=? where id=?`,[name,id])let result = await promisePool.query(`delete from students where id=?`,[id])res.send({ok:1,data:result[0]})
})app.listen(3000)function getDBconfig(){return {host:'127.0.0.1',port:3306,user:'root',password:'',database:'test',connectionLimit:1 //一个连接池}
}
Socket编程
webSocket
应用场景:弹幕,媒体聊天,协同编辑,基于位置的应用,体育实况更新,股票基金报价实时更新。
webSocket并不是全新的协议,而是利用了http协议来建立连接,必须由浏览器发起,因为请求协议是一个标准的http请求,格式如下:
GET ws://loacalhost:3000/ws/chat HTTP/1.1
Host:localhost
Upgrade:websocket
Connection:Upgrade
origin: http://localhost:3000
Sec-webSocket-Key:client-random-string
Sec- WebSocket-Version: 13
该请求和普通的HTTP请求有几点不同:
1.get请求的地址不是类似/path/,而是以ws://开头的地址;
2.请求头Upgrade:websocket和connection:Upgrade 表示这个连接将要被转换为WebSocket连接。
3.Sec-webSocket-Key是用于标识这个连接,并非用与加密数据。
4.Sec- WebSocket-Version指定了WebSocket的协议版本。
如果服务器接收该请求,就会有如下反应:
HTTP/1.1 101 Switching Protocols
Upgrade:webSocket
Connection:Upgrade
Sec-WebSocket-Accept: server-random-string
该响应代码101表示本次连接的http协议即将被更高,更改后就是Upgrade:websocket指定的WebSocket协议。
版本号和子协议规定了双方能理解的数据格式,以及是否支持压缩等。如果仅仅使用WebSocket
的api,就不用关心这些。
现在一个webSocket连接就能建立成功,浏览器和服务器可以随时主动发送消息给对方。消息有两种,一种是文本,一种是二进制,通常我们发送json格式文本,在浏览器处理会很方便。
为什么WebSocket链接可以事项全双工通信,而http不可以?实际上http是建立在tcp协议之上的,tcp协议本身就实现了全双工通信,但是http协议的请求-响应机制限制了全双工通信,websocket链接建立以后,其实就是简单规定了一下,接下来,咱们不用http协议,直接互相发消息。
安全的websocket连接机制和https类似。首先浏览器用wss://xxx创建websocket连接时,回先通过https创建安全的连接。然后,该https链接升级为websocket连接,底层走的仍然是安全的SSL/TSL协议。
浏览器支持
很显然,要支持websocket通信,浏览器要支持这个协议,才能发出ws://xxx的请求。目前支持的主流浏览器如下:chrome,firefox,ie>1=0,Safari>=6,Android >=4.4 ,ios>=8。
服务器支持
由于websocket是一个协议,服务器具体怎么实现,取决于所用的编程语言和框架本身。node.js本身支持的协议包括tcp和http,要支持websocket协议,需要对node.js提供法的httpServer做额外的开发。已经有若干基于node.js的稳定可靠的webSocket实现,我们直接用npm安装即可。
ws模块
服务器
npm init
npm i ws express
//webSocket响应
const WebSocket = require('ws')
const { WebSocketServer } = WebSocket
const wss = new WebSocketServer({ port: 8080 })
wss.on('connection', (ws) => {ws.on('message', (data) => {console.log(data);wss.clients.forEach((client) => { //给所有用户转发//检查所有用户是否处于连接状态,不用发送给自己if (client!==ws && client.readyState == WebSocket.OPEN) { client.send(data,{binary:false})//数据为非二进制数据,否则会成为blob类型}})})ws.send("欢迎来到聊天室")
})
客户端
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head>
<body>chatRoom
<script>let ws = new WebSocket("ws://localhost:8080")ws.onopen = ()=>{console.log('连接成功');}ws.onmessage = (msgObj)=>{console.log(msgObj);}ws.onerror = ()=>{console.log('err');}
</script>
</body>
</html>
获取客户端请求参数,做登录鉴权
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head>
<body>chatRoom
<script>const WebSocketType = {Error:0, //错误走这里GroupList:1,GroupChat:2,SingleChat:3}let ws = new WebSocket(`ws://localhost:8080?token=${localStorage.getItem("token")}`)ws.onopen = ()=>{console.log('连接成功');}ws.onmessage = (msgObj)=>{console.log(JSON.parse(msgObj.data));const {data} = JSON.parse(msgObj)switch(data.type){case WebSocket.Error:localStorage.removeItem("token")break;case WebSocket.GroupChat:console.log(data)break;}}ws.onerror = ()=>{console.log('err');}
</script>
</body>
</html>
//webSocket响应
const WebSocket = require('ws')
const JWT = require('./jwt.js')
const { WebSocketServer } = WebSocket
const wss = new WebSocketServer({ port: 8080 })
wss.on('connection', (ws,req) => {const myUrl = new URL(req.url,"http://127.0.0.1:3000")let mytoken = myUrl.searchParams.get('token')const payload = JWT.verify(myToken)if(myToken){ws.send(createMessage(WebSocket.GroupChat,null,"欢迎来到聊天室"))}else{ws.send(createMessage(WebSocketType.Error,null,'token过期'))}
})const WebSocketType = {Error:0, //错误走这里GroupList:1,GroupChat:2,SingleChat:3}
function createMessage(type,user,data){return JSON.stringify({type,user,data})
}
注意在登录时,还是使用httpx协议获取token,在聊天的页面使用ws协议
给前端返回数据
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head>
<body>chatRoom
<script>const WebSocketType = {Error:0, //错误走这里GroupList:1,GroupChat:2,SingleChat:3}let ws = new WebSocket(`ws://localhost:8080?token=${localStorage.getItem("token")}`)ws.onopen = ()=>{console.log('连接成功');}ws.onmessage = (msgObj)=>{console.log(JSON.parse(msgObj.data));const {data} = JSON.parse(msgObj)switch(data.type){case WebSocket.Error:localStorage.removeItem("token")location.href='/login'break;case WebSocket.GroupList:console.log(JSON.parse(data))break;case WebSocket.GroupChat:console.log(data)break;}}ws.onerror = ()=>{console.log('err');}
</script>
</body>
</html>
ws.send({type:1}) //获取所有用户ws.send({type:2,data:"你好") //群发给所有人ws.send({type:3,data:"你好",'tom') //私聊
//webSocket响应
const WebSocket = require('ws')
const JWT = require('./jwt.js')
const { WebSocketServer } = WebSocket
const wss = new WebSocketServer({ port: 8080 })
wss.on('connection', (ws,req) => {const myUrl = new URL(req.url,"http://127.0.0.1:3000")let mytoken = myUrl.searchParams.get('token')const payload = JWT.verify(myToken)if(myToken){ws.send(createMessage(WebSocket.GroupChat,null,"欢迎来到聊天室"))ws.user = payload //user中包含username以及password}else{ws.send(createMessage(WebSocketType.Error,null,'token过期'))}ws.on('message', (data) => {console.log(data);const msgObj = JSON.parse(data)switch(msgObj.type){//获取所有成员case WebSocketType.GroupList:let userList = Array.from(wss.clients).map(el=>el.user)sendAll()bresk;//群发case WebSocketType.GroupChat:wss.clients.forEach((client) => { //给所有用户转发//检查所有用户是否处于连接状态,不用发送给自己if (client!==ws && client.readyState == WebSocket.OPEN) { client.send(createMessage(WebSocketType.GroupChat,ws.user,msgObj.data),{binary:false})//数据为非二进制数据,否则会成为blob类型}})bresk;case WebSocketType.SingleChat:wss.clients.forEach((client) => {if (client.user.username == msgObj.to && client.readyState == WebSocket.OPEN) { client.send(createMessage(WebSocketType.SingleChat,ws.user,msgObj.data),{binary:false})//数据为非二进制数据,否则会成为blob类型}})bresk;}})ws.on('close',()=>{//断开连接时候的回调sendAll()})
})const WebSocketType = {Error:0, //错误走这里GroupList:1,GroupChat:2,SingleChat:3}
function createMessage(type,user,data){return JSON.stringify({type,user,data})
}function sendAll(){wss.clients.forEach((client) => { //给所有用户转发//检查所有用户是否处于连接状态,不用发送给自己if (client.readyState == WebSocket.OPEN) { ws.send(createMessge(WebSocket.GroupList,null,JSON.stringify(userList)))}})
}
参考文档:ws - npm
socket.io模块
有express/koa集成的用法可以自己选择,能够自定义的创建想要的事件,参考文档:socket.io - npm
npm i socket.io
前端需要socket.io.min.js,参考:https://github.com/socketio/socket.io/blob/main/client-dist/socket.io.min.js
在复制后放在本地public文件夹下
固定的事件:connection,disconnect,其他可自定义,传递数据会自动转为JSON或解析JSON。
const express = require('express')
const JWT = require("./util/jwt")
const app = express()app.use(express.static('public'))
const server = require('http').createServer(app)
const io = require('socket.io')(server)
io.on('connection', (socket, req) => {const payload = JWT.verify(socket.handshake.query.token)console.log(payload);if (payload) {console.log(222);socket.user = payload//发送欢迎socket.emit("connect", { user: socket.user, data: '欢迎来到聊天室' })//获取列表socket.on('groupList', () => {//用户数据存储在io.sockets.sockets,以map对象存储const arr = Array.from(io.sockets.sockets).map(item => item[1].user)//发送所有人io.sockets可以直接发所有人io.sockets.emit('groupList', { data: arr.filter(el=>el) })//首次或刷新会出现报错的情况,是因为用户会有个断开再连接的过程,所以或出现undfined的情况,过滤后就不会了//断开连接时的再次发送所有人listsocket.on('disconnect', () => {socket.emit('groupList', { data: arr })})//群聊socket.on("groupChat", (msg) => {//所有人都发// io.sockets.emit('groupChat',{user:socket.user, data:msg})// 除了自己不发,其他人发socket.broadcast.emit('groupChat', { user: socket.user, data: msg })})socket.on("single", (msg) => {Array.from(io.sockets.sockets).forEach(item => {if (item[1].user.username == msg.to) {item[1].emit("single", { user: socket.user, msg: msg.data })}})})})} else {socket.emit("error", { errorMessage: "token过期" })}})
server.listen(3000)
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head>
<body>chatRoom<script src="./js/socketio.js"></script>
<script>//const socket = io() //默认连接localhos:3000const socket = io(`ws://localhost:3000?token=${localStorage.getItem("token")}`)socket.on("connect",(msg)=>{console.log(msg);})socket.on("error",(msg)=>{localStorage.removeItem("token")location.href = '/login'})socket.on("groupList",(msg)=>{console.log(msg);})socket.on("groupChat",(msg)=>{console.log(msg);})socket.on("single",(msg)=>{console.log(msg);})socket.emit('groupChat',{data:"群聊"})socket.emit('single',{data:"私聊",to:"**"})
</script>
</body>
</html>
mocha
单元测试是用来对弈歌迷快,一个函数或者一个类来进行正确性检验的测试工作,mocha是js的一种单元测试框架,既可以咋浏览器环境下运行,也可以在node.js环境下运行。使用mocha只需要专注于编写单元测试本身,然偶让mocha去自动运行所有的测试,并给出测试结果。
特点:既可以测试简单的js函数,又可以测试异步代码,因为异步是js的特性之一;可以自动运行所有测试,也可以只运行特定的测试;可以支持before,after,beforeEach和afterEach来编写初始化代码。
编写测试
npm i initnpm i mocha
"scripts": {"test": "mocha"},
npm test
//sum.js 测试文件
function sum(...rest){return rest.reduce((pre,next)=>{return pre + next},0)
}module.exports = sum
配合mocha测试,创建test文件夹,将要执行的test1.js文件放入文件夹下。
const sum = require("../sum")
const { describe } = require("mocha");
const assert = require("assert")//node内置模块断言函数//describe 一组测试,可嵌套
describe("大的组1测试",()=>{describe('小的组1测试',()=>{//it 一个测试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)})})describe('小的组2测试',()=>{})
})
describe("大的组2测试",()=>{})
chai断言库
mocha允许你使用任意的断言库,比如:
should.js BDD风格贯穿始终
expect.js expect()样式断言
chai expect(),assert()和should风格的断言
better-assert C风格的自文档化的assert()
unexpected 可扩展到饿BDD断言工具
npm i chai
// assert风格
const chai = require('chai')
const { describe } = require("mocha");
const assert = chai.assert
const sum = require("../sum")//describe 一组测试,可嵌套
describe("大的组1测试",()=>{describe('小的组1测试',()=>{//it 一个测试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)})})describe('小的组2测试',()=>{})
})
describe("大的组2测试",()=>{})
//should风格
const chai = require('chai')
const { describe } = require("mocha");
const sum = require("../sum")
chai.should()//describe 一组测试,可嵌套
describe("大的组1测试",()=>{describe('小的组1测试',()=>{//it 一个测试it('sum()结果返回为0',()=>{sum().should.exist.and.equal(0)//sum.should.be.a('string')//sum.should.not.equal(6)//sum.should.have.length(5)})})describe('小的组2测试',()=>{})
})
describe("大的组2测试",()=>{})
//expect风格
const chai = require('chai')
const { describe } = require("mocha");
const sum = require("../sum")
const expect = chai.expect//describe 一组测试,可嵌套
describe("大的组1测试",()=>{describe('小的组1测试',()=>{//it 一个测试it('sum()结果返回为0',()=>{expect(sum()).to.equal(0)//expect(sum()).to.be.at.most(5)//expect(sum()).to.be.at.least(3)//expect(sum()).to.be.at.within(1,4)//expect(sum()).to.exist//expect(sum()).to.be.a('string')//expect(sum()).to.not.equal('你好')//expect(sum()).to.have.length(5)})})describe('小的组2测试',()=>{})
})
describe("大的组2测试",()=>{})
异步测试
const fs = require("fs")
const fsp = fs.promises
const { describe } = require("mocha");
const assert = require("assert")//describe 一组测试,可嵌套
describe('异步测试1', () => {//it 一个测试it('异步读取文件', (done) => {fs.readFile('./1.txt','utf8',(err,data)=>{if(err){done(err)}else{assert.strictEqual(data,'12344')done()}})})
})
describe('异步测试2', () => {//it 一个测试it('异步读取文件', async () => {let data = await fsp.readFile('./1.txt','utf8')assert.strictEqual(data,'123446')})
})
http测试
实现能够在测试时启动服务器
sudo npm i supertest
//两种
const { describe } = require("mocha");
const assert = require("assert")
const axios = require('axios')
describe('测试接口',()=>{it('返回接口数据',async ()=>{let res = await axios.get('http://localhost:3000/')assert.strictEqual(res.data,"你好")})
})----------------------------------------
//能够自己启动服务器并且关闭
const { describe } = require("mocha");
const supertest = require('supertest')
const app = require("../app")
describe('测试接口',()=>{let server = app.listen(3000)it('返回接口数据',async ()=>{await supertest(server).get("/").expect("Content-Type",/text\/plain/).expect(200,'你好')})after(()=>{ //mocha钩子函数server.close()//结束执行后关闭服务器})
})
//对应上述两种
const koa = require('koa')
const app = new koa()app.use((ctx)=>{ctx.body="你好"
})app.listen(3000)---------------------------------------------------const koa = require('koa')
const app = new koa()app.use((ctx)=>{ctx.body="你好"
})// app.listen(3000)module.exports = app
钩子函数
const { describe } = require("mocha");
const supertest = require('supertest')
const app = require("../app")
describe('测试接口', () => {let server;it('返回接口数据', async () => {await supertest(server).get("/").expect("Content-Type", /text\/plain/).expect(200, '你好')})before(() => {//在用所有例执行之前调用server = app.listen(3000) })after(() => { //所有用例执行后执行,关闭服务器server.close()})beforeEach(()=>{//在每个用例执行之前})afterEach(()=>{//在每个用例执行后})
})
目前基础就是这些,有后续的再补充。
相关文章:
Nodejs 相关知识
Nodejs是一个js运行环境,可以让js开发后端程序,实现几乎其他后端语言实现的所有功能,能够让js与其他后端语言平起平坐。 nodejs是基于v8引擎,v8是Google发布的开源js引擎,本身就是用于chrome浏览器的js解释部分&#…...
【vue+elementUI】输入框样式、选择器样式、树形选择器和下拉框样式修改
输入框样式、选择器样式和下拉框样式修改 1、输入框和选择器的样式修改:2、下拉弹框样式A. 选择器的下拉弹框样式修改B. 时间选择器的下拉弹框样式修改C. vue-treeselect树形下拉框样式 1、输入框和选择器的样式修改: 写在style中不能加scoped࿰…...
JavaScript - canvas - 放大镜
效果 示例 项目结构: 源码: <!DOCTYPE html> <html><head><meta charset"utf-8" /><title>放大镜</title><style type"text/css">div {width: 200px;height: 200px;display: inline-bl…...
PY32F003F18之输入捕获
输入捕获是定时器的功能之一,配合外部引脚,捕获脉宽时间或采集周期。 CPU中的定时器最基本的功能就是计数功能,其次是输入捕获(IC),再次就是比较输出(OC),还有就是使用引脚对外部时钟进行计数,触发信号捕捉…...
科目三基础四项(一)
第一天,基础操作,仪表,方向,挡位 按照模块来 1、方向盘两手在两侧 编辑 转向时的角度,只用:向左540,向右180 向左打和向右打的角度要抵消,回正 掉头向左打满再回 注意…...
C语言入门Day_24 函数与指针
目录 前言: 1.指针和数组 2.函数和指针 3.易错点 4.思维导图 前言: 我们知道数组是用来存储多个数据的,以及我们可以用指针来指向一个变量。那么我们可以用指针来指向一个数组中的数据么? 指针除了可以像指向一个变量一样指…...
9月21日,每日信息差
今天是2023年9月21日,以下是为您准备的14条信息差 第一、谷歌高管已经广泛讨论了在2027年之前将博通作为人工智能芯片供应商的可能性 第二、清华系团队宣布研发出千亿参数“制药版ChatGPT”,覆盖药物立项、临床前研究、临床试验的各阶段,作…...
【FAQ】安防监控系统/视频云存储/监控平台EasyCVR服务器解释器出现变更该如何修改?
安防视频监控/视频集中存储/云存储/磁盘阵列EasyCVR平台可拓展性强、视频能力灵活、部署轻快,可支持的主流标准协议有国标GB28181、RTSP/Onvif、RTMP等,以及支持厂家私有协议与SDK接入,包括海康Ehome、海大宇等设备的SDK等。平台既具备传统安…...
Python手写人脸识别
Python手写人脸识别 引言 人脸识别是一种通过计算机视觉和模式识别技术来识别和验证人脸的技术。Python是一种广泛使用的编程语言,它提供了许多强大的库和工具来实现人脸识别。 在Python中,可以使用多种方法来实现人脸识别,包括基于特征提取的方法、基于深度学习的方法等…...
我的Qt作品(19)使用Qt写一个轻量级的视觉框架---第2章,仿海康VM实现思维导图拖拽方式的算法流程图
上次写的第1章介绍了主界面的设计。 https://blog.csdn.net/libaineu2004/article/details/130277151 本次是第2章,主要介绍流程图的运行。 目前市面上视觉框架很多,主要有列表图方式和流程图方式。海康VM的流程图方式比较受用户的喜爱和欢迎…...
仿写Timi记账
项目仿照Timi记账,本 APP 仅用作学习,如有侵权联系删除,项目地址:Timi记账 TIMI记账项目 简单功能对于tableview向上延伸部分采用了insertSubview形式:添加特殊字体添加.ttf文件获取plist文件数据 计算器功能说明简单逻…...
Java语言实现 比较两个Date日期的先后
在 Java 中,可以使用 Date 类的 compareTo() 方法或 before()、after() 方法来比较两个 Date 类型的日期的先后顺序。 使用 compareTo() 方法: Date date1 ...; // 第一个日期 Date date2 ...; // 第二个日期int result date1.compareTo(date2); if (…...
el-table 指定层级展开
先来看看页面默认全部展开时页面的显示效果:所有节点被展开,一眼望去杂乱无章! 那么如何实现只展开指定的节点呢?最终效果如下:一眼看去很舒爽。 干货上代码: <el-table border v-if"refreshTabl…...
3288S Android11 适配红外遥控功能(超详细)
目录 一、rk3288平台红外遥控介绍二、原理图分析三、配置设备树并使能红外遥控功能四、打开红外打印功能,查看红外遥控的用户码和键值五、将查看到的红外遥控用户码和键值添加到设备树和.kl文件六、Android红外遥控.kl文件映射知识和使用添加新的.kl文件七、补充&am…...
Linux高性能服务器编程 学习笔记 第三章 TCP协议详解
与IP协议相比,TCP协议更靠近应用层,因此在应用程序中有更强的可操作性。一些重要的socket选项都和TCP协议相关。 本章从以下方面讨论TCP协议: 1.TCP头部信息。TCP头部信息出现在每个TCP报文段中,用于指定通信的源端端口号、目的端…...
【云原生】Kubernetes学习笔记
部署 在部署前强调几点 不要使用IPv6, 很多组件都不支持IPv6不要使用最新版本, 最新版本非常不稳定, 甚至可能存在无法运行的bug不要版本更新, 安装后就将版本固定下来, 新的版本可能会引入新功能, 或移除旧功能, 导致Kubernetes无法运行 Kubeadm介绍 K8s是由多个模块构成的…...
[Machine Learning][Part 2]监督学习的实现
目录 线性模型实现: cost function :代价函数或者损失函数——衡量模型优劣的一个指标 理论: 代码实现: 梯度下降——自动寻找最小的cost function 代价函数 梯度的概念: 梯度下降公式: 实现一个简单的监督学习…...
【计算机毕业设计】基于SpringBoot+Vue大学生心理健康管理系统的开发与实现
博主主页:一季春秋博主简介:专注Java技术领域和毕业设计项目实战、Java、微信小程序、安卓等技术开发,远程调试部署、代码讲解、文档指导、ppt制作等技术指导。主要内容:毕业设计(Java项目、小程序等)、简历模板、学习资料、面试题…...
下载水果FLStudio21.2软件安装更新教程
编曲是一种对音乐创作过程中涉及的元素和步骤进行组织和安排的艺术形式。对于想要学习编曲的人来说,以下是一些有用的建议: 1. 学习基础知识 在开始学习编曲之前,你需要掌握一些基础知识,例如音乐理论、乐器演奏和数字音乐制作技…...
人工智能机器学习-飞桨神经网络与深度学习
飞桨神经网络与深度学习-机器学习 目录 飞桨神经网络与深度学习-机器学习 1.机器学习概述 2.机器学习实践五要素 2.1.数据 2.2.模型 2.3.学习准则 2.4.优化算法 2.5.评估标准 3.实现简单的线性回归模型 3.1.数据集构建 3.2.模型构建 3.3.损失函数 3.4.模型优化 3…...
linux部署页面内容
/bin:该目录包含了常用的二进制可执行文件,如ls、cp、mv、rm等等。 /boot:该目录包含了启动Linux系统所需的文件,如内核文件和引导加载程序。 /dev:该目录包含了所有设备文件,如硬盘、光驱、鼠标、键盘等等…...
若依框架集成WebSocket带用户信息认证
一、WebSocket 基础知识 我们平时前后台请求用的最多的就是 HTTP/1.1协议,它有一个缺陷, 通信只能由客户端发起,如果想要不断获取服务器信息就要不断轮询发出请求,那么如果我们需要服务器状态变化的时候能够主动通知客户端就需要用…...
0基础学习VR全景平台篇 第101篇:企业版功能-子账号分配管理
大家好,欢迎观看蛙色VR官方系列——后台使用课程! 本期为大家带来蛙色VR平台,企业版教程-子账号分配管理功能! 功能位置示意 一、本功能将用在哪里? 子账号分配管理功能,主要用于企业版用户为自己服务的终…...
adb 命令集
adb 查看app启动时间 1.清除时间 adb shell am start -S com.android.systemui/.SystemUIService2.启动应用并记录 adb shell am start -W <PACKAGE_NAME>/.<ACTIVITY_NAME>TotalTime: 491 adb 查看分辨率、dpi 分辨率 adb shell wm sizePhysical size: 1080…...
分享78个Python源代码总有一个是你想要的
分享78个Python源代码总有一个是你想要的 源码下载链接:https://pan.baidu.com/s/1ZhXDsVuYsZpOUQIUjHU2ww?pwd8888 提取码:8888 下面是文件的名字。 12个python项目源码 Apache Superset数据探查与可视化平台v2.0.1 API Star工具箱v0.7.2 Archery…...
springcloud3 指定nacos的服务名称和配置文件的group,名称空间
一 指定读取微服务的配置文件 1.1 工程结构 1.2 nacos的配置 1.配置文件 2.内容 1.3 微服务的配置文件 1.bootstrap.yml内容 2.application.yml文件内容 1.4 验证访问 控制台: 1.5 nacos服务空间名称和groupid配置 1.配置文件配置 2.nacos的查看...
go-redis简单使用
目录 一:官方文档和安装方式二:简单案例使用 一:官方文档和安装方式 官方中文文档:https://redis.uptrace.dev/zh/guide/go-redis.html安装:go get github.com/redis/go-redis/v9 二:简单案例使用 简单的…...
33. 搜索旋转排序数组-二重二分查找
33. 搜索旋转排序数组-二分查找 整数数组 nums 按升序排列,数组中的值 互不相同 。 在传递给函数之前,nums 在预先未知的某个下标 k(0 < k < nums.length)上进行了 旋转,使数组变为 [nums[k], nums[k1], …, n…...
mysql自动删除过期的binlog
一、binlog_expire_logs_seconds 配置项 mysql 8.0使用配置项 binlog_expire_logs_seconds 设置binlog过期时间,单位为秒。 mysql旧版本使用配置项 expire_logs_days 设置binlog过期时间,单位为天,不方便测试。 在 8.0 使用 expire_logs_d…...
Java面向对象(1)
static静态变量 public class Student {static String name;private double score;public Student(){};public Student(double score) {this.score score;}public double getScore() {return score;}public void setScore(double score) {this.score score;} }public class t…...
学校网站系统/推广赚钱的软件排行
Ubuntu系统安装opencv3.2看这里《Linux\Ubuntu 16.04配置Opencv》 写文章用的opencv2.4.13,opencv3.2与2.4.13编译和环境变量配置一样!!! 换系统了,OpenCV3.2也出来了,改变貌似还挺大的,特别是…...
将网站制作成app/360免费建站
http://blog.chinaaet.com/justlxy/p/5100053251...
wordpress支持中文用户名/今日头条指数查询
"The secret of change is to focus all of your energy, not on fighting the old but on building the new.—— Dan Millman"请问视图是什么?视图相关语句有哪些?视图在什么场景下使用?夺命三连更多精彩文章请关注公众号『Pytho…...
网页传奇排名/seo在线培训机构
从现在起,珍惜时间,好好拼搏。 从现在起,好好对待身边的每一个人,建立好的亲情,友情或是爱情。 从现在起,喂马劈柴,周游世界,学会欣赏,学会做人。 从现在起,学…...
沈阳工伤保险做实在哪个网站/策划
鄢志杰,阿里云资深算法专家,人机交互首席科学家。研究领域主要包括语音识别、语音合成、说话人识别验证、OCR/手写识别、机器学习算法等。长期担任语音领域顶级学术会议及期刊的专家评审,并拥有多项美国及PCT专利。 以下为内容全文ÿ…...
网站建设信息推荐/广告优化师适合女生吗
tail 默认显示后十行tail -n 2 /etc/passwd 显示最后两行tail -c 3 /etc/passwd 显示最后三个符tail -f /var/log/messages 监视一个不断增长,不断变化的文件.转载于:https://blog.51cto.com/sngyqd/394232...