西部数码 网站管理,网站建设推广注册公司,江门网站建设咨询,济南小程序定制效果展示 github moment-server github地址 moment github地址 moment-manage github地址 articles 聊聊毕业设计系列 --- 项目介绍 聊聊毕业设计系列 --- 系统实现 前言 在上一篇文章中#xff0c;主要是对项目做了介绍#xff0c;并且对系统分析和系统设计做了大概的介绍。… 效果展示 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 TokenJWT它是一种轻量的认证规范也用于接口的认证。我们知道HTTP协议是一种无状态的协议这便意味着每个请求都是独立的当用户提供了用户名和密码来对我们的应用进行用户认证那么在下一次请求的时候用户需要再进行一次用户的认证才可以因为根据HTTP协议我们并不能知道是哪个用户发出的请求本系统采用了token的鉴权机制。这个token必须要在每次请求时传递给服务端它应该保存在请求头里另外服务端要支持CORS(跨来源资源共享)策略一般我们在服务端这么做就可以了Access-Control-Allow-Origin: *。 在用户身份认证这一块有很多方法最常见的像cookie session。那么他们三之间又有什么区别这里有两篇文章介绍的挺全面。 正确理解HTTP短连接中的Cookie、Session和Token小白必读闲话HTTP短连接中的Session和Tokentoken 与 session的区别在于它不同于传统的session认证机制它不需要在服务端去保留用户的认证信息或其会话的信息。系统一旦比较大都会采用机器集群来做负载均衡这需要多台机器由于session是保存在服务端那么就要 去考虑用户到底是在哪一台服务器上进行登录的这便是一个很大的负担。 那么就有人想问了你这个系统这么小为什么不使用传统的session机制呢哈~因为之前自己的项目一般都是使用session做登录没使用过token想尝试尝试入入坑~~哈哈哈~ 实现思路 JWT主要的实现思路如下 在用户登录成功的时候创建token保存于数据库中,并返回给客户端。客户端之后的每一次请求都要带上token在请求头里加入Authorization并加上token.在服务端进行验证token的有效性在有效期内返回200状态码token过期则返回401状态码如下图所示 centerJWT请求图/center 在node中主要用了jsonwebtoken这个模块来创建JWTjsonwebtoken的使用请查看jsonwebtoken文档。项目中创建token的中间件createToken如下 /*** createToken.js*/
const jwt require(jsonwebtoken); // 引入jsonwebtoken模块
const secret 我是密钥//登录时核对用户名和密码成功后应用将用户的iduser_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是domainhttps://upload-z0.qiniup.com,而base64图片的上传则是headers:{Content-Type:application/octet-stream}domain是domainhttps://upload-z0.qiniup.com/putb64/-1所以他们请求的时候token放的地方不同base64就像上面所说的放在请求头Authorization中而正常的放在form-data中。在服务端通过接口请求来获取七牛云上传token客户端获取到七牛云token通过不同方案将token带上。 base64的上传 headers:{Content-Type:application/octet-stream} 和 domainhttps://upload-z0.qiniup.com/putb64/-1token放在请求头Authorization中。正常图片和音视频的上传 headers: {Content-Type:multipart/form-data}和domainhttps://upload-z0.qiniup.comtoken 放在 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, domainhttps://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中主要采用了NodemailerNodemailer是一个简单易用的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 adminmomentin.cntransporter.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 TokenJWT身份认证机制从而实现用户和用户登录状态数据的对应。 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)
}) 客服端代码 scriptexport 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 classauthor文 / {{(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中间层的话新的架构如下 前后端的职能 总结 已经毕业一段时间了写文章是为了回顾。本人水平一般见谅见谅。这个产品的实现一个人扛在其中充当了各种角色要有一点点产品思维要有一点点设计的想法要会数据库设计要会后端开发挺繁琐的。最难的点个人感觉还是数据库设计数据库要一开始就要设计的很完整不然到后面的添添补补就会很乱很乱当然这个基础是产品要非常清晰刚开始自己心中对产品可能是个模糊的定义想想差不多是那样于是乎就开始搞~~导致于后面数据库设计的不是很满意。由于时间关系现在的产品中有些小模块还没完成但是大部分的功能结构已经完成算是个成型的产品当然是一个没有经过测试的产品哈哈哈哈要是有测试的话那就哈哈哈哈你懂得。 前路漫漫吾将上下而求索~ 完 谢谢~~