综合应用服务端知识点搭建项目
下载安装所需的第三方模块
npm init -y
npm i express cors mysql
# express 用于搭建服务器
# cors 用于解决跨域
# mysql 用于操作数据库# 后面用到什么,再下载
创建app.js
之前,我们开启一个服务器,js文件名,一般都是 01-xxx.js
03-xxx.js
。
对于正常的一个项目来说,用于开启服务的js文件,一般都叫做 app.js
// 开启服务
const express = require('express');
const app = express();
app.listen(3006, () => console.log('server running~'));
使用路由模块精简项目结构
使用路由模块的原因(意义)
- 试想,如果项目有100个路由(接口),全部放到
app.js
中,这样的代码不好维护,效率也比较低下 - 所以需要按照路由(接口)功能的不同,分别使用小文件存放这些路由(接口),这些小文件就叫做路由模块
- 比如,登录注册相关的,全部放到
./routers/login.js
中 - 比如,文章处理相关的,全部放到
./routers/article.js
中 - 比如,书籍管理相关的,全部放到
./routers/books.js
中
- 比如,登录注册相关的,全部放到
代码如何实现
- 路由文件中的代码
- 加载express
- 创建路由对象
- 将路由(接口)挂载到路由对象上
- 导出路由对象
/*** 1. 加载express* 2. 创建路由对象* 3. 把路由挂载到路由对象上* 4. 导出路由对象*/const express = require('express');
const router = express.Router();// 下面是图书管理相关的路由
router.get('/getbooks', (req, res) => {res.send('我是获取书籍的接口');
});router.post('/addbook', (req, res) => {});router.get('/delbook', (req, res) => {});module.exports = router;
- app.js 中的代码
- 加载路由模块,并注册成中间件。注册的时候,可以统一加前缀
// 开启服务
const express = require('express');
const app = express();
app.listen(3006, () => console.log('server running~'));// 加载路由模块,并注册成中间件
let books = require('./routers/books');
app.use('/api', books);
开启服务(nodemon app.js) ,然后通过浏览器来测试一下你的接口是否能够正常访问
创建数据库、数据表
数据库,还可以使用昨天的 user 数据库。
在里面创建一张数据表(books)
添加几条模拟数据:
粗略的完成获取书籍的接口
-
目前:
- 数据准备好了
- 接口能够正常访问了
-
下面要做的事情:
- getbooks接口中,通过mysql模块,查询所有的书籍,并把结果响应给客户端
- 中间件解决跨域问题
getbooks接口
router.get('/getbooks', async (req, res) => {// 查询数据库user、数据表books里面的数据,把查询的结果响应给客户端const mysql = require('mysql'); // require加载一个模块后,会缓存起来const conn = mysql.createConnection({host: 'localhost',user: 'root',password: '12345678',database: 'user' // 填写数据库名});conn.connect();conn.query('select * from books', (err, result) => {if (err) return console.log(err);res.json({status: 200,msg: '获取成功',data: result});});conn.end();
});
应用级别的中间件解决跨域
- 应用级别的中间件
- 写到app.js中的中间件
- 该中间件会影响所有的路由
- 路由级别的中间件
- 写到 路由文件中的中间件
- 该中间件只会影响当前的路由文件
因为整个项目的全部路由都需要解决跨域问题,所以需要定义应用级别的中间件
所以,在app.js中,加入如下代码:
// 配置中间件,解决跨域问题
app.use((req, res, next) => {res.set({'Access-Control-Allow-Origin': '*'});next();
});
封装db.js
试想,后面还有很多路由中需要对数据库进行操作,难道每次执行SQL语句,都要写5个步骤(操作MySQL的5个步骤)吗?
答:肯定不是,那样的话,代码太多。代码复用性太差了
解决办法:封装
具体做法:
- 创建 db.js
- 里面封装 使用mysql模块的5个步骤
- 导出函数
- 其他路由中,需要加载(导入)db模块,然后调用函数即可
db.js 中的代码:
function db(sql, params, callback) {const mysql = require('mysql');const conn = mysql.createConnection({host: 'localhost',user: 'root',password: '12345678',database: 'user' // 填写数据库名});conn.connect();conn.query(sql, params, callback);conn.end();
}// 导出函数
module.exports = db;
getbooks接口中使用:
// 获取书籍接口
router.get('/getbooks', async (req, res) => {// 查询数据库user、数据表books里面的数据,把查询的结果响应给客户端db('select * from books', null, (err, result) => {if (err) return console.log(err);res.json({status: 200,msg: '获取成功',data: result});});
});
使用Promise
如果查询书籍信息的时候,还要查询总记录数,怎么办?
答:嵌套查询,嵌套的层数太多,就会形成回调地狱
解决办法:使用Promise
具体做法:
- 在db.js中,加个一个封装Promise的函数db2
- db2中调用db函数,从而完成Promise的封装
- 导出db2
function db(sql, params, callback) {const mysql = require('mysql');const conn = mysql.createConnection({host: 'localhost',user: 'root',password: '12345678',database: 'user' // 填写数据库名});conn.connect();conn.query(sql, params, callback);conn.end();
}function db2 (sql, params) {return new Promise((resolve, reject) => {// 这里写异步代码db(sql, params, (err, result) => {err ? reject(err) : resolve(result);});});
}// 导出函数
module.exports = db2;
优化上述两个函数:
function db (sql, params = null) {const mysql = require('mysql'); // require加载一个模块后,会缓存起来const conn = mysql.createConnection({host: 'localhost',user: 'root',password: '12345678',database: 'user' // 填写数据库名});return new Promise((resolve, reject) => {// 这里写异步代码conn.connect();conn.query(sql, params, (err, result) => {err ? reject(err) : resolve(result);});conn.end();});
}// 导出函数
module.exports = db;
完整的图示
Web 开发模式
目前主流的 Web 开发模式有两种,分别是:
- 基于服务端渲染的传统 Web 开发模式
- 基于前后端分离的新型 Web 开发模式
服务端渲染的开发模式
特点:
-
所有的web资源由同一个服务器统一管理(前后端代码必须放到一起)
-
页面和页面中使用的数据,由服务器组装,最后将完整的HTML页面响应给客户端
代码:
// 用于开启服务const fs = require('fs');
const express = require('express');
const app = express();
app.listen(3000, () => console.log('启动了'));// 显示首页的接口
app.get('/index.html', (req, res) => {// res.send('1111')fs.readFile('./public/index.html', 'utf-8', (err, data) => {if (err) return console.log(err);data = data.replace('{{title}}', '悯农');data = data.replace('{{content}}', '锄禾日当午,汗滴禾下土');res.send(data);});
});
真实的服务端渲染模式和前后端分离的模式,难度上差不多。
优点:
- **前端耗时少。**因为服务器端负责动态生成 HTML 内容,浏览器只需要直接渲染页面即可。尤其是移动端,更省电。
- 有利于SEO。因为服务器端响应的是完整的 HTML 页面内容,所以爬虫更容易爬取获得信息,更有利于 SEO(搜索引擎)。
缺点:
-
**占用服务器端资源。**即服务器端完成 HTML 页面内容的拼接,如果请求较多,会对服务器造成一定的访问压力。
-
不利于前后端分离,开发效率低。使用服务器端渲染,则无法进行分工合作,尤其对于前端复杂度高的项目,不利于 项目高效开发。
前后端分离的开发模式
特点:
- 依赖于Ajax技术。
- 后端不提供完整的 HTML 页面内容,而 是提供一些 API 接口
- 前端通过 Ajax 调用后端提供的 API 接口,拿到 json 数据 之后再在前端进行 HTML 页面的拼接,最终展示在浏览器上。
简而言之,前后端分离的 Web 开发模式,就是后端只负责提供 API 接口,前端使用 Ajax 调用接口的开发模式。
优点:
- **开发体验好。**前端专注于 UI 页面的开发,后端专注于api 的开发,且前端有更多的选择性。
- **用户体验好。**Ajax 技术的广泛应用,极大的提高了用户的体验,可以轻松实现页面的局部刷新。
- **减轻了服务器端的渲染压力。**因为页面最终是在每个用户的浏览器中生成的。
缺点:
- **不利于SEO。**因为完整的 HTML 页面需要在客户端动态拼接完成,所以爬虫对无法爬取页面的有效信息。(解决方 案:利用 Vue、React 等前端框架的 SSR (server side render)技术能够很好的解决 SEO 问题!)
如何选择 Web 开发模式
不谈业务场景而盲目选择使用何种开发模式都是耍流氓。
-
比如企业级网站(公司的网站),主要功能是展示而没有复杂的交互,并且需要良好的 SEO,则这时我们就需要使用服务器端渲染;
-
而类似后台管理页面,交互性比较强,不需要 SEO 的考虑,那么就可以使用前后端分离的开发模式。
-
另外,具体使用何种开发模式并不是绝对的,为了同时兼顾了首页的渲染速度和前后端分离的开发效率,一些网站采用了 首屏服务器端渲染,即对于用户最开始打开的那个页面采用的是服务器端渲染,而其他的页面采用前后端分离开发模式。
身份认证机制
对于服务端渲染和前后端分离这两种开发模式来说,分别有着不同的身份认证方案:
- 服务端渲染推荐使用 Session 认证机制(Session也会用到Cookie)
- 前后端分离推荐使用 JWT 认证机制
Cookie
原理
实现身份认证
- 搭建基础的服务器
-
下载安装第三方模块
express
和cookie-parser
-
创建app.js
-
加载所需模块
const express = require('express');
const cookieParser = require('cookie-parser');
-
- 中间件配置 cookie-parser
app.use(cookieParser())
- 实现三个路由
- /login.html (里面直接响应login.html页面)
- /api/login
- /index.html (里面直接响应index.html页面)
- 创建存放index页面的public文件夹
- 创建index.html
- 创建login.html
- 完成登录接口
- 如果登录成功,设置cookie。
res.cookie('key', 'value', 配置项);
- 跳转到 /index.html 路由
- 如果登录成功,设置cookie。
- /index.html 路由中,根据cookie判断是否登录,从而完成身份认证
详见代码
const express = require('express');
const cookieParser = require('cookie-parser');
const path = require('path');const app = express();
app.listen(3000, () => console.log('启动了'));// 接收POST请求体
app.use(express.urlencoded({extended: false}));
// 配置cookie-parser
app.use(cookieParser());// 准备三个路由// 用于显示登录页面
app.get('/login.html', (req, res) => {// sendFile方法,可以读取文件,并将读取的结果响应给客户端// 要求,参数必须是一个绝对路径res.sendFile(path.join(__dirname, 'public', 'login.html'));
});// 用于完成登录验证的(判断账号密码是否正确的接口)
app.post('/api/login', (req, res) => {// console.log(req.body);// 约定,假设账号是 admin、密码是123if (req.body.username === 'admin' && req.body.password === '123') {// 登录成功,跳转到index.html// 设置cookie// res.cookie('key', 'value', '选项');// res.cookie('isLogin', 1); // 没有填选项,默认cookie有效期是会话结束res.cookie('isLogin', 1, {maxAge: 2*60*1000});res.send('<script>alert("登录成功"); location.href="/index.html";</script>');} else {// 登录失败}
});// 显示index.html页面的
app.get('/index.html', (req, res) => {// 获取cookie// console.log(req.cookies);if (req.cookies.isLogin && req.cookies.isLogin === '1') {res.sendFile(path.join(__dirname, 'public', 'index.html'));} else {// 没有登录res.send('<script>alert("请先登录"); location.href="/login.html";</script>');}
});
优缺点
- 优点
- 体积小
- 客户端存放,不占用服务器空间
- 浏览器会自动携带,不需要写额外的代码,比较方便
- 缺点
- 客户端保存,安全性较低。但可以存放加密的字符串来解决
- 可以实现跨域,但是难度大,难理解,代码难度高
- 不适合前后端分离式的开发
适用场景
- 传统的服务器渲染模式
- 存储安全性较低的数据,比如视频播放位置等
Session
原理
实现身份认证
-
搭建基础的服务器
-
下载安装第三方模块
express
和express-session
-
创建app.js
-
加载所需模块
const express = require('express');
const session = require('express-session');
-
-
中间件配置 session
app.use(session({secret: 'adfasdf', // 这个随便写saveUninitialized: false,resave: false }))
-
实现三个路由
- /login.html (里面直接响应login.html页面)
- /api/login
- /index.html (里面直接响应index.html页面)
-
创建存放index页面的public文件夹
- 创建index.html
- 创建login.html
-
完成登录接口
-
如果登录成功,使用session记录用户信息。
req.session.isLogin = 1; req.session.username = req.body.username;
-
跳转到 /index.html 路由
-
-
/index.html 路由中,根据session判断是否登录,从而完成身份认证
详见代码
const express = require('express');
const session = require('express-session');
const path = require('path');const app = express();
app.listen(3000, () => console.log('启动了'));// 接收POST请求体
app.use(express.urlencoded({extended: false}));
// 配置session
app.use(session({secret: 'asdf23sfsd23',// 下面两项,设置成true或者false,都可以。使用内存存储session的时候,下面两项没作用saveUninitialized: false,resave: false
}));// 准备三个路由// 用于显示登录页面
app.get('/login.html', (req, res) => {// sendFile方法,可以读取文件,并将读取的结果响应给客户端// 要求,参数必须是一个绝对路径res.sendFile(path.join(__dirname, 'public', 'login.html'));
});// 用于完成登录验证的(判断账号密码是否正确的接口)
app.post('/api/login', (req, res) => {// console.log(req.body);// 假设账号任意,密码必须是123if (req.body.password === '123') {// 假设登录成功// req.session.xxxx = 'yyyy'req.session.isLogin = 1;
/*<p>欢迎你:{{username}}</p>
*/req.session.username = req.body.username;// 做出响应res.send('<script>alert("登录成功"); location.href="/index.html";</script>');}
});// 显示index.html页面的
app.get('/index.html', (req, res) => {// 获取session req.sessionif (req.session.isLogin && req.session.isLogin == 1) {const fs = require('fs');fs.readFile('./public/index.html', 'utf-8', (err, data) => {if (err) return console.log(err);// 更换用户名data = data.replace('{{username}}', req.session.username);res.send(data);});} else {res.send('<script>alert("请登录"); location.href="/login.html";</script>');}});
优缺点
- 优点
- 服务端存放,安全性较高
- 浏览器会自动携带cookie,不需要写额外的代码,比较方便
- 适合服务器端渲染模式
- 缺点
- 会占用服务器端空间
- session实现离不开cookie,如果浏览器禁用cookie,session不好实现
- 不适合前后端分离式的开发
适用场景
- 传统的服务器渲染模式
- 安全性要求较高的数据可以使用session存放,比如用户私密信息、验证码等