1.概述
Express 是一个自身功能极简,完全是由路由和中间件构成一个的 web 开发框架:从本质上来说,一个 Express 应用就是在调用各种中间件。
2.express与koa的选择
Express
- Express 的优点是线性逻辑:路由和中间件完美融合,通过中间件形式把业务逻辑细分,简化,一个请求进来经过一系列中间件处理后再响应给用户,再复杂的业务也是线性了,清晰明了。
- Express 是基于 callback 来组合业务逻辑。Callback 有两大硬伤,一是不可组合,二是异常不可捕获。
- Express 通常会要借助 async、bluebird 等异步库。但即便有了这类异步库,当涉及到共享状态数据时,仍然不得不写出嵌套异步逻辑。
koa
- 借助 promise 和 generator 的能力,丢掉了 callback,完美解决异步组合问题和异步异常捕获问题。
- koa 把 express 中内置的 router、view 等功能都移除了,使得框架本身更轻量化。
Express 和 Koa 最明显的差别就是 Handler 的处理方法,一个是普通的回调函数,一个是利用生成器函数(Generator Function)来作为响应器。往里头儿说就是 Express 是在同一线程上完成当前进程的所有 HTTP 请求,而 Koa 利用 co 作为底层运行框架,利用 Generator 的特性,实现“协程响应”
3.express搭建简单的本地服务示例
var express=require('express'); //引入express
var app=express(); //初始化app
app.set('port',process.env.PORT||8080); //设置端口号
//处理各种请求
app.get('/list',function(req,res){
res.send(req.url);
})
app.post('/list',function(req,res){
res.send('post'+req.url);
})
app.all('/all',function(req,res){//匹配所有方法
res.send('all'+req.url);
})
app.use(function(req,res){ //处理404页面
res.type('text/plain');
res.status(404);
res.send('404-Not Found');
})
app.use(function(err,req,res,next){ //定制500页面(错误处理)
console.error(err.stack);
res.type('text/plain');
res.status(500);
res.send('500-server error');
})
app.listen(app.get('port'),function(){ //监听端口
console.log('start on'+app.get('port'));
})
4.模板解析(动态返回客户端页面,防止重复资源过多)
模板的选择
准则:性能(模板引擎要快)、前后两端兼容性好、可读(语法简单)-
ejs模板示例(express配合ejs模板引擎工作)
app.jsconst express=require('express'); const app = express(); app.listen(8080); app.set('view engine','ejs')//设置模板引擎为ejs app.set('views',./views)//设置模板引擎存放的根目录在./views文件夹下(当ejs模板存放在./views文件下时,这个设置是可以注释掉的) app.get('请求地址',function(res,req){ res.render('文件地址',{data:realData})//data为绑定到模板中的动态数据(文件地址可以忽略.ejs后缀和./view前缀) })
views/index.ejs中ejs的语法示例
<% for(var i=0; i<supplies.length; i++) { %> <li> <a href='supplies/<%= supplies[i] %>'> <%= data[i] %> </a> </li> <% } %> <% include header.ejs%>//引入其他模板(组件化)
5.req请求对象
url的组成
- 协议 : http 、https、file 、ftp 等
- 域名: eg: www.baidu.com
- 端口:http默认80端口、https默认443端口等
- 路径:/home、index.html等
- 查询字符串:?test=1&name=0(一般用encodingURIComponent()解码)
- 信息片段: #history(用于锚点)
req的属性
1. req.params
一个数组,包含命名过的路由参数
2. req.param()
返回命名的路由参数
3. req.query
一个对象,包含以键值对存放的查询字符串参数
4. req.body
一个对象,包含post请求发送的数据(需要使用body-parse中间件)
5. req.route
关于当前匹配路由的信息
6. req.cookies/req.signedCookies
一个对象,包含从客户端传递过来的cookie值
7. req.headers
从客户端接收到的请求头信息
8. req.accept([type])
一个简单的方法,用来确定客户端是否接受一个或一组指定的类型
9. req.ip
客户端的ip地址
10. req.path
请求路径(不包含协议主机端口或查询字符串)
11. req.host
主机名
12. req.xhr
请求是ajax时返回true
13. req.secure
判断协议是https就返回true
14. req.url
文件路径+查询字符串
6.res响应对象
1. res.status(code)
设置http的状态码,默认是200([状态码详解](https://www.jianshu.com/p/bbfbd63ac4f0))
2. res.set(name,value)
设置响应头,这通常不用手动设置
3. res.cookie(name,value,[options])/res.clearCookie(name,[options])
设置或清除cookie的值(需要中间件支持)
4. res.redirect([status],url);
重定向,默认重定向代码是302(临时重定向)
5. res.send(body),res.send(status,body)
向客户端发送响应及可选择的状态码。默认内容类型是text/html
6. res.json(json),res.json(status,json)
向客户端发送json对象或数组,及可选择的状态码
7. res.jsonp(json),res.jsonp(status,json)
向客户端发送jsonp,及可选择的状态码
8. res.type(type)
设置content-type信息,相当于res.set('content-type','type');
9. res.formate(object)
根据接受的请求头不同发送不同的数据res.formate({'text/plain':'hi there','text/html':'<i>hi there<i>'})
10. res.sendFile(path,[option],callback)
根据路径读取指定文件,并将内容发送给客户端
11.res.local,res.render(view,[locals],callback)
res.locals是一个对象,包含用于视图渲染的默认上下文(默认一个空对象)
res.render渲染一个视图,并响应给客户端
7.post请求处理
-
表单键值对
form表单提交:action 地址、method 提交方式、name value键值对、submit提交按钮app.use(require('body-parser')()) app.post('url',function(req,res){ console.log(req.body)//发送的键值对 res.send('ok'); })
表单大文件(可以用 formidable上传)
ajax提交(和表单的处理方式一样)
8.get请求处理
app.get('url',function(req,res){
console.log(req.query)//拿到接收的参数
console.log(req.path)//拿到请求的路径
//根据实际情况作出响应
res.send('ok');
})
9.中间件
概念:
- 中间件是一种函数的封装方式,具体来说就是封装在程序中处理HTTP请求的功能,实际上,中间件是一个有三个参数的函数:一个请求对象,一个响应对象,一个next函数(还有一种4个参数的形式,用来做错误处理)
特点
- 每个中间件都可以控制流程是否继续进行
- req,res 相同对象
- 如果出错了 转交错误处理中间件进行处理 next()函数调用的时候里边添加参数即可
- 处理错误的中间件放在最后处理 否则不起作用
中间件如何工作的
- 路由处理器(app.get app.post等)被看做只处理特定HTTP谓词(GET/POST)请求的中间件,也可以将中间件看做可以处理全部HTTP谓词(GET/POST)谓词的的路由处理器(基本上等同于app.all,可以处理任何HTTP谓词)
app.get('someUrl.',function(req,res,next){......}) app.post('someUrl.',function(req,res,next){......}) app.all('someUrl.',function(req,res,next){......})
- 路由处理器第一个参数必须是路径。如果想匹配所有的路径,只需使用
/*
。中间件也可以使用路径作为第一个参数,但是是可选的(如果忽略这个参数,他会匹配所有路径,就像指定了/*
)
app.use('someUrl.',function(req,res,next){......})
- 路由处理器和中间件的参数中都有回调函数,这个函数有 2个(req,res) 、 3个(req,res,next) 、 4个参数 (err,req,res,next用于错误处理)
- 如果不调用next(),管道就会停止,也不会再有处理器或者中间件做后续处理。如果你不调用next(),则应该发送一个响应到客户端(res.send()、res.json() 、res.render()等);如果不这样做,客户端会被挂起最终导致超时。
- 如果调用了next(),就不要再发送响应到客户端。
实例
var express=require('express');
var app=express();
//中间件
app.use(function(req,res,next){//可以传路径也可以不传路径
console.log('all');
next();//这里不调用next后边的中间件不会执行
})
app.use('/a',function(req,res){
console.log('/a 路由停止')
res.send('a')
})
app.use('/a',function(req,res){
console.log('/a 不会调用')
})
app.use('/b',function(req,res,next){
console.log('/b 路由传递')
next();
})
app.use(function(req,res,next){
console.log('除了a都打印');
next();
})
app.use('/b',function(req,res,next){
console.log('bbb');
throw new Error('b失败')
})
app.use('/b',function(err,req,res,next){
console.log('b接收错误');
next(err);
})
app.use('/c',function(err,req){
console.log('ccc');
throw new Error('c失败')
})
app.use('/c',function(err,req,res,next){
console.log('c错误传递');
next(err);
})
app.use(function(err,req,res,next){//用来处理上边的中间件中抛出的错误
console.log('服务器错误');
res.send(500)
})
app.use(function(req,res){
console.log('未找到路由');
res.send(404)
})
app.listen('8080');
常用的中间件
- body-parser
安装:npm install body-parser --save
const bodyParser=require('body-parser')//引入
app.use(bodyParser())//使用
- cookie-parser
安装:npm install cookie-parser --save
const cookieParser=require('body-parser')//引入
app.use(cookieParser(秘钥放在这里))//使用
- cookie-session提供cookie存储的会话支持,一定要把他连在cookie-parser后面连入
安装:npm install cookie-session --save
const cookieParser=require('body-parser')//引入
app.use(cookieParser(秘钥放在这里))//使用
- express-session提供会话ID(存在cookie里)的会话支持。默认存在内存里,不适用于生产环境,并且可配置为使用数据库存储。
安装:npm install express-session --save
const expressSession=require('body-parser')//引入
app.use(expressSession())//使用
- static 返回静态文件
app.use(express.static('public'))//public是静态文件的根目录
http://localhost:3000/images/kitten.jpg//请求的是public/images/kitten.jpg文件
- csurf 防范跨域请求伪造(CSRF)攻击,因为它要使用会话,所以必须放在express-session中间件的后面。
安装:npm install csurf --save
const csurf =require('csurf ')//引入
app.use(csurf ())//使用
- morgan(提供自动日志纪录支持,所有请求都会被纪录)
安装:npm install morgan--save
const morgan=require('morgan')//引入
app.use(morgan())//使用
Connect
在express4.0版本之前,express捆绑了Connect,它包含了大量的中间件,使很多中间件看起来像express的一部分,显得臃肿,所以4.0以后Connect从express中移除了,所以4.0以后的中间件大部分是需要自己手动安装的。
- Connect中的中间件大部分是很基础的(源码很重要),现在也可以手动安装来使用
npm install connect --save
10.路由
路由参数(同vue的路由)
app.get('/list/:name',function(req,res,next){
if(req.params.name){//拿到路径中name代表的值
res.send(req.params.name)//把name代表的值发送给客户端
}else{
next()//如果没有name路径就往下传递
}
})
多个路由参数
app.get('/list/:city/:name',function(req,res,next){
console.log(req.params.city)//拿到city参数
console.log(req.params.name)//拿到name参数
res.send('')//响应客户端
})
路由设计
- 原则
- 给路由处理器用命名函数
- 把路由放在单独的文件中(router.js),并根据功能区把路由分开
- 路由组织应该是可扩展的(路由增加而不会被搞乱)
- 减少重复的代码(可以使用自动化的基于视图的路由处理器)
- 组织路由的方式实例
app.js 服务文件
./controller/router.js 路由文件const express=require('express'); const app=express(); //各种需要使用的中间件 const router=require('./controller/router')(app); //引入路由文件 ... app.listen(8082);
main.js路由处理函数文件const main=require('./main'); //引入路由处理函数文件 module.exports=function(app){ //导出所有路由 app.get('/',main.showIndex);//首页显示静态资源文件 app.get('/getAllCon',main.getAllCon); //获取首页的文章加载 app.get('/getData',main.showdata);//首页显示 默认显示最新消息 app.get('/showLogin',main.showLogin);//显示登录页 app.get('/exit',main.exit);//退出登录 } ...
//各种需要使用的插件,中间件 exports.showIndex=function(){...};//导出所有的处理函数 exports.getAllCon=function(){...}; exports.showdata=function(){...}; exports.showLogin=function(){...}; exports.exit=function(){...} ...
11.REST API(表现层状态转化)接口架构思想
说明
(1)每一个URI代表一种资源;
(2)客户端和服务器之间,传递这种资源的某种表现层;
(3)客户端通过四个HTTP动词,对服务器端资源进行操作,实现"表现层状态转化"。
规范的问题
- 因为多人开发时,会导致处理请求方式(get、post乱用),接口不统一(接口名写法千差万别),会导致各种状况都有,后期维护是个大问题,再就是请求方式也不对。
- 所以RESTFUL API的结构设计就诞生了
-
接口命名规范
域名://将api部署在专用域名下: http://api.example.com / /或者将api放在主域名下: http://www.example.com/api/
版本:
将API的版本号放在url中。 http://www.example.com/app/1.0/ http://www.example.com/app/1.2/
路径:
路径表示API的具体网址。每个网址代表一种资源。 资源作为网址,网址中不能有动词只能有名词,一般名词要与数据库的表名对应。 一般来说,数据库中的表都是同种记录的"集合"(collection),所以API中的名词也应该使用复数。 http://www.example.com/app/1.0/lists http://www.example.com/app/1.0/names
-
使用标准的HTTP方法
使用标准的HTTP方法:对于资源的具体操作类型,由HTTP动词表示。 常用的HTTP动词有四个。
GET :从服务器取出资源(一项或多项) POST :在服务器新建资源。 PUT :在服务器更新资源。 DELETE :从服务器删除资源。
过滤信息
如果记录数量很多,服务器不可能都将它们返回给用户。API应该提供参数,过滤返回结果。 eg: ?limit=10:指定返回记录的数量 ?page=2&per_page=100:指定第几页,以及每页的记录数。 ?sortby=name&order=asc:指定返回结果按照哪个属性排序,以及排序顺序。
示例:
- 一个URI实现增删改查所有操作
//CRUD(增删改查) 操作学生用户的接口 /rest/1.0/userinfo
//增加一个id为1的用户
post /rest/1.0/userinfo data: {id:1,name:'张三'}
//删除id为1的用户
delete /rest/1.0/userinfo?id=1
//修改id为1的用户
put /rest/1.0/userinfo?id=1 data:{id:1,name:'李四'}
//查询所有用户
get /rest/1.0/userinfo
//查询id为1的用户信息
get /rest/1.0/userinfo?id=1
12.静态资源
不会基于每个请求而改变的资源
性能优化
- 压缩合并静态资源
- 静态资源托管给发布网络CDN,能够启用缓存,根据地理位置进行优化(CDN系统能够实时地根据网络流量和各节点的连接、负载状况以及到用户的距离和响应时间等综合信息将用户的请求重新导向离用户最近的服务节点上。)
- 设置服务器缓存
13.缓存的几种方式
如果请求的资源 和 它最后一次修改时一样就返回 Status Code 304 Not Modified,浏览器走缓存
请求头对应的响应头
If-Modified-Since(与资源最后更新时间比较) 和 last-modified 对应
if-none-match(与Etag比较是否不一致) 和 etag 对应
后台express处理last-modified和etag方式
last-modified 请求的静态资源最后一次修改的时间 GMT格式
//取得最后修改时间
var lastModified=new Date(req.headers['if-modify-since']);
//console.log(lastModified)
fs.stat(filename,function(err,stat){
//判断if-modify-since的值是否跟资源的最后修改时间一致
if(stat.mtime.getTime()==lastModified.getTime()){
//console.log(lastModified.getTime(),stat.mtime.getTime())
res.status=304;
res.send('');
}else{
res.setHeader(200,{'Last-Modified':stat.mtime.toGMTString()});
fs.createReadStream(filename).pipe(res);
}
})
etag 把请求的静态资源通过摘要算法计算得到的散列值
var ifNoneMatch=req.headers['if-none-match'];
if(ifNoneMatch==getHash(data)){
res.status=304;
res.send('');
}else{
res.setHeader(200,{'Etag':getHash(data)});
fs.createReadStream(filename).pipe(res);
}
强缓存设计两个响应首部(服务器告诉浏览器缓存一个月,那么浏览器一个月都不会发起请求,除非人为清浏览器的缓存)
expires 静态资源过期时间 浏览器端GMT时间
cache-control 缓存多少秒 (单位是秒)
后台express处理expires和Cache-Control方式
var expires=new Date(Date.now()+10*1000);
res.setHeader('Expires',expires.toUTCString());
res.setHeader('Cache-Control',"max-age=10");
res.end(data);
14.安全
1.协议https
https协议基于服务器上的SSL证书,端口默认443
2.跨站请求伪造(CSRF)攻击(伪造登录的安全数据)
防范方法:给浏览器传一个唯一的令牌