这个项目最初实际上是fork他人的项目。当初想打仗下mongodb数据库,找个例子进修下,厥后改着改着就面目一新了。背景和数据库重构,前端增添了登录注册功用,仅保留了博客设置页面,然则也优化了。
一、功用特性
- 一个基础的博客内容治理器功用,如宣布并治理文章等
- 每一个用户能够经由过程注册具有本身的博客
- 支撑markdown语法编辑
- 支撑代码高亮
- 能够治理博客页面的链接
- 博客页面临挪动端适配优化
- 账户治理(修正暗码)
- 页面充足大气、酷炫嘿
二、用到的手艺和完成思绪:
2.1 前端:Vue百口桶
- Vue.js
- Vue-Cli
- Vue-Resource
- Vue-Validator
- Vue-Router
- Vuex
- Vue-loader
2.2 后端
- Node.js
- mongoDB (mongoose)
- Express
2.3 东西和言语
- Webpack
- ES6
- SASS
- Jade
2.4 团体思绪:
- Node服务端除了主页和首页外,不做模板衬着,衬着交给浏览器完成
- Node服务端不做任何路由切换的内容,这部份交给Vue-Router完成
- Node服务端只用来吸收要求,查询数据库并用来返回值
所以如许做前后端险些完整解耦,只需约定好restful作风的数据接口,和数据存取花样就OK啦。
后端我用了mongoDB做数据库,并在Express中经由过程mongoose操纵mongoDB,省去了庞杂的命令行,经由过程Javascript操纵无疑轻易了许多。
三、更新内容
在本来项目的基础上,做了以下更新:
- 数据库从新设想,改成以用户分组的subDocs数据库构造
- 应数据库修改,一切接口从新设想,并一致采纳和网易立马理财一致的接口作风
- 删除本来旅客情势,增添登录注册功用,支撑弹窗登录。
- 增添首页,展现最新宣布文章和注册用户
- 增添修正暗码,登出,注销等功用。
- 优化pop弹窗组件,越发智能,更多设置项,靠近网易$.dialog组件。而且一套代码仅修正了下css,完成雷同接口下pc端弹窗和wap端toast功用。
- 增添挪动端适配
- 优化本来代码,修复部份bug。
更多的更新内容请移步项目CMS-of-Blog_Production和CMS-of-Blog。
四、中心代码剖析
原作者也写过剖析的文章。这里,重要剖析一下我更新的部份。
4.1. 数据库
对原数据库举行从新设想,改成以用户分组的subDocs数据库构造。如许以用户为一个团体的数据库构造越发清楚,同时也更轻易操纵和读取。代码以下:
var mongoose = require('mongoose'),
Schema = mongoose.Schema
articleSchema = new Schema({
title: String,
date: Date,
content: String,
}),
linkSchema = new Schema({
name: String,
href: String,
newPage: Boolean
}),
userSchema = new Schema({
name: String,
password: String,
email: String,
emailCode: String,
createdTime: Number,
articles: [articleSchema],
links: [linkSchema]
}),
User = mongoose.model('User', userSchema);
mongoose.connect('mongodb://localhost/platform')
mongoose.set('debug', true)
var db = mongoose.connection
db.on('error', function () {
console.log('db error'.error)
})
db.once('open', function () {
console.log('db opened'.silly)
})
module.exports = {
User: User
}
代码一开始新定义了三个Schema:articleSchema、linkSchema和userSchema。而userSchema里又嵌套了articleSchema和linkSchema,构成了以用户分组的subDocs数据库构造。Schema是一种以文件情势存储的数据库模子骨架,不具备数据库的操纵能力。然后将将该Schema宣布为Model。Model由Schema宣布天生的模子,具有笼统属性和行动的数据库操纵对。由Model能够竖立的实体,比方新注册一个用户就会竖立一个实体。
数据库竖立了以后须要去读取和操纵,能够看下注册时发送邮箱验证码的这段代码感觉下。
router.post('/genEmailCode', function(req, res, next) {
var email = req.body.email,
resBody = {
retcode: '',
retdesc: '',
data: {}
}
if(!email){
resBody = {
retcode: 400,
retdesc: '参数毛病',
}
res.send(resBody)
return
}
function genRandomCode(){
var arrNum = [];
for(var i=0; i<6; i++){
var tmpCode = Math.floor(Math.random() * 9);
arrNum.push(tmpCode);
}
return arrNum.join('')
}
db.User.findOne({ email: email }, function(err, doc) {
if (err) {
return console.log(err)
} else if (doc && doc.name !== 'tmp') {
resBody = {
retcode: 400,
retdesc: '该邮箱已注册',
}
res.send(resBody)
} else if(!doc){ // 第一次点击猎取验证码
var emailCode = genRandomCode();
var createdTime = Date.now();
// setup e-mail data with unicode symbols
var mailOptions = {
from: '"CMS-of-Blog ?" <tywei90@163.com>', // sender address
to: email, // list of receivers
subject: '亲爱的用户' + email, // Subject line
text: 'Hello world ?', // plaintext body
html: [
'<p>您好!祝贺您注册成为CMS-of-Blog博客用户。</p>',
'<p>这是一封发送验证码的注册认证邮件,请复制一下验证码填写到注册页面以完成注册。</p>',
'<p>本次验证码为:' + emailCode + '</p>',
'<p>上述验证码30分钟内有用。假如验证码失效,请您登录网站<a href="https://cms.wty90.com/#!/register">CMS-of-Blog博客注册</a>从新请求认证。</p>',
'<p>感谢您注册成为CMS-of-Blog博客用户!</p><br/>',
'<p>CMS-of-Blog开辟团队</p>',
'<p>'+ (new Date()).toLocaleString() + '</p>'
].join('') // html body
};
// send mail with defined transport object
transporter.sendMail(mailOptions, function(error, info){
if(error){
return console.log(error);
}
// console.log('Message sent: ' + info.response);
new db.User({
name: 'tmp',
password: '0000',
email: email,
emailCode: emailCode,
createdTime: createdTime,
articles: [],
links: []
}).save(function(err) {
if (err) return console.log(err)
// 半小时内假如不注册胜利,则在数据库中删除这条数据,也就是说验证码会失效
setTimeout(function(){
db.User.findOne({ email: email }, function(err, doc) {
if (err) {
return console.log(err)
} else if (doc && doc.createdTime === createdTime) {
db.User.remove({ email: email }, function(err) {
if (err) {
return console.log(err)
}
})
}
})
}, 30*60*1000);
resBody = {
retcode: 200,
retdesc: ''
}
res.send(resBody)
})
});
}else if(doc && doc.name === 'tmp'){
// 在邮箱验证码有用的时间内,再次点击猎取验证码(相似省略)
...
}
})
})
背景接受到发送邮箱验证码的要求后,会初始化一个tmp的用户。经由过程new db.User()
会竖立一个User的实例,然后实行save()
操纵会将这条数据写到数据库里。假如在半小时内没有注册胜利,经由过程婚配邮箱,然后db.User.remove()
将这条数据删除。更多详细用法请移步官方文档。
4.2. 背景
将一切要求分为三种:
- ajax异步要求,一致途径:
/web/
- 大众页面部份,如博客首页、登录、注册等,一致途径:
/
- 与博客用户id相干的博客部份,一致途径:
/:id/
如许每一个用户都能够具有本身的博客页面,详细代码以下:
var express = require('express');
var path = require('path');
var favicon = require('serve-favicon');
var logger = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');
var routes = require('./index');
var db = require('./db')
var app = express();
// view engine setup
app.set('views', path.join(__dirname, '../'));
app.set('view engine', 'jade');
// uncomment after placing your favicon in /public
//app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')));
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());
app.use('/public',express.static(path.join(__dirname, '../public')));
// 大众ajax接口(index.js)
app.use('/web', routes);
// 大众html页面,比方登录页,注册页
app.get('/', function(req, res, next) {
res.render('common', { title: 'CMS-blog' });
})
// 跟用户相干的博客页面(路由的第一个参数只婚配与处置惩罚的相干的,不越权!)
app.get(/^\/[a-z]{1}[a-z0-9_]{3,15}$/, function(req, res, next) {
// format猎取要求的path参数
var pathPara = req._parsedUrl.pathname.slice(1).toLocaleLowerCase()
// 查询是不是对应有响应的username
db.User.count({name: pathPara}, function(err, num) {
if (err) return console.log(err)
if(num > 0){
res.render('main', { title: 'CMS-blog' });
}else{
// 自定义毛病处置惩罚
res.status(403);
res.render('error', {
message: '该用户还没有开通博客。<a href="/#!/register">去注册</a>',
});
}
})
})
// catch 404 and forward to error handler
app.use(function(req, res, next) {
var err = new Error('Not Found');
err.status = 404;
next(err);
});
// error handlers
// development error handler
// will print stacktrace
if (app.get('env') === 'development') {
app.use(function(err, req, res, next) {
res.status(err.status || 500);
res.render('error', {
message: err.message,
error: err
});
});
}
module.exports = app;
详细的ajax接口代码人人能够看server文件夹下的index.js文件。
4.3. pop/toast组件
在原项目基础上,优化了pop弹窗组件,越发智能,更多设置项,靠近网易$.dialog组件。使而且一套代码仅修正了下css,完成雷同接口下pc端弹窗和wap端toast功用。由于有部份花样化参数代码在vuex的action里,有时间,能够将这个进一步整理成一个vue组件,轻易人人运用。
4.3.1 pop/toast组件设置参数申明
-
pop
: 弹窗的显现与否, 依据content参数,有内容则为true -
css
: 自定义弹窗的class, 默以为空 -
showClose
: 为false则不显现封闭按钮, 默许显现 -
closeFn
: 弹窗点击封闭按钮以后的回调 -
title
: 弹窗的题目,默许’温馨提醒’, 假如不想显现title, 直接传空 -
content
(required): 弹窗的内容,支撑传html -
btn1
: ‘按钮1案牍|按钮1款式class’, 花样化后为btn1Text和btn1Css -
cb1
: 按钮1点击以后的回调,假如cb1没有明白返回true,则默许按钮点击后封闭弹窗 -
btn2
: ‘按钮2案牍|按钮2款式class’, 花样化后为btn2Text和btn2Css -
cb2
: 按钮2点击以后的回调,假如cb2没有明白返回true,则默许按钮点击后封闭弹窗。按钮参数不传,案牍默许’我知道了’,点击封闭弹窗 -
init
: 弹窗竖立后的初始化函数,能够用来处置惩罚庞杂交互(注重弹窗肯定如果从pop为false变成true才会实行) -
destroy
: 弹窗消逝以后的回调函数 -
wapGoDialog
: 在挪动端时,要不要走弹窗,默许false,走toast
4.3.2 pop/toast组件代码
模板
<template>
<div class="m-dialog" :class="getPopPara.css">
<div class="dialog-wrap">
<span class="close" @click="handleClose" v-if="getPopPara.showClose">+</span>
<div class="title" v-if="getPopPara.title">{{getPopPara.title}}</div>
<div class="content">{{{getPopPara.content}}}</div>
<div class="button">
<p class="btn" :class="getPopPara.btn1Css" @click="fn1">
<span>{{getPopPara.btn1Text}}</span>
</p>
<p class="btn" :class="getPopPara.btn2Css" @click="fn2" v-if="getPopPara.btn2Text">
<span>{{getPopPara.btn2Text}}</span>
</p>
</div>
</div>
</div>
</template>
剧本
import {pop} from '../vuex/actions'
import {getPopPara} from '../vuex/getters'
import $ from '../js/jquery.min'
export default{
computed:{
showDialog(){
return this.getPopPara.pop
}
},
vuex: {
getters: {
getPopPara
},
actions: {
pop
}
},
methods: {
fn1(){
let fn = this.getPopPara.cb1
let closePop = false
// 假如cb1函数没有明白返回true,则默许按钮点击后封闭弹窗
if(typeof fn == 'function'){
closePop = fn()
}
// 初始值为false, 所以没传也默许封闭
if(!closePop){
this.pop()
}
// !fn && this.pop()
},
fn2(){
let fn = this.getPopPara.cb2
let closePop = false
// 假如cb1函数没有明白返回true,则默许按钮点击后封闭弹窗
if(typeof fn == 'function'){
closePop = fn()
}
// 初始值为false, 所以没传也默许封闭
if(!closePop){
this.pop()
}
// !fn && this.pop()
},
handleClose(){
// this.pop()要放在末了,由于先实行一切参数就都变了
let fn = this.getPopPara.closeFn
typeof fn == 'function' && fn()
this.pop()
}
},
watch:{
'showDialog': function(newVal, oldVal){
// 弹窗翻开时
if(newVal){
// 增添弹窗支撑键盘操纵
$(document).bind('keydown', (event)=>{
// 回车键实行fn1,会涌现重复弹窗bug
if(event.keyCode === 27){
this.pop()
}
})
var $dialog = $('.dialog-wrap');
// 挪动端改成相似toast,经由过程变动款式,既不须要增添toast组件,也不须要变动代码,一致pop要领
if(screen.width < 700 && !this.getPopPara.wapGoDialog){
$dialog.addClass('toast-wrap');
setTimeout(()=>{
this.pop();
$dialog.removeClass('toast-wrap');
}, 2000)
}
//调解弹窗居中
let width = $dialog.width();
let height = $dialog.height();
$dialog.css('marginTop', - height/2);
$dialog.css('marginLeft', - width/2);
// 弹窗竖立的初始化函数
let fn = this.getPopPara.init;
typeof fn == 'function' && fn();
}else{
// 弹窗封闭时
// 注销弹窗翻开时注册的事宜
$(document).unbind('keydown')
// 弹窗消逝回调
let fn = this.getPopPara.destroy
typeof fn == 'function' && fn()
}
}
}
}
4.3.3 pop/toast组件参数花样化代码
为了运用轻易,我们在运用的时刻举行了简写。为了让组件能辨认,须要在vuex的action里对传入的参数花样化。
function pop({dispatch}, para) {
// 假如没有传入任何参数,默许封闭弹窗
if(para === undefined){
para = {}
}
// 假如只传入字符串,花样化内容为content的para对象
if(typeof para === 'string'){
para = {
content: para
}
}
// 设置默许值
para.pop = !para.content? false: true
para.showClose = para.showClose === undefined? true: para.showClose
para.title = para.title === undefined? '温馨提醒': para.title
para.wapGoDialog = !!para.wapGoDialog
// 没有传参数
if(!para.btn1){
para.btn1 = '我知道了|normal'
}
// 没有传class
if(para.btn1.indexOf('|') === -1){
para.btn1 = para.btn1 + '|primary'
}
let array1 = para.btn1.split('|')
para.btn1Text = array1[0]
// 可能会传多个class
for(let i=1,len=array1.length; i<len; i++){
if(i==1){
// class为disabled属性不加'btn-'
para.btn1Css = array1[1]=='disabled'? 'disabled': 'btn-' + array1[1]
}else{
para.btn1Css = array1[i]=='disabled'? ' disabled': para.btn1Css + ' btn-' + array1[i]
}
}
if(para.btn2){
if(para.btn2.indexOf('|') === -1){
para.btn2 = para.btn2 + '|normal'
}
let array2 = para.btn2.split('|')
para.btn2Text = array2[0]
for(let i=1,len=array2.length; i<len; i++){
if(i==1){
para.btn2Css = array2[1]=='disabled'? 'disabled': 'btn-' + array2[1]
}else{
para.btn2Css = array2[i]=='disabled'? ' disabled': para.btn2Css + ' btn-' + array2[i]
}
}
}
dispatch('POP', para)
}
为了让挪动端兼容pop弹窗组件,我们采纳mediaQuery对挪动端款式举行了变动。增添参数wapGoDialog
,表明我们在挪动端时,要不要走弹窗,默许false,走toast。如许能够一套代码就能够兼容pc和wap。
跋文
这里重要剖析了下背景和数据库,而且比较简单,人人能够去看源码。总之,这是一个不错的前端入手背景和数据库的例子。功用比较丰富,而且能够进修下vue.js。