# 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:'删除成功'});
})
1
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 = '跨域的网址'
1
2
3

jsonp 的原理是动态创建 script 标签,通过 src 属性跨域访问某个文件,因为 script 天生支持跨域,所以能够得到访问的文件的响应内容,响应内容会自动进入 script 标签中,当作是 js 代码被自动执行,所以要求响应内容必须是一个函数,该函数我们需要在前端自己定义。

# cors 跨域资源共享(重点)

目前都是使用前后端分离的形式做项目,即前端是一个网站,后端是另一个网站,前端通过 ajax 技术发起请求,访问后端网站提供的接口,这就形成了跨域访问,默认时会被后端网站拦截下来,会报错,比较常见的解决方案是 cors,指的是后端授权别人访问我们的后端网站,授权代码如下:

app.all('*', function (req, res, next) {
    res.setHeader('Access-Control-Allow-Origin','*');
    next();
})
1
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);
1
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);
1
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>
1

先登录,获取token

axios.post('/login').then(res=>{
    var {token} = res.data;
    localStorage.setItem("token", token);
})
1
2
3
4

然后访问敏感页,看看是否具有权限访问。

var data = "token="+localStorage.getItem("token");
axios.post('/look', data).then(res=>{
    alert(JSON.stringify(res.data));
})
1
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);
1
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);
1
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
1
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/','')+'">
    `);
});
1
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>
1
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>
1
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>
1
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>
1
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>
1
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>
1
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('响应结束');
});
1
2
3
4