# Node 自学笔记
# 一、Node.js 基础
# 1、什么是 Node.js
Node.js 是一个 javascript 运行环境,他让 javascript 可以开发后端程序,实现几乎其他后端语言实现的所有功能,可以与 php、java、python、.net、ruby 等后端语言平起平坐。
Node.js 基于 v8 引擎,v8 是 google 发布的开源 javascript 引擎。
# 2、Node.js 的特性
- Node.js 语法完全是 js 语法,只要你懂 js 基础就可以学会 Node.js 后端开发
- Node.js 超强的高并发能力,实现高性能服务器
- 开发周期短、开发成本低、学习成本低
# 3、Node.js 安装
官网下载,管理员权限安装,傻瓜式安装
node -v //查看版本 有就安装好了
# 4、nodemon 安装
自动重启 node 服务
npm i nodemon -g
运行
nodemon file-name
# 5、Hello world
新建一个 js 文件
//hello.js | |
console.log("hello world"); |
进入终端,cd 到 hello.js 所在的文件夹,运行命令
node hello
回车执行,打印结果
hello world
# 6、模块 包 commonJS
模块化开发、commonJS 规范
a.js
function test(){ | |
console.log('aaa'); | |
} | |
module.exports = test; |
b.js
function test(){ | |
console.log('bbb'); | |
} | |
module.exports = test; |
c.js
function test(){ | |
console.log('ccc'); | |
} | |
module.exports = test; |
index.js
const moduleA = require('./a'); | |
const moduleB = require('./b'); | |
const moduleC = require('./c'); | |
// console.log(a); //[Function test] | |
moduleA(); //aaa | |
moduleB(); //bbb | |
moduleC(); //ccc |
多方法导出
a.js
function test(){} | |
function upper(){} | |
module.exports = { | |
test, | |
upper | |
} | |
// 或者 | |
exports.test = test; | |
exports.upper = upper; | |
// 或者 | |
export function test(){} | |
export function upper(){} |
# 7、Npm&Yarn
Npm
打开终端
npm init //初始化
npm install package-name -g (uninstall,update)
npm install package-name --save-dev (uninstall,update) //早期版本必须加 高版本可以省略
npm list -g //-g 全局依赖 安装列表
npm info package-name //包详细信息
npm info package-name version //获取最新版本
npm install md5@ //指定依赖版本安装
npm outdated //过时版本
"dependencies":{"md5":"^2.1.0"} ^ 表示 如果 直接 npm install 将会 安装 md5 2.*.* 最新版本
"dependencies":{"md5":"~2.1.0"} ~ 表示 如果 直接 npm install 将会 安装 md5 2.1.* 最新版本
"dependencies":{"md5":"*"} * 表示 如果 直接 npm install 将会 安装 md5 最新版本
package-lock.json 锁定依赖版本
package.json 记录开发和产品模式所依赖的包
Nrm
Nrm (npm registry manager) 是 npm 的镜像源管理工具,有时候国外资源太慢,使用这个就可以快速地在 npm 源间切换
手动切换方法 npm config set registry https://registry.npm.taobao.org
安装 nrm
在命令行执行命令
npm install -g nrm //全局安装nrm
使用 nrm
执行命令 nrm ls 查看可选的源。其中,带 * 的是当前使用的源,上面的输出表明当前源是官方源
切换 nrm
如果要切换到 taobao 源,执行命令 nrm use taobao
测试速度
你还可以通过 nrm test 测试相应源的相应速度
nrm test
扩展:中国 npm 镜像
这是一个完整 npmjs.org 镜像,你可以用此代替官方版本 (只读),同步频率目前为 10 分钟一次以保证尽量与官方服务同步
npm install -g cnpm --registry=https://registry.npmmirror.com
Yarn
npm install -g yarn
对比 npm
- 速度超快 yarn 缓存了每个下载过的包,所以在此使用时无需重复下载。同时利用并行下载以最大化资源利用率,因此安装速度超快
- 超级安全 在执行代码之前,yarn 会通过算法校验每个安装包的完整性
开始新项目
yarn init
添加依赖包
yarn add package-name
yarn add package-name@version
yarn add package-name--dev
升级依赖包
yarn upgrade package-name@version
移除依赖包
yarn remove package-name
安装项目的全部依赖
yarn install //npm 同理
、
# 8、ES 模块化写法
npm init //生成package.json文件,在main后面添加 "type":"module",
单导出
a.js 导出
const moduleA = {}; | |
export default moduleA; |
index.js 引入
import moduleA from './a.js'; | |
console.log(moduleA); //{} |
多导出
b.js
const moduleB = { | |
getName(){ | |
return "moduleB"; | |
} | |
} | |
export { | |
moduleB | |
} |
index.js
import {moduleB} from './b.js'; | |
console.log(moduleB.getName()); //moduleB |
# 9、内置模块
# 1.http 模块
要使用 http 服务器和客户端,则必须 require ('http')
const http = require('http'); | |
// 创建服务器从本地接收数据 | |
http.createServer((req,res)=>{ //request 浏览器传来的东西 result 服务器传过去的东西 | |
//res.writeHead(200,{"content-Type":"text/plain;charset=utf-8"}); | |
res.writeHead(200,{"content-Type":"text/html;charset=utf-8"}); // 默认 | |
// 接收浏览器传的参数 返回渲染的内容 | |
res.write("hello world"); | |
res.write("hello node"); | |
res.write("hello"); | |
res.write(`<html><b>hello world</b></html>`); | |
res.end("[1,2,3]"); | |
//end 之后不再往浏览器输出东西 | |
}).listen(3000,()=>{ | |
console.log('server start...'); | |
}); |
模拟页面路由
const http = require('http'); | |
// 创建服务器 | |
http.createServer((req,res)=>{ //request 浏览器传来的东西 result 服务器传过去的东西 | |
// 接收浏览器传的参数 返回渲染的内容 | |
res.writeHead(renderStatus(req.url),{"content-Type":"text/html;charset=utf-8"}) | |
if(req.url === '/favicon.ico'){ | |
//todo 读取本地图标 | |
return; | |
} | |
console.log(req.url); | |
res.write(renderHTML(req.url)) | |
res.end(); | |
}).listen(3000,()=>{ | |
console.log('server start...'); | |
}); | |
function renderStatus(url){ | |
let arr = ['/home','/list']; | |
return arr.includes(url) ? 200 : 404; | |
} | |
function renderHTML(url){ | |
switch(url){ | |
case "/home": | |
return `<html> | |
<b>hello world</b> | |
<div>大家好</div> | |
</html>`; | |
case '/list': | |
return `<html> | |
<b>list页面</b> | |
</html>`; | |
case '/api/list': | |
return ` | |
["list1","list2","list3"]`; | |
case '/api/home': | |
return `{name:"home"}`; | |
default: | |
return `<html><div>404 NOT FOUND</div></html>`; | |
} | |
} |
模块化写法
const http = require('http'); | |
const {renderHTML} = require('./module/renderHTML'); | |
const {renderStatus} = require('./module/renderStatus'); | |
// 创建服务器 | |
let server = http.createServer(); | |
server.on("request",(req,res)=>{ //request 浏览器传来的东西 result 服务器传过去的东西 | |
// 接收浏览器传的参数 返回渲染的内容 | |
res.writeHead(renderStatus(req.url),{"content-Type":"text/html;charset=utf-8"}) | |
if(req.url === '/favicon.ico'){ | |
//todo 读取本地图标 | |
return; | |
} | |
console.log(req.url); | |
res.write(renderHTML(req.url)) | |
res.end(); | |
}) | |
server.listen(3000,()=>{ | |
console.log('server start...'); | |
}); |
# 2.url 模块
# 2.1 parse
url.parse (req.url); // 解析浏览器访问的地址
Url { | |
protocol: null, | |
slashes: null, | |
auth: null, | |
host: null, | |
port: null, | |
hostname: null, | |
hash: null, | |
search: '?a=1', | |
query: 'a=1', | |
pathname: '/api/list', | |
path: '/api/list?a=1', | |
href: '/api/list?a=1' | |
} |
const http = require('http'); | |
const url = require('url'); | |
const {renderHTML} = require('./module/renderHTML'); | |
const {renderStatus} = require('./module/renderStatus'); | |
// 创建服务器 | |
let server = http.createServer(); | |
server.on("request",(req,res)=>{ //request 浏览器传来的东西 result 服务器传过去的东西 | |
// 接收浏览器传的参数 返回渲染的内容 | |
let pathName = url.parse(req.url).pathname; //req.url 为 /api/list?a=1 | |
// console.log(url.parse(req.url).pathname); // /api/list | |
res.writeHead(renderStatus(pathName),{"content-Type":"text/html;charset=utf-8"}) | |
if(req.url === '/favicon.ico'){ | |
//todo 读取本地图标 | |
return; | |
} | |
res.write(renderHTML(pathName)) | |
res.end(); | |
}) | |
server.listen(3000,()=>{ | |
console.log('server start...'); | |
}); |
url.parse (req.url,true); 自动将 query 转换为 json 格式
Url { | |
protocol: null, | |
slashes: null, | |
auth: null, | |
host: null, | |
port: null, | |
hostname: null, | |
hash: null, | |
search: '?a=1', | |
query: [Object: null prototype] { a: '1' }, /////////********* | |
pathname: '/api/list', | |
path: '/api/list?a=1', | |
href: '/api/list?a=1' | |
} |
# 2.2 format
将对象结构转换为地址与 parse 相反
const url = require('url'); | |
const urlObj = { | |
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 parseObj = url.format(urlObj); | |
console.log(parseObj); //https://www.baidu.com:443/ad/index.html?id=8&name=mouse#tag=110 |
# 2.3 resolve
进行 url 的拼接
const url = require('url'); | |
let a = url.resolve('/one/two/three','four'); | |
let b = url.resolve('http://example.com/','/one'); | |
let c = url.resolve('http://example.com/one','two'); | |
console.log(a + ',' + b + ',' + c); | |
// 打印结果:/one/two/four,http://example.com/one,http://example.com/two |
# 2.4 新版用法
const http = require('http'); | |
const {URL} = require('url'); | |
const {renderHTML} = require('./module/renderHTML'); | |
const {renderStatus} = require('./module/renderStatus'); | |
// 创建服务器 | |
let server = http.createServer(); | |
server.on("request",(req,res)=>{ //request 浏览器传来的东西 result 服务器传过去的东西 | |
// 接收浏览器传的参数 返回渲染的内容 | |
if(req.url === '/favicon.ico'){ | |
//todo 读取本地图标 | |
return; | |
} | |
// 新版 | |
const myURL = new URL(req.url,'http://127.0.0.1:3000/'); | |
console.log(myURL); | |
/*-------------------------------------- | |
URL { | |
href: 'http://127.0.0.1:3000/api/list?a=1', | |
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: '/api/list', | |
search: '?a=1', | |
searchParams: URLSearchParams { 'a' => '1' }, | |
hash: '' | |
} | |
*/-------------------------------------- | |
let pathName = myURL.pathname; | |
res.writeHead(renderStatus(pathName),{"content-Type":"text/html;charset=utf-8"}) | |
res.write(renderHTML(pathName)); | |
res.end(); | |
}) | |
server.listen(3000,()=>{ | |
console.log('server start...'); | |
}); | |
//URL 的更多方法见 node 官网 | |
for(let [key,value] of myURL.searchParams){ | |
console.log(key,value); | |
} |
fileURLToPath('file:///你好.txt'); // 文件网址路径转换为文件路径 | |
pathToFileURL('/some/path%.c'); // 文件路径转换为网址路径 | |
const myURL = new URL('https://a:b@测试?abc#foo'); | |
urlToHttpOptions(urlToHttpOptions(myURL)); // 跟 format 一样 |
# 3.querystring 模块
# 3.1 parse
字符串解析成对象
let str = "name=zhangsan&age=21&location=changsha"; | |
let querystring = require('querystring'); | |
let obj = querystring.parse(str); | |
console.log(obj); | |
/* | |
{ | |
name: 'zhangsan', | |
age: '21', | |
location: 'changsha' | |
} | |
*/ |
# 3.2 stringify
对象解析成字符串
const querystring = require('querystring'); | |
let myObj = { | |
a:1, | |
b:2, | |
c:3 | |
} | |
let myStr = querystring.stringify(myObj); | |
console.log(myStr); | |
//a=1&b=2&c=3 |
# 3.3 escape&unescape
特殊符号转义防注入
let str1 = 'id=3&city=北京&url=https://www.baidu.com'; | |
let escaped = querystring.escape(str); | |
console.log(escaped); | |
//name%3Dzhangsan%26age%3D21%26location%3Dchangsha | |
let str2 = querystring.unescape(escaped); | |
console.log(str2); | |
//name=zhangsan&age=21&location=changsha |
# 4.http 模块补充
# 4.1 接口:jsonp
const http = require('http'); | |
const url = require('url'); | |
http.createServer((req,res)=>{ | |
let urlObj = url.parse(req.url,true); | |
console.log(urlObj.query.callback); | |
switch(urlObj.pathname){ | |
case '/api/aaa': | |
res.end(`${urlObj.query.callback}:(${JSON.stringify({ | |
name:'liuxin', | |
age:21 | |
})})`); | |
break; | |
default: | |
res.end('404'); | |
} | |
}).listen(3000); |
# 4.2 跨域 CORS
const http = require('http'); | |
const url = require('url'); | |
http.createServer((req,res)=>{ | |
let urlObj = url.parse(req.url,true); | |
// console.log(urlObj.query.callback); | |
res.writeHead(200,{ | |
"Content-Type":"application/json;charset=utf-8", | |
"access-control-allow-origin":"*" // 解决跨域 允许所有访问 | |
}); | |
switch(urlObj.pathname){ | |
case '/api/aaa': | |
res.end(`${JSON.stringify({ | |
name:'liuxin', | |
age:21 | |
})}`); | |
break; | |
default: | |
res.end('404'); | |
} | |
}).listen(3000); |
# 4.2.1 get
node 当客户端,跟其他服务端要数据,然后转发给前端
const http = require('http'); | |
const https = require('https'); | |
const url = require('url'); | |
http.createServer((req,res)=>{ | |
let urlObj = url.parse(req.url,true); | |
// console.log(urlObj.query.callback); | |
res.writeHead(200,{ | |
"Content-Type":"application/json;charset=utf-8", | |
"access-control-allow-origin":"*" | |
}); | |
switch(urlObj.pathname){ | |
case '/api/aaa': | |
// 客户端 去猫眼请求数据 | |
httpget(res); | |
break; | |
default: | |
res.end('404'); | |
} | |
}).listen(3000); | |
function httpget(response){ | |
let data = ""; | |
https.get('https://i.maoyan.com/api/mmdb/movie/v3/list/hot.json?ct=%E5%86%B7%E6%B0%B4%E6%B1%9F&ci=606&channelId=4',(res)=>{ | |
res.on('data',(chunk)=>{ // 不断的从其他服务器拿取数据 | |
data += chunk; | |
}) | |
res.on('end',()=>{ // 数据拿取完毕将会触发 | |
// console.log(data); | |
response.end(data); | |
}) | |
}) | |
} |
传 response 容易提高耦合度,开发中要降低耦合度 (解耦)
const http = require('http'); | |
const https = require('https'); | |
const url = require('url'); | |
http.createServer((req,res)=>{ | |
let urlObj = url.parse(req.url,true); | |
// console.log(urlObj.query.callback); | |
res.writeHead(200,{ | |
"Content-Type":"application/json;charset=utf-8", | |
"access-control-allow-origin":"*" | |
}); | |
switch(urlObj.pathname){ | |
case '/api/aaa': | |
// 客户端 去猫眼请求数据 | |
httpget((data)=>{ // 通过回调函数的形式减少方法之间过于紧密的联系 | |
res.end(data); | |
}); | |
break; | |
default: | |
res.end('404'); | |
} | |
}).listen(3000); | |
function httpget(cb){ | |
let data = ""; | |
https.get('https://i.maoyan.com/api/mmdb/movie/v3/list/hot.json?ct=%E5%86%B7%E6%B0%B4%E6%B1%9F&ci=606&channelId=4',(res)=>{ | |
res.on('data',(chunk)=>{ | |
data += chunk; | |
}) | |
res.on('end',()=>{ | |
// console.log(data); | |
// response.end(data); | |
cb(data); | |
}) | |
}) | |
} |
# 4.2.2 post
const http = require('http'); | |
const https = require('https'); | |
const url = require('url'); | |
http.createServer((req,res)=>{ | |
let urlObj = url.parse(req.url,true); | |
// console.log(urlObj.query.callback); | |
res.writeHead(200,{ | |
"Content-Type":"application/json;charset=utf-8", | |
"access-control-allow-origin":"*" | |
}); | |
switch(urlObj.pathname){ | |
case '/api/aaa': | |
// 客户端 去猫眼请求数据 | |
httppost((data)=>{ | |
res.end(data); | |
}); | |
break; | |
default: | |
res.end('404'); | |
} | |
}).listen(3000); | |
function httppost(cd){ | |
let data = ''; | |
let options = { | |
hostname:'m.xiaomiyoupin.com', // 域名 | |
port:'443', // 端口 | |
path:'/mtop/market/search/placeHolder', // 路径 | |
method:'post', // 方法 | |
headers:{ | |
"Content-Type":"application/json", // 返回数据格式 json | |
//"Content-Type":"application/x-www-form-urlencoded" | |
} | |
} | |
let req = https.request(options,(res)=>{ | |
res.on("data",chunk=>{ | |
data += chunk; | |
}) | |
res.on('end',()=>{ | |
cd(data); | |
}) | |
}); | |
//req.write("name=zhangsan&age=21"); | |
req.write(JSON.stringify([{},{'baseParam':{"ypClient":1}}])); //post 请求所需要的参数 | |
req.end(); | |
} |
# 4.3 爬虫
cheerio 模块
npm i cheerio
爬取猫眼网站示例
// const { Cheerio } = require('cheerio'); | |
const http = require('http'); | |
const https = require('https'); | |
const url = require('url'); | |
const cheerio = require('cheerio'); | |
http.createServer((req,res)=>{ | |
let urlObj = url.parse(req.url,true); | |
// console.log(urlObj.query.callback); | |
res.writeHead(200,{ | |
"Content-Type":"application/json;charset=utf-8", | |
"access-control-allow-origin":"*" | |
}); | |
switch(urlObj.pathname){ | |
case '/api/aaa': | |
// 客户端 去猫眼请求数据 | |
httpget((data)=>{ | |
res.end(spider(data)); | |
}); | |
break; | |
default: | |
res.end('404'); | |
} | |
}).listen(3000); | |
function httpget(cb){ | |
let data = ""; | |
https.get('https://i.maoyan.com/',(res)=>{ | |
res.on('data',(chunk)=>{ | |
data += chunk; | |
}) | |
res.on('end',()=>{ | |
// console.log(data); | |
// response.end(data); | |
cb(data); | |
}) | |
}) | |
} | |
function spider(data){ | |
// cheerio | |
let $ = cheerio.load(data); | |
let $moviewlist = $('.column.content'); | |
// console.log($moviewlist); | |
let movies = []; | |
$moviewlist.each((index,value)=>{ | |
movies.push({ | |
title:$(value).find('.title').text(), | |
grade:$(value).find('.grade').text(), | |
actor:$(value).find('.actor').text() | |
}); | |
}) | |
console.log(movies); | |
return JSON.stringify(movies); | |
} |
# 5.event 模块
订阅发布模式
const EventEmitter = require('events'); | |
class MyEventEmitter extends EventEmitter{} | |
const event = new MyEventEmitter(); | |
event.on('play',(movie)=>{ | |
console.log(movie); | |
}); | |
event.emit('play','我和我的祖国'); // 我和我的祖国 | |
event.emit('play','中国机长'); // 中国机长 |
运用示例
const http = require('http'); | |
const https = require('https'); | |
const { EventEmitter } = require('events'); | |
const url = require('url'); | |
let event = null; | |
http.createServer((req,res)=>{ | |
let urlObj = url.parse(req.url,true); | |
// console.log(urlObj.query.callback); | |
res.writeHead(200,{ | |
"Content-Type":"application/json;charset=utf-8", | |
"access-control-allow-origin":"*" | |
}); | |
switch(urlObj.pathname){ | |
case '/api/aaa': | |
// 客户端 去猫眼请求数据 | |
event = new EventEmitter(); // 每次请求重新监听 | |
event.on('play',(data)=>{ | |
// console.log(data); | |
res.end(data); | |
}) | |
httpget(); | |
break; | |
default: | |
res.end('404'); | |
} | |
}).listen(3000); | |
function httpget(cb){ | |
let data = ""; | |
https.get('https://i.maoyan.com/api/mmdb/movie/v3/list/hot.json?ct=%E5%86%B7%E6%B0%B4%E6%B1%9F&ci=606&channelId=4',(res)=>{ | |
res.on('data',(chunk)=>{ | |
data += chunk; | |
}) | |
res.on('end',()=>{ | |
event.emit('play',data); | |
}) | |
}) | |
} |
# 6.fs 文件操作模块
const fs = require('fs'); | |
//Sync 所有方法名后面添加这个表示同步、阻塞执行完该方法才执行下一个方法不加 sync 的是异步方法 | |
// 同步记得 try catch 捕获错误,防止服务器崩掉 | |
// 创建文件夹 | |
fs.mkdir('./avatar',(err)=>{ | |
if(err && err.code === 'EEXIST'){ | |
console.log('创建文件夹失败,目录已经存在'); | |
} | |
}) | |
// 文件夹改名 | |
fs.rename('./avatar',"./avatar1",(err)=>{ | |
if(err && err.code === 'ENOENT'){ | |
console.log('重命名失败,目录已经存在'); | |
} | |
}) | |
// 删除文件夹 | |
fs.rmdir('./avatar1', (err) => { | |
if (err && err.code === 'ENOENT') { | |
console.log('删除失败,目录不存在'); | |
} | |
if(err && err.code === 'ENOTEMPTY'){ | |
console.log('删除失败,目录不为空'); | |
} | |
}) | |
// 写文件 反复写会覆盖 | |
fs.writeFile('./a.txt','你好',(err)=>{ | |
console.log(err); | |
}) | |
// 追加 | |
fs.appendFile('./a.txt','\nhello world',(err)=>{ | |
console.log(err); | |
}) | |
// 读文件 buffer 对象需要转换 | |
fs.readFile('./a.txt',(err,data)=>{ | |
if(!err){ | |
console.log(data.toString('utf-8')); | |
} | |
}) | |
fs.readFile('./a.txt',"utf-8",(err,data)=>{ | |
if(!err){ | |
console.log(data); | |
} | |
}) | |
// 删除文件 | |
fs.unlink('./a.txt',(err)=>{ | |
if(err && err.code === 'ENOENT'){ | |
console.log('删除失败,文件不存在'); | |
} | |
}) | |
// 读取文件夹 | |
fs.readdir('../event模块',(err,data)=>{ | |
if(!err){ | |
console.log(data); //[ 'get.js', 'index.js' ] | |
} | |
}) | |
fs.stat('../http-爬虫',(err,data)=>{ | |
console.log(data.isFile()); // 是不是文件 | |
console.log(data.isDirectory()); // 是不是文件夹 | |
}) |
// 异步处理 | |
const fs = require('fs').promises | |
fs.mkdir('./avatar').then(data=>{ | |
console.log(data); | |
}); | |
// 更简便的方法 | |
fs.readdir('./avatar').then(async (data)=>{ | |
await Promise.all(data.map(item=>fs.unlink(`./avatar/${item}`))); | |
await fs.rmdir('./avatar'); | |
}) |
# 7.stream 流模块
大文件
stream 是 node.js 提供的又一个仅在服务区端可用的模块,目的是支持” 流 “这种数据结构。
什么是流?流是一种抽象的数据结构。想象水流,当在水管中流动时,就可以从某个地方 (例如自来水厂) 源源不断的打到另一个地方 (比如你家洗碗池)。我们也可以把数据看成是数据流,比如你敲键盘的时候,就可以把每个字符一次连起来,看成字符流。这个流是从键盘输入到应用程序,实际上它还对应着一个名字:标准输入流 (stdin)。
如果应用程序把字符一个一个输出到显示器上,这一可以看成是一个流,这个流也有名字:标准输出流 (stdout)。流的特点是数据是有序的,而且必须依次读取,或者依次写入,不能像 array 那样随机定位。
有些流用来读取数据,比如从文件读取数据时,可以打开一个文件流,然后从文件流中不断地读取数据。有些流用来写入数据,比如向文件写入数据时,只需要把数据不断地往文件流中写进去就可以了。
在 nodejs 中,流也是一个对象,我们只需要响应流的事件就可以了:data 事件表示流的数据已经可以读取了,end 事件表示这个流已经到末尾了,没有数据可以读取了,error 事件表示出错了。
# 7.1 基本用法
const fs = require('fs'); | |
// 打开一个流 读 | |
let rs = fs.createReadStream('./sample.txt','utf-8'); | |
rs.on('data',function(chunk){ | |
console.log('Data:'); | |
console.log(chunk); | |
}); | |
rs.on('end',function(){ | |
console.log('End'); | |
}) | |
rs.on('error',function(err){ | |
console.log('Error:' + err); | |
}) | |
// 写 | |
const ws = fs.createWriteStream('./sample.txt','utf-8'); | |
ws.write('内容\n'); | |
ws.write('我是具体内容'); | |
ws.end(); |
注意,data 事件可能会有多次,每次传递的 chunk 是流的一部分数据。要以流的形式写入文件,只需要不断调用 write () 方法,最后以 end () 结束。
# 7.2 复制文件
边读边写,通过 stream 自带的 pipe 方法实现
将 blog.txt 文件里的内容写到文件 sample.txt 中
const fs = require('fs'); | |
// 读 | |
const rs = fs.createReadStream('./blog.txt','utf-8'); | |
// 写 | |
const ws = fs.createWriteStream('./sample.txt','utf-8'); | |
rs.pipe(ws); |
# 8.zlib 模块
压缩文件
const http = require('http'); | |
const fs = require('fs'); | |
const zlib = require('zlib'); | |
const gzip = zlib.createGzip(); | |
http.createServer((req,res)=>{ | |
const rs = fs.createReadStream('./01.js','utf-8'); | |
res.writeHead(200,{"Content-Type":'application/x-javascript;charset=utf-8',"Content-Encoding":"gzip"});// 告诉浏览器文件的压缩方式,不然读取乱码 | |
rs.pipe(gzip).pipe(res);// 进行压缩 | |
}).listen(3000,()=>{ | |
console.log('server start....'); | |
}); |
# 9.crypto
cryto 模块的目的是为了提供通用的加密和哈希算法。用纯 javascript 代码实现这些功能不是不可能,但速度会非常慢。Node.js 用 C/C++ 实现这些算法之后,通过 crypto 这个模块暴露为 javascript 接口,这样用起来方便,运行速度快。
md5 是一种常用的哈希算法,用于给人以数据一个” 签名 “。这个签名通常用一个十六进制的字符串表示。
# 9.1 hash
const crypto = require('crypto'); | |
const hash = crypto.createHash('sha1'); | |
const hash = crypto.createHash('md5'); | |
hash.update('hello world'); | |
console.log(hash.digest('base64')); | |
console.log(hash.digest('hex')); |
# 9.2 crypto
const crypto = require('crypto'); | |
const hmac = crypto.createHmac('sha256','liuxin'); | |
hmac.update('123456'); | |
console.log(hmac.digest('hex')); |
update () 方法默认编码为 utf-8,也可以传入 buffer
# 9.3 aes
aes 是一种常用的对称加密算法,加解密都用同一个密钥。crypto 模块提供了 AES 支持,但是需要自己封装好函数,便于使用。
const crypto = require('crypto'); | |
// 加密 | |
function encrypt(key,iv,data){ | |
let dep = crypto.createCipheriv('aes-128-cbc',key,iv); | |
return dep.update(data,'binary','hex') + dep.final('hex'); | |
} | |
// 128 16 | |
let key = 'abcdef1234567890'; | |
let iv = 'ghijklmn12345678'; | |
let data = 'liuxin'; | |
let crypted = encrypt(key,iv,data); | |
console.log(crypted); | |
// 解密 | |
function decrypt(key,iv,crypted){ | |
crypted = Buffer.from(crypted,'hex').toString('binary'); //16 进制转换为 Buffer 对象,在转换为 2 进制 | |
let dep = crypto.createDecipheriv('aes-128-cbc',key,iv); | |
return dep.update(crypted,'binary','utf-8') + dep.final('utf-8'); | |
} | |
let decrypted = decrypt(key,iv,crypted); | |
console.log(decrypted); |
# 10. 路由
# 10.1 基础
index.js
const server = require('./server'); | |
const route = require('./route'); | |
const apiRouter = require('./api'); | |
// 合并路由 | |
server.use(route); | |
server.use(apiRouter); | |
server.start(); |
server.js
const http = require('http'); | |
const Router = {}; | |
// Object.assign(Router,route); | |
// Object.assign(Router,apiRouter); | |
// console.log(Router); | |
// 用于合并所有路由 | |
function use(obj){ | |
Object.assign(Router,obj); | |
} | |
function start() { | |
// 创建服务器 | |
http.createServer((req, res) => { | |
const myURL = new URL(req.url, 'http://127.0.0.1/'); | |
// console.log(myURL.pathname); | |
try { | |
Router[myURL.pathname](res); | |
} catch (err) { | |
// 如果是不存在的路由则跳转到 404 | |
Router['/404'](res); | |
} | |
}).listen(3000, () => { | |
console.log('server start ......'); | |
}) | |
} | |
exports.start = start; | |
exports.use = use; |
./route/index.js
const fs = require('fs'); | |
function render(res,path,type="",code){ | |
res.writeHead(code || 200, { "Content-Type": type==""?"text/html;charset=utf8":type }); | |
res.write(fs.readFileSync(path), 'utf-8'); | |
res.end(); | |
} | |
const route = { | |
"/login": (res) => { | |
render(res,'./static/login.html',); | |
}, | |
"/home": (res) => { | |
render(res,'./static/home.html'); | |
}, | |
"/404":(res,fs)=>{ | |
render(res,'./static/404.html',404); | |
} | |
} | |
module.exports = route; |
./api/index.js
function render(res,data,type="",code){ | |
res.writeHead(code || 200, { "Content-Type": type==""?"application/json;charset=utf8":type }); | |
res.write(data); | |
res.end(); | |
} | |
const apiRouter = { | |
"/api/login":(res)=>{ | |
render(res,'{ok:1}'); | |
} | |
} | |
module.exports = apiRouter; |
# 10.2 获取参数
login.html
<!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> | |
<div> | |
<div> | |
用户名: | |
<input type="text" id="username"> | |
</div> | |
<div> | |
密码: | |
<input type="password" id="password"> | |
</div> | |
<div> | |
<button id="login">登录-get</button> | |
<button id="login_post">登录-post</button> | |
</div> | |
</div> | |
</body> | |
<script> | |
let loginBtn = document.querySelector('#login'); | |
let loginPost = document.querySelector('#login_post'); | |
let username = document.querySelector('#username'); | |
let password = document.querySelector('#password'); | |
loginBtn.onclick = function(){ | |
// console.log(username.value,password.value); | |
fetch(`/api/login?username=${username.value}&password=${password.value}`).then(res=>res.json()).then(res=>{ | |
console.log(res); | |
}) | |
} | |
loginPost.onclick = function(){ | |
fetch(`/api/loginpost`,{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); | |
}) | |
} | |
</script> | |
</html> |
server.js
const http = require('http'); | |
const Router = {}; | |
// Object.assign(Router,route); | |
// Object.assign(Router,apiRouter); | |
// console.log(Router); | |
// 用于合并所有路由 | |
function use(obj){ | |
Object.assign(Router,obj); | |
} | |
function start() { | |
// 创建服务器 | |
http.createServer((req, res) => { | |
const myURL = new URL(req.url, 'http://127.0.0.1/'); | |
// console.log(myURL.pathname); | |
try { | |
Router[myURL.pathname](req,res); | |
} catch (err) { | |
// 如果是不存在的路由则跳转到 404 | |
Router['/404'](req,res); | |
} | |
}).listen(3000, () => { | |
console.log('server start ......'); | |
}) | |
} | |
exports.start = start; | |
exports.use = use; |
index.js 不变
./api/index.js
function render(res,data,type="",code){ | |
res.writeHead(code || 200, { "Content-Type": type==""?"application/json;charset=utf8":type }); | |
res.write(data); | |
res.end(); | |
} | |
const apiRouter = { | |
"/api/login":(req,res)=>{ | |
const myURL = new URL(req.url,'http://127.0.0.1'); | |
if(myURL.searchParams.get('username') === 'liuxin' && myURL.searchParams.get('password') === '123456'){ | |
render(res,'{"ok":"1"}'); | |
}else{ | |
render(res,'{"ok":"0"}'); | |
} | |
}, | |
"/api/loginpost":(req,res)=>{ | |
let data = ''; | |
req.on('data',chunk=>{ | |
data += chunk; | |
}) | |
req.on('end',()=>{ | |
// console.log(data); | |
data = JSON.parse(data); | |
// console.log(data); | |
if(data.username === 'liuxin' && data.password === '123456'){ | |
render(res,'{"ok":"1"}'); | |
}else{ | |
render(res,'{"ok":"0"}'); | |
} | |
}) | |
} | |
} | |
module.exports = apiRouter; |
./route/index.js
const fs = require('fs'); | |
function render(res,path,type="",code){ | |
res.writeHead(code || 200, { "Content-Type": type==""?"text/html;charset=utf8":type }); | |
res.write(fs.readFileSync(path), 'utf-8'); | |
res.end(); | |
} | |
const route = { | |
"/login": (req,res) => { | |
render(res,'./static/login.html',); | |
}, | |
"/home": (req,res) => { | |
render(res,'./static/home.html'); | |
}, | |
"/404":(req,res)=>{ | |
render(res,'./static/404.html',404); | |
} | |
} | |
module.exports = route; |
# 10.3 静态资源处理
使用场景是服务器上的一些 css、js 等静态资源需要引入到 html 文件中,实现行为分离,但是普通的引入是读取不到对应的静态文件的,这时候就需要对静态资源进行处理
login.html
<!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> | |
<link rel="stylesheet" href="css/login.css"> | |
</head> | |
<body> | |
<div> | |
<div> | |
用户名: | |
<input type="text" id="username"> | |
</div> | |
<div> | |
密码: | |
<input type="password" id="password"> | |
</div> | |
<div> | |
<button id="login">登录-get</button> | |
<button id="login_post">登录-post</button> | |
</div> | |
</div> | |
</body> | |
<script src="./js/login.js"></script> | |
</html> |
./route/index.js
const fs = require('fs'); | |
const path = require('path'); | |
const mime = require('mime'); | |
function render(res,path,type="",code){ | |
// console.log(type); | |
res.writeHead(code || 200, { "Content-Type": `${type?type:'text/html'};charset=utf8`}); | |
res.write(fs.readFileSync(path),'utf-8'); | |
res.end(); | |
} | |
const route = { | |
"/login": (req,res) => { | |
render(res,'./static/login.html',); | |
}, | |
"/home": (req,res) => { | |
render(res,'./static/home.html'); | |
}, | |
"/404":(req,res)=>{ | |
if(readStaticFile(req,res)){ | |
return; | |
} | |
render(res,'./static/404.html',404); | |
} | |
} | |
function readStaticFile(req,res){ | |
const myURL = new URL(req.url,"http://127.0.0.1:3000") | |
const pathname = path.join(__dirname,"../static",myURL.pathname); | |
// 如果是直接访问域名 则跳转 home 页面 | |
if(myURL.pathname === '/') return route['/home'](req,res); | |
if(fs.existsSync(pathname)){ | |
// 处理 | |
render(res,pathname,mime.getType(myURL.pathname.split('.')[1])); | |
return true; | |
}else{ | |
return false; | |
} | |
} | |
module.exports = route; |
# 二、Express
基于 Node.js 平台,快速、开放、极简的 web 开发框架
# 1、特色
# 1.web 应用
Express 是一个基于 Node.js 平台的极简、灵活的 web 应用开发框架,他提供一系列强大的特性,帮助你创建各种 web 和移动设备应用
# 2.API
丰富的 HTTP 快捷方法和任意排列的 Connect 中间件,让你创建健壮、友好的 API 变得既快速又简单。
# 3. 性能
Express 不对 node.js 已有的特性进行二次抽象,只是在他之上扩展了 web 应用所需的基本功能
# 2、安装
npm install express --save
const express = require('express'); | |
const app = express(); | |
app.get('/',(req,res)=>{ | |
//res.send 方法可以返回多种类型数据而不需要配置东西 | |
res.send('hello world'); | |
res.send('<html><h1>hello world</h1></html>'); | |
res.send({ | |
name:'zhangsan', | |
age:21, | |
sex:'男' | |
}) | |
}) | |
app.listen(3000,()=>{ | |
console.log('server start ....'); | |
}) |
# 3、路由
路由路径和请求方法一起定义了请求的端点,它可以是字符串、字符串模式或者正则表达式
//? 问号前的字符可有可无 | |
app.get('/ab?cd',(req,res)=>{ | |
res.send('ok'); | |
}); | |
//: 后面是占位符 | |
app.get('/ab/:id',(req,res)=>{ | |
res.send('ok'); | |
}) | |
//+ 前面的字符可以不限次数 | |
app.get('/ab+cd',(req,res)=>{ | |
res.send('ok'); | |
}) | |
//* 位置可以填入任意数量的字符 | |
app.get('/ef*gh',(req,res)=>{ | |
res.send('ok'); | |
}) | |
//() 可以与前面的除:以外组合使用 ()? 表示括号内的字符可有可无 以此类推 | |
app.get('/ab(cd)?ef',(req,res)=>{ | |
res.send('ok'); | |
}) |
正则模式
// 匹配任何路径中含有 a 的路径 | |
app.get(/a/,(req,res)=>{ | |
res.send('/a/'); | |
}); | |
// 只要是以 fly 结尾的路径就行 | |
app.get(/.*fly$/,(req,res)=>{ | |
res.send('fly'); | |
}) |
可以为请求处理提供多个回调函数,其行为类似于中间件,唯一区别是这些回调函数有可能调用 next (route) 方法而略过其他路由回调函数。可以利用该机制为路由定义前提条件,如果在现有路径上继续执行没有意义,则可将控制权交给剩下的路径。
app.get('/home',(req,res,next)=>{ | |
// 验证 token 是否过期 | |
console.log('验证token'); | |
next(); | |
},(req,res,next)=>{ | |
// 查询数据库 | |
console.log('查询数据库'); | |
next(); | |
},(req,res)=>{ | |
// 返回内容 | |
res.send({ | |
list:[1,2,3] | |
}) | |
}) |
next () 方法继续执行下一个回调函数
简介写法
const fn1 = (req,res,next)=>{ | |
// 验证 token 是否过期 | |
console.log('验证token'); | |
next(); | |
} | |
const fn2 = (req,res,next)=>{ | |
// 查询数据库 | |
console.log('查询数据库'); | |
next(); | |
} | |
const fn3 = (req,res)=>{ | |
// 返回内容 | |
res.send({ | |
list:[1,2,3] | |
}) | |
} | |
app.get('/home',[fn1,fn2,fn3]); |
这样写方法复用性更高
# 4、中间件
Express 是一个自身功能极简,完全是由路由和中间件构成一个 web 开发框架:从本质上说,一个 Express 应用就是在调用各种中间件
中间件 (middleware) 是一个函数,它可以访问请求对象 (request object (req)),响应对象 (response object (res)),和 web 应用中处于请求 - 响应循环流程中的中间件,一般被命名为 next 的变量
中间件的功能包括有
- 执行任何代码
- 修改请求和响应对象
- 终结请求 - 响应循环
- 调用堆栈中的下一个中间件
如果当前中间件没有终结请求 - 响应循环,则必须调用 next 方法将控制权交给下一个中间件,否则请求就会挂起
express 应用可使用如下几种中间件
- 应用级中间件
- 路由级中间件
- 错误处理中间件
- 内置中间件
- 第三方中间件
使用可选择挂载路径,可在应用级别或路由级别装载中间件。另外,你还可以同时装载一系列中间件函数,从而在一个挂载点上创建一个子中间件栈。
# 1. 应用级中间件
应用级中间件绑定到 app 对象 使用 app.use () 和 app.METHOD (), 其中,METHOD 是需要处理的 HTTP 请求的方法,例如 get,put,post 等等,全部小写。例如:
// 只要是 app 开头的都是应用级中间件 | |
//app.use(path,function); | |
//path 路径可不写,function 是要执行的函数 | |
// 如果 path 写明,则只会响应 path 请求,如果没有,前面也没有接口响应的话,就会执行这个中间件 | |
const fn1 = (req,res)=>{ | |
console.log(1); | |
res.send('hello world'); | |
} | |
app.use("/index",fn1); |
# 2. 路由级中间件
//index.js | |
const express = require('express'); | |
const app = express(); | |
const router = require('./router2/index'); | |
// 应用级别 | |
app.use((req,res,next)=>{ | |
console.log('验证token'); | |
next(); | |
}) | |
// 应用级别 | |
app.use('/',router); | |
app.listen(3000,()=>{ | |
console.log('server start .......'); | |
}) |
//router.js | |
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; |
# 3. 错误处理中间件
//index.js | |
const express = require('express'); | |
const app = express(); | |
const homeRouter = require('./router3/HomeRouter'); | |
const loginRouter = require('./router3/LoginRouter'); | |
// 应用级别 | |
app.use((req,res,next)=>{ | |
console.log('验证token'); | |
next(); | |
}) | |
// 应用级别 | |
app.use('/home',homeRouter); | |
app.use('/login',loginRouter); | |
// 当前面路由都没有匹配到,则到了错误处理 | |
app.use((err,req,res,next)=>{ | |
res.status(404).send('丢了'); | |
}) | |
app.listen(3000,()=>{ | |
console.log('server start .......'); | |
}) |
//HomeRouter.js | |
const express = require('express'); | |
const router = express.Router(); | |
// 路由级别中间件 | |
router.get('/',(req,res)=>{ | |
res.send('home'); | |
}) | |
module.exports = router; |
//LoginRouter.js | |
const express = require('express'); | |
const router = express.Router(); | |
// 路由级别中间件 | |
router.get('/',(req,res)=>{ | |
res.send('login'); | |
}) | |
module.exports = router; |
# 4. 内置中间件
express.static 是 express 唯一内置的中间件。它基于 serve-static,负责在 express 应用中提托管静态资源。每个应用可以有多个静态资源目录
app.use(express.static('public')); | |
app.use(express.static('uploads')); | |
app.use(express.static('files')); |
# 5. 第三方中间件
安装所需功能的 node 模块,并在应用中加载,可以在应用级加载,也可以在路由级加载
下面的例子安装并加载了一个解析 cookie 的中间件 cookie-parser
npm install cookie-parser
const express = require('express'); | |
const app = express(); | |
const cookieParser = require('cookie-parser'); | |
// 加载用于解析 cookie 的中间件 | |
app.use(cookieParser()) |
# 5、请求获取参数
get
// 路由级别 响应前端 get 请求 | |
router.get('/',(req,res)=>{ | |
console.log(req.query); | |
res.send(req.query); | |
}); |
post
// 获取 req.body 里的数据还需要引入一个解析 post 参数的中间件 | |
// 我们在 index.js 中引入 | |
// 配置解析 post 参数的中间件 不用下载第三方 新版本内置 | |
app.use(express.urlencoded({extended:false})); | |
//post 请求接收 json 参数 | |
app.use(express.json()); //post 参数 {name:'',age:100} | |
router.post('/',(req,res)=>{ | |
console.log(req.body); | |
res.send(req.body); | |
}) |
# 6、利用 express 托管静态文件
通过 express 内置的 express.static 可以方便的托管静态文件,例如图片、css、javascript 文件等
将静态资源文件所在的目录作为参数传递给 express.static 中间件就可以提供静态资源文件的访问了。例如,假设在 public 目录放置了图片、css 和 javascript 文件,你就可以
app.use(express.static('public')); |
现在,public 目录下面的文件就可以访问了
http://localhost:3000/images/kitten.jpg | |
http://localhost:3000/css/style.css | |
http://localhost:3000/js/app.js | |
http://localhost:3000/images/bg.png | |
http://localhost:3000/hello.html |
所有文件的路径都是相对于存放目录的,因此,存放静态文件的目录名不会出现 URL 中
如果你的静态资源存放在多个目录下,你可以多次调用 express.static 中间件
app.use(express.static('public')); | |
app.use(express.static('files')); |
示例
const express = require('express'); | |
const app = express(); | |
const LoginRouter = require('./route/LoginRouter'); | |
const HomeRouter = require('./route/HomeRouter'); | |
// 配置静态资源目录 | |
app.use(express.static('public')); | |
app.use(express.static('files')); | |
// 解析 post 请求 | |
app.use(express.urlencoded({extended:false})); | |
app.use(express.json()); | |
app.use((req,res,next)=>{ | |
console.log('验证token'); | |
next(); | |
}); | |
app.use('/login',LoginRouter); | |
app.use('/home',HomeRouter); | |
app.use((req,res)=>{ | |
res.status(404).send('404 not found'); | |
}) | |
app.listen(3000,()=>{ | |
console.log('server start .....'); | |
}) |
如果需要路由区分,则可以写成这样
app.use('/public',express.static('public')); |
# 7、服务端渲染 (模板引擎)
- 服务器渲染,后端嵌套模板,后端渲染模板,SSR (后端把页面组装)
- 做好静态页面,动态效果
- 把前端代码提供给后端,后端要把静态 html 以及里面的假数据给删掉,通过模板进行动态生成 html 的内容
- 前后端分离,BSR (前端中组装页面)
- 做好静态页面,动态效果
- Json 模拟,ajax 动态创建页面
- 真实接口数据,前后联调
- 把前端提供给后端静态资源文件夹
npm i ejs
需要在应用中进行如下设置才能让 express 渲染模板文件
-
views, 放模板文件的目录,比如
app.set('views','./views');
-
view engine,模板引擎,比如
app.set('view engine','ejs');
-
请求配置
const express = require('express');
const router = express.Router();
router.get('/',(req,res)=>{
// 渲染模板后返回给前端
res.render('login',{title:'111111111'}); // 自动找 views 文件夹下的 login.ejs
})
module.exports = router;
<!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>
login
<h1>推荐-<%=title%></h1>
</body>
</html>
<%%> 流程控制标签 写的是 if else,for
<%= %> 输出标签 原文输出 html 标签
<%- %> 输出标签 html 会被浏览器解析
<%# %> 注释标签
<%- include ('user/show',{user:user}) %> 导入公共的模板内容
<!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>
home
<ul>
<% for( let i = 0; i < list.length; i++ ) { %>
<li><%= list[i] %></li>
<% } %>
<%= myHtml %>
<%- myHtml %>
<%#我是注释%>
<%- include("./header.ejs",{isShowTitle:false}) %>
</ul>
</body>
</html>
header.js
<header>
<% if(isShowTitle){ %>
<h1>校园招聘</h1>
<% } %>
<h1>我是公共头部</h1>
</header>
引擎渲染 html 文件配置
// 配置模板引擎
app.set('views','./views');
app.set('view engine','html');
app.engine('html',require('ejs').renderFile); // 支持直接渲染 html 文件
# 8、express 生成器
安装
npm i -g express-generator
生成
//默认jade引擎
express myproject
//指定模板引擎
express myproject --view=ejs
运行
npm start
app.js
var createError = require('http-errors'); | |
var express = require('express'); | |
var path = require('path'); | |
var cookieParser = require('cookie-parser'); | |
var logger = require('morgan'); | |
var indexRouter = require('./routes/index'); | |
var usersRouter = require('./routes/users'); | |
var app = express(); | |
// 配置模板引擎 | |
app.set('views', path.join(__dirname, 'views')); | |
app.set('view engine', 'ejs'); | |
// 记录生成器 | |
app.use(logger('dev')); | |
//post 请求参数解析 | |
app.use(express.json()); | |
app.use(express.urlencoded({ extended: false })); | |
// 解析 cookie 的中间件 | |
app.use(cookieParser()); | |
// 配置静态资源目录 | |
app.use(express.static(path.join(__dirname, 'public'))); | |
// 注册路由级中间件 | |
app.use('/', indexRouter); | |
app.use('/users', usersRouter); | |
// catch 404 and forward to error handler | |
//404 错误中间件 | |
app.use(function(req, res, next) { | |
next(createError(404)); | |
}); | |
// error handler | |
app.use(function(err, req, res, next) { | |
// set locals, only providing error in development | |
//res.locals 里的东西会存在在 node 模板中 | |
res.locals.message = err.message; | |
res.locals.error = req.app.get('env') === 'development' ? err : {}; | |
// render the error page | |
res.status(err.status || 500); | |
res.render('error'); | |
}); | |
module.exports = app; |
服务端读取浏览器的 cookie 值
req.cookies // 当前端像服务端发送请求的时候就能够获取到前端的 cookie 值 | |
// 设置 cookie 这样就能往前端的浏览器添加一个 cookie | |
res.cookie('token','jskdfjslkfjsdkf'); |
# 三、MongoDB
# 1、关系型与非关系型数据库
区别
-
关系型数据库
- sql 语句增删改查操作
- 保持事务的一致性,事务机制 (回滚)
- mysql,sqlserver,db2,oracle
-
非关系型数据库
- no sql,not only sql
- 轻量,高效,自由
- mongoDB,Hbase,Redis
-
为什么选择 MongoDB
由于 MongoDB 独特的数据处理方式,可以将热点数据加载到内存,故而对查询来讲,会非常快 (当然也非常消耗内存)
同时由于采用了 BSON 的方式存储数据,故而对 JSON 格式的数据具有非常好的支持性以及友好的表结构修改性,文档式的存储方式,数据友好可见
数据库的分片集群负载具有非常好的扩展性以及非常不错的自动故障转移
sql 术语 | mongoDB 术语 | 解释 |
---|---|---|
database | database | 数据库 |
table | collection | 数据库表 / 集合 |
row | document | 数据记录行 / 文档 |
column | field | 数据字段 / 域 |
index | index | 索引 |
table joins | 表连接 \MongoDB 不支持 | |
primary key | primary key | 主键 \mongoDB 自动将_id 字段设置为主键 |
# 2、安装数据库
https://www.mongodb.com/docs/manual/tutorial/install-mongodb-on-windows/
https://www.mongodb.com/try/download/community
安装教程
https://blog.csdn.net/qq_44732146/article/details/119760448
作者跟视频上的不一样,所以数据库就跳过了,不实践了
# 3、启动数据库
# 4、在命令行中操作数据库
# 5、可视化工具进行增删改查
# 6、nodejs 连接操作数据库
npm i mongoose
const mongoose = require('mongoose'); | |
mongoose.connect("mongodb://127.0.0.1:27017/database"); | |
// 插入集合和数据,数据库 database 会自动创建 |
# 四、接口规范与业务分层
# 1、接口规范
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?id=1 --> delete /blog/Articles/1 删除一篇文章 |
使用方式
get http://www.blog.com/api/user 获取列表
post http://www.blog.com/api/user 创建用户
put http://www.blog.com/api/user 修改用户信息
delete http://www.blog.com/api/user 注销用户
过滤信息
用于补充规范一些通用字段
?limit=10 指定返回记录的数量
?offset=10 指定返回记录的开始位置
?page=2&per_page=100 指定第几页,以及每页的记录数
?sortby=name&order=asc 指定返回结果按照哪个属性排序,以及排序顺序
?state=close 指定筛选条件
# 2、业务分层
router.js 负责将请求分发给c层
controller.js C层负责处理业务逻辑(v与m之间的沟通)
views v层 负责展示页面
model m层 负责处理数据 增删改查
route 管理路由调用 contoller 获取前端数据调用 service 方法获取后端数据再返回给前端
# 五、登录鉴权
# 1、cookie&session
浏览器 post 账号密码 --> 服务端 --> 通过数据库校验账号密码 --> 校验成功服务端存放 cookie,将 cookieID 返回给前端 --> 前端请求接口携带 cookie--> 后端验证 cookie,校验成功对前端的请求进行处理 --> 将结果返回给前端
npm i express-session
index.js
const session = require('express-session'); | |
// 放在请求前面 | |
// 注册中间件 | |
app.use(session({ | |
name:'liuxinsystem', | |
secret:'this is session', // 服务器生成 session 的签名 | |
resave:true, // 重新设置 session 之后,过期时间重新计算 | |
saveUninitialized:true, // 强制将为初始化的 session 存储 | |
cookie:{ | |
maxAge:1000*60*60, // 过期时间 1 个小时 | |
secure:false, // 为 true 时候表示只有 https 协议才能访问 cookie | |
}, | |
rolling:true, // 默认为 true; 为 true 表示,超时前刷新,cookie 会重新计时;为 false 表示在超时前刷新多少次,都是按照第一次刷新开始计时 | |
store:MongoStore.create({ //MongoDB 的过时删除配置 | |
mongoUrl:'mongodb://127.0.0.1:27017/zhangsan_session', // 新创建一个数据库来存储 session | |
ttl:1000*60*10 // 过期时间 | |
}) | |
})) |
设置一个中间件做 session 过期校验
app.use((req,res,next)=>{ | |
// 排除不需要验证的接口 以 login 接口为示例 | |
if(req.url.includes('login')){ | |
next(); | |
return; | |
} | |
if(req.session.user){ | |
// 重新设置 | |
req.session.date = Date.now(); | |
next(); | |
}else{ | |
// 是接口 返回错误码 | |
// 不是接口 重定向 | |
req.url.includes('api') ? res.status(401).send({ok:0}) : res.redirect('/login'); | |
} | |
}) |
销毁
req.session.destory(()=>{ | |
res.send({ok:1}); | |
});// 销毁 cookie 销毁成功向前端返回数据 |
session 存放
session 是存放在内存中的,内存满了就会宕机,宕机会损失一些数据
可以在数据库设置一个线程,查询删除过时文档
npm i connect-mongo //安装mongodb的模块
但是放在数据库中用户量大也会有问题,cookie 每次都会直接带到服务端,容易被盗走安全信息,所以就用到 jwt
# 2、JSON WEB TOKEN(jwt)
当然,如果一个人的 token 被别人偷走了 (比 cookie 难多了),那也没办法,我也会认为小偷就是合法用户,这其实和一个的 session ID 被别人偷走一样
这样依赖,我就不保存 token 了,只是生成 token,然后验证 token,用 cpu 计算时间获取 session 存储空间
解除 session id 这个负担,可以说是无事一身轻,机器集群现在可以轻松的的做水平拓展,用户访问量增大,直接加机器就行
缺点
- 占带宽,每次都要消耗一些流量,如果每个月访问量很高的话,这个流量累计起来就是一笔不小的开销,而且一般 jwt 中存储的信息会更多。
- 无法在服务端注销,很难解决劫持问题
- 性能问题,jwt 的卖点之一就是加密签名,由于这个特性,接收方得以验证 jwt 是否有效且被信任。对于有着严格性能要求的 web 应用,这并不理想,尤其是对于单线程环境。
注意
CSRF 攻击的原因是浏览器会自动带上 cookie,而不会带上 token
以 CSRF 攻击为例
cookie:用户点击了连接,cookie 未失效,导致发起请求后后端以为是用户正常操作,于是进行扣款操作;
token:用户点击连接,由于浏览器不会自动带上 token,所以即使发了请求,后端的 token 验证不会通过,所以不会进行扣款操作
实现
//jsonwebtoken 封装 | |
const jsonwebtoken = require('jsonwebtoken'); | |
const secret = 'key'; | |
const JWT = { | |
generate(value,exprires){ | |
return jsonwebtoken.sign(value,secret,{expriesIn:exprires}); | |
}, | |
verify(token){ | |
try{ | |
return jsonwebtoken.verify(token,secret); | |
}catch(err){ | |
return false; | |
} | |
} | |
} | |
module.exports = JWT |
纯后端 ejs 模板无法使用 token,只能前后端分离使用
个人建议使用 jwt-simple 模块,更加简便易用
axios
npm i axios --save
示例
axios.post('/api/login',{ | |
username:username, | |
password:password | |
}).then(res=>{ | |
console.log(res.data); | |
}) |
axios 可以设置拦截器
// 任何请求前的方法 | |
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)=>{ | |
//console.log (' 请求成功后第一个调用的方法 '); | |
const {authorization} = response.headers; | |
authoriation && localStorage.setItem('token',authorization); | |
return response; | |
},(err)=>{ | |
return Promist.reject(err); | |
}) |
# 六、文件上传
Multer 是一个 nodejs 中间件,用于处理 multipart/form-data 类型的表单数据,它主要用于上传文件。他是卸载 busboy 之上非常高效。
注意
Multer 不会处理任何非 multipart/form-data 类型的表单数据
安装模块
npm i --save multer
前端需要在表单里修改请求头的类型,需要是 multipart/form-data 类型的,也可以直接修改 form 表单元素的 enctype 属性
<form action="/api/upload" enctype="multipart/form-data"> | |
<input type="file" name="avatar" /> | |
</form> |
如果是框架,可以这样
const formdata = new FormData(); | |
formdata.append("username",username.value); | |
formdata.append("password",password.value); | |
formdata.append('age',age.value); | |
formdata.append('avatar',avatar.value); | |
axios.post('/api/user',formdata,{ | |
headers:{ | |
"Content-Type":"multipart/form-data" | |
} | |
}).then(res=>{ | |
console.log(res.data); | |
}) |
multer 会添加一个 body 对象以及 file 或 files 对象到 express 的 request 对象中。body 对象包含表单的文本域信息,file 或 files 对象包含对象表单上传的文件信息
const upload = multer({desk:'uploads/'}); // 记得配置静态资源目录 | |
// 添加用户 | |
router.post('/user',upload.single('avatar'),UserController.addUser); //single 方法里填写的是你所需上传文件在前端表单中的 name |
如果是要重命名文件
const storage = multer.diskStorage({ | |
destination:function(req,file,cb){ | |
cb(null,'/tmp/my-uploads'); | |
}, | |
filename(req,file,cb){ | |
cb(null,file.fieldname + '-' + Date.now()) | |
} | |
}) | |
const upload = multer({storage:storage}); |
有两个选项可用,destination 和 filename。他们都是用来确定文件存储位置的函数。
destination 是用来确定上传的文件应该存储在哪个文件夹中。也可以提供一个 string (例如 '/tmp/uploads')。如果没有设置 destination,则使用操作系统默认的临时文件夹
注意 如果你提供的 destination 是一个函数,你需要负责创建文件夹。当提供一个字符串,multer 将确保这个文件夹是你创建的。
filename 用于确定文件夹中的文件名的确定。如果没有设置 filename,每个文件将设置一个随机文件名,并且是没有扩展名的。
注意 Multer 不会为你添加任何扩展名,你的程序应该返回一个完整的文件名。每个函数都传递了请求对象 (req) 和一些关于这个文件的信息 (file),有助于你的决定。
如果是多文件上传
app.post('/api/user',upload.array('avatar',11),(req,res,next)=>{ | |
//req.file 是 avatar 文件数组的信息 | |
//req.body 将具有文本域数据,如果存在的话 | |
}) |
# 七、APIDOC-API 文档生成工具
apidoc 是一个简单的 RESTful API 文档生成工具,他从代码注释中提取特定格式的内容生成文档。支持注入 go、java、C++、Rust 等大部分开发语言,具体可使用 apidoc lang 命令行查看所有的支持列表
apidoc 拥有以下特点
- 跨平台性 linux windows macOS 等都支持
- 支持语言广泛,即使是不支持,也很方便扩展
- 支持多个不同语言的多个项目生成一份文档
- 输出模板可自定义
- 根据文档生成 mock 数据
npm i -g apidoc
Add some apidoc comments anywhere in your source code:
/** | |
* @api {get} /user/:id Request User information | |
* @apiName GetUser | |
* @apiGroup User | |
* | |
* @apiParam {Number} id Users unique ID. | |
* | |
* @apiSuccess {String} firstname Firstname of the User. | |
* @apiSuccess {String} lastname Lastname of the User. | |
*/ |
示例
/** | |
* | |
* @api {post} /api/user addUser | |
* @apiName addUser | |
* @apiGroup userGroup | |
* @apiVersion 1.0.0 | |
* | |
* | |
* @apiParam {string} username 用户名 | |
* @apiParam {string} password 密码 | |
* @apiparam {file} avatar 头像 | |
* | |
* @apiSuccess (200) {json} msg 请求返回状态信息 | |
* | |
* @apiParamExample {multipart/form-data} Request-Example: | |
* { | |
* username:'zhangsan', | |
* password:'12345678', | |
* avatar:file | |
* } | |
* | |
* | |
* @apiSuccessExample {json} Success-Response: | |
* { | |
* code : 200, | |
* msg:' 添加用户成功 ' | |
* } | |
* | |
* | |
*/ | |
app.post('/api/user',async (req,res)=>{ | |
let {username,password} = req.body; | |
let params = [username,password]; | |
// 对头像文件的路径存储到数据库 | |
const avatar = req.file ? `/uploads/${req.file.filename}`:`/images/defualt.png`; | |
// 数据库操作 此处示例并没有对头像进行数据库存储 | |
let sql = 'insert into usertable (username,password,avatar) values (?,?,?);'; | |
try{ | |
let result = await db.exec(sql,params); | |
if(result && result.affectedRow > 0){ | |
res.json({ | |
code:200, | |
msg:'添加用户成功' | |
}); | |
}else{ | |
res.json({ | |
code:-200 | |
msg:'添加用户失败' | |
}) | |
} | |
}catch(err){ | |
//console.log(err); | |
res.json({ | |
code:500, | |
msg:'添加用户失败,服务器异常' | |
}) | |
} | |
}) |
配置生成文档
{ | |
"name":"测试接口文档", | |
"version":"1.0.0", | |
"description":"关于测试接口文档的描述", | |
"title":"测试接口" | |
} |
生成文档
apidoc -i .\route\ -o .\doc
ps: 这玩意是真的好用
# 八、Koa2
基于 nodejs 平台的下一代 web 开发框架
# 1、简介
koa 是由 express 原班人马打造的,致力于成为一个更小、更富有表现力、更健壮的 web 框架。使用 koa 编写 web 应用,通过组合不同的 generator,可以免除重复繁琐的回调函数嵌套,并极大地提升错误处理的效率。koa 不在内核方法中绑定任何中间件,他仅仅提供了一个轻量优雅的函数库,使得编写 web 应用变得得心应手
# 2、快速开始
# 2.1 安装 Koa2
npm init
npm i koa
const Koa = require('koa'); | |
const app = new Koa(); | |
//context == ctx 上下文 | |
app.use((ctx,next)=>{ | |
//ctx.req node 原生的 request 对象 | |
//ctx.res node 原生的 response 对象 | |
// 绕过 koa 的 response 处理是不被支持的,应避免使用一下 node 属性 | |
//ctx.response koa 的 response 对象 | |
//ctx.request koa 的 request 对象 | |
//ctx.response.body = 'hello world'; | |
//ctx.response.body = '<h1>hello world</h1>'; | |
//ctx.response.body = { | |
// name:'zhangsan', | |
// age:21, | |
// sex:'man' | |
//} | |
ctx.body = { | |
name:'zhangsan', | |
age:21, | |
sex:'man' | |
} | |
//ctx.request.xxx 跟 ctx.xxx 是等效的 | |
//ctx.response.xxx 跟 ctx.xxx 是等效的 | |
// 无需担心冲突,因为他们方法名基本没有重复,但还是有的比如 ctx.body | |
}); | |
app.listen(3000,()=>{ | |
console.log('server start...'); | |
}); |
# 2.2 启动 demo
nodemon index.js
# 3、koa vs express
通常都会说 koa 是洋葱模型,这重点在于中间件的设计。但是按照上面的分析,会发现 express 也是类似的,不同的是 express 中间件机制使用了 callback 实现,这样如果出现异步则可能会使你在执行顺序上感到困惑,因此如果我们想做接口耗时统计、错误处理 koa 的这种中间件模式处理起来更方便。最后一点响应机制也很重要,koa 不是立即响应,是整个中间件处理完成在最外层进行了响应,而 express 则是立即响应
# 3.1 更轻量
- koa 不提供内置的中间件
- koa 不提供路由,而是把路由这个库分离出来了 (koa/router)
# 3.2 Context 对象
koa 增加了一个 Context 的对象,作为这次请求的上下文对象 (在 koa2 中作为中间件的第一个参数传入)。同时 Context 上也挂载了 request 和 response 两个对象。和 express 类似,这两个对象都提供了大量的便捷方法辅助开发,这样的话对于在保存一些公有的参数的话变得更加合情合理
# 3.3 异步流程控制
express 采用 callback 来处理异步,koa v1 采用 generator,koa v2 采用 async/await
generator 和 async/await 使用同步的写法来处理异步,明显好于 callback 和 promise
# 3.4 中间件模型
express 基于 connect 中间件,线性模型
koa 中间件采用洋葱模型 (对于每个中间件,在完成一些事情后,可以非常优雅的将控制权传递给下一个中间件,并能够等待他完成,当后续的中间件完成处理后,控制权又回到了自己)
<img src="D:\daima\node\noteImage\ 洋葱模型.jpg" alt="洋葱模型" style="zoom:25%;" />
在 express 中,异步不管 async/await 怎么处理,都是达不到洋葱模型的。而 Koa 异步可以实现同步时的洋葱模型方式
# 4、路由
npm i koa-router
# 4.1 基本用法
const Koa = require('koa'); | |
const Router = require('koa-router'); | |
const app = new Koa(); | |
const router = new Router(); | |
router.post('/list',(ctx)=>{ | |
ctx.body = ['111','222','333']; | |
}); | |
//app.use(router.routes()); | |
app.use(router.routes()).use(router.allowedMethods()); | |
app.listen(3000,()=>{ | |
console.log('server start .....'); | |
}) |
allowedMethods () 方法用于约束请求方式,koa 默认只能 get 请求
const Koa = require('koa'); | |
const Router = require('koa-router'); | |
const app = new Koa(); | |
const router = new Router(); | |
// 增加 | |
router.post('/list',(ctx,next)=>{ | |
ctx.body = { | |
code:200, | |
msg:'添加信息成功' | |
}; | |
}).get('/list',(ctx,next)=>{ | |
ctx.body = { | |
code:200, | |
msg:'获取信息成功', | |
data:['111','222','333'] | |
} | |
}).put('/list',(ctx,next)=>{ | |
ctx.body = { | |
code:200, | |
msg:'修改信息成功', | |
data:['111','222'] | |
} | |
}).delete('/list',(ctx,next)=>{ | |
ctx.body = { | |
code:200, | |
msg:'删除成功' | |
} | |
}) | |
app.use(router.routes()).use(router.allowedMethods()); | |
// app.use(router.routes()); | |
app.listen(3000,()=>{ | |
console.log('server start ....'); | |
}) |
封装
index.js
const Koa = require('koa'); | |
const Router = require('koa-router'); | |
const app = new Koa(); | |
const router = require('./routes/index'); | |
// 应用级 | |
app.use(router.routes()).use(router.allowedMethods()); | |
// app.use(router.routes()); | |
app.listen(3000,()=>{ | |
console.log('server start ....'); | |
}) |
./routes/index.js
const Router = require('koa-router'); | |
const listRouter = require('./list'); | |
const router = new Router(); | |
// 统一加前缀 | |
router.prefix('/api'); // 只要实例化了 router 的地方都可以 | |
router.use('/list',listRouter.routes(),listRouter.allowedMethods()); | |
router.redirect('/','/home'); // 重定向 | |
module.exports = router; |
./routes/list.js
const Router = require('koa-router'); | |
const router = new Router(); | |
// 增加 | |
router.post('/',(ctx,next)=>{ | |
ctx.body = { | |
code:200, | |
msg:'添加信息成功' | |
}; | |
}).get('/',(ctx,next)=>{ | |
ctx.body = { | |
code:200, | |
msg:'获取信息成功', | |
data:['111','222','333'] | |
} | |
}).put('/',(ctx,next)=>{ | |
ctx.body = { | |
code:200, | |
msg:'修改信息成功', | |
data:['111','222'] | |
} | |
}).delete('/',(ctx,next)=>{ | |
ctx.body = { | |
code:200, | |
msg:'删除成功' | |
} | |
}) | |
module.exports = router; |
# 5、静态资源
npm i koa-static
const Koa = require('koa'); | |
const Router = require('koa-router'); | |
const static = require('koa-static'); | |
const path = require('path'); | |
const app = new Koa(); | |
const router = require('./routes/index'); | |
app.use(static(path.join(__dirname,'public'))); | |
// 应用级 | |
app.use(router.routes()).use(router.allowedMethods()); | |
app.listen(3000,()=>{ | |
console.log('server start ....'); | |
}) |
# 6、获取请求参数
# 6.1 get 参数
在 koa 中,获取 get 请求数据源头是 koa 中 request 对象中的 query 方法或 querystring 方法,query 返回是格式化好的参数对象,querystring 返回的是请求字符串,由于 ctx 对 request 的 api 有直接引用的方式,所以获取 get 请求数据有两个途径
- 是从上下文中直接获取 请求对象 ctx.query,返回如 {a:1,b:2} 请求字符串 ctx.querystring,返回如 a=1&b=2
- 是从上下文的 request 对象中获取 请求对象 ctx.request.query,返回如 {a:1,b:2} 请求字符串 ctx.request.querystring,返回 a=1&b=2
# 6.2 post 参数
对于 post 请求的处理,koa-bodyparser 中间件可以把 koa2 上下文的 formData 数据解析到 ctx.request.body 中
const bodyParser = require('koa-bodyparser'); | |
// 使用 ctx.body 解析中间件 | |
app.use(bodyparesr); |
# 7、ejs 模板
# 7.1 安装
npm i koa-views --save
npm i ejs --save
# 7.2 使用模板引擎
const views = require('koa-views'); | |
app.use(views(path.join(__dirname,'views'),{extension:'ejs'})); |
注意 render 是个异步函数,要等待模板解析之后
const Router = require('koa-router'); | |
const router = new Router(); | |
router.get('/',async (ctx,next)=>{ | |
await ctx.render('home',{username:'张三'}); | |
}); | |
module.exports = router; |
<!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> |
# 8、cookie&session
# 8.1 cookie
koa 提供了从上下文直接读取,写入 cookie 的方法
- ctx.cookies.get (name,[options]) 读取上下文请求中的 cookie
- ctx.cookies.set (name,value,[options]) 在上下文中写入 cookie
// 设置 cookie | |
ctx.cookies.set('name','zhangsan'); | |
// 获取 cookie | |
console.log(ctx.cookies.get('name')); |
# 8.2 session
-
koa-session-minimal 适用于 koa2 的 session 中间件,提供存储介质的读写接口
const session = require('koa-session-minimal');
app.use(session({
key:'session_id',
cookie:{
maxAge:1000 * 60
}
}))
-
示例
index.js
const Koa = require('koa');
const static = require('koa-static');
const path = require('path');
const bodyParser = require('koa-bodyparser');
const views = require('koa-views');
const session = require('koa-session-minimal');
const app = new Koa();
const router = require('./routes/index');
app.use(static(path.join(__dirname,'public'))); // 静态资源
app.use(bodyParser()); // 获取 post 请求
// 配置模板引擎
app.use(views(path.join(__dirname,'views'),{extension:'ejs'}));
//session 配置
app.use(session({
key:'nameSessionID',
cookie:{
maxAge:5000
}
}))
//session 判断拦截
app.use(async (ctx,next)=>{
if(ctx.url.includes('login')){
await next();
return;
}
if(ctx.session.user){
// 如果一直访问,重新计时
ctx.session.date = Date.now();
await next();
}else{
ctx.redirect('/login');
}
})
// 应用级
app.use(router.routes()).use(router.allowedMethods());
app.listen(3000,()=>{
console.log('server start ....');
})
login.js
const Router = require('koa-router');
const router = new Router();
router.get('/',async (ctx,next)=>{
await ctx.render('login');
})
router.post('/login',(ctx,next)=>{
// console.log(ctx.request.body);
const {username,password} = ctx.request.body;
if(username === 'liuxin' && password === '123'){
// 设置 session id
ctx.session.user = {
username
}
ctx.body = {
code:200,
msg:'登录成功'
};
}else{
ctx.body = {
code:-200,
msg:'登陆失败'
}
}
})
module.exports = router;
# 9、JSON WEB TOKEN(jwt)
按照 koa 的语法照之前的用法用就可以了
# 10、文件上传
除了模块不同,使用的方法跟 express 的 multer 没有什么区别,注意 koa 的语法就好了
安装
npm install --save @koa/multer multer
使用
const multer = require('@koa/multer'); | |
const upload = multer({dest:'public/uploads/'}); | |
router.post('/',upload.single('avatar'),(ctx,next)=>{ | |
console.log(ctx.request.body,ctx.file); | |
ctx.body = { | |
code:200, | |
msg:'添加用户成功' | |
} | |
}) |
# 九、MySQL
# 1、介绍
付费的商用数据库
- oracle 典型的高富帅
- SQL server 微软自家产品,windows 定制专款
- DB2 IBM 的产品,听起来挺高端
- Sybase 曾经跟微软是好基友,后来关系破裂,现在家境惨淡
这些数据库都是不开源而且付费的,最大的好处是花了钱出了问题可以找厂家解决,不过在 web 的世界里,常常需要部署成千上万的数据库服务器,当然不能把大把大把的银子扔给厂家,所以,无论是 google、facebook 还是国内的 BAT,无一例外都选择了免费的开源数据库。
- MySQL 大家都在用,一般错不了
- PostgreSQL 学术气息有点重,其实挺不错,但知名度没有 MySQL 高
- sqlite 嵌入式数据库,适合桌面和移动应用
作为一个 js 全栈工程师,选择哪个免费数据库呢?当然是 MySQL,因为 MySQL 普及率最高,出了错,可以很容易找到解决方法,而且,围绕 MySQL 有一大堆监控和运维的工具,安装和使用都很方便
# 2、与非关系数据库的区别
关系型和非关系型数据库的主要差异是数据存储的方式。关系型数据库天然就是表格式的,因此存储在数据表的行和列中。数据表可以彼此关联协作存储,也很容易提取数据
与其相反,非关系型数据不适合存储在数据表的行和列中,而是大块组合在一起。非关系型数据通常存储在数据集中,就像文档、键值对或者图结构。你的数据及其特性是选择数据存储和提取方式的首要影响因素
关系型数据库最典型的数据结构是表,由二维表及其之间的联系所组成的一个数据组织
优点
- 易于维护 都是使用表结构,格式一致
- 使用方便 sql 语言通用,可用于复杂查询
- 复杂操作 支持 sql,可用于一个表以及多个表之间非常复杂的查询
缺点
- 读写性能比较差尤其是海量数据的高效率读写
- 固定的表结构,灵活度稍欠
- 高并发读写需求,传统关系型数据库来说,硬盘 I/O 是一个很大的瓶颈
非关系型数据库严格上不是一种数据库,应该是一种数据结构化存储方法的集合,可以是文档或者键值对等
优点
- 格式灵活 存储数据的格式可以是 key.value (键值对) 形式、文档形式、图片形式等等,使用灵活,应用场景广泛,而关系型数据库则只支持基础类型
- 速度快 nosql 可以使用硬盘或者随机存储器作为载体,而关系型数据库只能使用硬盘
- 高扩展性
- 成本低 nosql 数据库部署简单,基本都是开源软件
缺点
- 不提供 sql 支持
- 无事务处理
- 数据结构相对复杂,复杂查询方面稍欠
# 3、sql 语句
建表
CREATE TABLE studenttable(
stu_id INT PRIMARY KEY AUTO_INCREMENT,
stu_name VARCHAR(100) NOT NULL,
stu_score INT NOT NULL,
gender INT NOT NULL,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
class_id INT NOT NULL
);
插入
INSERT INTO studenttable (
stu_name,
stu_score,
gender,
class_id
)
VALUES
(
'张三',
100,
1,
1
);
查询
-- 查询studenttable表中的所有字段
SELECT * FROM studenttable;
-- 查询studenttable表中的某些字段
SELECT stu_id,stu_name,stu_score FROM studenttable;
-- 条件查询
-- 查询studenttable表中成绩大于等于80分的
SELECT * FROM studenttable WHERE stu_score >= 80;
-- 查询studenttable表中成绩大于等于80分的女生
SELECT * FROM studenttable WHERE stu_score >= 80 && gender = 2;
-- 模糊查询
SELECT * FROM studenttable WHERE stu_name LIKE '%小%';
-- 排序 默认从小到大(正序) desc(倒序)
SELECT * FROM studenttable ORDER BY stu_score;
SELECT * FROM studenttable ORDER BY stu_score DESC;
-- 分页查询 limit 限制查几个 offset表示偏移几个从哪查起
SELECT * FROM studenttable LIMIT 5 OFFSET 0;
-- 记录条数
SELECT COUNT(*) FROM studenttable;
SELECT COUNT(*) student_total FROM studenttable; -- student_total是别名
-- 多表查询
SELECT * FROM studenttable,classtable;
-- 这种多表查询又称笛卡尔查询,使用笛卡尔查询时要非常小心,由于结果集是目标表的行数乘积,对两个各自有100行记录的表进行笛卡尔查询将返回10000条记录,对两个各自有10000条记录的表进行笛卡尔查询将返回100000000条记录
-- 联表查询
SELECT a.stu_name stu_name,a.class_id class_id,b.class_name class_name FROM studenttable a INNER JOIN classtable b ON a.class_id = b.class_id;
-- 左连接 左边表有数据就放入记录
SELECT a.stu_name stu_name,a.class_id class_id,b.class_name class_name FROM studenttable a LEFT JOIN classtable b ON a.class_id = b.class_id;
-- 右连接 右边表有数据就放入记录
SELECT a.stu_name stu_name,a.class_id class_id,b.class_name class_name FROM studenttable a RIGHT JOIN classtable b ON a.class_id = b.class_id;
更新
UPDATE studenttable SET stu_score = 80 WHERE stu_id = 1;
删除
DELETE FROM studenttable WHERE stu_id = 1;
# 4、外键
注意
- InnoDB 支持事务,MyISAM 不支持事务。这是 MySql 将默认存储引擎从 MyISAM 变成 InnoDB 的重要原因之一
- InnoDB 支持外键,而 MyISAM 不支持,对一个包含外键的 InnoDB 表转为 MyISAM 会失败
CASCADE
在父表上 update/delete 记录时,同步 update/delete 掉子表的匹配记录
SET NULL
在父表上 update/delete 记录时,将子表上匹配记录的列设为 null (要注意子表的外键列不能为 not null)
NO ACTION
如果子表中有匹配的记录,则不允许对父表对应候选键进行 update/delete 操作
RESTRICT
同 no action 都是立即检查外键约束
# 5、nodejs 操作 mysql
npm i mysql2
const express = require('express'); | |
const app = express(); | |
const mysql = require('mysql2'); | |
app.get('/',async (req,res)=>{ | |
// 创建连接池 进行操作 | |
const config = getDBConfig(); | |
// let stu_id = 2; | |
const promisePool = mysql.createPool(config).promise(); | |
// let userInfo = await promisePool.query('select * from studenttable where stu_id = ?',[stu_id]); | |
let userInfo = await promisePool.query('select * from studenttable'); | |
res.send({ | |
code:200, | |
msg:'查询成功', | |
data:userInfo[0] | |
}) | |
}); | |
app.post('/',async (req,res)=>{ | |
const config = getDBConfig(); | |
let params = [ | |
'小明', | |
150, | |
2, | |
1 | |
] | |
const promisePool = mysql.createPool(config).promise(); | |
try{ | |
let result = await promisePool.query('insert into studenttable (stu_name,stu_score,gender,class_id) values (?,?,?,?)',params); | |
if(result[0].affectedRows > 0){ | |
res.send({ | |
code:200, | |
msg:'添加成功' | |
}) | |
}else{ | |
res.json({ | |
code:-200, | |
msg:'添加失败', | |
}) | |
} | |
}catch(err){ | |
res.json({ | |
code:500, | |
msg:err | |
}) | |
} | |
}) | |
app.put('/',async (req,res)=>{ | |
const config = getDBConfig(); | |
let params = [ | |
'小红', | |
150, | |
1, | |
1, | |
3 | |
] | |
const promisePool = mysql.createPool(config).promise(); | |
try{ | |
let result = await promisePool.query('update studenttable set stu_name = ?,stu_score = ?,gender = ?,class_id = ? where stu_id = ?',params); | |
if(result[0].affectedRows > 0){ | |
res.send({ | |
code:200, | |
msg:'修改成功' | |
}) | |
}else{ | |
res.json({ | |
code:-200, | |
msg:'修改失败', | |
}) | |
} | |
}catch(err){ | |
res.json({ | |
code:500, | |
msg:err | |
}) | |
} | |
}) | |
app.delete('/',async (req,res)=>{ | |
const config = getDBConfig(); | |
let params = [ | |
2 | |
] | |
const promisePool = mysql.createPool(config).promise(); | |
try{ | |
let result = await promisePool.query('delete from studenttable where stu_id = ?',params); | |
if(result[0].affectedRows > 0){ | |
res.send({ | |
code:200, | |
msg:'删除成功' | |
}) | |
}else{ | |
res.json({ | |
code:-200, | |
msg:'删除失败', | |
}) | |
} | |
}catch(err){ | |
res.json({ | |
code:500, | |
msg:err | |
}) | |
} | |
}) | |
app.listen(3000,()=>{ | |
console.log('server start ....'); | |
}) | |
function getDBConfig(){ | |
return { | |
host:'127.0.0.1', | |
port:'3306', | |
user:'root', | |
password:'root', | |
database:'node_test', | |
connectionLimit:1 | |
} | |
} |
# 10、socket
# 1、websocket 介绍
应用场景 弹幕 媒体聊天 协同编辑 基于位置的应用 体育实况更新 股票基金报价实时更新
WebSocket 并不是全新的协议,而是利用了 http 协议来建立连接。我们来看看 WebSocket 连接是如何创建的
首先,websocket 连接必须由浏览器发起,因为请求协议是一个标准的 http 请求,格式如下
GET ws://localhost: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 请求有几点不同
- get 请求的地址不是类似 /path/,而是以 ws:// 开头的地址
- 请求头 Upgrade:webscoket 和 Connection:Upgrade 表示这个连接将要被转换为 WebSocket 连接
- Sec-webSocket-Key 是用于标识这个连接,并非用于加密数据
- 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/tls 协议
浏览器支持
很显然,要支持 websocket 通信,浏览器得支持这个协议,这样才能发出 ws://xxx 的请求。目前支持 websocket 的主流浏览器如下
- chrome
- firefox
- ie>=10
- sarafi>=6
- android>=4.4
- IOS>=8
服务器支持
由于 websocket 是一个协议,服务器具体怎么实现,取决于所有编程语言和框架本身。nodejs 本身支持的协议包括 tcp 协议和 http 协议,要支持 websocket 协议,需要对 nodejs 提供的 httpserver 做额外的开发。已经有若干基于 nodejs 的稳定可靠的 websocket 实现,我们直接用 npm 安装使用即可
# 2、ws 模块
服务器
const express = require('express'); | |
const path = require('path'); | |
const app = express(); | |
const WebSocket = require('ws'); | |
const WebSocketServer = WebSocket.WebSocketServer; | |
app.use(express.static(path.join(__dirname,'public'))); | |
app.get('/',(req,res)=>{ | |
res.json({code:200}); | |
}) | |
//websocket | |
const wss = new WebSocketServer({port:8080}); | |
wss.on('connection',(ws)=>{ | |
ws.on('message',(data)=>{ | |
console.log('%s',data); | |
// 转发给其他人 | |
wss.clients.forEach((client)=>{ | |
if(client !== ws && client.readyState === WebSocket.OPEN){ | |
client.send(data,{binary:false}); | |
} | |
}) | |
}) | |
ws.send('欢迎来到聊天室'); | |
}) | |
app.listen(3000); |
客户端
<!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> | |
</body> | |
<script> | |
const ws = new WebSocket('ws://localhost:8080'); | |
ws.onopen = ()=>{ | |
console.log('连接成功'); | |
//ws.send (' 有人加入聊天室 ') | |
} | |
ws.onmessage = (res)=>{ | |
console.log(res.data); | |
} | |
ws.onerror = (err)=>{ | |
console.log('error'); | |
} | |
</script> | |
</html> |
# 3、socket.io
npm i socket.io