綜合應用服務端知識點搭建項目
下載安裝所需的第三方模塊
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存放,比如用戶私密信息、驗證碼等