聊聊毕业设计系列 --- 系统实现
效果展示
github
moment-server github地址
moment github地址
moment-manage github地址
articles
聊聊畢業(yè)設(shè)計(jì)系列 --- 項(xiàng)目介紹
聊聊畢業(yè)設(shè)計(jì)系列 --- 系統(tǒng)實(shí)現(xiàn)
前言
在上一篇文章中,主要是對(duì)項(xiàng)目做了介紹,并且對(duì)系統(tǒng)分析和系統(tǒng)設(shè)計(jì)做了大概的介紹。那么接下來這篇文章會(huì)對(duì)系統(tǒng)的實(shí)現(xiàn)做介紹,主要是選擇一些比較主要的模塊或者說可拿出來與大家分享的模塊。好了,接入正題吧~~
MongoDB
服務(wù)端這邊使用的是Express框架,數(shù)據(jù)庫使用的是MongoDB,通過Mongoose模塊來操作數(shù)據(jù)庫。這邊主要是想下對(duì)MongoDB做個(gè)介紹,當(dāng)然看官了解的話直接往下劃~~
在項(xiàng)目開始前要確保電腦是否安裝mongoDB,下載點(diǎn)我,圖像化工具Robo 3T 點(diǎn)我,下載好具體怎么配置還請(qǐng)問度娘或Google吧,本文不做介紹了哈。注意:安裝完mongoDB的時(shí)候進(jìn)行項(xiàng)目時(shí)要把lib目錄下的mongod服務(wù)器打開哈~~
MongoDB 是一個(gè)基于分布式文件存儲(chǔ)的數(shù)據(jù)庫,是一個(gè)介于關(guān)系型數(shù)據(jù)庫和非關(guān)系型數(shù)據(jù)庫之間的開源產(chǎn)品,它是功能最為豐富的非關(guān)系型數(shù)據(jù)庫,也是最像關(guān)系型數(shù)據(jù)庫的。但是和關(guān)系型數(shù)據(jù)庫不同,MongoDB沒有表和行的概念,而是一個(gè)面向集合、文檔的數(shù)據(jù)庫。其中的文檔是一個(gè)鍵值對(duì),采用BSON(Binary Serialized Document Format),BSON是一種類似于JSON的二進(jìn)制形式的存儲(chǔ)格式,并且BSON具有表示數(shù)據(jù)類型的擴(kuò)展,因此支持的數(shù)據(jù)非常豐富。MongoDB有兩個(gè)很重要的數(shù)據(jù)類型就是內(nèi)嵌文檔和數(shù)組,而且在數(shù)組內(nèi)可以嵌入其他文檔,這樣一條記錄就能表示非常復(fù)雜的關(guān)系。
Mongoose是在node.js異步環(huán)境下對(duì)MongoDB進(jìn)行簡(jiǎn)便操作的對(duì)象模型工具,能從數(shù)據(jù)庫提取任何信息,可以用面向?qū)ο蟮姆椒▉碜x寫數(shù)據(jù),從而使操作MongoDB數(shù)據(jù)庫非常便捷。Mongoose中有三個(gè)非常重要的概念,便是Schema(模式),Model(模型),Entity(實(shí)體)。
Mongoose中有一個(gè)東西個(gè)人感覺非常主要,那便是populate,通過populate他可以很方便的與另一個(gè)集合建立關(guān)系。如下,user集合可以與article集合、user集合本身進(jìn)行關(guān)聯(lián),根據(jù)其內(nèi)嵌文檔的特性,這樣子他便可以內(nèi)嵌子文檔,子文檔中有可以內(nèi)嵌子文檔,這樣子它返回的數(shù)據(jù)就會(huì)異常的豐富。
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();服務(wù)端主要是操作數(shù)據(jù)庫,對(duì)數(shù)據(jù)庫進(jìn)行增刪改查(CRUD)等操作。項(xiàng)目中的接口,Mongoose的各種方法這邊就不對(duì)其做詳細(xì)介紹,大家可以查看Mongoose文檔。
用戶身份認(rèn)證實(shí)現(xiàn)
介紹
本系統(tǒng)的用戶身份認(rèn)證機(jī)制采用的是JSON Web Token(JWT),它是一種輕量的認(rèn)證規(guī)范,也用于接口的認(rèn)證。我們知道,HTTP協(xié)議是一種無狀態(tài)的協(xié)議,這便意味著每個(gè)請(qǐng)求都是獨(dú)立的,當(dāng)用戶提供了用戶名和密碼來對(duì)我們的應(yīng)用進(jìn)行用戶認(rèn)證,那么在下一次請(qǐng)求的時(shí)候,用戶需要再進(jìn)行一次用戶的認(rèn)證才可以,因?yàn)楦鶕?jù)HTTP協(xié)議,我們并不能知道是哪個(gè)用戶發(fā)出的請(qǐng)求,本系統(tǒng)采用了token的鑒權(quán)機(jī)制。這個(gè)token必須要在每次請(qǐng)求時(shí)傳遞給服務(wù)端,它應(yīng)該保存在請(qǐng)求頭里,另外,服務(wù)端要支持CORS(跨來源資源共享)策略,一般我們?cè)诜?wù)端這么做就可以了Access-Control-Allow-Origin: *。
在用戶身份認(rèn)證這一塊有很多方法,最常見的像cookie ,session。那么他們?nèi)g又有什么區(qū)別,這里有兩篇文章介紹的挺全面。
- 正確理解HTTP短連接中的Cookie、Session和Token
- 小白必讀:閑話HTTP短連接中的Session和Token
token 與 session的區(qū)別在于,它不同于傳統(tǒng)的session認(rèn)證機(jī)制,它不需要在服務(wù)端去保留用戶的認(rèn)證信息或其會(huì)話的信息。系統(tǒng)一旦比較大,都會(huì)采用機(jī)器集群來做負(fù)載均衡,這需要多臺(tái)機(jī)器,由于session是保存在服務(wù)端,那么就要 去考慮用戶到底是在哪一臺(tái)服務(wù)器上進(jìn)行登錄的,這便是一個(gè)很大的負(fù)擔(dān)。
那么就有人想問了,你這個(gè)系統(tǒng)這么小,為什么不使用傳統(tǒng)的session機(jī)制呢?哈~因?yàn)橹白约旱捻?xiàng)目一般都是使用session做登錄,沒使用過token,想嘗試嘗試入入坑~~哈哈哈~
實(shí)現(xiàn)思路
JWT主要的實(shí)現(xiàn)思路如下:
如下圖所示:
<center>JWT請(qǐng)求圖</center>
在node中主要用了jsonwebtoken這個(gè)模塊來創(chuàng)建JWT,jsonwebtoken的使用請(qǐng)查看jsonwebtoken文檔。項(xiàng)目中創(chuàng)建token的中間件createToken如下
/*** createToken.js*/ const jwt = require('jsonwebtoken'); // 引入jsonwebtoken模塊 const secret = '我是密鑰'//登錄時(shí):核對(duì)用戶名和密碼成功后,應(yīng)用將用戶的id(user_id)作為JWT Payload的一個(gè)屬性 module.exports = function(user_id){const token = jwt.sign({user_id: user_id}, secret, { //密鑰expiresIn: '24h' //過期時(shí)間設(shè)置為24h。那么decode這個(gè)token的時(shí)候得到的過期時(shí)間為:創(chuàng)建token的時(shí)間+設(shè)置的值});return token; };return 出來的 token 類似eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiYWRtaW4iLCJpYXQiOjE1MzQ2ODQwNzAsImV4cCI6MTUzNDc3MDQ3MH0.Y3kaglqW9Fpe1YxF_uF7zwTV224W4W97MArU0aI0JgM。我們仔細(xì)看這字符串,分為三段,分別被 "." 隔開。現(xiàn)在我們分別對(duì)前兩段進(jìn)行base64解碼如下:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 ===> {"alg":"HS256","typ":"JWT"} 其中 alg是加密算法名字,typ是類型eyJ1c2VyX2lkIjoiYWRtaW4iLCJpYXQiOjE1MzQ2ODQwNzAsImV4cCI6MTUzNDc3MDQ3MH0 ===> {"user_id":"admin","iat":1534684070,"exp":1534770470} 其中 name是我們儲(chǔ)存的內(nèi)容,iat創(chuàng)建的時(shí)間戳,exp到期時(shí)間戳。Y3kaglqW9Fpe1YxF_uF7zwTV224W4W97MArU0aI0JgM ===> 最后一段是由前面兩段字符串,HS256加密后得到。所以前面的任何一個(gè)字段修改,都會(huì)導(dǎo)致加密后的字符串不匹配。當(dāng)我們根據(jù)用戶的id創(chuàng)建獲取到token之后,我們需要把token返回到客戶端,客戶端對(duì)其在本地(localStorage)保存, 客戶端之后的每一次請(qǐng)求都要帶上token,在請(qǐng)求頭里加入Authorization,并加上token,服務(wù)端進(jìn)行驗(yàn)證token的有效性。那么我們?nèi)绾悟?yàn)證token的有效性呢? 所以我們需要checkToken這個(gè)中間件來檢測(cè)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(); //接口需要認(rèn)證但是有沒帶上token,返回401未授權(quán)狀態(tài)碼return}const token = authorization.split(' ')[1];try {let tokenContent = await jwt.verify(token, secret); //如果token過期或驗(yàn)證失敗,將拋出錯(cuò)誤next(); //執(zhí)行下一個(gè)中間件} catch (err) {console.log(err)res.status(401).end(); //token過期或者驗(yàn)證失敗返回401狀態(tài)碼} }那么現(xiàn)在咱們只要在需要用戶認(rèn)證的接口上,在操作數(shù)據(jù)之前,加上checkToken中間件即可,如下調(diào)用:
//更新用戶信息 router.post('/updateUserInfo', checkToken, User.updateUserInfo) //如果checkToken檢測(cè)不成功,它便返回401狀態(tài)碼,不會(huì)對(duì)User.updateUserInfo做任何操作, 只有檢測(cè)token成功,才能處理User.updateUserInfo我們?nèi)绾伪WC每次請(qǐng)求都能在請(qǐng)求頭里加入Authorization,并加上token,這就要用到Axios的請(qǐng)求攔截,并且也用到了它的響應(yīng)攔截,因?yàn)樵诜?wù)端返回401狀態(tài)碼之后應(yīng)要執(zhí)行登出操作,清楚本地token的存儲(chǔ),具體代碼如下:
//request攔截器 instance.interceptors.request.use(config => {//每次發(fā)送請(qǐng)求之前檢測(cè)本地是否存有token,都要放在請(qǐng)求頭發(fā)送給服務(wù)器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, ''); //加上系統(tǒng)接口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 => { //默認(rèn)除了2XX之外的都是錯(cuò)誤的,就會(huì)走這里if(error.response){switch(error.response.status){case 401:console.log(error.response)store.dispatch('ADMIN_LOGINOUT'); //可能是token過期,清除它router.replace({ //跳轉(zhuǎn)到登錄頁面path: '/login',query: { redirect: '/dashboard' } // 將跳轉(zhuǎn)的路由path作為參數(shù),登錄成功后跳轉(zhuǎn)到該路由});}}return Promise.reject(error.response);} );其中的if else 是因?yàn)楸鞠到y(tǒng)的圖片,音視頻是放在七牛云,上傳需要七牛云上傳base64圖片的時(shí)候token是放在請(qǐng)求頭的,正常的圖片上傳不是放在請(qǐng)求頭,所以這邊對(duì)token做了區(qū)分,如何接入七牛云也會(huì)在下面模塊介紹到。
七牛云接入
本系統(tǒng)的圖片,音視頻是放在七牛云,所以需要接入七牛云。七牛云分了兩種情況,正常圖片和音視頻的上傳和base64圖片的上傳,因?yàn)槠吲T圃趯?duì)他們兩者上傳的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',所以他們請(qǐng)求的時(shí)候token放的地方不同,base64就像上面所說的放在請(qǐng)求頭Authorization中,而正常的放在form-data中。在服務(wù)端通過接口請(qǐng)求來獲取七牛云上傳token,客戶端獲取到七牛云token,通過不同方案將token帶上。
服務(wù)端通過qiniu這個(gè)模塊進(jìn)行創(chuàng)建token,服務(wù)端代碼如下:
/*** 構(gòu)建一個(gè)七牛云上傳憑證類* @class QN*/ const qiniu = require('qiniu') //導(dǎo)入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 -七牛云默認(rèn)外鏈域名,(可選參數(shù))*/constructor (accessKey, secretKey, bucket, origin) {this.ak = accessKeythis.sk = secretKeythis.bucket = bucketthis.origin = origin}/*** 獲取七牛云文件上傳憑證* @param {number} time - 七牛云憑證過期時(shí)間,以秒為單位,如果為空,默認(rèn)為7200,有效時(shí)間為2小時(shí)*/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() //每次調(diào)用都創(chuàng)建一個(gè)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圖片的上傳,因?yàn)槠吲T圃趯?duì)他們兩者上傳的Content-Type和domain(域)有所不同,所以的token請(qǐng)求存放的位置有所不同,因此要區(qū)分,客戶端調(diào)用上傳代碼如下:
//根據(jù)獲取到的上傳憑證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圖片的上傳//根據(jù)獲取到的上傳憑證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 就創(chuàng)建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路由權(quán)限模塊
系統(tǒng)的后臺(tái)管理面向的是合作作者和管理員,涉及到兩種角色,故此要做權(quán)限管理。不同的權(quán)限對(duì)應(yīng)著不同的路由,同時(shí)側(cè)邊欄的菜單也需根據(jù)不同的權(quán)限,異步生成,不同于以往的服務(wù)端直接返回路由表,由前端動(dòng)態(tài)生成,接下來介紹下登錄和權(quán)限驗(yàn)證的思路:
代碼有點(diǎn)多,這邊就直接放流程圖哈~~
<center>權(quán)限路由流程圖</center>
最近正好也在公司做中后臺(tái)項(xiàng)目,公司的中后臺(tái)項(xiàng)目的這邊是由服務(wù)端生成路由表,前端進(jìn)行直接渲染,畢竟公司的一整套業(yè)務(wù)比較成熟。但是我們會(huì)在想能不能由前端維護(hù)路由表,這樣不用到時(shí)候項(xiàng)目迭代,前端每增加頁面都要讓服務(wù)端兄弟配一下路由和權(quán)限,當(dāng)然前提可能是項(xiàng)目比較小的時(shí)候。
賬號(hào)模塊
賬號(hào)模塊是業(yè)務(wù)中最為基礎(chǔ)的模塊,承擔(dān)著整個(gè)系統(tǒng)所有的賬號(hào)相關(guān)的功能。系統(tǒng)實(shí)現(xiàn)了用戶注冊(cè)、用戶登錄、密碼修改、找回密碼功能。
系統(tǒng)的賬號(hào)模塊使用了郵件服務(wù),針對(duì)普通用戶的注冊(cè)采用了郵件服務(wù)來發(fā)送驗(yàn)證碼,以及密碼的修改等操作都采用了郵件服務(wù)。在node.js中主要采用了Nodemailer,Nodemailer是一個(gè)簡(jiǎn)單易用的Node.js郵件發(fā)送組件,它的使用可以摸我摸我摸我,通過此模塊進(jìn)行郵件的發(fā)送。你們可能會(huì)問,為什么不用短信服務(wù)呢?哈~因?yàn)槎绦欧?wù)要錢,哈哈哈
/* * 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密碼,是你設(shè)置的smtp授權(quán)碼} }));let clientIsValid = false; const verifyClient = () => {transporter.verify((error, success) => {if (error) {clientIsValid = false;console.warn('郵件客戶端初始化連接失敗,將在一小時(shí)后重試');setTimeout(verifyClient, 1000 * 60 * 60);} else {clientIsValid = true;console.log('郵件客戶端初始化連接成功,隨時(shí)可發(fā)送郵件');}}); }; verifyClient();const sendMail = mailOptions => {if (!clientIsValid) {console.warn('由于未初始化成功,郵件客戶端發(fā)送被拒絕');return false;}mailOptions.from = '"ShineTomorrow" <admin@momentin.cn>'transporter.sendMail(mailOptions, (error, info) => {if (error) return console.warn('郵件發(fā)送失敗', error);console.log('郵件發(fā)送成功', info.messageId, info.response);}); };exports.sendMail = sendMail;賬號(hào)的注冊(cè)先是填寫email,填寫好郵箱之后會(huì)通過Nodemailer發(fā)送一封含有有效期的驗(yàn)證碼郵件,之后填寫驗(yàn)證碼、昵稱和密碼即可完成注冊(cè),并且為了安全考慮,對(duì)密碼采用了安全哈希算法(Secure Hash Algorithm)進(jìn)行加密。賬號(hào)的登錄以賬號(hào)或者郵箱號(hào)加上密碼進(jìn)行登錄,并且采用上文所說的JSON Web Token(JWT)身份認(rèn)證機(jī)制,從而實(shí)現(xiàn)用戶和用戶登錄狀態(tài)數(shù)據(jù)的對(duì)應(yīng)。
<center>我的郵件長(zhǎng)這樣?(可自己寫郵件模板)</center>
實(shí)時(shí)消息推送
當(dāng)用戶被人關(guān)注、評(píng)論被他人回復(fù)和點(diǎn)贊等一些社交性的操作的時(shí)候,在數(shù)據(jù)存儲(chǔ)完成后,服務(wù)端應(yīng)需要及時(shí)向用戶推送消息來提醒用戶。消息推送模塊采用了Socket.io來實(shí)現(xiàn),socket.io封裝了websocket,不支持websocket的情況還提供了降級(jí)AJAX輪詢,功能完備,設(shè)計(jì)優(yōu)雅,是開發(fā)實(shí)時(shí)雙向通訊的不二手段。
通過 socket.io,用戶每打開一個(gè)頁面,這個(gè)頁面都會(huì)和服務(wù)端建立一個(gè)連接。在服務(wù)端可以通過連接的socket的id屬性來匹配到一個(gè)建立連接的頁面。所以用戶的ID和socket的id,是一對(duì)多的關(guān)系,即一個(gè)用戶可能在登錄后打開多個(gè)頁面。而socket.io沒有提供從服務(wù)端向某個(gè)用戶單獨(dú)發(fā)送消息的功能,更沒有提供向某個(gè)用戶打開的所有頁面推送消息的功能。但是socket.io提供了room的概念,即群組。在建立websocket時(shí),客戶端可以選擇加入某個(gè)room,如果這個(gè)room沒有存在則自動(dòng)新建一個(gè),否則直接加入,服務(wù)端可以向某個(gè)room中的所有客戶端推送消息。
根據(jù)這個(gè)特性,設(shè)計(jì)將用戶的ID作為room的名字,當(dāng)某個(gè)用戶打開頁面建立連接時(shí),會(huì)選擇加入以自己用戶ID為名字的room。這樣,在用戶ID為名字的 room中,加入的都是用戶自己打開的頁面建立的連接。從而向某個(gè)用戶推送消息,可以直接通過向以此用戶的ID為名字的room發(fā)送消息,這樣就會(huì)推送到用戶打開的所有頁面。
有了想法后我們就開始魯吧~,在服務(wù)端中socket.io在客戶端中使用vue-socket.io, 服務(wù)端代碼如下:
/* * app.js中 */ const server = require('http').createServer(app); const io = require('socket.io')(server); global.io = io; //全局設(shè)上io值, 因?yàn)樵谄渌K要用到 io.on('connection', function (socket) {// setTimeout(()=>{// socket.emit('nodeEvent', { hello: 'world' });// }, 5000)socket.on('login_success', (data) => { //接受客戶端觸發(fā)的login_success事件//使用user_id作為房間號(hào)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}`); }); /* * 某業(yè)務(wù)模塊 */ //例如某文章增加評(píng)論 io.in(newMusicArticle.author.user_id._id).emit('receive_message', newMessage); //實(shí)時(shí)通知客戶端receive_message事件 sendMail({ //發(fā)送郵件to: newMusicArticle.author.user_id.email,subject: `Moment | 你有未讀消息哦~`,text: `啦啦啦,我是賣報(bào)的小行家~~ ?`,html: emailTemplate.comment(sender, newMusicArticle, content, !!req.body.reply_to_id) })客服端代碼:
<script>export default {name: 'App',data () {return {}},sockets:{connect(){},receive_message(val){ //接受服務(wù)端觸發(fā)的事件,進(jìn)行客戶端實(shí)時(shí)更新數(shù)據(jù)if (val){console.log('服務(wù)端實(shí)時(shí)通信', 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', { //通知服務(wù)端login_success 事件, 傳入iduser_id: JSON.parse(window.localStorage.getItem('user_info'))._id})}},} </script>評(píng)論模塊
評(píng)論模塊是為了移動(dòng)端WebApp下的文章下為用戶提供關(guān)于評(píng)論的一些操作。系統(tǒng)實(shí)現(xiàn)了對(duì)文章的評(píng)論,評(píng)論的點(diǎn)贊功能,熱門評(píng)論置頂以及評(píng)論的回復(fù)功能。在評(píng)論方面存在著各種各樣的安全性問題,比如XSS攻擊(Cross Site Scripting,跨站腳本攻擊)以及敏感詞等問題。預(yù)防XSS攻擊使用了xss模塊, 敏感詞過濾使用text-censor模塊。
一些思考
在開發(fā)的時(shí)候經(jīng)常會(huì)遇到這個(gè)問題,接口數(shù)據(jù)問題。有時(shí)候服務(wù)端返回的數(shù)據(jù)并不是我們想要的數(shù)據(jù),前端要對(duì)數(shù)據(jù)進(jìn)行再一步的處理。
例如服務(wù)端返回的某個(gè)字段為null或者服務(wù)端返回的數(shù)據(jù)結(jié)構(gòu)太深,前端需要不斷去判斷數(shù)據(jù)結(jié)構(gòu)是否真的返回了正確的東西,而不是個(gè)null 或者undefined~
我們前端都要這么去處理過濾:
<div class="author">文 / {{(musicArticleInfo.author && musicArticleInfo.author.user_id) ? musicArticleInfo.author.user_id.username : '我叫這個(gè)名字'}} </div>這就引出了一個(gè)思考:
對(duì)數(shù)據(jù)的進(jìn)一步封裝處理,必然渲染性能方面會(huì)存在問題,而且我們要時(shí)刻擔(dān)心數(shù)據(jù)返回的問題。如果應(yīng)用到公司的業(yè)務(wù),我們應(yīng)該如何處理呢 ?
首屏渲染問題一直是單頁應(yīng)用的痛點(diǎn),那么除了常用的性能優(yōu)化,我們還有什么方法優(yōu)化的嗎 ? 這個(gè)項(xiàng)目雖然面向的是移動(dòng)端用戶,可能不存在SEO問題,如果做成pc端的話,像文章這類的應(yīng)用,SEO都是必須品。
對(duì)于上面提出的問題,node的出現(xiàn)讓我們看到了解決方案,那就常說的Node中間層,當(dāng)然本項(xiàng)目中是不存在Node中間層,而是直接作為后端語言處理數(shù)據(jù)庫。
由于大部分的公司后端要么是php要么是java,一般不把node直接作為后端語言,如果有使用到node,一般是作為一個(gè)中間層的形式存在。
對(duì)于第一個(gè)問題的解決:我們可以在中間層做接口轉(zhuǎn)發(fā),在轉(zhuǎn)發(fā)的過程中做數(shù)據(jù)處理。而不用擔(dān)心數(shù)據(jù)返回的問題。
對(duì)于第二個(gè)問題的解決:有了Node中間層的話,那么我們可以把首屏渲染的任務(wù)交給nodejs去做,次屏的渲染依然走之前的瀏覽器渲染。
有Node中間層的話,新的架構(gòu)如下:
前后端的職能:
總結(jié)
已經(jīng)畢業(yè)一段時(shí)間了,寫文章是為了回顧。本人水平一般,見諒見諒。這個(gè)產(chǎn)品的實(shí)現(xiàn),一個(gè)人扛,在其中充當(dāng)了各種角色,要有一點(diǎn)點(diǎn)產(chǎn)品思維,要有一點(diǎn)點(diǎn)設(shè)計(jì)的想法,要會(huì)數(shù)據(jù)庫設(shè)計(jì),要會(huì)后端開發(fā),挺繁瑣的。最難的點(diǎn)個(gè)人感覺還是數(shù)據(jù)庫設(shè)計(jì),數(shù)據(jù)庫要一開始就要設(shè)計(jì)的很完整,不然到后面的添添補(bǔ)補(bǔ),就會(huì)很亂很亂,當(dāng)然這個(gè)基礎(chǔ)是產(chǎn)品要非常清晰,剛開始自己心中對(duì)產(chǎn)品可能是個(gè)模糊的定義,想想差不多是那樣,于是乎就開始搞~~導(dǎo)致于后面數(shù)據(jù)庫設(shè)計(jì)的不是很滿意。由于時(shí)間關(guān)系,現(xiàn)在的產(chǎn)品中有些小模塊還沒完成,但是大部分的功能結(jié)構(gòu)已經(jīng)完成,算是個(gè)成型的產(chǎn)品,當(dāng)然是一個(gè)沒有經(jīng)過測(cè)試的產(chǎn)品哈哈哈哈,要是有測(cè)試的話,那就哈哈哈哈你懂得。
前路漫漫,吾將上下而求索~
完
謝謝~~
總結(jié)
以上是生活随笔為你收集整理的聊聊毕业设计系列 --- 系统实现的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: linux下模仿用户左键,linux 鼠
- 下一篇: linux只允许从ttyS0设备登录,l