一 接口
1.1 node处理http请求
- 从浏览器输入地址 到 显示 的过程是什么?
- DNS解析,建立TCP连接,发送HTTP请求
- server 接收到http请求,处理并返回
- 客户端接收到返回的数据,处理数据、渲染页面
- 处理HTTP请求
-
npm init
初始化项目 -
const http = require(“http”); const server = http.createServer((req, res) => { res.end(“hello world”) }); server.listen(8000)
-
get请求和querystring
-
index.js
const http = require("http"); const querystring = require("querystring"); const server = http.createServer((req, res) => { console.log("method: ", req.method); //==> GET const url = req.url; console.log("url: ", url); req.query = querystring.parse(url.split("?")[1]); console.log("qurey: ", req.query); res.end(JSON.stringify(req.query)); }); server.listen(8000);
-
node index.js
-
-
post请求和postdata
- Postman / chrome 插件 模拟post请求
- index.js
const http = require("http"); const server = http.createServer((req, res) => { if (req.method === "POST") { // 数据格式 console.log("content-type:", req.headers["content-type"]); // 接收数据 let postData = ""; req.on("data", (chunk) => { postData += chunk.toString(); }); req.on("end", () => { console.log(postData); res.end("hello world"); }); } }); server.listen(8000);
-
路由
-
综合示例
-
const http = require("http"); const querystring = require("querystring"); const server = http.createServer((req, res) => { const { method, url } = req; const path = url.split("?")[0]; const query = querystring.parse(url.split("?")[1]); // 设置返回格式为JSON res.setHeader("Content-type", "application/json"); // 返回的数据 const resData = { method, url, path, query, }; if (method === "GET") { res.end(JSON.stringify(resData)); } if (method === "POST") { let postData = ""; req.on("data", (chunk) => { postData += chunk.toString(); }); req.on("end", () => { resData.postData = postData; res.end(JSON.stringify(resData)); }); } }); server.listen(8000);
-
-
1.2 搭建开发环境
-
nodemon监测文件变化,自动重启node
-
cross-env设置环境变量,兼容mac / window / linux
-
package.json
{ "main": "bin/www.js", “scripts”:{ “dev”: “cross-env NODE_ENV=dev nodemon ./bin/www.js”, “prd”: “cross-env NODE_ENV=production nodemon ./bin/www.js” } }
- process.env.NODE_ENV
-
bin/www.js
只关心HTTP纯技术层const http = require('http') const serverHandle = require('../app') const PORT = 8000 const server = http.createServer(serverHandle) server.listen(PORT)
-
app.js
const serverHandle = (req, res) => { // 设置返回格式为JSON res.setHeader('Content-type', 'application/json') // 获取 path req.path = req.url.split('?')[0] // 解析 query req.query = querystring.parse(req.url.split('?')[1]) })
1.3 开发接口(暂不连数据库,不考虑登录)
- 初始化路由
-
app.js
const handleBlogRouter = require('./src/router/blog') const handleUserRouter = require('./src/router/user') const serverHandle = (req, res) => { // 设置返回格式为JSON res.setHeader('Content-type', 'application/json’)}; // 获取 path const url = req.url; req.path = url.split(‘?’)[0] // 解析 query req.query = querystring.parse(req.url.split('?')[1]) // 处理blog路由 const blogData = handleBlogRouter(req, res); if(blogData){ res.end(JSON.stringify(blogData)); return; } // 处理user const userData = handleUserRouter(req, res); if(userData){ res.end(JSON.stringify(userData)); return; } // 未命中路由,返回404 res.writeHead(404, {“Content-type”: “text-plain”}); res.write(“404 Not Found\n”); res.end(); } module.exports = serverHandle
-
src/router/blog.js
const handleBlogRouter = (req, res) => { const method = req.method // GET POST const id = req.query.id // 获取博客列表 if(method === 'GET' && req.path === '/api/blog/list') { return { msg: 'this is blog list api' } } // ... } module.exports = handleBlogRouter
-
src/router/user.js
const handleUserRouter = (req, res) => { const method = req.method // GET POST const id = req.query.id // 登录 if(method === ‘POST’ && req.path === '/api/user/login’) { return { msg: 'this is login api' } } // ... } module.exports = handleUserRouter
-
-
src/model/resModel.js
// 只关心状态class BaseModel { constructor(data, message) { if(typeof data === 'string') { this.message = data data = null message = null } if(data) { this.data = data } if(message) { this.message = message } } } class SuccessModel extends BaseModel { constructor(data, message) { super(data, message) this.errno = 0 } } class ErrorModel extends BaseModel { constructor(data, message) { super(data, message) this.errno = -1 } } module.exports = { SuccessModel, ErrorModel }
- src/router/blog.js
// 只关心路由const { getList} = require('../controller/blog’); const { SuccessModel, ErrorModel } = require('../model/resModel’); const handleBlogRouter = (req, res) => { const method = req.method // GET POST const id = req.query.id // 获取博客列表 if(method === 'GET' && req.path === '/api/blog/list') { const author = req.query.author || ‘’; const keyword = req.query.keyword || '' ; const result = getList(author, keyword) return result.then(listData => { return new SuccessModel(listData) }) } // ... } module.exports = handleBlogRouter
- src/router/blog.js
-
- 返回假数据
-
src/controller/blog.js
只关心数据 操作数据库const getList = (author, keyword)=>{ return [ { id:1, title: “title1”, content: “test text”, createTime: 1546620491112, author: ‘z3’ }, // ... ] } module.exports = { getList }
-
app.js
// 处理 post data const getPostData = (req) => { const promise = new Promise((resolve, reject) => { if(req.method !== 'POST') { resolve({}) return } if(req.headers['content-type'] !== 'application/json') { resolve({}) return } let postData = '' req.on('data', chunk => { postData += chunk.toString() }) req.on('end', () => { if(!postData) { resolve({}) return } resolve( JSON.parse(postData) ) }) }) return promise }
-
const serverHandle = (req, res) => { // 设置返回格式为JSON res.setHeader('Content-type', 'application/json') // 获取 path req.path = req.url.split('?')[0] // 解析 query req.query = querystring.parse(req.url.split('?')[1]) // post data getPostData(req).then(postData => { req.body = postData // 登录路由 const userResult = handleUserRouter(req, res) if(userResult) { userResult.then(userData => { res.end( JSON.stringify(userData) ) }) return } // 处理博客路由 const blogResult = handleBlogRouter(req, res) if(blogResult) { blogResult.then(blogData => { res.end( JSON.stringify(blogData) ) }) return } // 未命中路由 res.writeHead(404, {'Content-type': 'text/plain'}) res.write('404 Not Found!\n') res.end() }) }
-
-
二 MySQL数据存储
2.1 建库
- 在MySQLWorkbench中 点库图标建库
或CREATE SCHEMA `myblog` ;
2.2 建表
- 在MySQLWorkbench中, 左侧myblog > Tables > 右键 > create table > users or blogs
- datatype
- varchar(50) 长度50
- longtext 长文本 最多4G大小
- int 整数
- bigint(20)
- PK主建 不能重复
- NN不能为空
- AI自动增加
- 点击右下 apply 按钮 完成
2.3 表操作
- 使用sql语句
-
use myblog; show tables; -- 注释 -- 如报错 Error Code: 1175. You are using safe update mode an... 执行下面语句 SET SQL_SAFE_UPDATES=0
-
- 增
-
insert into users(username, `password`, realname) values('zhang3', '123', '张三'); insert into users(username, `password`, realname) values('li4', '123', '李四');
-
- 删
-
-- 删 delete from users where username='li4'; -- 软删 update users set state='0' where username='li4'; -- 查state不等于0 使用<> select * from users where state<> ‘0’;
-
- 改
-
-- 改 update users set realname="李四2" where username='li4';
-
- 查
-
-- 查 select * from users; select id, username from users; -- 查 where 条件查询 select * from users where username="zhang3"; select * from users where username="zhang3" and `password`='123'; select * from users where username="zhang3" or `password`='123'; -- 查 like 模糊查询 select * from users where username like "%zhang%"; -- 查 order by 排序 select * from users order by id; -- 查 order desc 倒序 select * from users order by id desc;
-
2.4 Node操作mySQL
-
npm i mysql
-
index.js
const mysql = require("mysql"); //创建链接对象 const con = mysql.createConnection({ host: "localhost", user: "root", password: "XXXXXX", port: "3306", database: "myblog", }); // 开始连接 con.connect(); // 执行 sql 语句 const sql = "select * fromt users"; con.query(sql, (err, result) => { if (err) { console.error(err); return; } console.log(result); }); // 关闭连接 con.end();
-
-- 解决VSCode无法连接 errno: 1064 use mysql; alter user 'root'@'localhost' identified with mysql_native_password by 'XXXXXX'; flush privileges;
-
src/conf/db.js
const env = process.env.NODE_ENV let MYSQL_CONF if(env === 'dev') { MYSQL_CONF = { host: 'localhost', user: 'root', password: ‘XXXXXX’, port: '3306', database: 'myblog' } } if(env === 'production') { MYSQL_CONF = { host: 'localhost', user: 'root', password: 'XXXXXX', port: '3306', database: 'myblog' } } module.exports = { MYSQL_CONF }
-
src/db/mysql.js
const mysql = require('mysql') const { MYSQL_CONF } = require('./../conf/db') // 创建链接对象 const con = mysql.createConnection(MYSQL_CONF) // 开始连接 con.connect() // 统一执行 sql 的函数 function exec(sql) { const promise = new Promise((resolve, reject) => { con.query(sql, (err, result) => { if(err) { reject(err) return } resolve(result) }) }) return promise } module.exports = { exec }
2.5 API对接mySQL
-
src/controller/blog.js
只关心数据 操作数据库const { exec } = require('./../db/mysql') const getList = (author, keyword) => { let sql = `select * from blogs where 1=1 ` if(author) { sql += `and anthor='${author}' ` } if(keyword) { sql += `and title like '%${keyword}%' ` } sql += `order by createtime desc;` return exec(sql) } // ... module.exports = { getList }
-
src/router/blog.js
// 只关心路由const { getList} = require('../controller/blog’); const { SuccessModel, ErrorModel } = require('../model/resModel’); const handleBlogRouter = (req, res) => { const method = req.method // GET POST const id = req.query.id // 获取博客列表 if(method === 'GET' && req.path === '/api/blog/list') { const author = req.query.author || ‘’; const keyword = req.query.keyword || '' ; const result = getList(author, keyword) return result.then(listData => { return new SuccessModel(listData) }) } // ... } module.exports = handleBlogRouter
-
app.js
const handleBlogRouter = require('./src/router/blog') const serverHandle = (req, res) => { // ... // 处理blog路由 const blogData = handleBlogRouter(req, res); if(blogData){ res.end(JSON.stringify(blogData)); return; } // 处理user // 未命中路由,返回404 } module.exports = serverHandle
三 登录
3.1 cookie 和 session
- cookie
-
- cookie是存在浏览器的一段字符串(最大5kb)
- 跨域不共享
- 格式如k1=v1;k2=v2;k3=k3; 因此可存储结构化数据
- 每次发送http请求,会将请求域的cookie一起发送给server
- server 可修改cookie并返回给浏览器
- 浏览器中也可通过JS修改cookie(有限制)
- cookie是存在浏览器的一段字符串(最大5kb)
- JS操作cookie
-
// 累加 document.cookie = ‘k1=100;’
-
- server端操作cookie
-
// 解析cookie req.cookie = {}; const cookieStr = req.headers.cookie || ""; // console.log(cookieStr); cookieStr.split(";").map(item => { if (!item) return; const arr = item.split("="); const key = arr[0].trim(); const value = arr[1].trim(); req.cookie[key] = value; });
-
使用httpOnly限制前端修改cookie, expires设置过期时间
res.setHeader( "Set-Cookie", `userid=${userId};path='/';httpOnly;expires=${getCookieExpires()}` );
-
- cookie 登录流程
- 1.前端发送 username password in cookie 到 后端
2. 后端做验证
3. 验证成功 userid写入cookie,设置httpOnly 和 expires给前端
4. 前端在次访问后端,浏览器将自动带userid cookid
5. 后端验证userid 判断是否登录
- 1.前端发送 username password in cookie 到 后端
-
- session
- cookie 存在的问题
- 问题:会暴露username,很危险。
解决:可在cookie中存储userid,server端对应username -- 此为session
- 问题:会暴露username,很危险。
- session代码示例
-
// session数据 const SESSION_DATA = {};
-
// 解析session let needSetCookie = false; let userId = req.cookie.userid; if (userId) { if (!SESSION_DATA[userId]) { SESSION_DATA[userId] = {}; } } else { needSetCookie = true; userId = `${Date.now()}_${Math.random()}`; SESSION_DATA[userId] = {}; } req.session = SESSION_DATA[userId];
-
router/user.js
后端设置用户数据到req.session, 保证安全// 登陆 if (method === "POST" && req.path === "/api/user/login") { const { username, password } = req.body; const result = login(username, password); return result.then(data => { if (data.username) { // 设置session req.session.username = data.username; req.session.realname = data.realname; return new SuccessModel(data); } return new ErrorModel("登录失败"); }); }
-
- cookie 存在的问题
3.2 session写入redis
- session的问题
- SESSION_DATA 占用nodeJS内存
- 访问量过大,内存暴增
- 正式上线后运行是多进程,进程之间内存无法共享
- SESSION_DATA 占用nodeJS内存
- redis
-
- 数据存放在内存中
- 相比mysql,访问速度快
- 成本更高,可存储的数据更少
- 数据存放在内存中
-
- 为何sessin适合用redis
-
- session访问频繁,对性能要求极高
- session可不考虑断电丢失数据的问题
- session数据量不会太大
- session访问频繁,对性能要求极高
-
- 为何网站数据不适用redis
-
- 操作频率不高
- 断电不能丢失,必须保留
- 数据量太大,内存成本太高
- 操作频率不高
-
- redis基础
-
安装
brew install redis
启动server
redis-server
启动cli
redis-cli
set get
set myname zhang3 get myname 查看所有key ```keys *``` 删除key ```del myname
-
3.3 开发登录功能 node链接redis
- demo
npm i redis --save
-
const redis = require('redis') const client = redis.createClient(6379,'127.0.0.1'); client.on('error',err=>{ console.error(err) }) client.set('myname','zhangsan2',redis.print) client.get('myname',(err,val)=>{ if(err){ console.log(err); return; } console.log('val:',val); client.quit() })
- 封装工具函数
-
config/db.js
const env = process.env.NODE_ENV // 配置 let REDIS_CONF if (env === 'dev') { REDIS_CONF = { port: 6379, host: '127.0.0.1' } } if (env === 'production') { // redis REDIS_CONF = { port: 6379, host: '127.0.0.1' } } module.exports = { REDIS_CONF }
-
db/redis.js
const redis = require("redis"); const { REDIS_CONF } = require("../config/db"); // 创建客户端, 不quit() 因为是单例模式 const client = redis.createClient(REDIS_CONF.port, REDIS_CONF.host); client.on("error", err => { console.error(err); }); const set = (key, val) => { if (typeof val === "object") { // 这边需要val是个string val = JSON.stringify(val); } client.set(key, val, redis.print); }; const get = key => { const promise = new Promise((resolve, reject) => { client.get(key, (err, val) => { if (err) { reject(err); return; } // 未取到数据时,返回null if (val === void 0) { resolve(null); return; } // try catch 是为了兼容JSON.parse try { resolve(JSON.parse(val)); } catch (error) { resolve(val); } }); }); return promise; }; module.exports = { set, get };
-
router/blog.js
const loginCheck = req => { console.log("req.session",req.session); if (!req.session.username) { return Promise.resolve( new ErrorModel('尚未登录') ) } } // 新建博客接口 if (method === "POST" && req.path === "/api/blog/new") { const loginCheckResult = loginCheck(req) if (loginCheckResult) { // 未登录 return loginCheckResult } // 已登录 req.body.author = req.session.username // 假数据 const result = newBlog(req.body) return result.then(data => { return new SuccessModel(data) }) }
-
app.js
const { get, set } = require("./src/db/redis"); // 解析session(使用redis) let needSetCookie = false; let userId = req.cookie.userid; console.log("userId", userId); if (!userId) { needSetCookie = true; userId = `${Date.now()}_${Math.random()}`; // 初始化redis 中session的初始值 set(userId, {}); } // 为req创建一个sessionId属性, req.sessionId = userId; get(req.sessionId) .then((sessionData) => { if (!sessionData) { // 初始化redis 中session的初始值 set(req.sessionId, {}); // 设置session req.session = {}; } else { req.session = sessionData; } return getPostData(req); }) .then((postData) => { req.body = postData; const blogResult = handleBlogRouter(req, res); if (blogResult) { blogResult.then((blogData) => { if (needSetCookie) { res.setHeader( "Set-Cookie", `userid=${userId};path='/';httpOnly;expires=${getCookieExpires()}` ); } res.end(JSON.stringify(blogData)); }); return; } const userResult = handleUserRouter(req, res); if (userResult) { userResult.then((userData) => { if (needSetCookie) { res.setHeader( "Set-Cookie", `userid=${userId};path='/';httpOnly;expires=${getCookieExpires()}` ); } res.end(JSON.stringify(userData)); }); return; } // 未命中路由: 返回404 res.writeHead(404, { "Content-type": "text/plain", }); res.write("404 Not Found\n"); res.end(); });
-
四 日志
4.1 日志类型
- 访问日志 access log
- 自定义日志(包括自定义事件、错误记录等)
4.2 node文件操作 stream
-
const fs = require("fs"); const path = require("path"); const fileName = path.resolve(__dirname, "data.jon.txt"); // read file fs.readFile(fileName, (err, data) => { if (err) { console.error(err); return; } // data为二进制,需转换为字符串 console.log(data.toString()); }); // write file const content = "new content\n"; const opt = { flag: "a", // 追加, 覆盖用 w }; fs.writeFile(fileName, content, opt, (err) => { if (err) { console.error(err); return; } }); // exists fs.exists(fileName, (exist) => { console.log(exist); });
-
pipe
const path = require("path") const fs = require("fs") const fileName1 = path.resolve(__dirname,'data.txt') const fileName2 = path.resolve(__dirname,'data-back.txt') const readSteam = fs.createReadStream(fileName1) const writeSteam = fs.createWriteStream(fileName2) readSteam.pipe(writeSteam) readSteam.on('data',chunk=>{ console.log(chunk.toString()); }) readSteam.on('end',()=>{ console.log("copy done"); })
-
const http = require('http'); const path = require("path") const fs = require("fs") const fileName1 = path.resolve(__dirname,'data.txt') const server = http.createServer((req, res)=>{ if(req.method === ‘GET’){ const readSteam = fs.createReadStream(fileName1) readSteam.pipe(res) } }) server.listen(8000)
-
4.3 日志功能开发和使用
-
src/logs/access.log
src/logs/error.log
src/logs/event.log -
src/utils/log.js
const path = require('path') const fs = require("fs") // 写日志 function writeLog(writeStream, log) { writeStream.write(log + '\n') } // 生成writeStream function createWriteStream(fileName) { const fullFileName = path.join(__dirname, "../../logs/", fileName) const writeStream = fs.createWriteStream(fullFileName, { flags: 'a' }) return writeStream; } // 写访问日志 const accessWriteStream = createWriteStream('access.log') function access(log) { writeLog(accessWriteStream, log) } module.exports = { access }
-
src/app.js
const serverHandle = (req, res) => { // 记录 access log access( `${req.method} -- ${req.url} -- ${ req.headers["user-agent"] } -- ${Date.now()}` ); //...
4.4 日志文件拆分、日志内容分析
-
- 日志内容会慢慢积累,放在一个文件中不好处理
- 按时间划分日志文件:2020-10-10.access.log
- 实现方式:linux的crontab命令
- 日志内容会慢慢积累,放在一个文件中不好处理
- crontab
- 设置定时任务,格式,*****command
- 将access.log拷贝并重命名为2020-10-10.access.log
- 清空access.log文件,继续积累日志
-
!/bin/sh cd /Uders/code-demo/logs.log cp access.log $(data +%Y-%m-%d).access.log echo “” > access.log
- 日志分析
- 如针对access.log日志,分析chrome的占比
- 日志是按行存储的,一行就是一条日志
- 使用nodejs的readline(基于stream,效率高)
- src/utils/readline.js
const fs = require("fs"); const path = require("path"); const readline = require("readline"); // 文件名 const fileName = path.join(__dirname, "../../logs", "access.log"); // 创建 read stream const readStream = fs.createReadStream(fileName); // 创建 radline 对象 const rl = readline.createInterface({ input: readStream, }); let chromeNum = 0; let sum = 0; //逐行读取 rl.on("line", (lineData) => { if (!lineData) { return; } //记录总行数 sum++; const arr = lineData.split(" -- "); if (arr[2] && arr[2].indexOf("Chrome") > 0) { // 累加 chrome 数量 chromeNum++; } }); rl.on("close", () => { console.log("chrome 占比:" + chromeNum / sum); });
五 安全
* server端攻击方式非常多,预防手段也非常多
- 本文只讲解常见能通过web server(nodejs)层面预防的
- 有些攻击需要硬件和服务来支持(需要OP支持),如DDOS
5.1 SQL注入
-
- 最原始、最简单的攻击
- 攻击方式:输入一个sql片段,最终拼接成一段攻击代码
- 预防措施:使用mysql的escape函数处理输入内容即可
-
const mysql = require('mysql') const login = (username, password) => { username = mysql.escape(username); password = mysql.escape(password); // ... };
- 最原始、最简单的攻击
5.2 XSS攻击
-
- 攻击方式:在页面展示内容中掺杂JS代码,以获取网页信息
-
预防措施:转换生成JS的特殊字符
-
& => & < => < > => > “ => " ‘ => ' / => /
-
blog.js
const xss = require('xss'); const newBlog = (blogData = {}) => { const title = xss(blogData.title) //... }
-
- 攻击方式:在页面展示内容中掺杂JS代码,以获取网页信息
5.3 密码加密
-
- 攻击方式:获取用户名和密码,再去尝试登录其他系统
- 预防措施:将密码加密
-
const crypto = require("crypto"); // 密匙 const SECRET_KEY = "AbEdQe_123#$"; // md5加密 function md5(content) { let md5 = crypto.createHash("md5"); return md5.update(content).digest("hex"); } // 加密函数 function genPassword(password) { const str = `password=${password}&key=${SECRET_KEY}`; return md5(str); }
- 攻击方式:获取用户名和密码,再去尝试登录其他系统
六 使用express重构
6.1 中间件原理
-
安装
npm i express-generator -g
创建目录
express blog-express
package.json
“scripts”:{ “dev”: “cross-env NODE_ENV=dev nodemon ./bin/www” }
-
app.use((req, res, next) => { console.log("请求开始...", req.method, req.url); next(); }); app.use('/api', (req, res, next)=>{ console.log('处理 /api 路由') next() }) app.get("/api/get-cookit", (req, res, next) => { console.log("get /api/get-cookit 路由"); res.json({ erron: 0, data: "ok" }); }); app.post("/api/get-post-data", (req, res, next) => { console.log("post /api/get-post-data 路由"); res.json({ erron: 0, data: "ok" }); }); app.use((req, res, next) => { // 404 res.json({ erron: 1, data: 404 }); });
- 通过next() 往下串连
- 可传多和函数
app.get("/api/get-cookit", loginCheck, (req, res, next) => { console.log("get /api/get-cookit 路由"); res.json({ erron: 0, data: "ok" }); });
6.2 使用中间件机制
-
app.js
// 将GET, 解析到req.cookie var cookieParser = require('cookie-parser'); // 解析JSON POST数据,挂载到req.body app.use(express.json()); // 解析非JSON格式,挂载到req.body app.use(express.urlencoded({ extended: false }));
-
app.js 日志
// 记录log var logger = require('morgan'); const ENV = process.env.NODE_ENV; if (ENV !== "production") { // 开发环境 / 测试环境 app.use(logger("dev")); } else { // 线上环境 const logFileName = path.join(__dirname, "logs", "access.log"); const writeStream = fs.createWriteStream(logFileName, { flags: "a", }); app.use( logger("combined", { stream: writeStream, }) ); }
- 开发环境输出到控制台
线上环境输出到access.log
- 开发环境输出到控制台
-
app.js session
// session redis const session = require('express-session') const RedisStore = require('connect-redis')(session) const redisClient = require("./db/redis"); const sessionStore = new RedisStore({ client: redisClient, }); app.use( session({ secret: "WJiol#23123_", cookie: { // path: '/', // 默认配置 // httpOnly: true, // 默认配置 maxAge: 24 * 60 * 60 * 1000, }, store: sessionStore, }) );
- db/redis.js
const redis = require('redis') const { REDIS_CONF } = require('../conf/db.js') // 创建客户端 const redisClient = redis.createClient(REDIS_CONF.port, REDIS_CONF.host) redisClient.on('error', err => { console.error(err) }) module.exports = redisClient
- db/redis.js
-
app.js router
const userRouter = require('./routes/user’); app.use('/api/user', userRouter);
-
routes/index.js
var express = require('express'); var router = express.Router(); // 与上层app.use中 ’/api/blog’一起拼接成了 ‘/api/blog/list’ router.get(‘/list’, function(req, res, next) { res.json({ errno: 0, data:’OK’ }); });
-
routes/user.js
var express = require('express'); var router = express.Router(); const { login } = require('../controller/user') const { SuccessModel, ErrorModel } = require('../model/resModel') router.post('/login', function(req, res, next) { const { username, password } = req.body const result = login(username, password) return result.then(data => { if (data.username) { // 设置 session req.session.username = data.username req.session.realname = data.realname res.json( new SuccessModel() ) return } res.json( new ErrorModel('登录失败') ) }) }); module.exports = router;
-
router/blog.js
const loginCheck = require('../middleware/loginCheck') router.post('/new', loginCheck, (req, res, next) => { req.body.author = req.session.username const result = newBlog(req.body) return result.then(data => { res.json( new SuccessModel(data) ) }) })
- loginCheck.js
const { ErrorModel } = require('../model/resModel') module.exports = (req, res, next) => { if (req.session.username) { next() return } res.json( new ErrorModel('未登录') ) }
- loginCheck.js
-
-
app.js 404
// 优化404页面提示文本 var createError = require('http-errors'); app.use(function(req, res, next) { next(createError(404)); });
6.3 中间件实现思路
- 分析
- 通过app.use注册
- 遇到http请求,根据path和method判断触发哪些
- 实现next机制,即上一个通过next触发下一个
七 使用Koa2重构
7.1 开发koa2
-
安装
npm i koa-generator -g Koa2 koa2-test npm i & npm run dev
-
app.js 登录
const session = require('koa-generic-session') const redisStore = require('koa-redis') const user = require('./routes/user') // session 配置 app.keys = ['WJiol#23123_'] app.use(session({ // 配置 cookie cookie: { path: '/', httpOnly: true, maxAge: 24 * 60 * 60 * 1000 }, // 配置 redis store: redisStore({ // all: '127.0.0.1:6379' // 写死本地的 redis all: `${REDIS_CONF.host}:${REDIS_CONF.port}` }) })) app.use(user.routes(), user.allowedMethods())
- routes/user.js
const router = require('koa-router')() const { login } = require('../controller/user') const { SuccessModel, ErrorModel } = require('../model/resModel') // 路由前缀 router.prefix('/api/user') router.post('/login', async function (ctx, next) { const { username, password } = ctx.request.body const data = await login(username, password) if (data.username) { // 设置 session ctx.session.username = data.username ctx.session.realname = data.realname // 返回前端数据 ctx.body = new SuccessModel() return } ctx.body = new ErrorModel('登录失败') }) module.exports = router
- routes/user.js
-
app.js logger
const logger = require('koa-logger') const path = require('path') const fs = require('fs') const morgan = require('koa-morgan') // logger app.use(async (ctx, next) => { const start = new Date() await next() const ms = new Date() - start console.log(`${ctx.method} ${ctx.url} - ${ms}ms`) }) const ENV = process.env.NODE_ENV if (ENV !== 'production') { // 开发环境 / 测试环境 app.use(morgan('dev')); } else { // 线上环境 const logFileName = path.join(__dirname, 'logs', 'access.log') const writeStream = fs.createWriteStream(logFileName, { flags: 'a' }) app.use(morgan('combined', { stream: writeStream })); }
7.2 分析koa2中间件原理
八 上线配置
8.1 线上环境的挑战
- 服务器稳定性
- 充分利用服务器硬件资源,以便提高性能
- 线上日志记录
8.2 PM2可解决以上问题
- 进程守护,系统崩溃自动重启
- 启动多进程,充分利用CPU和内存
- 自带日志记录功能
8.3 PM2
- 下载安装
-
npm i pm2 -g pm2 --version
-
- 基本使用
- package.json
"scripts": { "prd": "cross-env NODE_ENV=production pm2 start bin/www", },
- package.json
- 常用命令
pm2 start app.js
pm2 list
pm2 restart <AppName>/<id>
pm2 stop <AppName>/<id>
pm2 delete <AppName>/<id>
pm2 info <AppName>/<id>
pm2 log <AppName>/<id>
pm2 moint <AppName>/<id>
- 进程守护
-
模拟错误
if(req.url === ‘/err’){ throw new Error() }
-
pm2 list
查看到 restart 变多,知道重启了pm2 log app
通过pm2 log 查看日志分析原因
-
- 常用配置
- pm2.conf.json
{ "apps": { "name": "pm2-test-server", "script": "app.js", "watch": true, "ignore_watch": [ "node_modules", "logs" ], "instances": 4, // 多进程 "error_file": "logs/err.log", "out_file": "logs/out.log", "log_date_format": "YYYY-MM-DD HH:mm:ss" } }
- pm2.conf.json