# Node服务
现在讲究的是前后端彻底分离的开发方式,在 Express 中,我们做的路由,就当作是后端接口,即 apiserver 或 webservice。
app.use(express.static('public')) 中的 public 文件夹,看作是前端的根目录,即 webserver。
# 一、Restful API 规范
后端在配置路由时,应有一套约定俗成的规范,说白了就是让路由具有语义,比如:
- get 访问 user 时,表示获取用户;
- post 访问 user 时,表示添加用户;
- put 访问 user 时,表示修改用户;
- delete 访问 user 时,表示删除用户;
CRUD 操作:GET读取(Read);POST新建(Create);PUT更新(Update);PATCH更新(Update),通常是部分更新;DELETE删除(Delete)
开发人员所做的路由中,只要做出来这种语义,就相当于实现了 Restful 规范了。
app.get('/user', (req, res)=>{
res.json({code:1, text:'获取成功'});
})
app.post('/user', (req, res)=>{
res.json({code:0, text:'添加失败'});
})
app.put('/user', (req, res)=>{
res.json({code:1, text:'修改成功'});
})
app.delete('/user', (req, res)=>{
res.json({code:1, text:'删除成功'});
})
2
3
4
5
6
7
8
9
10
11
12
无论哪种路由,最终响应的内容应该是json,然后有属性描述描述这事是否成功。上文中code就是状态属性,它的值自己定义。
# 二、编写接口文档
进到企业接到项目后,通常是由多人协同工作,一起完成项目,这里面有产品经理、UI、前端、后端、运维、测试等工种,每人的职责是不同的。
前端与后端的配合工作,通常需要依赖接口文档,这个接口文档,通常是由后端提供。
接口文档
method | url | 参数 | 返回值 | 说明 |
---|---|---|---|---|
get | news | id | {id,title,content...} | 根据指定id得到的一条数据 |
post | news | title,content | {id...} | 向news表中添加一条数据,添加成功后返回这条数据的id |
接口调试工具 Postman 或 insomnia 可以快速测试接口是否正常
# 三、同源策略和跨域访问
# jsonp(重点)
var script = document.createElement('script')
document.getElementByTagName('head')[0].appendChild(script)
script.src = '跨域的网址'
2
3
jsonp 的原理是动态创建 script 标签,通过 src 属性跨域访问某个文件,因为 script 天生支持跨域,所以能够得到访问的文件的响应内容,响应内容会自动进入 script 标签中,当作是 js 代码被自动执行,所以要求响应内容必须是一个函数,该函数我们需要在前端自己定义。
# cors 跨域资源共享(重点)
目前都是使用前后端分离的形式做项目,即前端是一个网站,后端是另一个网站,前端通过 ajax 技术发起请求,访问后端网站提供的接口,这就形成了跨域访问,默认时会被后端网站拦截下来,会报错,比较常见的解决方案是 cors,指的是后端授权别人访问我们的后端网站,授权代码如下:
app.all('*', function (req, res, next) {
res.setHeader('Access-Control-Allow-Origin','*');
next();
})
2
3
4
cors 这种解决方案的前提是,我们能够修改后端代码,如果不能修改代码,就应该选择代理方案解决跨域问题。
# http-proxy-middleware 代理跨域
代理的核心是,前端直接跨域访问别人的网站时,被拦截了,我们又无权修改别人的网站,这时我们可以使用我们的后端访问别人的网站,后端是没有跨域拦截这一说法的,即,前端做不了的事,交给后端做,我们的后端跨域拿到了别人的数据后,我们的前端访问我们自己的后端,拿到这个数据。
// 引用依赖
var express = require('express');
// cnpm i http-proxy-middleware -S
var proxy = require('http-proxy-middleware');
// proxy 中间件的选择项
var options = {
target: 'http://www.example.org', // 目标服务器 host
changeOrigin: true, // 默认false,是否需要改变原始主机头为目标URL
pathRewrite: {
'^/api/old-path' : '/api/new-path', // 重写请求,比如我们源访问的是api/old-path,那么请求会被解析为/api/new-path
'^/api/remove/path' : '/path' // 同上
}
};
// 创建代理
var exampleProxy = proxy(options);
// 使用代理
var app = express();
app.use('/api', exampleProxy);
app.listen(3000);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 四、鉴权
鉴权(authentication)是指验证用户是否拥有访问系统的权利。
比如新闻网站,后台发送新闻消息,并不是什么人都有权发送新闻消息的,需要用户登录成功后,才有权发新闻消息,怎样实现呢?
# token 鉴权流程
jwt: ( json web token )
网站中有些页面任何人都能浏览,但有些页面属于敏感页,只有登录成功的人才能浏览。
- 用户登录成功后,后端生成token字符串,后端把token返回给前端
- 前端接收token,用storage本地保存
- 前端访问敏感页面时,把storage中的token传递给后端
- 后端接收前端传过来的token,与早期登录时创建的token验证是否一致
- 如果一致,则表示有权限;如果不一致,则表示用户没有权限访问该页
# 服务器
app.js 服务器端
const express = require("express");
const app = express();
app.use(express.static("public"));
// cnpm i jsonwebtoken -S
const jwt = require('jsonwebtoken');
// 先设置 token
app.all("/login", function(req, res){
var token = jwt.sign({
data: 'wangyang'
}, 'secret', { expiresIn: 60 });
/*
expiresIn:单位为秒,60*60即1小时。
secret:秘钥,随意写,和verify中相同即可,作用是将data内容加密。
data:'wangyang' 这个是随便写的,我们想在token中保存什么数据。
*/
res.json({"token":token});
})
// 然后访问某个页面,验证一下token是否合法
app.all("/look", function(req, res){
var token = req.body.token || req.query.token;
console.log(token);
// 同步的写法
//var decoded = jwt.verify(token, 'secret');
//console.log('decoded:', decoded.data) //
// 如果失效,则直接报错,所以可以用 try catch
// 异步的写法
/**/
jwt.verify(token, 'secret', function(err, decoded) {
if( err ){
res.json( {"msg":"token验证失败"} );
}else{
res.json( {"msg":"token验证成功", "data":decoded.data} )
}
});
})
app.listen(8088);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# 客户端
index.html 客户端
<script src="https://cdn.bootcss.com/axios/0.19.0-beta.1/axios.min.js"></script>
先登录,获取token
axios.post('/login').then(res=>{
var {token} = res.data;
localStorage.setItem("token", token);
})
2
3
4
然后访问敏感页,看看是否具有权限访问。
var data = "token="+localStorage.getItem("token");
axios.post('/look', data).then(res=>{
alert(JSON.stringify(res.data));
})
2
3
4
整体的鉴权就做完了,现在最主流的就是这种做法。很多年前,在 jwt 没有流行的时候,大家还会用 session 或 cookie 做权鉴,但现在基本上没有人这么做了。
现在的 session 和 cookie 通常是用来做跨页面保存数据的。
# 五、session
Session 翻译过来就是会话的意思,浏览器打开-关闭,一个会话就结束了,Session就无效了。
var express = require('express');
var app = express();
// npm i express-session -S
var session = require("express-session");
// 使用session时,必须设置secret,密钥,对数据进行加密处理
app.use(session({"secret":"wy"}));
// 创建 session
app.get('/', function (req, res) {
req.session.a = "中文abc123,任意数据";
req.session.b = 456;
res.send(`<a href='/look'>查看session</a>`);
})
// 查看 session
app.get('/look', function (req, res) {
console.log(req.session);
res.end("get");
})
app.listen(8081);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 六、cookie
每当发起一个请求时,会把本地的cookie传给服务器,所以我们要先有cookie,然后才能在服务器端获取cookie。
- session 只能用服务器方式操作,而 cookie 即可以在客户端操作,也可以在服务器操作。
- session 内容保存到服务器内存中,而 cookie 内容保存到客户端硬盘上。
- session 的时效为会话,而 cookie 可以通过 expires 来设置时效。
var express = require('express');
var app = express();
// cnpm i cookie-parser -S
var cookieParser = require('cookie-parser');
app.use(cookieParser());
app.get('/', function (req, res) {
res.cookie('a', '你好', { maxAge: 60000}); // 当前时间的60秒后过期
res.cookie('b', '中文abc123', { expires: new Date(Date.now() + 60000)});
res.cookie('c', '会话时间');
res.cookie('d', '路径', { path: '/'});
res.send(`<a href='/look'>查看cookie</a>`);
})
app.get('/look', function (req, res) {
console.log(req.cookies)
res.end("get");
})
app.listen(8081);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 七、crypto加密
数据库在保存一些敏感内容,比如密码这类的数据时,不会明文将数据保存到数据库,而是把数据做个加密,然后在保存,这样做的好处是,就算系统管理员打开了数据库,他也不知道存储的密码是啥。
// crypto 是系统模块,无需 install 下载
var crypto = require('crypto');
// 创建 md5 加密规则,md5是关键字
var md5 = crypto.createHash('md5');
// 原始内容
var message = 'hello';
// 加密后的内容,加密时先做utf8编码,然后做16进制转换
var digest = md5.update(message, 'utf8').digest('hex'); // hex表示16进制
console.log(digest);
// 将原始内容hello加密后显示如下16进制内容
// 5d41402abc4b2a76b9719d911017c592
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 八、图片
# multer上传模块
const express = require('express');
const app = express();
const fs = require('fs');
app.use(express.static('public'));
app.use(express.static('uploads'));
app.listen(8080);
// cnpm i multer -S
const multer = require('multer');
// dest 描述的是上传的文件保存到什么位置
// single 表示只上传一个文件,file1表示这个文件的名字叫啥
const upload = multer({"dest":"uploads/"}).single('file1');
// 中间件
// 无论访问哪个路由页面,后端都判断一下是不是有上传文件,
// 如果有上传文件,则使用multer模块进行保存
app.use(upload)
// 路由
app.post('/chk', (req, res)=>{
console.log('file:', req.file);
var newFile = req.file.path+'_'+req.file.originalname;
fs.renameSync(req.file.path, newFile);
res.send(`
<a href="/">上传完毕 重新上传</a><br>
<img src="'+newFile.replace('uploads/','')+'">
`);
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
以上代码仅仅是服务器端接收上传数据,保存上传文件的代码,完整的上传功能还需要前端配合,前端需要把文件提交过来,提交文件的方法有很多种,比如表单、ajax、axios、jquery、fetch等。
# 表单上传
<form action="/chk" method="post" enctype="multipart/form-data">
<input type="input" name="input1" />
<input type="file" name="file1" />
<input type="submit" value="提交">
</form>
2
3
4
5
# ajax上传
<form id="form1" name="form1">
<input type="input" name="input1" id="input1" />
<input type="file" name="file1" id="file1" />
<input type="submit" value="提交">
</form>
<script>
form1.onsubmit = e=>{
e.preventDefault(); // 阻止表单提交的默认行为
// ajax 上传文件
var xhr = new XMLHttpRequest();
xhr.open("POST", "/chk", true);
xhr.onreadystatechange = function(){
if( xhr.readyState==4 && xhr.status==200 ){
alert(xhr.responseText);
}
}
xhr.upload.onprogress = function(e){
console.log( e.loaded+'/'+e.total);
}
// 方法1:直接写表单,但要注意其文本域和文件域必须有name属性
//var fd = new FormData(form1);
// 方法2:每一项单独设置(这种方法可以把表单去掉)
var fd = new FormData();
fd.set("input1", input1.value);
fd.set("file1", file1.files[0])
xhr.send(fd);
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# axios 上传
<form id="form1" name="form1">
<input type="input" name="input1" id="input1" />
<input type="file" name="file1" id="file1" />
<input type="submit" value="提交">
</form>
<script src="https://cdn.bootcss.com/axios/0.19.0-beta.1/axios.min.js"></script>
<script>
form1.onsubmit = e=>{
e.preventDefault();
var fd = new FormData(form1);
axios.post('/chk', fd).then(res=>{
console.log(res.data)
})
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# jquery 上传
<form id="form1" name="form1">
<input type="input" name="input1" id="input1" />
<input type="file" name="file1" id="file1" />
<input type="submit" value="提交">
</form>
<script src="https://cdn.bootcss.com/jquery/1.12.4/jquery.min.js"></script>
<script>
form1.onsubmit = e=>{
e.preventDefault();
var fd = new FormData(form1);
$.ajax({
url: '/chk',
type:'POST',
cache: false, // 不缓存此页面
processData: false, // 不编码上传内容
contentType: false, // 发送信息至服务器的编码
data: fd,
success: function(data){
console.log(data)
}
})
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# fetch上传
<form id="form1" name="form1">
<input type="input" name="input1" id="input1" />
<input type="file" name="file1" id="file1" />
<input type="submit" value="提交">
</form>
<script>
form1.onsubmit = e=>{
e.preventDefault();
var fd = new FormData(form1);
let request = new Request('/chk', {
method: 'POST',
//credentials: 'include', // 跨域时传递cookie
body: fd,
});
fetch(request).then(response=>response.text()).then(result=>{
console.log(result);
})
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# enctype
enctype 属性规定在发送到服务器之前应该如何对表单数据进行编码。
- application/x-www-form-urlencoded 在发送前编码所有字符(默认)
- multipart/form-data 不对字符编码。在使用包含文件上传控件的表单时,必须使用该值。
- text/plain 空格转换为 "+" 加号,但不对特殊字符编码。
# base64
Base64是一种基于64个可打印字符来表示二进制数据的方法。
有些后端开发者不喜欢用文件的形式存储上传过来的图片,他们希望用一组字符串来表示图片,所以他们会选择base64。
优势:节省了硬盘空间,方便文件备份。
不足:开发成本增加。
客户端
<form id="form1" name="form1">
<input type="file" name="file1" id="file1" />
<img id="img1" />
<input type="submit" value="提交">
</form>
<script src="https://cdn.bootcss.com/axios/0.19.0-beta.1/axios.min.js"></script>
<script>
var base64;
file1.onchange = e=>{
// 用文件对象来记录文件内容
var oFReader = new FileReader();
// 文件域中的第一个文件(注:文件域是可以设置多选的,所以下标0表示选择的第一个文件)
var file = file1.files[0];
// 读文件,把内容载入到变量中,内容就是这个文件的base64。
oFReader.readAsDataURL(file);
oFReader.onloadend = function(oFRevent){
base64 = oFRevent.target.result;
//console.log(base64);
img1.src = base64
}
}
form1.onsubmit = e=>{
e.preventDefault();
var fd = new FormData();
fd.append('file1', base64);
axios.post('/chk', fd).then(res=>{
console.log(res.data)
})
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
服务器端
后端body数据,无法使用body-parser解析,但可以使用multer解析。
app.post('/chk', (req, res)=>{
console.log('body:', req.body);
res.send('响应结束');
});
2
3
4