效果展示
github
moment-server github地址
moment github地址
moment-manage github地址
articles
聊聊畢業設計系列 --- 項目介紹
聊聊畢業設計系列 --- 系統實現
前言
在上一篇文章中,主要是對項目做了介紹,并且對系統分析和系統設計做了大概的介紹。那么接下來這篇文章會對系統的實現做介紹,主要是選擇一些比較主要的模塊或者說可拿出來與大家分享的模塊。好了,接入正題吧~~
MongoDB
服務端這邊使用的是Express框架,數據庫使用的是MongoDB,通過Mongoose模塊來操作數據庫。這邊主要是想下對MongoDB做個介紹,當然看官了解的話直接往下劃~~
在項目開始前要確保電腦是否安裝mongoDB,下載點我,圖像化工具Robo 3T 點我,下載好具體怎么配置還請問度娘或Google吧,本文不做介紹了哈。注意:安裝完mongoDB的時候進行項目時要把lib目錄下的mongod服務器打開哈~~
MongoDB 是一個基于分布式文件存儲的數據庫,是一個介于關系型數據庫和非關系型數據庫之間的開源產品,它是功能最為豐富的非關系型數據庫,也是最像關系型數據庫的。但是和關系型數據庫不同,MongoDB沒有表和行的概念,而是一個面向集合、文檔
的數據庫。其中的文檔是一個鍵值對,采用BSON(Binary Serialized Document Format),BSON是一種類似于JSON的二進制形式的存儲格式,并且BSON具有表示數據類型的擴展,因此支持的數據非常豐富。MongoDB有兩個很重要的數據類型就是內嵌文檔和數組
,而且在數組內可以嵌入其他文檔,這樣一條記錄就能表示非常復雜的關系。
Mongoose是在node.js異步環境下對MongoDB進行簡便操作的對象模型工具,能從數據庫提取任何信息,可以用面向對象的方法來讀寫數據,從而使操作MongoDB數據庫非常便捷。Mongoose中有三個非常重要的概念,便是Schema(模式),Model(模型),Entity(實體)。
- Schema: 一種以文件形式存儲的數據庫模型骨架,不具備數據庫的操作能力,創建它的過程如同關系型數據庫建表的過程,如下:
//Schema
const mongoose = require('mongoose');
const Schema = mongoose.Schema;const UserSchema = new Schema({token: String,is_banned: {type: Boolean, default: false}, //是否禁言enable: { type: Boolean, default: true }, //用戶是否有效is_actived: {type: Boolean, default: false}, //郵件激活username: String,password: String,email: String, //email唯一性code: String,email_time: {type: Date},phone: {type: String},description: { type: String, default: "這個人很懶,什么都沒有留下..." },avatar: { type: String, default: "http://p89inamdb.bkt.clouddn.com/default_avatar.png" },bg_url: { type: String, default: "http://p89inamdb.bkt.clouddn.com/FkagpurBWZjB98lDrpSrCL8zeaTU"},ip: String,ip_location: { type: Object },agent: { type: String }, // 用戶ualast_login_time: { type: Date },.....
});
- Model: 由Schema發布生成的模型,具有抽象屬性和行為的數據庫操作對象
//生成一個具體User的model并導出
const User = mongoose.model("User", UserSchema); //第一個參數是集合名,在數據庫中會把Model名字字母全部變小寫和在后面加復數s//執行到這個時候你的數據庫中就有了 users 這個集合module.exports = User;
- Entity: 由Model創建的實體,他的操作也會影響數據庫,但是它操作數據庫的能力比Model弱
const newUser = new UserModel({ //UserModel 為導出來的 Useremail: req.body.email,code: getCode(),email_time: Date.now()});
Mongoose中有一個東西個人感覺非常主要,那便是populate
,通過populate他可以很方便的與另一個集合建立關系。如下,user集合可以與article集合、user集合本身進行關聯,根據其內嵌文檔的特性,這樣子他便可以內嵌子文檔,子文檔中有可以內嵌子文檔,這樣子它返回的數據就會異常的豐富。
const user = await UserModel.findOne({_id: req.query._id, is_actived: true}, {password: 0}).populate({path: 'image_article',model: 'ImageArticle',populate: {path: 'author',model: 'User'}}).populate({path: 'collection_film_article',model: 'FilmArticle',}).populate({path: 'following_user',model: 'User',}).populate({path: 'follower_user',model: 'User',}).exec();
服務端主要是操作數據庫,對數據庫進行增刪改查(CRUD)等操作。項目中的接口,Mongoose的各種方法這邊就不對其做詳細介紹,大家可以查看Mongoose文檔。
用戶身份認證實現
介紹
本系統的用戶身份認證機制采用的是JSON Web Token(JWT)
,它是一種輕量的認證規范,也用于接口的認證。我們知道,HTTP協議是一種無狀態的協議,這便意味著每個請求都是獨立的,當用戶提供了用戶名和密碼來對我們的應用進行用戶認證,那么在下一次請求的時候,用戶需要再進行一次用戶的認證才可以,因為根據HTTP協議,我們并不能知道是哪個用戶發出的請求,本系統采用了token的鑒權機制。這個token必須要在每次請求時傳遞給服務端,它應該保存在請求頭里,另外,服務端要支持CORS(跨來源資源共享)策略,一般我們在服務端這么做就可以了Access-Control-Allow-Origin: *。
在用戶身份認證這一塊有很多方法,最常見的像cookie ,session。那么他們三之間又有什么區別,這里有兩篇文章介紹的挺全面。
- 正確理解HTTP短連接中的Cookie、Session和Token
- 小白必讀:閑話HTTP短連接中的Session和Token
token 與 session的區別在于,它不同于傳統的session認證機制,它不需要在服務端去保留用戶的認證信息或其會話的信息。系統一旦比較大,都會采用機器集群來做負載均衡,這需要多臺機器,由于session是保存在服務端,那么就要 去考慮用戶到底是在哪一臺服務器上進行登錄的,這便是一個很大的負擔。
那么就有人想問了,你這個系統這么小,為什么不使用傳統的session機制呢?哈~因為之前自己的項目一般都是使用session做登錄,沒使用過token,想嘗試嘗試入入坑~~哈哈哈~
實現思路
JWT主要的實現思路如下:
- 在用戶登錄成功的時候創建token保存于數據庫中,并返回給客戶端。
- 客戶端之后的每一次請求都要帶上token,在請求頭里加入Authorization,并加上token.
- 在服務端進行驗證token的有效性,在有效期內返回200狀態碼,token過期則返回401狀態碼
如下圖所示:
<center>JWT請求圖</center>
在node中主要用了jsonwebtoken
這個模塊來創建JWT,jsonwebtoken的使用請查看jsonwebtoken文檔。項目中創建token的中間件createToken如下
/*** createToken.js*/
const jwt = require('jsonwebtoken'); // 引入jsonwebtoken模塊
const secret = '我是密鑰'//登錄時:核對用戶名和密碼成功后,應用將用戶的id(user_id)作為JWT Payload的一個屬性
module.exports = function(user_id){const token = jwt.sign({user_id: user_id}, secret, { //密鑰expiresIn: '24h' //過期時間設置為24h。那么decode這個token的時候得到的過期時間為:創建token的時間+設置的值});return token;
};
return 出來的 token 類似eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiYWRtaW4iLCJpYXQiOjE1MzQ2ODQwNzAsImV4cCI6MTUzNDc3MDQ3MH0.Y3kaglqW9Fpe1YxF_uF7zwTV224W4W97MArU0aI0JgM
。我們仔細看這字符串,分為三段,分別被 "." 隔開。現在我們分別對前兩段進行base64解碼如下:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 ===> {"alg":"HS256","typ":"JWT"} 其中 alg是加密算法名字,typ是類型eyJ1c2VyX2lkIjoiYWRtaW4iLCJpYXQiOjE1MzQ2ODQwNzAsImV4cCI6MTUzNDc3MDQ3MH0 ===> {"user_id":"admin","iat":1534684070,"exp":1534770470} 其中 name是我們儲存的內容,iat創建的時間戳,exp到期時間戳。Y3kaglqW9Fpe1YxF_uF7zwTV224W4W97MArU0aI0JgM ===> 最后一段是由前面兩段字符串,HS256加密后得到。所以前面的任何一個字段修改,都會導致加密后的字符串不匹配。
當我們根據用戶的id創建獲取到token之后,我們需要把token返回到客戶端,客戶端對其在本地(localStorage)保存, 客戶端之后的每一次請求都要帶上token,在請求頭里加入Authorization,并加上token,服務端進行驗證token的有效性。那么我們如何驗證token的有效性呢? 所以我們需要checkToken這個中間件來檢測token的有效性。
/*** checkToken*/
const jwt = require('jsonwebtoken');
const secret = '我是密鑰'module.exports = async ( req, res, next ) => {const authorization = req.get('Authorization');if (!authorization) {res.status(401).end(); //接口需要認證但是有沒帶上token,返回401未授權狀態碼return}const token = authorization.split(' ')[1];try {let tokenContent = await jwt.verify(token, secret); //如果token過期或驗證失敗,將拋出錯誤next(); //執行下一個中間件} catch (err) {console.log(err)res.status(401).end(); //token過期或者驗證失敗返回401狀態碼}
}
那么現在咱們只要在需要用戶認證的接口上,在操作數據之前,加上checkToken中間件即可,如下調用:
//更新用戶信息
router.post('/updateUserInfo', checkToken, User.updateUserInfo) //如果checkToken檢測不成功,它便返回401狀態碼,不會對User.updateUserInfo做任何操作, 只有檢測token成功,才能處理User.updateUserInfo
我們如何保證每次請求都能在請求頭里加入Authorization,并加上token,這就要用到Axios的請求攔截,并且也用到了它的響應攔截,因為在服務端返回401狀態碼之后應要執行登出操作,清楚本地token的存儲,具體代碼如下:
//request攔截器
instance.interceptors.request.use(config => {//每次發送請求之前檢測本地是否存有token,都要放在請求頭發送給服務器if(localStorage.getItem('token')){if (config.url.indexOf('upload-z0.qiniup.com/putb64') > -1){config.headers.Authorization = config.headers['UpToken']; //加上七牛云上傳token}else {config.headers.Authorization = `token ${localStorage.getItem('token')}`.replace(/(^\")|(\"$)/g, ''); //加上系統接口token}}console.log('config',config)return config;},err => {console.log('err',err)return Promise.reject(err);}
);//response攔截器
instance.interceptors.response.use(response => {return response;},error => { //默認除了2XX之外的都是錯誤的,就會走這里if(error.response){switch(error.response.status){case 401:console.log(error.response)store.dispatch('ADMIN_LOGINOUT'); //可能是token過期,清除它router.replace({ //跳轉到登錄頁面path: '/login',query: { redirect: '/dashboard' } // 將跳轉的路由path作為參數,登錄成功后跳轉到該路由});}}return Promise.reject(error.response);}
);
其中的if else 是因為本系統的圖片,音視頻是放在七牛云,上傳需要七牛云上傳base64圖片的時候token是放在請求頭的,正常的圖片上傳不是放在請求頭,所以這邊對token做了區分,如何接入七牛云也會在下面模塊介紹到。
七牛云接入
本系統的圖片,音視頻是放在七牛云,所以需要接入七牛云。七牛云分了兩種情況,正常圖片和音視頻的上傳和base64圖片的上傳,因為七牛云在對他們兩者上傳的Content-Type
和domain(域)
有所不同,正常圖片和音視頻的Content-Type是headers: {'Content-Type':'multipart/form-data'}
domain是domain='https://upload-z0.qiniup.com'
,而base64圖片的上傳則是headers:{'Content-Type':'application/octet-stream'}
domain是domain='https://upload-z0.qiniup.com/putb64/-1'
,所以他們請求的時候token放的地方不同,base64就像上面所說的放在請求頭Authorization
中,而正常的放在form-data
中。在服務端通過接口請求來獲取七牛云上傳token,客戶端獲取到七牛云token,通過不同方案將token帶上。
- base64的上傳:
headers:{'Content-Type':'application/octet-stream'}
和domain='https://upload-z0.qiniup.com/putb64/-1'
,token放在請求頭Authorization
中。 - 正常圖片和音視頻的上傳:
headers: {'Content-Type':'multipart/form-data'}
和domain='https://upload-z0.qiniup.com'
,token 放在form-data
中。
服務端通過qiniu
這個模塊進行創建token,服務端代碼如下:
/*** 構建一個七牛云上傳憑證類* @class QN*/
const qiniu = require('qiniu') //導入qiniu模塊
const config = require('../config')
class QN {/*** Creates an instance of qn.* @param {string} accessKey -七牛云AK* @param {string} secretKey -七牛云SK* @param {string} bucket -七牛云空間名稱* @param {string} origin -七牛云默認外鏈域名,(可選參數)*/constructor (accessKey, secretKey, bucket, origin) {this.ak = accessKeythis.sk = secretKeythis.bucket = bucketthis.origin = origin}/*** 獲取七牛云文件上傳憑證* @param {number} time - 七牛云憑證過期時間,以秒為單位,如果為空,默認為7200,有效時間為2小時*/upToken (time) {const mac = new qiniu.auth.digest.Mac(this.ak, this.sk)const options = {scope: this.bucket,expires: time || 7200}const putPolicy = new qiniu.rs.PutPolicy(options)const uploadToken = putPolicy.uploadToken(mac)return uploadToken}
}exports.QN = QN;exports.upToken = () => {return new QN(config.qiniu.accessKey, config.qiniu.secretKey, config.qiniu.bucket, config.qiniu.origin).upToken() //每次調用都創建一個token
}
//獲取七牛云token接口
const {upToken} = require('../utils/qiniu')app.get('/api/uploadToken', (req, res, next) => {const token = upToken()res.send({status: 1,message: '上傳憑證獲取成功',upToken: token,})})
由于正常圖片和音視頻的上傳和base64圖片的上傳,因為七牛云在對他們兩者上傳的Content-Type
和domain(域)
有所不同,所以的token請求存放的位置有所不同,因此要區分,客戶端調用上傳代碼如下:
//根據獲取到的上傳憑證uploadToken上傳文件到指定域//正常圖片和音視頻的上傳uploadFile(formdata, domain='https://upload-z0.qiniup.com',config={headers:{'Content-Type':'multipart/form-data'}}){console.log(domain)console.log(formdata)return instance.post(domain, formdata, config)},//base64圖片的上傳//根據獲取到的上傳憑證uploadToken上傳base64到指定域uploadBase64File(base64, token, domain = 'https://upload-z0.qiniup.com/putb64/-1', config = {headers: {'Content-Type': 'application/octet-stream',},}){const pic = base64.split(',')[1];config.headers['UpToken'] = `UpToken ${token}`return instance.post(domain, pic, config)},
function upload(Vue, data, callbackSuccess, callbackFail) {//獲取上傳token之后處理Vue.prototype.axios.getUploadToken().then(res => {if (typeof data === 'string'){ //如果是base64const token = res.data.upTokenVue.prototype.axios.uploadBase64File(data, token).then(res => {if (res.status === 200){callbackSuccess && callbackSuccess({data: res.data,result_url: `http://p89inamdb.bkt.clouddn.com/${res.data.key}`})}}).catch((error) => {callbackFail && callbackFail({error})})}else if (data instanceof FormData){ //如果是FormDatadata.append('token', res.data.upToken)data.append('key', `moment${Date.now()}${Math.floor(Math.random() * 100)}`)Vue.prototype.axios.uploadFile(data).then(res => {if (res.status === 200){callbackSuccess && callbackSuccess({data: res.data,result_url: `http://p89inamdb.bkt.clouddn.com/${res.data.key}`})}}).catch((error) => {callbackFail && callbackFail({error})})}else {const formdata = new FormData() //如果不是formData 就創建formDataformdata.append('token', res.data.upToken)formdata.append('file', data.file || data)formdata.append('key', `moment${Date.now()}${Math.floor(Math.random() * 100)}.${data.file.type.split('/')[1]}`)// 獲取到憑證之后再將文件上傳到七牛云空間console.log('formdata',formdata)Vue.prototype.axios.uploadFile(formdata).then(res => {console.log('res',res)if (res.status === 200){callbackSuccess && callbackSuccess({data: res.data,result_url: `http://p89inamdb.bkt.clouddn.com/${res.data.key}` //返回的圖片鏈接})}}).catch((error) => {console.log(error)callbackFail && callbackFail({error})})}})
}export default upload
路由權限模塊
系統的后臺管理面向的是合作作者和管理員,涉及到兩種角色,故此要做權限管理。不同的權限對應著不同的路由,同時側邊欄的菜單也需根據不同的權限,異步生成,不同于以往的服務端直接返回路由表,由前端動態生成,接下來介紹下登錄和權限驗證的思路:
- 登錄:當用戶填寫完賬號和密碼后向服務端驗證是否正確,驗證通過之后,服務端會返回一個token,拿到token之后前端會根據token再去拉取一個getAdminInfo的接口來獲取用戶的詳細信息(如用戶權限,用戶名等等信息)。
- 權限驗證:通過token獲取用戶對應的role,動態根據用戶的role算出其對應有權限的路由,通過vue-router的beforeEach進行全局前置守衛再通過router.addRoutes動態掛載這些路由。
代碼有點多,這邊就直接放流程圖哈~~
<center>權限路由流程圖</center>
最近正好也在公司做中后臺項目,公司的中后臺項目的這邊是由服務端生成路由表,前端進行直接渲染,畢竟公司的一整套業務比較成熟。但是我們會在想能不能由前端維護路由表,這樣不用到時候項目迭代,前端每增加頁面都要讓服務端兄弟配一下路由和權限,當然前提可能是項目比較小的時候。
賬號模塊
賬號模塊是業務中最為基礎的模塊,承擔著整個系統所有的賬號相關的功能。系統實現了用戶注冊、用戶登錄、密碼修改、找回密碼功能。
系統的賬號模塊使用了郵件服務,針對普通用戶的注冊采用了郵件服務來發送驗證碼,以及密碼的修改等操作都采用了郵件服務。在node.js中主要采用了Nodemailer,Nodemailer是一個簡單易用的Node.js郵件發送組件,它的使用可以摸我摸我摸我,通過此模塊進行郵件的發送。你們可能會問,為什么不用短信服務呢?哈~因為短信服務要錢,哈哈哈
/*
* email 郵件模塊
*/const nodemailer = require('nodemailer');
const smtpTransport = require('nodemailer-smtp-transport');
const config = require('../config')const transporter = nodemailer.createTransport(smtpTransport({host: 'smtp.qq.com',secure: true,port: 465, // SMTP 端口auth: {user: config.email.account,pass: config.email.password //這里密碼不是qq密碼,是你設置的smtp授權碼}
}));let clientIsValid = false;
const verifyClient = () => {transporter.verify((error, success) => {if (error) {clientIsValid = false;console.warn('郵件客戶端初始化連接失敗,將在一小時后重試');setTimeout(verifyClient, 1000 * 60 * 60);} else {clientIsValid = true;console.log('郵件客戶端初始化連接成功,隨時可發送郵件');}});
};
verifyClient();const sendMail = mailOptions => {if (!clientIsValid) {console.warn('由于未初始化成功,郵件客戶端發送被拒絕');return false;}mailOptions.from = '"ShineTomorrow" <admin@momentin.cn>'transporter.sendMail(mailOptions, (error, info) => {if (error) return console.warn('郵件發送失敗', error);console.log('郵件發送成功', info.messageId, info.response);});
};exports.sendMail = sendMail;
賬號的注冊先是填寫email,填寫好郵箱之后會通過Nodemailer發送一封含有有效期的驗證碼郵件,之后填寫驗證碼、昵稱和密碼即可完成注冊,并且為了安全考慮,對密碼采用了安全哈希算法(Secure Hash Algorithm)進行加密。賬號的登錄以賬號或者郵箱號加上密碼進行登錄,并且采用上文所說的JSON Web Token(JWT)身份認證機制,從而實現用戶和用戶登錄狀態數據的對應。
<center>我的郵件長這樣?(可自己寫郵件模板)</center>
實時消息推送
當用戶被人關注、評論被他人回復和點贊等一些社交性的操作的時候,在數據存儲完成后,服務端應需要及時向用戶推送消息來提醒用戶。消息推送模塊采用了Socket.io
來實現,socket.io封裝了websocket,不支持websocket的情況還提供了降級AJAX輪詢,功能完備,設計優雅,是開發實時雙向通訊的不二手段。
通過 socket.io,用戶每打開一個頁面,這個頁面都會和服務端建立一個連接。在服務端可以通過連接的socket的id屬性來匹配到一個建立連接的頁面。所以用戶的ID和socket的id,是一對多的關系,即一個用戶可能在登錄后打開多個頁面。而socket.io沒有提供從服務端向某個用戶單獨發送消息的功能,更沒有提供向某個用戶打開的所有頁面推送消息的功能。但是socket.io提供了room的概念,即群組。在建立websocket時,客戶端可以選擇加入某個room,如果這個room沒有存在則自動新建一個,否則直接加入,服務端可以向某個room中的所有客戶端推送消息。
根據這個特性,設計將用戶的ID作為room的名字,當某個用戶打開頁面建立連接時,會選擇加入以自己用戶ID為名字的room。這樣,在用戶ID為名字的 room中,加入的都是用戶自己打開的頁面建立的連接。從而向某個用戶推送消息,可以直接通過向以此用戶的ID為名字的room發送消息,這樣就會推送到用戶打開的所有頁面。
有了想法后我們就開始魯吧~,在服務端中socket.io
在客戶端中使用vue-socket.io
, 服務端代碼如下:
/*
* app.js中
*/
const server = require('http').createServer(app);
const io = require('socket.io')(server);
global.io = io; //全局設上io值, 因為在其他模塊要用到
io.on('connection', function (socket) {// setTimeout(()=>{// socket.emit('nodeEvent', { hello: 'world' });// }, 5000)socket.on('login_success', (data) => { //接受客戶端觸發的login_success事件//使用user_id作為房間號socket.join(data.user_id);console.log('login_success',data);});
});
io.on('disconnect', function (socket) {socket.emit('user disconnected');
});server.listen(config.port, () => {console.log(`The server is running at http://localhost:${config.port}`);
});
/*
* 某業務模塊
*/
//例如某文章增加評論
io.in(newMusicArticle.author.user_id._id).emit('receive_message', newMessage); //實時通知客戶端receive_message事件
sendMail({ //發送郵件to: newMusicArticle.author.user_id.email,subject: `Moment | 你有未讀消息哦~`,text: `啦啦啦,我是賣報的小行家~~ ?`,html: emailTemplate.comment(sender, newMusicArticle, content, !!req.body.reply_to_id)
})
客服端代碼:
<script>export default {name: 'App',data () {return {}},sockets:{connect(){},receive_message(val){ //接受服務端觸發的事件,進行客戶端實時更新數據if (val){console.log('服務端實時通信', val)this.$notify(val.content)console.log('this method was fired by the socket server. eg: io.emit("customEmit", data)')}}},mixins: [mixin],mounted(){if (!!JSON.parse(window.localStorage.getItem('user_info'))){this.$socket.emit('login_success', { //通知服務端login_success 事件, 傳入iduser_id: JSON.parse(window.localStorage.getItem('user_info'))._id})}},}
</script>
評論模塊
評論模塊是為了移動端WebApp下的文章下為用戶提供關于評論的一些操作。系統實現了對文章的評論,評論的點贊功能,熱門評論置頂以及評論的回復功能。在評論方面存在著各種各樣的安全性問題,比如XSS攻擊(Cross Site Scripting,跨站腳本攻擊)以及敏感詞等問題。預防XSS攻擊使用了xss
模塊, 敏感詞過濾使用text-censor
模塊。
一些思考
- 接口數據問題
在開發的時候經常會遇到這個問題,接口數據問題。有時候服務端返回的數據并不是我們想要的數據,前端要對數據進行再一步的處理。
例如服務端返回的某個字段為null或者服務端返回的數據結構太深,前端需要不斷去判斷數據結構是否真的返回了正確的東西,而不是個null 或者undefined~
我們前端都要這么去處理過濾:
<div class="author">文 / {{(musicArticleInfo.author && musicArticleInfo.author.user_id) ? musicArticleInfo.author.user_id.username : '我叫這個名字'}}
</div>
這就引出了一個思考:
對數據的進一步封裝處理,必然渲染性能方面會存在問題,而且我們要時刻擔心數據返回的問題。如果應用到公司的業務,我們應該如何處理呢 ?
- 頁面性能優化和SEO問題
首屏渲染問題一直是單頁應用的痛點,那么除了常用的性能優化,我們還有什么方法優化的嗎 ? 這個項目雖然面向的是移動端用戶,可能不存在SEO問題,如果做成pc端的話,像文章這類的應用,SEO都是必須品。
對于上面提出的問題,node的出現讓我們看到了解決方案,那就常說的Node中間層,當然本項目中是不存在Node中間層,而是直接作為后端語言處理數據庫。
由于大部分的公司后端要么是php要么是java,一般不把node直接作為后端語言,如果有使用到node,一般是作為一個中間層的形式存在。
對于第一個問題的解決:我們可以在中間層做接口轉發,在轉發的過程中做數據處理。而不用擔心數據返回的問題。
對于第二個問題的解決:有了Node中間層的話,那么我們可以把首屏渲染的任務交給nodejs去做,次屏的渲染依然走之前的瀏覽器渲染。
有Node中間層的話,新的架構如下:
前后端的職能:
總結
已經畢業一段時間了,寫文章是為了回顧。本人水平一般,見諒見諒。這個產品的實現,一個人扛,在其中充當了各種角色,要有一點點產品思維,要有一點點設計的想法,要會數據庫設計,要會后端開發,挺繁瑣的。最難的點個人感覺還是數據庫設計,數據庫要一開始就要設計的很完整,不然到后面的添添補補,就會很亂很亂,當然這個基礎是產品要非常清晰,剛開始自己心中對產品可能是個模糊的定義,想想差不多是那樣,于是乎就開始搞~~導致于后面數據庫設計的不是很滿意。由于時間關系,現在的產品中有些小模塊還沒完成,但是大部分的功能結構已經完成,算是個成型的產品,當然是一個沒有經過測試的產品哈哈哈哈,要是有測試的話,那就哈哈哈哈你懂得。
前路漫漫,吾將上下而求索~
完
謝謝~~