聊聊畢業設計系列 --- 系統實現

ShineTomorrow發表於2018-08-26

效果展示

管理系統

WebApp

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(實體)。

  1. 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 }, // 使用者ua
    last_login_time: { type: Date },
    .....
});
複製程式碼
  1. Model: 由Schema釋出生成的模型,具有抽象屬性和行為的資料庫操作物件
//生成一個具體User的model並匯出
const User = mongoose.model("User", UserSchema);  //第一個引數是集合名,在資料庫中會把Model名字字母全部變小寫和在後面加複數s

//執行到這個時候你的資料庫中就有了 users 這個集合

module.exports = User;

複製程式碼
  1. Entity: 由Model建立的實體,他的操作也會影響資料庫,但是它運算元據庫的能力比Model弱
const newUser = new UserModel({          //UserModel 為匯出來的 User
            email: 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。那麼他們三之間又有什麼區別,這裡有兩篇文章介紹的挺全面。

token 與 session的區別在於,它不同於傳統的session認證機制,它不需要在服務端去保留使用者的認證資訊或其會話的資訊。系統一旦比較大,都會採用機器叢集來做負載均衡,這需要多臺機器,由於session是儲存在服務端,那麼就要 去考慮使用者到底是在哪一臺伺服器上進行登入的,這便是一個很大的負擔。

那麼就有人想問了,你這個系統這麼小,為什麼不使用傳統的session機制呢?哈~因為之前自己的專案一般都是使用session做登入,沒使用過token,想嘗試嘗試入入坑~~哈哈哈~

實現思路

JWT主要的實現思路如下:

  1. 在使用者登入成功的時候建立token儲存於資料庫中,並返回給客戶端。

  2. 客戶端之後的每一次請求都要帶上token,在請求頭裡加入Authorization,並加上token.

  3. 在服務端進行驗證token的有效性,在有效期內返回200狀態碼,token過期則返回401狀態碼

如下圖所示:

JWT請求圖

JWT請求圖

在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-Typedomain(域)有所不同,正常圖片和音視訊的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帶上。

  1. base64的上傳: headers:{'Content-Type':'application/octet-stream'}domain='https://upload-z0.qiniup.com/putb64/-1',token放在請求頭Authorization中。
  2. 正常圖片和音視訊的上傳: 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 = accessKey
        this.sk = secretKey
        this.bucket = bucket
        this.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-Typedomain(域)有所不同,所以的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'){  //如果是base64
            const token = res.data.upToken
            Vue.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){  //如果是FormData
            data.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 就建立formData
            formdata.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
複製程式碼

路由許可權模組

系統的後臺管理面向的是合作作者和管理員,涉及到兩種角色,故此要做許可權管理。不同的許可權對應著不同的路由,同時側邊欄的選單也需根據不同的許可權,非同步生成,不同於以往的服務端直接返回路由表,由前端動態生成,接下來介紹下登入和許可權驗證的思路:

  1. 登入:當使用者填寫完賬號和密碼後向服務端驗證是否正確,驗證通過之後,服務端會返回一個token,拿到token之後前端會根據token再去拉取一個getAdminInfo的介面來獲取使用者的詳細資訊(如使用者許可權,使用者名稱等等資訊)。

  2. 許可權驗證:通過token獲取使用者對應的role,動態根據使用者的role算出其對應有許可權的路由,通過vue-router的beforeEach進行全域性前置守衛再通過router.addRoutes動態掛載這些路由。

程式碼有點多,這邊就直接放流程圖哈~~

許可權路由流程圖

許可權路由流程圖

最近正好也在公司做中後臺專案,公司的中後臺專案的這邊是由服務端生成路由表,前端進行直接渲染,畢竟公司的一整套業務比較成熟。但是我們會在想能不能由前端維護路由表,這樣不用到時候專案迭代,前端每增加頁面都要讓服務端兄弟配一下路由和許可權,當然前提可能是專案比較小的時候。

賬號模組

賬號模組是業務中最為基礎的模組,承擔著整個系統所有的賬號相關的功能。系統實現了使用者註冊、使用者登入、密碼修改、找回密碼功能。

系統的賬號模組使用了郵件服務,針對普通使用者的註冊採用了郵件服務來傳送驗證碼,以及密碼的修改等操作都採用了郵件服務。在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)身份認證機制,從而實現使用者和使用者登入狀態資料的對應。

郵件長這樣

我的郵件長這樣?(可自己寫郵件模板)

實時訊息推送

當使用者被人關注、評論被他人回覆和點贊等一些社交性的操作的時候,在資料儲存完成後,服務端應需要及時向使用者推送訊息來提醒使用者。訊息推送模組採用了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 事件, 傳入id
                    user_id: JSON.parse(window.localStorage.getItem('user_info'))._id
                })
            }
        },
    }
