一、express
官网参考:https://expressjs.com/zh-cn/
- express 是 nodejs 最常用的 web server 框架
- express 下载、安装、使用、express 中间件机制
- 开发接口、连接数据库、实现登录、日志记录
- 分析 express 中间件原理
1. 介绍 express
安装(使用脚手架 express-generator)
shellnpm install express-generator -g express express-test npm i & npm start
初始化代码,处理路由
- 新建项目shell
express express-blog npm i nodemon cross-env -D # 配置 启动脚本 "dev": "cross-env NODE_ENV=dev nodemon ./bin/www"
- 介绍 app.js
- 各个插件的作用js
// 处理未定义的路由 const createError = require('http-errors'); const express = require('express'); const path = require('path'); // 解析 cookie const cookieParser = require('cookie-parser'); // 日志 const logger = require('morgan'); // 定义的路由接口 const indexRouter = require('./routes/index'); const usersRouter = require('./routes/users'); const app = express(); // view engine setup app.set('views', path.join(__dirname, 'views')); app.set('view engine', 'jade'); // 注册 app.use(logger('dev')); app.use(express.json()); app.use(express.urlencoded({ extended: false })); 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 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.message = err.message; res.locals.error = req.app.get('env') === 'dev' ? err : {}; // render the error page res.status(err.status || 500); res.render('error'); }); module.exports = app;
- 各个插件的作用
- 新建项目
中间件的使用和执行过程
shellnpm init -y; npm i express -D;
jsconst express = require('express'); // 本次 http 请求的实例 const app = express(); app.use((req, res, next) => { console.log('请求开始',req.method, req.url); next(); }) app.use((req, res, next) => { req.cookie = { userId: '123' } next() }) app.use((req, res, next) => { // 假设处理 post data // 异步 setTimeout(() => { req.body = { a: 100, b: 200 } next(); }) }) app.use('/api', (req, res, next) => { console.log('处理 /api 路由'); next() }) app.get('/api', (req, res, next) => { console.log('get /api 路由'); next() }) app.post('/api', (req, res, next) => { console.log('post /api 路由'); next() }) // 模拟登陆验证 function loginCheck(req, res, next) { console.log('模拟登陆成功'); setTimeout(() => { next() }) } app.get('/api/get-cookie', loginCheck, (req, res, next) => { console.log('get /api/get-cookie 路由'); res.json({ errno: 0, data: req.cookie }) }) app.post('/api/get-post-data', (req, res, next) => { console.log('get /api/get-post-data 路由'); res.json({ errno: 0, data: req.body }) }) app.use('/api', (req, res, next) => { console.log('处理 404'); res.json({ errno: -1, msg: '404 not fount' }) }) app.listen(3000, () => { console.log('server is running on port 3000!') })
总结
- 初始化代码中,各个插件的作用
- express 如何处理路由
- express 中间件
2. express 开发blog接口
- 插件安装shell
npm i mysql xss -S;
- 登录
使用 express-session 和 connect-redis,简单方便
shellnpm i express-session -S
req.session 保存登录信息,登录校验做成 express 中间件
- app.js 中注册javascript
const session = require('express-session'); app.use(session({ secret: 'ASDASD_12', cookie: { path: '/', // 默认配置 httpOnly: true, // 默认配置 maxAge: 24 * 60 * 60 * 1000 } }));
- app.js 中注册
ioredis connect-redis
- 安装命令shell
npm i ioredis connect-redis -S
- 搭配 session 使用,在 app.js 中javascript
// redis const RedisStore = require('connect-redis')(session); const redisClient = require('./db/redis'); const sessionStore = new RedisStore({ client: redisClient }) app.use(session({ secret: 'ASDASD_12', cookie: { path: '/', // 默认配置 httpOnly: true, // 默认配置 maxAge: 24 * 60 * 60 * 1000, }, store: sessionStore }));
jsconst Redis = require('ioredis'); const { REDIS_CONF } = require('../config/db'); // 创建客户端 const redisClient = new Redis(REDIS_CONF.port, REDIS_CONF.host); redisClient.on('error', err => { console.error(err) }); module.exports = redisClient
- 安装命令
中间件 middleware/loginCheck.js
- loginCheck.jsjs
const { ErrorModel } = require('../model/resModel'); module.exports = (req, res, next) => { if (req.session.username) { next() return; } res.json(new ErrorModel('未登录')) }
- 业务中应用js
const express = require('express'); const router = express.Router(); const { getList, getDetail, newBlog, updateBlog, deleteBlog } = require('../controller/blog'); const { SuccessModel, ErrorModel } = require('../model/resModel'); const loginCheck = require('../middleware/loginCheck') router.get('/list', (req, res, next) => { const author = req.query.author || ''; const keyword = req.query.keyword || ''; const result = getList(author, keyword); return result.then(listData => { res.json(new SuccessModel(listData)); }) }); router.get('/detail', (req, res, next) => { const result = getDetail(req.query.id); return result.then(data => { return new SuccessModel(data); res.json(new SuccessModel(data)) }) }); 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)) }) }) router.post('update', loginCheck, (req, res, next) => { const result = updateBlog(req.query.id, req.body); return result.then(data => { if (data) { res.json(new SuccessModel()) } else { res.json(new ErrorModel('更新博客失败')) } }) }) router.post('/del', loginCheck, (req, res, next) => { const author = req.session.username; // 假数据 const result = deleteBlog(req.query.id, author); return result.then(data => { if (data) { res.json(new SuccessModel()) } else { res.json(new ErrorModel('删除博客失败')) } }) }) module.exports = router;
- loginCheck.js
3. 日志
- access log 记录,直接使用脚手架推荐的 morgan
- 自定义日志使用 console.log 和 console.error 即可
- 日志文件拆分、日志内容分析
- express 日志拆分
- package.jsonshell
"scripts": { "start": "node ./bin/www", "dev": "cross-env NODE_ENV=dev nodemon ./bin/www", "production": "cross-env NODE_ENV=production nodemon ./bin/www" }
- app.jsjavascript
const ENV = process.env.NODE_ENV; if (ENV !== 'production') { app.use(logger('dev') ); } else { app.use(logger('combined', { stream: process.stdout }) ); }
- 新建 logs/access.log
- 配置格式 参考:https://github.com/expressjs/morgan
- app.jsjavascript
const path = require('path'); const fs = require('fs') const ENV = process.env.NODE_ENV; if (ENV !== 'production') { app.use(logger('dev') ); } else { const logFileName = path.resolve(__dirname, 'logs', 'access.log') const writeStream = fs.createWriteStream(logFileName, { flags: 'a' }) app.use(logger('combined', { stream: writeStream }) ); }
- app.js
- package.json
4. express 中间件原理
- 分析
- app.use 用来注册中间件,先收集起来
- 遇到 http 请求,根据 path 和 method 判断触发哪些
- 实现 next 机制,即上一个通过 next 触发下一个
- 实现
- 模拟实现js
const http = require('http'); const slice = Array.prototype.slice; class LikeExpress { constructor() { // 存放中间件的列表 this.routes = { all: [], // app.use get: [], // app.get post: [], // app.post } } register(path) { const info = {}; if (typeof path === 'string') { info.path = path; // 从第二个参数开始 转换为数组,存入stack console.log('arguments--->', JSON.stringify(arguments)) info.stack = slice.call(arguments, 1); } else { info.path = '/'; // 从第一个参数开始 转换为数组,存入stack info.stack = slice.call(arguments, 0); } return info; } use() { console.log('use info--->', arguments) const info = this.register.apply(this, arguments); this.routes.all.push(info) } get() { const info = this.register.apply(this, arguments); this.routes.get.push(info); } post() { const info = this.register.apply(this, arguments); this.routes.post.push(info) } match(method, url) { let stack = []; if (url === '/favicon.ico') { return stack; } // 获取 routes let curRoutes = []; curRoutes = curRoutes.concat(this.routes.all); curRoutes = curRoutes.concat(this.routes[method]); curRoutes.forEach(routeInfo => { console.log('routeInfo--->', JSON.stringify(routeInfo.path)) console.log('url--->', url) if (url.trim().indexOf(routeInfo.path.trim()) === 0) { // url === '/api/get-cookie' 且 routeInfo.path === '/' // url === '/api/get-cookie' 且 routeInfo.path === '/api' // url === '/api/get-cookie' 且 routeInfo.path === '/api/get-cookie' stack = stack.concat(routeInfo.stack) } }) console.log('stack--->', JSON.stringify(stack)) return stack; } // 核心的 next 机制 handler(req, res, stack) { const next = () => { // 拿到第一个匹配的中间件 const middleware = stack.shift(); if (middleware) { // 执行中间件函数 middleware(req, res, next); } } next() } callBack() { return (req, res) => { res.json = (data) => { res.setHeader('Content-Type', 'application/json'); res.end( JSON.stringify(data) ) } const url = req.url; const method = req.method.toLowerCase(); const resultList = this.match(method, url); this.handler(req, res, resultList) } } listen(...args) { const server = http.createServer(this.callBack()); server.listen(...args) } } module.exports = () => { return new LikeExpress() }
- 测试js
const express = require('./like-express'); // 本次 http 请求的实例 const app = express(); app.use((req, res, next) =>{ console.log('请求开始',req.method, req.url); next(); }) app.use(function(req, res, next) { req.cookie = { userId: '123' } next() }) app.use(function(req, res, next) { // 假设处理 post data // 异步 setTimeout(() => { req.body = { a: 100, b: 200 } next(); }) }) app.use('/api', function(req, res, next) { console.log('处理 /api 路由'); next() }) app.get('/api', function(req, res, next) { console.log('get /api 路由'); next() }) app.post('/api',function (req, res, next) { console.log('post /api 路由'); next() }) // 模拟登陆验证 function loginCheck(req, res, next) { console.log('模拟登陆成功'); setTimeout(() => { next() }) } app.get('/api/get-cookie', loginCheck, function(req, res, next) { console.log('get /api/get-cookie 路由'); res.json({ errno: 0, data: req.cookie }) }) app.listen(8002, function() { console.log('server is running on port 8002!') })
- 模拟实现