Express 是一个保持最小规模的灵活的 Node.js Web 应用程序开发框架,为 Web 和移动应用程序提供一组强大的功能。
文章只是初步学习,务必关注 《Express官网》
Express 框架的核心特性如下:
- 通过设置中间件来处理HTTP请求。
- 通过路由来执行不同的HTTP请求操作。
- 通过模板来渲染HTML页面。
1️⃣ Hello Express
1.1❀ 普通搭建
1.初始化项目
npm init
npm i express --save
2.创建服务器
- 创建应用实例
- 设置路由
- 开启监听
//app.js
//导入express模块
const express = require("express");
//创建应用
const app = express();
//设置路由
app.get("/", (req, res) => {
//输出响应
res.json(req.headers);
console.log("发生请求!");
});
//开启监听
app.listen(8080, () => {
console.log("listen on 8080");
});
3.运行应用
node app.js
此时,已开启监听本地8080端口
1.2❀ 脚手架搭建
通过应用生成器工具 express-generator 可以快速创建一个应用的骨架。 更具体参数和模板请看:Express 应用程序生成器
npx express-generator --view=pug myapp
cd myapp
npm install
set DEBUG=myapp:*
npm start
通过 Express 应用生成器创建应用只是众多方法中的一种。你可以不使用它,也可以修改它让它符合你的需求。
2️⃣ 路由
路由是指应用程序如何根据指定的路由路径和指定的HTTP请求方法(GET和POST等)来处理请求。
在Express 应用中,每个路由可以有一个或多个处理函数,这些函数会在路由匹配时执行。
路由采用以下结构定义:
app.METHOD(PATH, HANDLER)
- app:应用实例。
- METHOD:小写get和post等的HTTP请求方法。
- PATH:路由路径。
- HANDLER:路由匹配时执行的函数。
2.1❀ 路由方法
Express 支持所有的HTTP请求方法
以下是官网提供的HTTP请求方法,实在太多了,估计用得上的也就几个!
通过app.all()方法可以只使用一个路由来处理所有指定path的请求方法
app.all('/',(req,res)=>{res.send('拦截了路径为/的所有方法');})
2.2❀ 路由路径
路由路径可以是字符串,字符串模式或正则表达式。
查询字符串(一般称为GET参数):类似?param=xx 不是路由路径的一部分。
(1)字符串
app.get("/about.txt", (req, res) => {});
(2)字符串模式
?表示前面的单元最多出现一次 +表示前面的单元最少出现一次
app.get('/ab?c', (req, res) => {});
(3)正则表达式
只要路径包含user即可
app.get(/user/, (req, res) => {});
完完全全是/admin才可以
app.get(/^\/admin$/, (req, res) => {});
2.3❀ 路由参数
路由参数(:var
)用于捕获URL中个位置的值。捕获的值将填充到req.params对象中。
app.get('/users/:userID(\\d+)/:userName', (req, res) => {
res.send(req.params);
});
(\\d+)
可选项 跟在路由参数后面,限定变量格式
2.4❀ 路由函数
function(request,response,next)
next是可选的,指的是下一个路由函数。一个路由可以有多个路由函数来处理。执行next()方法才会处理后面一个路由函数。
next()是可以有参数的,参数可以是:字符串、数字、Error对象等等;
假如没有没有下一个路由函数,却调用next()不加参数,将访问不到!如果加了参数,比如next(1),相当于调用send(1)。
app.all(
"/route",
(req, resp, next) => {
console.log(`${req.method}-${next}`);
next(); //执行下一个路由函数 因为next指向的就是下一个路由函数
},
(req, resp) => {
resp.send("here");
}
);
还有一种等价写法:
app.all(path,[func1,func2])//同理func1函数体需要执行next()
2.5❀ 公共路由路径
路由路径相同,请求方法不同。使用app.route()方便模块化管理
app
.route(/^\/admin$/)
.get((req, res) => {
res.send(`${req.method}`);
})
.post((req, res) => {
res.send(`${req.method}`);
});
2.6❀ 模块化路由
使用Express的Router对象可以创建模块化的对象,实现路由和入口JS的解耦。
优点:
- 统一的路由前缀
- 便于维护
- 模块化
user.js 与user相关的路由配置
const express =require('express');
const router=express.Router();
router.post('/login',(req,resp)=>{
resp.send(req.path)
})
router.post('/register',(req,resp)=>{
resp.send(req.path)
})
module.exports=router
timeline.js 与timeline相关的路由配置
const express =require('express');
const router=express.Router();
router.get('/list',(req,resp)=>{
resp.send(req.path)
})
module.exports=router
index.js路由入口,统一管理其他子路由
const express =require('express');
const app=express();
const user=require('./user');
const timeline=require('./timeline');
app.use('/user',user);
app.use('/timeline',timeline);
app.listen(8080,()=>{console.log('listen on 8080');})
最终生成的路由:
- POST /user/login
- POST /user/register
- GET /timeline/list
3️⃣ 请求对象
每个路由函数都有一个request对象,以下是request对象的常用属性名:
属性名 | 说明 | 备注 |
---|---|---|
method | 请求方法 | |
path | 请求路径 | 不包含GET参数 |
url | 请求URL | 除域名外的完整URL,包含GET参数 |
query | GET参数对象 | |
params | 路由参数对象 | |
headers | 请求报头 | |
cookie | 请求cookie | 需要使用cookie-parser中间件 |
ip | 客户端ip | |
body | post请求体 | 需要使用body-parser中间件 |
3.1❀ 获取请求cookie
Express默认不解析请求报头的Cookie。需要借助cookie-parser中间件。
npm install cookie-parser --save
const express = require('express');
const cookieParser = require('cookie-parser');
const app=express();
app.use(cookieParser());// 使用中间件
app.get('/',(req,resp)=>{
resp.json({ cookies:req.cookies });
});
app.listen(8080,()=>{
console.log('listen on 8080');
});
3.2❀ 获取请求体
不能像cookie-parser一样直接调用构造函数来使用,因为在webpack4中bodyParser()构造器已经弃用。
const express = require('express');
const bodyParser = require('body-parser');
const app=express();
app.use(bodyParser.urlencoded({
extended: true
}));
app.use(bodyParser.json());
app.post('/',(req,resp)=>{
resp.json({ body:req.body });
});
app.listen(8080,()=>{
console.log('listen on 8080');
});
4️⃣ 响应对象
每个路由函数都会接收一个响应对象,通过调用响应对象的方法,可以将响应发送到客户端,并结束请求处理。如果路由函数调用响应对象的任何方法,则客户端请求将被挂起,直到客户端超时。
4.1❀ 响应状态码
resp.status(statusCode)
app.get("/", (req, res) => {
res.status(200).end();
});
4.2❀ 响应头
resp.set(filed[,value])
resp.set({[field],value})
res.set('Content-Type','text/plain')
res.send('hello');
4.3❀ 下载文件
通过Content-Disposition 响应报头提示客户端下载文件。
resp.download(path[, filename][, options][,callback])
- path:需要提供给客户端下载的服务端文件路径。
- filename:客户端下载文件时的别名。
- options:下载选项。
- callback:回调函数。
app.get("/download", (req, res) => {
res.download("x.txt", "xxx.txt", (err) => {
if (err) {
console.warn("下载失败", err);
}
});
});
4.4❀ 结束响应过程
resp.end([chunk][,encoding][,callback])
//chunk相应数据
resp.end('hello world')
4.5❀ 重定向
resp.redirect([statusCode,]path)
默认状态码为302
app.get("/test", (req, res) => {
res.redirect(301,'http://www.ivotoo.com')
});
4.6❀ 渲染HTML模板页面
resp.render(view[,locals][,callback])
- view:视图名称。
- locals:传递到视图的变量对象,视图可以访问到这些变量并进行渲染。
- callback:回调函数。如果提供,该方法将返回可能的错误和HTML 字符串,但不是自动发送HTML到客户端
以下是渲染用户主页的示例:
app.get('/user/home',(reg,resp)=>{
resp.render('user/home',{name:'joe'} )
});
4.7❀ 设置 Cookie
resp.cookie(name,value[,options])
options :
- domain:域名。默认为当前域名。
- expires GMT:到期时间。如果未设置或者设置为0,则浏览器关闭后Cookie失效。
- httpOnly:将 Cookie标记为只有HTTP服务器能访问(客户端JS无法访问)。
- maxAge:以毫秒为单位的过期时间。通常比expires选项使用方便。
- path: Cookie 路径。
- secure:标记为仅在HTTPS协议下才发送。
- signed:是否对Cookie签名。
app.get("/cok", (req, res) => {
res.cookie('name','joe',{
httpOnly:true,
maxAge:24*3600*1000
}).end()
});
4.8❀ 发送HTTP响应
resp.send([body])
body 响应内容。支持string、Buffer、Object、Array.
- String: Content-Type将自动设置为 text/html.
- Buffer: Content-Type将自动设置为 application/octet-stream.
- Object或Array:Content-Type将自动设置为 application/json.
5️⃣中间件
中间件是一个函数,在应用的请求-响应周期中,能够访问请求对象、响应对象和next函数。
中间件可以执行以下任务:
- 执行逻辑代码。
- 更改请求和响应对象。
- 结束请求-响应周期。
- 调用下一个中间件。
如果当前的中间件没有调用next(),也没有结束请求-响应周期,则该请求将被挂起。
中间件有路由中间件和全局中间件两种。全局中间件对任何请求都会生效,路由中间件只对特定的路由生效。
5.1❀ 全局中间件
function logger(req,resp,next){
console.log(`${req.method}`);
next()
}
app.use(logger)
所有访问都会通过logger函数
5.2❀ 路由中间件
function logger(req,resp,next){
console.log(`${req.method}`);
next()
}
app.get("/", logger,(req, res) => {//挂载中间件
...
})
只有访问指定路由才会经过中间件
🌰响应时长中间件
const express = require("express");
const app = express();
function responseTime(req, resp, next) {
const start = Date.now();
resp.once("finish", function () {
console.log("处理时长", Date.now() - start);
});
next();
}
app.use(responseTime);
app.get("/abc", (req, res) => {
setTimeout(() => {
res.end();
}, 1000);
});
app.listen(8080, () => {
console.log("listen on 8080");
});
6️⃣错误处理
6.1❀ 同步错误
app.get('/',(req,resp)=>{
if(!req.query.name)
throw new Error('缺少参数name')
resp.end('ok');
})
这是Express报错默认模板
6.2❀ 异步错误
一般是指发生在回调函数中的错误,需要通过next(err)才能捕获异步错误
const fs = require("fs");
app.get("/abc", (req, res,next) => {
fs.readFile("G:/nodejs/wordps.txt", "utf-8", function (err, data) {
if (err) {
next(err);
return;
}
next(data);
});
})
6.2❀ 捕捉错误日志并输出到日志文件
错误处理函数
function(err,req,resp,next)
错误处理中间件需要挂载在应用末尾,这样才能保证捕获前面产生的错误
const fs = require("fs");
const express =require('express');
const app=express();
app.get("/abc", (req, res,next) => {
throw new Error('发生错误')
})
app.use((err,req,resp,next)=>{
fs.appendFile('error.txt',`${req.method} ${req.url} Error: ${err.message}`,'utf-8',(err)=>{
next(err);
})
})
app.use((err,req,resp,next)=>{
resp.json({
path:req.path,
message:err.message
})
})
app.listen(8080,()=>{
console.log('listen on 8080');
})
7️⃣模板渲染
模板引擎能够在应用中使用静态模板文件,在运行时,可以根据实际变量替换模板中的变量,并将渲染后的HTML 发送给客户端。
Express 可以使用jade、pug、mustache 和ejs 作为模板引擎,默认的是jade。但目前用ejs的比较多。
ejs模板
要渲染模板文件,需要安装对应的模板引擎,还需要更改app设置。
app.set('views', './templates');//设置模板文件目录
app.set ('view enqine', 'ejs');//设置模板引擎
🌰
tenplate/site/index.ejs
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<% if(show) {
for (let index = 0; index < people.length; index++) {
%>
<%=people[index]%>
<%
}}
%>
</body>
</html>
index.js
const express=require('express');
const app=express();
app.set('views','./templates');
app.set('view engine','ejs');
app.get('/',(req,resp)=>{
resp.render('site/index',{show:true,people:['Joe','Jane']})
})
app.listen(8080,()=>{
console.log('8080');
})
8️⃣留言板项目开发
此项目暂未使用数据库,后面的文章我会介绍结合数据库使用。
项目功能:
- 首页呈现留言列表和发布留言按钮(点击跳转到留言填写界面)
- 留言填写界面提供信息录入表单和发布按钮(点击发布浏览并跳至首页)
mkdir express-board
cd express-board
npm init -y
npm i express ejs body-parser --save
templates/publish.ejs
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>发布留言</title>
</head>
<body>
<form action="/publish" method="POST">
<fieldset>
<legend>发表留言</legend>
<div>
<label>姓名:</label><input type="text" name="username" required />
</div>
<div>
<label>内容:</label><textarea name="content"></textarea>
</div>
<div>
<button type="submit">发表</button>
<button type="reset">重置</button>
</div>
</fieldset>
</form>
</body>
</html>
templates/index.ejs
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>留言板</title>
</head>
<body>
<a href="/publish"><button>发表留言</button></a>
<% if(message.length==0){ %>
<p>当前没有留言</p>
<% }else{ %>
<table border="1" cellspacing="0" cellpadding="0">
<thead>
<tr>
<th>留言时间</th>
<th>留言人</th>
<th>留言内容</th>
</tr>
</thead>
<tbody>
<% message.forEach(it=>{%>
<tr>
<td><%= it.time%></td>
<td><%= it.username%></td>
<td><%= it.content%></td>
</tr>
<% }) }%>
</tbody>
</table>
</body>
</html>
index.js
const express =require('express');
const bodyParser=require('body-parser');
const app=express();
let message=[]
app.set('views','./templates');
app.set('view engine','ejs');
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({
extended: true
}));
app.get('/',(req,resp)=>{
resp.render('index',{message});
})
app.route('/publish').get((req,resp)=>{
resp.render('publish');
}).post((req,resp)=>{
const now =(new Date()).toLocaleString();
if(!req.body.username||!req.body.content){
throw new Error('信息缺失');
}
message.push({username:req.body.username,content:req.body.content,time:now})
resp.redirect('/')
})
app.listen(8080,()=>{
console.log('listen on 8080');
})