- 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.body
res.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()
// 在最后都要执行 next
app.use((req, res, next) => {
console.log('请求开始...', req.method, req.url)
next()
})
app.use((req, res, next) => {
// 假设在处理 cookie
req.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) => {
// 不执行 next
console.log('get /api/get-cookie')
res.json({
errno: 0,
data: req.cookie
})
})
app.post('/api/get-post-data', (req, res, next) => {
// 不执行 next
console.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) => {
// 不执行 next
console.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 => {
// 不用 return
res.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 就解析 session
app.use(session({
// 类似于一个密匙
secret: 'Lb@#c_13323',
// 配置 cookie
cookie: {
// 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.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('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)
// 监控 error
redisClient.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 => {
// 不用 return
res.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.username
const result = newBlog(req.body)
// 这里的 data 就是返回新建成功的博客的 id
return result.then(data => {
res.json(
new SuccessModel(data)
)
})
})
update blog
同上
router.post('/update', loginCheck, (req, res, next) => {
req.body.author = req.session.username
const 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.username
const 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_ENV
if (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.slice
class LikeExpress {
constructor() {
// 存放中间件列表
this.routes = {
// all, get, post 就是分别以app.use, app.get, app.post 存放的中间件
all: [],
get: [],
post: []
}
}
// 不管是 use,get,post,listen 都要先注册的函数,内部共有的,分析参数
register(path) {
// 中间件的第一个参数可能是回调函数,也可能是path
const 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
}
// 获取 routes
let 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格式,所以要设置 setHeader
res.setHeader('Content-type', 'application/json')
res.end(
JSON.stringify(data)
)
}
const url = req.url
const 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) => {
// 假设在处理 cookie
console.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