- 2021-11-10
- 9-1 开始
- 9-2 开始
- 9-3 介绍 express 的入口代码
- 2021-11-11
- 9-4 演示 express 如何处理路由
- 2021-11-16
- 9-5 express中间件
- 9-6 express 介绍的总结
- 9-7 express 开发博客项目-初始化环境
- 2021-11-17
- 9-8 express 处理 session
- 9-9 session 连接 redis
- 2021-11-18
- 9-10 登录中间件
- 9-11 开发路由
- 2021-11-19
- 9-12 介绍 morgan
- 9-13 使用 morgan 写日志
- 9-14 中间件原理介绍
- 2021-11-23
- 9-15 中间件原理-代码实现
- 9-16 测试中间件代码实现-总结
2021-11-10
9-1 开始
使用 express
不关注底层内容,更关心业务内容
- express 是 nodejs 最常用的 web server 框架
- 什么是框架?
- 封装基本的API,制定一些标准,让开发更加简单,更加关注到数据、业务
- 有基本的流程和标准,有一个基本的解决方案
- 不要以为 express 过时了
- 周下载量还有千万
目录
- express 下载、安装和使用,express 中间件机制
- 中间件机制可以有更加优雅的方式实现之前的一些功能
- 开发接口,连接数据库,实现登录,日志记录
- 分析 express 中间件原理
9-2 开始
介绍 express
- 安装(使用脚手架 express-generator)
- 初始化代码介绍,处理路由
- 使用中间件
安装 express
- 首先全局安装
- npm install express-generator -g
- 然后创建一个 express 的项目
- express express-test
- 最后启动项目
- npm install & npm start
实操
首先全局安装 express
然后在 nodejs-learning 中创建 express 项目
然后进入后启动
访问 localhost:3000 端口
成功!
然后安装 nodemon 和 cross-env
npm i nodemon cross-env --save-dev
然后打开看 express 默认提供的 www.js 和我们之前在 blog-1 中创建的对比一下,发现我们之前创建的只是提供 http 服务。在 express 中提供的会相对复杂一点。
有 views 和 public 存在,是因为 express 是为了提供一个全栈的开发环境。
但是我们现在推崇前后端分离的开发方式,因此这里给前端提供的模板,css,js就不管了
9-3 介绍 express 的入口代码
介绍 app.js
- 各个插件的作用
- 思考各个插件的实现原理(结合之前学过的知识)
- 处理 get 请求和 post 请求
- createError:处理错误路由情况
- cookieParser:解析 cookie
- logger(使用 morgan):生成日志
- var app = express():这里的 app 是初始化的
- express.json():替换原来的 getPostData,可以直接从 req.body 中拿到数据,当 content-type 是 JSON 格式时获取
- express.urlencoded():从 req.body 拿到表单格式的数据,当 content-type 是 urlencoded 格式时获取
- app.use(‘/‘, indexRouter) 和 app.use(‘/users’, usersRouter) 是用来注册路由,是要加一个根的路由
- -> 拼接 / 和 indexRouter 或者 usersRouter 里面的路由 来进行访问
- 404调试 和 500 调试
不要深究细节,但要理解 cookiePaser 和 json() 的原理
2021-11-11
9-4 演示 express 如何处理路由
express 处理 get 和 post 请求
get 请求
新建 blog.js 和 user.js
然后尝试在 blog.js 中使用 get 请求返回 mock 数据
blog.js 中使用 res.json 返回数据
var express = require('express');var router = express.Router();router.get('/list', function(req, res, next) {res.json({errno: 0,data: [1, 2, 3]})});module.exports = router;
来到 app.js 中,引入 blog.js 中导出的 router 作为 blogRouter,然后 use 一下,这样就可以拼接为 /api/blog/list
...const blogRouter = require('./routes/blog')...app.use('/api/blog', blogRouter)...
post 请求
在 user.js 中操作
大概修改一下,和 get 请求类似
var express = require('express');var router = express.Router();router.post('/login', function(req, res, next) {// 因为使用了 express.json() 所以可以直接从 req.body 中解析 post 数组const { username, password } = req.bodyres.json({username,password})});module.exports = router;
然后来到 app.js 中引用,和 get 请求类似
...const userRouter = require('./routes/user')...app.use('/api/user', userRouter)...
在 postman 中测试一下:
成功!
如果使用 urlencoded 的也同样可以,因为之前已经做了兼容

2021-11-16
9-5 express中间件
中间件机制
- 有很多 app.use
- 代码中的 next 参数是什么?
- 带着这些疑问,先看一段代码演示
代码演示
创建一个目录: express-test
然后 npm init -y 一下,将入口改为 app.js
然后安装一下 express
npm i express
然后写一系列测试的代码用来体会中间件的使用方法
const express = require('express')// 本次 http 请求的实例const app = express()// 在最后都要执行 nextapp.use((req, res, next) => {console.log('请求开始...', req.method, req.url)next()})app.use((req, res, next) => {// 假设在处理 cookiereq.cookie = {useId: 'abc123'}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()})app.get('/api/get-cookie', (req, res, next) => {// 不执行 nextconsole.log('get /api/get-cookie')res.json({errno: 0,data: req.cookie})})app.post('/api/get-post-data', (req, res, next) => {// 不执行 nextconsole.log('get /api/get-post-data')res.json({errno: 0,data: req.body})})app.use((req, res, next) => {console.log('处理 404');res.json({errno: -1,msg: '404 not found'})})app.listen(3000, () => {console.log('server is running on port 3000')})
app.use 如果第一个参数没有写路由的话,会默认执行
执行到 next() 的时候,其实是执行后续的符合条件的(路由条件)方法,比如 app.use app.get
运行一下:


总结:会根据路由名和方法来决定是否执行,如果有 next 才继续向下执行
因此每一个 app.use 就是一个个的中间件,有三个参数的方法
多中间件模式
// 模拟登录验证function loginCheck(req, res, next) {console.log('模拟登录成功')setTimeout(() => {next()})}app.get('/api/get-cookie', loginCheck, (req, res, next) => {// 不执行 nextconsole.log('get /api/get-cookie')res.json({errno: 0,data: req.cookie})})

9-6 express 介绍的总结
通过上节课的学习再看之前拿脚手架搭的东西,就可以知道所有的 app.use 就是中间件
以上全是中间件
总结
- 初始化代码中,各个插件的作用
- express 如何处理路由
- blog.js user.js
- express中间件
- next
- 匹配规则
9-7 express 开发博客项目-初始化环境
express 开发接口
- 初始化项目,之前的部分代码可以复用
- controller 和 连接 mysql的代码都可以被复用
- 开发路由,并实现登录
- express 的路由
- 登录使用框架很方便
- 记录日志
初始化环境
- 安装插件 mysql xss
- mysql controller resModel 相关代码可以复用
- 初始化路由
代码演示
进入 blog-express
首先把没用的代码注释一下
在 app.js 中
报 favicon 的错的问题:
解决:
https://www.itranslater.com/qa/details/2582472172027511808
- 打开Chrome /问题浏览器
- 直接导航到favicon.ico文件,例如 [http:// localhost:3000 / favicon.ico]
- 通过按F5或相应的浏览器刷新(重新加载)按钮来刷新favicon.ico URL。
- 关闭浏览器并打开您的网站-瞧,您的收藏夹图标已更新!
然后安装 mysql 和 xss 的插件
npm i mysql xss —save
然后将连接 mysql 和 nodejs 的文件复制过来(mysql.js)
还有 conf 中的 db.js(数据库的连接内容)
同理还有 controller 和 model
以及加密用的 crypto (utils/crypt.js)
然后修改 get list 的路由数据
router.get('/list', function(req, res, next) {// express 的 req.query 是原生自带的,不用手动解析let author = req.query.author || ''const keyword = req.query.keyword || ''// // 修改登录权限// if (req.query.isadmin) {// // 管理员界面// const loginCheckResult = loginCheck(req)// if (loginCheckResult) {// // 未登录情况// return loginCheckResult// }// // 强制查询自己的博客// author = req.session.username// }const result = getList(author, keyword)return result.then(listData => {// 不用 returnres.json(new SuccessModel(listData))})});
2021-11-17
9-8 express 处理 session
登录
- 使用 express-session 和 connect-redis 来登录,简单方便
- req.session 保存登录信息,登录校验做成 express 中间件
首先安装 express-session(能轻松实现 session 功能)
npm i express-session
然后在 app.js 中引用并写中间件
...// 引入const session = require('express-session')...app.use(cookieParser());// 解析完 cookie 就解析 sessionapp.use(session({// 类似于一个密匙secret: 'Lb@#c_13323',// 配置 cookiecookie: {// path 和 httpOnly 都是默认值,可以不写path: '/',httpOnly: true,// 失效时间,不同于之前在 blog-1 中做的 expires// 24小时失效maxAge: 24 * 60 * 60 * 1000}}))....
然后 npm run dev 测试一下
成功~
成功写入 cookie 一个 值,为 connect.sid
9-9 session 连接 redis
登录继续操作
第一步:拷贝之前的登录路由然后修改
有了 express-session 之后,不需要同步到 redis 中
测试登录
// 登录路由相关代码router.post('/login', function(req, res, next) {const { username, password } = req.bodyconst result = login(username, password)return result.then(data => {if (data.username) {// 设置 sessionreq.session.username = data.usernamereq.session.realname = data.realnameres.json(new SuccessModel())return}res.json(new ErrorModel('login failed'))})});
第二步:测试登录成功与否
写测试代码
// 测试登录router.get('/login-test', (req, res, next) => {if (req.session.username) {res.json({errno: 0,msg: '测试登录成功'})return}res.json({errno: -1,msg: '测试登录失败'})})
然后登录网页测试 http-server 并且启动 nginx
根据之前设置的 nginx ,将 blog-express 的监听端口改为 8000(www 文件中)
然后测试链接:http://localhost:8080/api/user/login-test
将数据放到 redis 中
安装两个插件:redis 和 connect-redis
npm i redis connect-redis —save
然后拷贝之前的连接 redis 的代码
const { REDIS_CONF } = require('../conf/db')// 创建客户端const redisClient = redis.createClient(REDIS_CONF.port, REDIS_CONF.host)// 监控 errorredisClient.on('error', err => {console.error(err)})module.exports = redisClient
然后访问 http://localhost:8080/index.html 发现会有 session了
“sess:eYuIt3r7OO-_4NjN6_I0bD0uOKRwOGk6”
2021-11-18
9-10 登录中间件
然后执行下登录
登录成功之后再看下 key
get sess:6kplosAnfb8mc8to-NJvWN43PA6W9IoI
里面有了 username 的值和 realname 的值
证明已经连接好了
然后写一个登录的中间件
新建 middleware 文件夹(src/middleware)然后写一个 loginCheck.js 用来检测 login 成功与否
const { ErrorModel } = require('../model/resModel')module.exports = (req, res, next) => {if (req.session.username) {next()return}res.json(new ErrorModel('未登录'))}
9-11 开发路由
isAdmin 登录
修改判断是否登录逻辑
然后修改完整的获取数据的逻辑
router.get('/list', function(req, res, next) {// express 的 req.query 是原生自带的,不用手动解析let author = req.query.author || ''const keyword = req.query.keyword || ''// 修改登录权限if (req.query.isadmin) {// 管理员界面if (req.session.username == null) {// 未登录情况res.json(new ErrorModel('未登录'))return}// 强制查询自己的博客author = req.session.username}const result = getList(author, keyword)return result.then(listData => {// 不用 returnres.json(new SuccessModel(listData))})});
这样当访问 localhost:8080/admin.html 时,则必须登录才能访问,不然会返回错误。
这样登录后,才会访问成功
get detail
将之前原生获取修改一下,所有的返回替代成 res.json
router.get('/detail', function(req, res, next) {const result = getDetail(req.query.id)return result.then(listData => {res.json(new SuccessModel(listData))})})
new blog
同上,只不过是 post 请求
router.post('/new', loginCheck, (req, res, next) => {req.body.author = req.session.usernameconst result = newBlog(req.body)// 这里的 data 就是返回新建成功的博客的 idreturn result.then(data => {res.json(new SuccessModel(data))})})
update blog
同上
router.post('/update', loginCheck, (req, res, next) => {req.body.author = req.session.usernameconst result = updateBlog(req.query.id, req.body)return result.then(data => {if (data) {res.json(new SuccessModel())return}res.json(new ErrorModel('update failed'))})})
delete blog
同上
router.post('/del', loginCheck, (req, res, next) => {const author = req.session.usernameconst result = deleteBlog(req.query.id, author)return result.then(data => {if (data) {res.json(new SuccessModel())return}res.json(new ErrorModel('update failed'))})})
这种 中间件的方式比较好,结构清晰,更易读,更好控制,符合设计模式
2021-11-19
9-12 介绍 morgan
回顾一下,之前我们记录日志是怎么办的:
1.首先讲了文件操作,发现文件操作会有性能的问题
2.然后讲了如何通过stream流来实现文件的写入,一行一行的写入日志
3.然后讲了如何拆分日志文件,分析日志crontab按时间来拆分
4.最后讲了如何分析日志,通过 readline 的方式一行一行读取来分析日志
日志
下面来讲在 express 中来做日志相关的内容
- access log 记录,直接使用脚手架推荐的 morgan
- 自定义日志还是使用 console.log 和 console.err 即可
- 日志文件拆分,日志内容分析,之前讲过不再赘述
9-13 使用 morgan 写日志
根据环境变更日志书写格式
我们现在在控制台中已经可以显示一些访问记录了
分析一下:依次为访问的方式,路由,相应时间,数据量(content-length)
但是有问题:
1.获取的数据种类太少
2.数据都在控制台上,不是以文件的形式存储
解决:
修改 morgan 的中间件传入参数
通过查询 express/morgan(github) 中,发现更改app.use 中的模式即可,有不同的输出在控制台内容的方式
如果把 ‘dev’ 改为 ‘combined’ 后,就可以输出更多的内容
线上环境中就用这个 combined 的模式
如何把这两种格式的数据比较灵活的结合起来用呢?
通过设置环境变量来控制不同环境来使用不同格式
首先在 package.json 中写入 生成环境的脚本
{..."scripts": {..."prd": "cross-env NODE_ENV=production nodemon ./bin/www.js"},...}
然后修改 app.js
const ENV = process.env.NODE_ENVif (ENV !== 'production') {// 开发环境或者测试环境app.use(logger('dev'));} else {// 线上环境app.use(logger('combined', {stream: process.stdout}));}
因为要写日志到文件中,所以 stream 流不能设置为 stdout 了
设置 access.log 日志
新建 logs/access.log 在根目录下
写入要引用 path(文件路径)和 fs(文件操作)
然后修改线上环境的写入方式
...const fs = require('fs')...if (ENV !== 'production') {...} else {// 线上环境const logFileName = path.join(__dirname, 'logs', 'access.log')const writeStream = fs.createWriteStream(logFileName, {flags: 'a'})app.use(logger('combined', {stream: writeStream}));}
然后去切换到 prd 运行(npm run prd)看下 access.log 里的内容
设置自定义日志
自定义日志就是在任何位置添加自己想要的内容
在里面添加内容 list 这个 api 中添加内容
如果能触发,这些都还是在控制台中打印出来的
暂时先不提怎么将这些内容放到日志文件中,讲 pm2 会继续弄
pm2 可以方便收集到日志中
总结
- 写法上的改变,如 req.query, res.json
- 使用 express-session, connect-redis, 登录中间件
- 使用 morgan 控制日志输出
9-14 中间件原理介绍
express 中间件原理介绍
express 定义了一些套路,能让自己定义的一些内容和它定义好的一些内容方便在 express 中运行
学习的目的:花费相对少的时间知道这个事情做出来的,也不是造轮子。
- 回顾中间件的使用
- 分析如何实现
- 代码演示-100行代码作用
回顾中间件示例
app.use 执行,三个参数具体实行,然后 next 进行下一步,app.get/post 执行路由
至少实现的接口:app.listen, app.use, app.post, app.get, next()
分析
- app.use 用来注册中间件
- 遇到 http 请求,根据 path (‘/‘)和 method (get、post)判断触发哪些
- 实现 next 机制,即上一个通过 next 触发下一个
2021-11-23
9-15 中间件原理-代码实现
新建 lib/express/like-express.js
然后新建一系列内容 用来模拟 express
const http = require('http')const slice = Array.prototype.sliceclass LikeExpress {constructor() {// 存放中间件列表this.routes = {// all, get, post 就是分别以app.use, app.get, app.post 存放的中间件all: [],get: [],post: []}}// 不管是 use,get,post,listen 都要先注册的函数,内部共有的,分析参数register(path) {// 中间件的第一个参数可能是回调函数,也可能是pathconst info = {}if (typeof path === 'string') {info.path = path// 把参数从第二个开始的后续全部取出来,放到 stack 中,slice.call 赋值为数组info.stack = slice.call(arguments, 1)} else {// 把参数从第一个开始的后续全部取出来,放到 stack 中,slice.call 赋值为数组info.path = '/'info.stack = slice.call(arguments, 0)}return info}use() {// 将 use 方法所有的参数全部放到 info 中去const info = this.register.apply(this, arguments)// 将所有的参数(中间件)放到all 这个列表中去this.routes.all.push(info)}get() {// 同上const info = this.register.apply(this, arguments)// 将所有的参数(中间件)放到 all 这个列表中去this.routes.get.push(info)}post() {// 同上const info = this.register.apply(this, arguments)// 将所有的参数(中间件)放到 all 这个列表中去this.routes.post.push(info)}listen() {}}// 工厂函数module.exports = () => {return new LikeExpress()}
书写 listen 方法
// 解构参数,参数个数不定listen(...args) {const server = http.createServer(this.callback())// 参数透传server.listen(...args)}
书写 callback 方法
match(method, url) {let stack = []// 浏览器自带的小图标,忽略掉if (url === '/favicon.ico') {return stack}// 获取 routeslet curRoutes = []// 拼接 all 中所有的 路由curRoutes = curRoutes.concat(this.routes.all)// 然后根据 method 是什么来继续拼接curRoutes = curRoutes.concat(this.routes[method])curRoutes.forEach(routeInfo => {if (url.indexOf(routeInfo.path) === 0 ) {// 这种情况:当前的 url === '/api/get-cookie' 且 routeInfo.path === '/'// 当前的 url === '/api/get-cookie' 且 routeInfo.path === '/path'// 当前的 url === '/api/get-cookie' 且 routeInfo.path === '/api/get-cookie'// 符合以上类似的情况,就将 url 放到 stack 中去stack = stack.concat(routeInfo.stack)}})return stack}callback() {return (req, res) => {res.json = (data) => {// 因为要返回JSON格式,所以要设置 setHeaderres.setHeader('Content-type', 'application/json')res.end(JSON.stringify(data))}const url = req.urlconst method = req.method.toLowerCase()// 通过 url 和 method 来区分哪些 参数 需要访问,哪些不需要// 然后将 url 和 method 传入到 match 方法中const resultList = this.match(method, url)this.handle(req, res, resultList)}}
然后书写 handle 方法
// 核心的 next 机制handle(req, res, stack) {const next = () => {// 拿到第一个匹配的中间件const middleWare = stack.shift()if (middleWare) {// 执行中间件函数middleWare(req, res, next)}}next()}
1.定义存放的组件,this.routes
2.定义 use() get() post() register(),分别存放 stack
3.callback() 定义 res.json
4.match() 定义如何访问
5.handle() 定义 next() 函数运行机制
9-16 测试中间件代码实现-总结
新建一个 test.js 用来测试
const express = require('./like-express')// 本次 http 请求的实例const app = express()app.use((req, res, next) => {console.log('请求开始...', req.method, req.url)next()})app.use((req, res, next) => {// 假设在处理 cookieconsole.log('处理 cookie ...')req.cookie = {userId: 'abc123'}next()})app.use('/api', (req, res, next) => {console.log('处理 /api 路由')next()})app.get('/api', (req, res, next) => {console.log('get /api 路由')next()})// 模拟登录验证function loginCheck(req, res, next) {setTimeout(() => {console.log('模拟登陆成功')next()})}app.get('/api/get-cookie', loginCheck, (req, res, next) => {console.log('get /api/get-cookie')res.json({errno: 0,data: req.cookie})})app.listen(8000, () => {console.log('server is running on port 8000')})
成功
总结
- 使用框架开发的好处(相比之前不使用框架)
- 便利性
- express 的使用和路由处理,以及操作 session redis 日志等
- express 中间件的使用和原理
下一步
- JS 的异步回调带来了很多问题,Promise 也不能解决所有
- nodejs 已经全面支持 async/await 语法,要用起来
- Koa2 支持 async/await
