安装
npm install -g express-generator // 全局安装 express 脚手架
express '项目名称'
cd '项目名称'
npm install // 安装依赖
npm start // 启动项目
使用 nodemon 和 cross-env
安装这两个插件可以让我们通过 npm run dev
来启动项目,并对项目实时修改进行自动响应。
npm install nodemon cross-env --save-dev
修改 package.json
中的 scripts
"scripts": {
"start": "node ./bin/www",
"dev": "cross-env NODE_ENV=dev nodemon ./bin/www"
},
分析 app.js 中的代码
var createError = require('http-errors'); // 404
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser'); // 引入解析 cookie 的中间件
var logger = require('morgan'); // 引入记录日志文件插件
// 引入了2个路由,routers 文件夹的的路由文件
var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');
var app = express();
// app.use() 使用中间件
app.use(logger('dev')); // 使用引入的日志文件
app.use(express.json()); // 响应 post 请求 json 格式
app.use(express.urlencoded({ extended: false })); // 响应 post 请求非 json 格式,常用表单结构数据
app.use(cookieParser()); // 使用引入的中间件 cookie-parser 解析 cookie
app.use('/', indexRouter); // 跟路由指向 indexRouter
app.use('/users', usersRouter); // /users 这个路由指向 usersRouter 下级路由 /users/info等等
app.use(function(req, res, next) { // 找不到的路径跳转 404
next(createError(404));
});
// 对程序错误的一些处理,抛出错误信息和对应的状态码
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');
});
Express 如何处理路由
var express = require('express');
var router = express.Router();
// get 请求写法
router.get('/list', function(req, res, next) {
// req.query 解析 get 请求中传递过来的参数
const { author, keyword } = req.query
res.json({
author: author
})
});
// post 请求写法
router.post('/login', function(req, res, next) {
// req.body 解析 post 请求中传递过来的参数
const { username, password } = req.body
res.json({
errno: 0,
data: {
username,
password
}
})
});
module.exports = router;
了解 Express 中间件
通过上面的代码,我们发现了一堆 app.use()
和 {req, res, next}
,那么 app.use()
和 next()
有啥用呢?那么它们到底有啥用呢,来看下面这个栗子
// 新开一个文件夹 test.js 只引入 express
const express = require('express')
const app = express()
app.use((req, res, next) => {
console.log('请求开始...', req.method, req.url)
next()
})
app.use((req, res, next) => {
// 假设在处理 cookie
req.cookie = {
userId: '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
app.get('/api', (req, res, next) => {
console.log('处理 get /api 路由')
next()
})
// 使用 app.post
app.post('/api', (req, res, next) => {
console.log('处理 post /api 路由')
next()
})
上面的代码中,我们一共用了 3 次 app.use()
未加请求路径的情况,1 次 app.user('/api')
的情况,然后又分别用了 app.get()
和 app.post()
,并且每个回调函数里面都加上了 next
并在结尾处使用了 next()
方法。那么我们接下来去通过一个不加 next
的 get
请求来试一试,上面哪些会被执行
app.get('/api/get-cookie', (req, res, next) => {
console.log('get /api/get-cookie')
res.json({
errnon: 0,
data: req.cookie
})
})
app.listen(3000, () => {
console.log("server is running...")
})
命令行输出了如下内容
// 命令行输出
请求开始... GET /api/get-cookie
处理 /api 路由
get /api/get-cookie
// 页面输出
{
errnon: 0,
data: {
userId: "abc123" // cookie 被写入
}
}
这里我们发现,app.use()
未加请求路径和 app.user('/api')
都被执行打印了出来,而 app.get('/api')
和 app.post('/api')
都未被执行,这里我们先得出结论,只要我们访问一个路径,那么我们入口文件 app.js
中的所有 app.use()
不加路径的部分都会被执行。即使加了路径,例如我们访问的路径是 app.use(/api/get-cooki)
而上面定义的路径是 app.use('/api')
它是我们访问路径的上一级路径,那么它也会被执行。而这些 app.use(() => {})
里面被执行的里面的函数就是中间件,它们通过最后的 next()
方法依次往下执行(不符合执行条件的自动忽略,)。
我们通过 post
请求访问 /api/get-post-data
路径
app.post('/api/get-post-data', (req, res, next) => {
console.log('post /api/get-post-data')
res.json({
errno: 0,
data: req.body
})
})
app.listen(3000, () => {
console.log("server is running...")
})
命令行输出了如下内容
// 命令行输出
请求开始... POST /api/get-post-data
处理 /api 路由
post /api/get-post-data
// 页面输出
{
"errno": 0,
"data": {
"a": 100,
"b": 200
}
}
输出基本和 get
相同,首先会去输出 app.use()
中没有路径的,然后再输出 app.use()
中当前路径的上一级路径内容,其它会忽略。
我们去访问一个不存在的路由,看看 404 not found
的情况下上面的代码是如何输出的,这里如果我们直接访问 /aaa
app.use((req, res, next) => {
console.log('处理404')
res.json({
errno: '-1',
msg: '404 not found'
})
})
app.listen(3000, () => {
console.log("server is running...")
})
命令行输出如下内容:
请求开始... GET /api/aaaaa
处理404
// 页面输出
{
errno: "-1",
msg: "404 not found"
}
因为我们访问的是 /aaa
它本身已经是根路径下的第一个路径,所以只会执行 app.use()
中没有路径的,其它都不会执行,所以在这里我们可以得出 app.use()
里面是都是使用的中间件,而我们初始化好的一些内容都是通过 app.use()
应用到全局中,每次我们请求一个接口地址,都会先从最上面的 app.use()
开始去执行。了解这些后这里我们可以做一个登陆验证的中间件,当我们访问 /api/get-cookie
时如下栗子
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({
errnon: 0,
data: req.cookie
})
})
上面的栗子中,我们有一个假设有一个函数 loginCheck
来验证登陆成功或者失败,假设成功,然后给出 next()
,那么请求 /api/get-cookie
之后命令行会输出登陆成功。
如果我们给出失败呢,如下栗子:
function loginCheck(req, res, next) {
setTimeout(() => {
res.json({
errno: -1,
msg: '登陆失败'
})
})
}
app.get('/api/get-cookie', loginCheck, (req, res, next) => {
console.log('get /api/get-cookie')
res.json({
errnon: 0,
data: req.cookie
})
})
登陆失败的情况下我们不给 next()
,也就是后面的回调函数(其实也可以理解为一个中间件)就不会执行了,页面只会输出如下内容,这样我们就可以根据返回值来判断该用户是否登录。
{
errno: -1,
msg: '登陆失败'
}
了解 Express 中如何处理 session
-
引入 express-session
npm install redis connect-redis --save-dev
-
做一个用户访问网页次数的 session 案例
// app.js中
const session = require('express-session')
app.use(session({ // 执行传入对应参数
secret: 'CcWc#1993_28', // 随便定义,用来生成cookie的密钥
cookie: {
path: '/', // 默认配置
httpOnly: true, // 默认配置
maxAge: 24 * 60 * 60 * 1000 // cookie的生效时间 时 * 分 * 秒 * 毫秒
}
}))
app.use('/api/user', userRouter)
// user.js
router.get('/session-test', (req, res, next) => {
const session = req.session
if (session.viewNum == null) {
session.viewNum = 0
}
session.viewNum++
res.json({
viewNum: session.viewNum
})
})
使用 express-session 简单处理登陆信息
router.post('/login', function(req, res, next) {
const { username, password } = req.body
let result = loginCheck(username, password)
return result.then(data => {
if (data.username) {
// 设置 session
req.session.username = data.username
res.json({
new SuccessModel('已登录')
})
return
}
res.json(
new ErrorModel('未登陆')
)
})
});
// 是否登录测试接口
router.get('/login-test', (req, res, next) => {
if (req.session.username) {
res.json({
error: 0,
msg: '已登录'
})
} else {
res.json({
error: -1,
msg: '未登录'
})
}
})
将 session 数据存入 redis 中
- 安装
redis
和connect-redis
npm install redis connect-redis --save-dev
- 将安装的
redis
和connect-redis
引入
// app.js 中
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: 'CcWc#1993_28',
cookie: {
maxAge: 24 * 60 * 60 * 1000
},
store: sessionStore
}));
// db => redis.js
const redis = require('redis')
const { REDIS_CONF } = require('../config/db')
const redisClient = redis.createClient(REDIS_CONF.port, REDIS_CONF.host)
redisClient.on('error', err => {
console.log(err)
})
module.exports = redisClient
// config => db.js
const env = process.env.NODE_ENV
// 配置
let REDIS_CONF
if (env === 'dev') {
// redis
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
}
建立一个判断是否登录的中间件 loginCheck.js
// loginCheck.js
const { ErrorModel } = require('./../model/resModel')
module.exports = (req, res, next) => {
// 能从 session 中获取到 username,就允许往下执行,否知就直接弹出'未登录'提示
if (req.session.username) {
next()
return
}
res.json(
new ErrorModel('未登录')
)
}
如何使用这个中间件呢,如下栗子
// 引入上面的中间件 loginCheck.js
router.post('/del', loginCheck, function (req, res) {
let result = delBlog(req.query.id, req.session.username)
return result.then(data => {
if (data) {
res.json(
new SuccessModel('删除成功')
)
} else {
new ErrorModel('删除失败')
}
})
}
利用 Express 中的 morgan 写日志
写入系统日志
我们分析过 app.js
中知道脚手架本身提供了对应的写日志工具,我们将其中的代码单独拎出来
var logger = require('morgan');
app.use(logger('dev')); // dev 代表开发模式
// app.use(logger('dev', {
// stream: process.stdout // 默认写入命令行中,无须配置
// }))
因为系统默认帮我们配置了 logger('dev')
所以我们操作网页的时候命令行就已经为我们输出了我们每一次点击执行的动作,如下:
GET /api/blog/detail?id=14 304 6.149 ms - -
POST /api/blog/update?id=14 200 16.031 ms - 36
GET /api/blog/list?isadmin=1 200 2.083 ms - 995
POST /api/blog/del?id=13 200 13.825 ms - 36
GET /api/blog/list?isadmin=1 200 1.784 ms - 912
POST /api/blog/del?id=12 200 2.853 ms - 36
GET /api/blog/list?isadmin=1 200 1.994 ms - 829
POST /api/blog/del?id=11 200 14.773 ms - 36
GET /api/blog/list?isadmin=1 200 1.274 ms - 737
-
dev
开发环境下的格式,简单
app.use(logger('dev', {
stream: process.stdout // 默认写入方式,命令行写入
}))
-
combined
比较完善的格式,一般线上环境用
app.use(logger('combined', {
stream: process.stdout // 默认写入方式,命令行写入
}))
combined
在命令行中记录格式如下栗子:
::ffff:127.0.0.1 - - [29/Aug/2020:09:34:20 +0000] "GET /api/blog/detail?id=14 HTTP/1.0" 200 103 "http://localhost:8080/edit.html?id=14" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.89 Safari/537.36"
::1 - - [29/Aug/2020:09:34:23 +0000] "POST /api/blog/update?id=14 HTTP/1.0" 200 36 "http://localhost:8080/edit.html?id=14" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.89 Safari/537.36"
::ffff:127.0.0.1 - - [29/Aug/2020:09:34:25 +0000] "GET /api/blog/list?isadmin=1 HTTP/1.0" 200 737 "http://localhost:8080/admin.html" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.89 Safari/537.36"
还有一些其它的格式,如:common
、short
、 tiny
等,可以去官方文档地址中进行查看。
上面展示了 dev
和 combined
在命令行写入日志的基本情况,线上我们一般希望将日志写入自定义的文件中,那么我们就要更改 stream
的值来实现写入。
我们首先在 package.json
中配置一个线上环境:
"scripts": {
"start": "node ./bin/www",
"dev": "cross-env NODE_ENV=dev nodemon ./bin/www", // dev 开发测试环境
"prd": "cross-env NODE_ENV=production nodemon ./bin/www" // prd 线上环境
},
因为要写入文件中,所以我们应该在 app.js
中引入 fs
模块
// app.js
var fs = require('fs')
const ENV = process.env.NODE_ENV
if (ENV !== 'production') {
// 开发或测试环境
app.use(logger('dev'))
} else {
// 线上环境
// 新建一个 logs 日志文件夹,然后里面新建 access.log 文件,这是我们日志存放的地址
const logFileName = path.join(__dirname, 'logs', 'access.log')
const writeStream = fs.createWriteStream(logFileName, {
flags: 'a' // flags:'a' 追加写入 flags: 'w' 覆盖写入
})
app.use(logger('combined', {
stream: writeStream
}))
}
以线上环境重新启动程序
npm run prd
查看 access.log
文件中的写入内容
::1 - - [29/Aug/2020:10:04:29 +0000] "GET /api/blog/list?isadmin=1 HTTP/1.0" 200 34 "http://localhost:8080/admin.html" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.87 Safari/537.36 SLBrowser/6.0.1.6181"
::ffff:127.0.0.1 - - [29/Aug/2020:10:04:42 +0000] "POST /api/blog/new HTTP/1.0" 200 34 "http://localhost:8080/new.html" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.87 Safari/537.36 SLBrowser/6.0.1.6181"
::1 - - [29/Aug/2020:10:04:56 +0000] "POST /api/user/login HTTP/1.0" 200 36 "http://localhost:8080/login.html" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.87 Safari/537.36 SLBrowser/6.0.1.6181"
::ffff:127.0.0.1 - - [29/Aug/2020:10:04:56 +0000] "GET /api/blog/list?isadmin=1 HTTP/1.0" 200 335 "http://localhost:8080/admin.html" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.87 Safari/537.36 SLBrowser/6.0.1.6181"
我们所有项目上的点击动作就全部被写入到指定的日志文件中了。