完成的功能
- 阅览文章
- 有作者信息栏
- 根据当前用户判断是否可修改文章
- 修改并保存文章
依赖的第三方工具
- vue.js
- SuMarkdown
- jquery
- bootstrap-taginput
前言
我们先来分析一下这个功能的实现,其实这个过程中是需要很多数据的——作者的数据,文章的数据,当前用户的数据。如果分两个页面来做,显然许多异步数据会被重复加载,所以我决定把这一部分做成一个极小的单页面应用。
同时,在这一次的博客里,用到了许多Vue的重要特性,比如component,directive,life-cycle,是Vue一个不错的示例。
页面代码
主页面代码
html
<!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8"> <title> <%= title %> </title> <link rel="stylesheet" href="css/style.css"> <link rel="stylesheet" href="lib/bootstrap/dist/css/bootstrap.min.css"> <link rel="stylesheet" href="lib/suMarkdown/lib/highlight/styles/default.css"> <link rel="stylesheet" href="lib/bootstrap/bootstrap-tag/bootstrap-tagsinput.css"> <script src="lib/jquery/dist/jquery.min.js"></script> <script src="lib/bootstrap/dist/js/bootstrap.min.js"></script> <script src="lib/suMarkdown/lib/highlight/highlight.pack.js"></script> <script src="lib/suMarkdown/lib/marked/marked.js"></script> <script src="lib/bootstrap/bootstrap-tag/bootstrap-tagsinput.min.js"></script> <script src="lib/suMarkdown/js/suMarkdown.js"></script> </head> <body> <%include layout/left-bar.ejs%> <div v-component="{{currentView}}" v-ref='center'></div> <%include layout/alert.ejs%> <script src="js/blog.js"></script> </body>
阅览模板
html
<style> .log-bar{ position:fixed; top:10px; right:0; width:100%; z-index:999; padding-right:10px; } .log-bar *{ float:right; margin-right:10px; } .header-bar{ position:fixed; top:10px; right:0; width:100%; z-index:999; padding-right:10px; } .header-bar .btn-group{ top:15px; float:right; margin-right:6px; vertical-align:middle; } .header-bar .dropdown-toggle{ float:right; top:10px; margin-right:10px; position:relative; width:42px; height:42px; display:inline-block; } .header-bar .dropdown-toggle img{ display:block; width:100%; height:100%; border: 2px solid white; } .header-bar .dropdown-toggle .caret{ position:absolute; right:-8px; top:20px; } .header-bar .dropdown-menu{ left:auto; right:0; top:50px; position:absolute; z-index:1000; min-width:160px; padding:5px 5px; margin:2px 0 0; border: 1px solid #ccc; } .header-bar li{ line-height:20px; margin-bottom:5px; } .header-bar li a{ padding:3px 20px; clear:both; font-weight:normal; } .header-bar li a:hover{ background-color:#333333; color:white; } .header-bar li a span{ margin-right:8px; } .people { position: absolute; right: 70%; top: 80px; width: 160px; padding: 0 20px 20px 0; text-align: right; } .people .author { margin: 0; font-size: 16px; font-weight: bold; line-height: 24px; } .people img{ width: 60px; height: 60px; margin: 0 0 10px 0; display: inline-block; border: 2px solid white; } .people .about{ color: #999999; font-size: 12px; margin:0 0 10 } .people .sns{ margin-bottom:10px; } .sns a{ width:20px; margin-left:7px; display:inline-block; color:#555555; } .sns a img{ opacity:0.8; border-radius:3px; height:auto; max-width:100%; vertical-align:middle; border:0; } .container .article{ position:relative; line-height:30px; left:25%; width:60%; padding:0 40px 30px; } .article #blogHead h1{ font-size:50px; font-weight:700; line-height:70px; } .article #blogHead p{ font-size:14px; color:#999999; } .article #blogHead span{ margin:0 2%; } .article #blogBody{ font-size:17px; } </style> <div class="header-bar" v-show="login" > <a href="javascript:void(0)" class="dropdown-toggle" data-toggle="dropdown"> <img class="img-circle" v-attr="src: user.avatar"> <span class="caret"></span> </a> <ul class="dropdown-menu" role="menu"> <li> <a href="#"> <span class="glyphicon glyphicon-user"></span> 我的主页 </a> <a href="#"> <span class="glyphicon glyphicon-hand-right"></span> 我的小组 </a> <a href="#"> <span class="glyphicon glyphicon-cog"></span> 我的设置 </a> <a href="#"> <span class="glyphicon glyphicon-inbox"></span> 消息 </a> <a href="#"> <span class="glyphicon glyphicon-info-sign"></span> 帮助 </a> <a href="#"> <span class="glyphicon glyphicon-log-out"></span> 登出 </a> </li> </ul> <div class="btn-group"> <a href="javascript:void(0)" class="btn" v-on="click: edit()" v-show="editable"> <span class="glyphicon glyphicon-edit"></span> </a> </div> </div> <div class="log-bar" v-show="!login"> <a href='#'>登陆</a> <a href='#'>注册</a> </div> <div class="container"> <div class="people"> <a class="author" href="#"> <img class="img-circle" v-attr="src: author.avatar"> <br> {{blog.author}} </a> <div class="about"> <p>{{author.description}}</p> </div> <div class="sns"> <a href="#"> <img src="img/home.png"> </a> <a href="#"> <img src="img/home.png"> </a> <a v-href="author.page"> <img src="img/home.png"> </a> </div> </div> <div class="article"> <div id="blogHead"> <h1>{{blog.title}}</h1> <p> <span>发表时间 {{blog.date | toDate }}</span> <span>字数:{{blog.body.length}}</span> <span>阅读量:30</span> </p> </div> <div id="blogBody"> </div> </div> </div>
编辑模板
html
<style> body{ background-color: #f5f5f5; } .right-bar{ float:left; height: 100%; width:100px; padding: 2% 5%; } .center-page{ float: left; padding: 0 7% 0 7%; width:86%; vertical-align: middle; border-right: solid 1px; border-color: #cccccc; } .form-group h4{ position: relative; top:-12px; } form:first-child input{ font-size: 16px; } .alert { margin: 3% 5% 0; text-align: center; position: fixed; top: 80%; width:60%; } </style> <div class="container"> <div class="center-page" > <form class="form-horizontal" id="writePage"> <div class="form-group"> <div class="col-sm-1"></div> <h4 class="col-sm-2 control-label">标题</h4> <div class="col-sm-6"> <input class="form-control" v-model="myBlog.title"> </div> </div> <br> <div class="form-group"> <div class="col-sm-1"></div> <h4 class="col-sm-2 control-label">标签</h4> <div class="col-sm-6"> <select multiple class="form-control" data-role="tagsinput" v-tags="myBlog.tags"></select> </div> </div> <br> <div class="form-group"> <div class="col-sm-12 suMarkdown"> <style> .su-toolbar{ width:100%; height:45px; display: block; background: #ffffff; padding: 5px; border: solid 1px; border-color: #cccccc; } .su-toolbar .tool-block{ cursor: pointer; display: block; width:35px; margin:0 1%; height:35px; float:left; padding: 5px; } .su-toolbar .tool-block *{ left:20%; top:20%; } .su-toolbar .tool-block:hover{ background: #00ffff; } .su-toolbar button{ margin-top:5px; } .suEditor{ width:50%; float:left; display: block; } .suEditor textarea{ width:100%; height: 400px; background: #ffffff; tab-size: 4; border:solid 1px; border-top: none; border-color: #cccccc; padding: 20px; resize: none; } .suEditor textarea:focus{ background: #fff; border-color:#cccccc ; outline: none; } .suPreview{ width:50%; left:50%; float:left; background: #ffffff; height: 400px; display: block; overflow: auto; padding: 0 20px; border-right: solid 1px; border-bottom: solid 1px; border-color: #cccccc; } .suProgress{ width: 100%; } </style> <div class="suProgress progress"> <div class="progress-bar su-progress-bar" role="progressbar" aria-valuenow="60" aria-valuemin="0" aria-valuemax="100"> 0% </div> </div> <div class="su-toolbar"> <div class="tool-block su-tool-bold" title="加粗(Ctrl+B)" data-placement="top" data-toggle="tooltip"> <span class="glyphicon glyphicon-bold"></span> </div> <div class="tool-block su-tool-italic" title="斜体(Ctrl+I)" data-placement="top" data-toggle="tooltip"> <span class="glyphicon glyphicon-italic"></span> </div> <div class="tool-block su-tool-head" title="标题(Ctrl+H)" data-placement="top" data-toggle="tooltip"> <span class="glyphicon glyphicon-header"></span> </div> <div class="tool-block su-tool-link" title="链接(Ctrl+L)" data-placement="top" data-toggle="tooltip"> <span class="glyphicon glyphicon-link"></span> </div> <div class="tool-block su-tool-img" title="图片(Ctrl+G)" data-placement="top" data-toggle="tooltip"> <span class="glyphicon glyphicon-picture"></span> </div> <div class="tool-block su-tool-list" title="无序列表(Ctrl+U)" data-placement="top" data-toggle="tooltip"> <span class="glyphicon glyphicon-list"></span> </div> <div class="tool-block su-tool-orderlist" title="有序列表(Ctrl+O)" data-placement="top" data-toggle="tooltip"> <span class="glyphicon glyphicon-th-list"></span> </div> <div class="tool-block su-tool-code" title="单行代码(Ctrl+K)" data-placement="top" data-toggle="tooltip"> <span class="glyphicon glyphicon-asterisk"></span> </div> <div class="tool-block su-tool-quote" title="引用(Ctrl+Q)" data-placement="top" data-toggle="tooltip"> <span class="glyphicon glyphicon-comment"></span> </div> <div class="tool-block su-tool-plus" title="文件上传" data-placement="top" data-toggle="tooltip"> <span class="glyphicon glyphicon-upload"></span> </div> <input type="file" style="display: none" class="su-tool-upload" multiple> <a class="tool-block su-tool-help" href="http://lab.lepture.com/editor/markdown" title="帮助" data-placement="top" data-toggle="tooltip"> <span class="glyphicon glyphicon-info-sign"></span> </a> <div class="tool-block pull-right su-tool-fullscreen" title="全屏" data-placement="top" data-toggle="tooltip"> <span class="glyphicon glyphicon-fullscreen"></span> </div> <div class="tool-block pull-right su-tool-preview" title="预览" data-placement="top" data-toggle="tooltip"> <span class="glyphicon glyphicon-eye-open"></span> </div> </div> <div class="suEditor"> <textarea v-model="myBlog.body"></textarea> </div> <div class="suPreview"> </div> </div> </div> </form> <button class="btn btn-primary btn-lg pull-left" id="submit" v-on="click: submit()"> 发表 </button> <button class="btn btn-primary btn-lg pull-right" id="submit" v-on="click: cancel()"> 取消 </button> </div> <div class="right-bar" > <img class="img-circle" style="width: 90px;height: 90px;" v-attr="src: author.avatar"> </div> </div>
前端代码
Directive
javascript
/** * Created by suemi on 14-12-13. */ module.exports={ tag:function(Vue){ Vue.directive('tags',{ twoWay:true, bind: function () { var self=this; console.log(self); $(self.el).on('itemAdded',function(){ scope.blog.tags=$(this).val(); }); $(self.el).on('itemRemoved',function(){ scope.blog.tags=$(this).val(); }); }, update:function(){}, unbind:function(){ $(this.el).off(); } }); return module.exports; }, all:function(Vue){ for(var i in module.exports){ if(i==='all') return module.exports; else module.exports[i](Vue); } return module.exports; } };
让所有函数返回module.exports可以支持漂亮的链调的写法,同时还写了all函数以实现像angular一样(如下)
javascript
angular.module('demo',['app.directives']);
一次加载全部directive
Components
javascript
/** * Created by suemi on 14-12-13. */ module.exports={ readBlog:function(Vue){ var scope=Vue.component('readBlog',{ inherit:true, template:require('../templates/readBlog.html'), ready:function(){ $('#blogBody').html(marked(this.blog.body)); $('pre code').each(function(i,block){ hljs.highlightBlock(block); }); }, methods:{ edit:function(){ console.log(this); this.currentView='editBlog'; } }, filters:{ toDate:function(date){ var tmp=new Date(date); return tmp.getFullYear()+'-'+tmp.getMonth()+'-'+tmp.getDate()+' '+tmp.toTimeString().split(' ')[0]; } } }); return module.exports; }, editBlog:function(Vue){ var scope=Vue.component('editBlog',{ inherit:true, template:require('../templates/editBlog.html'), ready:function(){ console.log(this.$el); $.extend(this.myBlog,this.blog); //复制原文标签 $("input[data-role=tagsinput], select[multiple][data-role=tagsinput]",$(this.$el)).tagsinput(); var tmp=[]; $.extend(tmp,this.blog.tags); for(var i=0;i<tmp.length;i++) $('select').tagsinput('add',tmp[i]); SuMarkdown({ preview:true, upload:'/upload' }); $('.suPreview').html(marked(this.myBlog.body)); $('pre code').each(function(){hljs.highlightBlock(this);}); }, data:function(){ return { myBlog:{ title:'', body:'', tags:[], author:'' } }; }, methods:{ save:function(){ var tmp=window.location.href.split('/'); $.post(window.location.href,scope.myBlog).done(function(data){ if(data.success) { $.extend(scope.blog,scope.myBlog); scope.content=marked(scope.blog.body); } else{ if(!data.err.message) return; scope.msg=data.err.messgae; scope.display=true; } }).fail(function(){ scope.msg='未知错误,请重试'; scope.display=true; }); }, cancel:function(){ this.currentView='readBlog'; } } }); }, all:function(Vue){ for(var i in module.exports){ if(i==='all') return module.exports; else module.exports[i](Vue); } return module.exports; } };
main.js
javascript
/** * Created by suemi on 14-12-13. */ hljs.initHighlightingOnLoad(); var Vue=require('vue'); //Vue.config.debug=true; require('./directives.js').tag(Vue); require('./components.js').readBlog(Vue).editBlog(Vue); scope=new Vue({ el:'body', data:{ user:{ name:'', avatar:'' }, author:{ avatar:'' description:'', page:'' }, blog:{ title:'', tags:[], body:'', author:'', date:undefined, content:''//html of the blog }, msg:'', display:false, currentView:'readBlog', editable:true, login:false }, methods:{ getAuthor:function(){ var tmp=window.location.href.split('/'); $.get('/getBlog/'+tmp.pop()).done(function(data){ if(data.success){ $.extend(scope.user,data.user); $.extend(scope.author,data.author); $.extend(scope.blog,data.blog); $('#blogBody').html(marked(scope.blog.body)); $('pre code').each(function(){hljs.highlightBlock(this);}); //scope.blog.content=marked(scope.blog.body); if(scope.user.name!=='') scope.login=true; if(scope.user.name!==scope.blog.author) scope.editable=false; } else window.location.href='/404'; }).fail(function(){ window.location.href='/404'; }); }, } }); scope.getAuthor();
后端代码
javascript
/** * * Created by suemi on 14-12-4. */ var Err=usf.module.tool.Err, EndHanler=usf.module.tool.EndHandler, msg=usf.module.msg, Then=usf.lib.then, User=usf.db.def.User, Blog=usf.db.def.Blog; function writeBlog(req,res){ req.session.uname='suemi'; if(req.body.author!==req.session.uname) return res.json({ success:false, err:new Err('用户名不一致') }); (new Blog(req.body)).save(function(err){ if(!err) res.json({ success:true, err:null }); else res.json({ success:false, err: new Err('后台错误,稍后再试') }); }); } function editBlog(req,res){ Then(function(cont){ if(!req.session.uname) return new Err('未登录'); Blog.findById(req.params.blogID,cont); }).then(function(cont,doc){ if(!doc) return new Err(msg.BLOG.blogNone); if(doc.author!==req.session.uname) return new Err('权限不足'); delete req.body._id; doc=tool.union(doc,req.body); doc.save(cont); }).then(function(cont){ res.json({ success:true, err:null }); }).fail(EndHandler); } function getBlog(req,res){ var tmp={}; tmp.user={}; tmp.author={}; Then(function(cont){ Blog.findById(req.params.blogID,cont); }).then(function(cont,doc){ if(!doc) return new Err(msg.BLOG.blogNone); tmp.blog=doc; User.findOne({username:doc.author},cont); }).then(function(cont,doc){ if(!doc) return new Err('数据错误'); tmp.author.name=doc.username; tmp.author=tool.union(tmp.author,doc.profile); if(req.session.uname) User.findOne({username:req.session.uname},cont); else cont(); }).then(function(cont,doc){ if(doc){ tmp.user.name=doc.username; tmp.user=tool.union(tmp.user,doc.profile); } res.json({ success:true, user:tmp.user, author:tmp.author, blog:tmp.blog, err:null }); }).fail(EndHandler); } module.exports= { writeBlog: writeBlog, editBlog: editBlog, getBlog: getBlog };