</script>
複製程式碼

評論模組

評論模組是為了移動端WebApp下的文章下為使用者提供關於評論的一些操作。系統實現了對文章的評論,評論的點贊功能,熱門評論置頂以及評論的回覆功能。在評論方面存在著各種各樣的安全性問題,比如XSS攻擊(Cross Site Scripting,跨站指令碼攻擊)以及敏感詞等問題。預防XSS攻擊使用了xss模組, 敏感詞過濾使用text-censor模組。

一些思考

  1. 介面資料問題

在開發的時候經常會遇到這個問題,介面資料問題。有時候服務端返回的資料並不是我們想要的資料,前端要對資料進行再一步的處理。

例如服務端返回的某個欄位為null或者服務端返回的資料結構太深,前端需要不斷去判斷資料結構是否真的返回了正確的東西,而不是個null 或者undefined~

聊聊畢業設計系列 --- 系統實現

我們前端都要這麼去處理過濾:

<div class="author">
    文 / {{(musicArticleInfo.author && musicArticleInfo.author.user_id) ? musicArticleInfo.author.user_id.username : '我叫這個名字'}}
</div>
複製程式碼

這就引出了一個思考:

對資料的進一步封裝處理,必然渲染效能方面會存在問題,而且我們要時刻擔心資料返回的問題。如果應用到公司的業務,我們應該如何處理呢 ?

  1. 頁面效能優化和SEO問題

首屏渲染問題一直是單頁應用的痛點,那麼除了常用的效能優化,我們還有什麼方法優化的嗎 ? 這個專案雖然面向的是移動端使用者,可能不存在SEO問題,如果做成pc端的話,像文章這類的應用,SEO都是必須品。


對於上面提出的問題,node的出現讓我們看到了解決方案,那就常說的Node中間層,當然本專案中是不存在Node中間層,而是直接作為後端語言處理資料庫。

由於大部分的公司後端要麼是php要麼是java,一般不把node直接作為後端語言,如果有使用到node,一般是作為一箇中間層的形式存在。

對於第一個問題的解決:我們可以在中間層做介面轉發,在轉發的過程中做資料處理。而不用擔心資料返回的問題。

對於第二個問題的解決:有了Node中間層的話,那麼我們可以把首屏渲染的任務交給nodejs去做,次屏的渲染依然走之前的瀏覽器渲染。


有Node中間層的話,新的架構如下:

新架構

前後端的職能:

聊聊畢業設計系列 --- 系統實現

總結

已經畢業一段時間了,寫文章是為了回顧。本人水平一般,見諒見諒。這個產品的實現,一個人扛,在其中充當了各種角色,要有一點點產品思維,要有一點點設計的想法,要會資料庫設計,要會後端開發,挺繁瑣的。最難的點個人感覺還是資料庫設計,資料庫要一開始就要設計的很完整,不然到後面的添添補補,就會很亂很亂,當然這個基礎是產品要非常清晰,剛開始自己心中對產品可能是個模糊的定義,想想差不多是那樣,於是乎就開始搞~~導致於後面資料庫設計的不是很滿意。由於時間關係,現在的產品中有些小模組還沒完成,但是大部分的功能結構已經完成,算是個成型的產品,當然是一個沒有經過測試的產品哈哈哈哈,要是有測試的話,那就哈哈哈哈你懂得 ~~~。

前路漫漫,吾將上下而求索~


謝謝~~

相關文章