项目介绍
几乎每个程序员都有一个博客梦。抱着学习前后端技术的心态花一个月左右的时间来完成这个个人项目。 这是一个使用vue2做前端框架,koa2做后端,mongodb数据库,搭建的单页面应用。网站的功能展示我自己的技术分享,和后台编辑书写博客,支持markdown语法。本次项目大量借鉴了BUPT-HJM大神的博客,从ui到代码细节,借鉴了很多。真的很感谢开源的大神。代码地址github,个人网站地址
项目搭建
因为前端选型使用vue,想用vue-cli一路平推省心省力来着,但是为了更加深入学习webpack,决定手动从头写起。 关于webpack的实现细节我会单独写一篇博客,因为内容实在太多,在这里只说明实现了哪些功能。
开发环境
现在前端开发还是很看重开发体验的,我之前在上上家公司前端开发时,开发环境一塌糊涂,以为业内都是这样(后来才知道是前端组长的不负责任)。上家公司的开发环境真的很舒服,不用过多的考虑兼容性,代码风格强制统一,热更新什么的一应俱全。 所以webpack构建的时候就加进去了热更新,使用postcss超前使用css新特性和自动前缀。 需要特别说明的是webpack热更新依赖的还是node,我们都知道webpack-dev-server这个插件是用了express框架作为热更新服务的,但是考虑到本次开发时的后台也是node,只不过用的是koa2,那么完全可以在开发过程中只启动一个node服务,来满足前端和后端的node需求。所以我没有使用webpack-dev-server这个webpack插件,而是自己实现。具体代码会在webpack那篇博客中细讲。
生产环境
生产环境的构建中,也是很常规的一些实现。例如代码压缩,css加前缀,资源文件的路径处理,图片压缩。开启多线程的打包功能,这里吐槽一句 刚开始打包时间是7秒左右,按照网上的打包优化建议一顿花里胡哨的折腾,结果打包时间涨到9秒了。。。 使用了 webpack引以为傲的很重要的功能,将vue文件拆分成不同的Chunk,配合vue-router按需引入
// vue-router
const HelloWorld = () => import('../components/HelloWorld.vue');
复制代码
打包的时候把js拆分,按需引入,好处在于js不至于体积过大。需要说明一下这种语法是es7的新特性,配置babel的时候坑很多。
总结
webpack更新换代太快了,很多时候没有中文文档,只能一知半解的查github, 但是随着webpack越来越成熟稳定,语法也固定下来,唯一需要我们注意的地方是眼花缭乱的插件,根据自己的需要合理添加。
前端细节
项目开发的时候前端是最熟悉的一部分,vue作为前端框架,对于这种中小型项目来说很合适。写博客网站也是为了学习,干脆用了vue全家桶。 vue-router,vuex,都在用。 因为我的审美和ui设计实在有限,大量借鉴(抄袭)BUPT-HJM。
首页和博客详情页
首页包括一个header组件,一个信息展示组件,博客列表页,和分页组件。稍微有点复杂的组件只有信息展示组件和分页组件。 信息展示组件用到了vue的插槽,slot ,默认是自我介绍。关于插槽啰嗦几句:很多初学vue的同学对于插槽的使用和理解很费解,对于vue中的props都会用,作为父组件给组件传值用,而插槽也是如此,只不过props传入的是data数据,而插槽传入的是html 。 分页组件其实是和vuex结合一起写的。vuex里面的state记录分页的当前页数,点击改变页数触发vuex里面的mutations去加载分页数据。 博客详情页数据的加载来源也是和vuex结合,从state里面获取文章id,去get请求博客细节。博客详情页有两个问题, 1.从后端取出来的数据是markdown原始数据,需要解析。 从网上找了一个很好用的解析插件marked,引入,稍微封装一下就可以用了,配置了一下代码块的样式。如果有兴趣可以去看看代码。 2.利用vue的 this.$nextTick 等dom元素渲染完成后,抽离博客中h1到h6的标签。来组合成菜单栏。
Array.from(this.$refs.post.querySelectorAll("h1,h2,h3,h4,h5,h6")).forEach(
(item, index) => {
item.id = item.localName + "-" + index
this.category.push({
tagName: item.localName,
text: item.innerText,
href: "#" + item.localName + "-" + index
})
}
);
复制代码
以上是核心代码,也是参考了大神的源码。重新组合成一个菜单栏,这时候就用到了之前信息展示组件的插槽功能。 css直接用了postcss,postcss好处在于它的语法和scss类似,而且丰富的插件可以使得使用很多超前的css语法,虽然我用的很少。关于postcss的插件和webpack配置,也不复杂,很常规的一些。细节见代码。本来不打算对移动端博客作兼容的,考虑到大家普遍还是手机浏览博客频率更高。所以对移动端做下兼容处理,而且postcss对于媒体查询的语法很友好
@custom-media --small-viewport (max-width: 850px);
@media (--small-viewport) {
.ArticlePage .articleDate{
margin-left: 0;
width: 100%;
}
.ArticlePage .articleDate .time{
margin-left: 0;
}
.abstract {
width: 100%;
}
}
复制代码
以上是对移动端做的媒体查询
以上是移动端和pc端不同的ui展示,也是大量借鉴了别人的博客
登陆页面和博客编辑页面
登陆鉴权
因为这个博客只是单纯自己用,所以很简单的写了一个登陆页面,用的是elementUI, 这里涉及到一个前端鉴权的功能,登陆成功时,会获取到一个token值有效期24小时。前端拿到token后,写入请求头中,后端某些(例如进去admin页面)请求需要验证token。
// 来自 vuex/mutations.js 作用是将获取到的token存入storage
[types.CREATE_TOKEN]: (state, res) => {
state.token = res.token
state.userInfo = res.name
sessionStorage.setItem('vue-blog-token', res.token)
sessionStorage.setItem('vue-blog-userName', res.name)
},
// 来自 封装的 js/http.js 作用是将token写入headers头部
const createToken = ()=>{
if(store.state.token){
// 全局设定header的token验证,注意Bearer后有个空格
axios.defaults.headers.common['Authorization'] = 'Bearer ' + store.state.token
}
}
// axios拦截返回,拦截token过期
axios.interceptors.response.use(function (response) {
return response
}, function (error) {
if (error.response.data.error.indexOf('token') !== -1) {
store.commit('DELETE_TOKEN')
}
return Promise.reject(error)
});
复制代码
以上是前端处理鉴权的核心代码。
博客编辑功能
博客后台是两部分构成1. 博客列表页,包括公开和没有公开的博客 ,2 markdown 编辑器。
这里寻找markdown编辑器还是费了一番周折,开始使用的是 vue-simplemde 开发环境下没有问题,但是打包生成以后报错webpack的babel-loader编译通不过,折腾好久我把这个npm包中涉及到编辑器的vue文件抽出来,vue-simp.vue文件直接引用,反而可以用了,真的是玄学。第二个难点是博客编辑功能的开发,涉及到新建,更新,删除,是否发布,也是bug最多的地方。 博客后台截图
前端总结
vue和vuex用的更加熟练了,webpack不敢说熟练,但是起码各种资源文件的处理和配置比以前得心应手。多少也锻炼了自己前端封装组件的能力。我们遇到的问题,别人也会遇到。多用谷歌和github总能解决的。 这里简单聊一下我对前端组件化的一些感想,我总共接触写过三种前端框架,vue,react,小程序。逐渐接受了一种组件化就是容器组件和展示组件。容器组件是指参与项目逻辑和响应用户指令的组件。例如分页组件,内部处理复杂的分页逻辑。展示组件是指只负责渲染的组件,例如博客列表组件,只接受数据,并且渲染。接受到用户指令后,例如父子组件传值或者vuex,redux等状态管理工具来传递给容器组件处理。往往展示组件可以被多次使用,抽取为公共组件。但是根据项目的不同,组件抽取的颗粒程度不同,组件的作用也不同。
后端细节
这是我第一次完整的使用koa2,刚开始不免有些无从下手,我还是相信程序员的学习和进步都是从学习大神代码开始的,参考大神代码,将服务端代码分为以下部分:
入口文件
因为在koa中使用es6和部分es7新特性,所以在入口文件中加入babel编译
// 来自server/start.js
require('babel-register')({
presets: [ 'env' ]
})
// Import the rest of our application.
module.exports = require('./index.js')
复制代码
就是很粗浅的配置了一下,所以真正的入口文件是 index.js, 在webpack那讲过开发的时候,前后端使用同一个node服务。判断是否开发环境,然后直接执行函数,将koa作为参数传入,去启动webpack的服务,具体会在关于webpack博客中细讲。
const app = new koa()
const isDeve = process.env.NODE_ENV === 'development'
if (isDeve) {
require('../build/server')(app)
}
复制代码
require引入的文件是webpack配置文件,这也是webpack和koa结合最核心的代码。至此入口文件中都是很常规的app.use()和连接mongoodb了。 有一个被我忽视的地方就是node怎么指向打包完成后的主页,开发模式中,目标文件夹中是看不到编译后的文件的,实时编译后的文件都保存到了内存中,我的前端代码被webpack 会使用inline mode(内联模式)。这种模式在 bundle 中注入客户端。而在生产环境中需要指明主页。
import serve from 'koa-static'
const home = serve(path.join(__dirname+"../../dist/"))
app.use(home)
复制代码
dist文件夹就是打包生成的前端代码,ko2会默认指定index.html为主页。
路由
用koa写接口koa-router官方推荐的,使用也简单,这里也不过多说明,koa-router和controllers结合一起使用,
import * as $ from '../../controllers/articles_controller'
import verify from '../../middleware/verify'
export default async(router) => {
router.get('/getArticles',$.getAllPublishArticles)
router.post('/saveArticle',verify,$.saveArticle)
router.get('/articleDetails',$.articleDetails)
router.post('/changeArticle',verify,$.changeArticle)
router.post('/deletaArticle',verify,$.deletaArticle)
}
复制代码
简单说明一下以上代码,实际的业务代码全部放在comtrollers下,路由只负责调取不同的comtroller,verify是鉴权,通过koa中间件,来实现接口是否现需要鉴权才能调用。这样就可以使得业务逻辑和路由的剥离,相比我之前写在一起要好的多。
controllers和middleware
后端代码最核心的部分就是controller,包括网站数据的存储,修改,删除,管理员登陆。虽然是最核心的代码,却也是最简单。就是增删改查。代码就不放了,感兴趣的大佬可以去github上自行查看github。
关于鉴权的功能,是放在中间件来实现的,同时在中间件还使用了koa-bodyparser,作用获取post提交数据。对于koa来讲这个中间件是使用率最高的了。
import jwt from 'jsonwebtoken'
import config from '../serverConfig/index'
export default async(ctx, next) => {
// console.log(ctx.get('Authorization'))
const authorization = ctx.get('Authorization')
if (authorization === '') {
ctx.throw(401, 'no token detected in http header \'Authorization\'')
}
const token = authorization.split(' ')[1]
let tokenContent
try {
tokenContent = await jwt.verify(token, config.jwt.secret)
} catch (err) {
if ('TokenExpiredError' === err.name) {
ctx.throw(401, 'token expired,请及时本地保存数据!')
}
ctx.throw(401, 'invalid token')
}
console.log('鉴权成功')
await next()
};
复制代码
以上代码是写在中间件的鉴权代码,用的是jwt,使用也很简单,功能就是检查header是否携带token,和token是否过期。关于koa中间件的高阶使用和源码之类的请原谅我的弱鸡,以后会慢慢补上来的。
mongodb
单独讲一下mongodb吧,关于服务器部署mongodb的文章一抓一大把这里也不再说明,代码中mongodb的操作使用的也是常规的mongoose,我作为非科班出身的前端程序员,数据库设计实在是短板,只能参考别人的设计。本网站的数据库设计分两个collection,一个是博客的collection,另一个是用户的collection。
在业务逻辑代码中,需要频繁的查询修改。好在mongoose的语法也足够简单。
export async function articleDetails(ctx) {
const articleID = ctx.query.articleID
const articleDetail = await Article.findOne({
_id: articleID
}).catch(err => {
ctx.throw(500, ERRMESG)
});
ctx.body = {
success: true,
articleDetail,
};
}
复制代码
上面代码中用到的findOne是mongoose查询语句的一种,参数是查询条件。其他的查询语句遇到了去官网查一下就行。
后端总结
对于koa的学习算是入门了,但是远远没有达到一个合格的标准,不是装逼,而是写的越多,发现自己越弱。中间件只会简单使用,koa对比express的优点在哪也说不出。要学的东西还很多。
上线过程
网站的服务器是从阿里云购买,域名sxywzg是我和女朋友姓名的缩写,(喂狗粮支线任务达成)。我购买服务器日期是十月末,买了没几天阿里云推送告诉我服务器双十一打折。。。在服务器上安装node,数据库,配置乱七八的东西不细说了。网上也有很多教程,重点说下nginx配置。
upstream vue{
server 127.0.0.1:3000;
}
server {
listen 80;
server_name www.sxywzg.cn;
location / {
proxy_set_header X-real-ip $remote_addr;
proxy_set_header X-Forward-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-Nginx-Proxy true;
proxy_pass http://vue;
proxy_redirect off;
}
}
复制代码
网站端口都是80,将端口3000转发到80上,server_name就是网站域名。就这么简单的配置我折腾好久,还是在同事的帮助下整出来。nginx的安装也很简单,自行搜索教程。
pm2
pm2我之前只是简单的让node服务在服务器跑起来的工具而已,其实远不止那么简单。我的代码是放在github上面的,每次更新代码都会提交到上面,pm2可以帮助我从git仓库拉取代码,并且重启服务。
{
"apps": [
{
"name": "vue-blog",
"script": "server/start.js",
"error_file": "server/logs/app-err.log",
"out_file": "server/logs/app-out.log",
"env_dev": {
"NODE_ENV": "development"
},
"env_production": {
"NODE_ENV": "production"
}
}
],
"deploy": {
"production": {
// 我服务器用户名
"user": "zhigang",
///服务器地址
"host": ["47.95.***.***"],
"port": "22",
//代码分支
"ref": "origin/master",
//代码仓库
"repo": "[email protected]:463755120/vue-blog.git",
//服务器存放代码地址
"path": "/home/zhigang/vue-blog",
"ssh_options": "StrictHostKeyChecking=no",
// 每次执行的命令
"post-deploy":"cnpm i && npm run build && pm2 start pm2.json --env production",
"env": {
"NODE_ENV": "production"
}
}
}
}
复制代码
以上是pm2.json配置,作用是一键上线代码,不用我们手动把代码在服务器中拉下来,然后跑命令。pm2帮你做这些累活。刚开始用这个功能,惊为天人,还有这么善解人意的工具,但是不知道是我的原因还是pm2的缺陷,项目第一次上线时pm2执行cnpm i下载的包不完整,我只能在服务器里手动 下载,以后就好使了。所以我需要更新网站时,把代码提交到git仓库,执行一下 npm run pm2 省时省力。
写在最后
写博客网站的原因有两个,第一个是为了检验自己能不能写一个完整的并且能上线的项目,第二个是为以后方便自己写博客。如果不总结自己的技术,用不了多久就会忘得差不多。上家公司的前端组长给我说忘了你是前端开发,你只是解决问题的程序员。我深以为然,公司可以把我们划分为前端开发,后端开发。但是我觉得从心里需要知道程序员是解决问题的人,不能因为是前端开发就只局限于自己的一亩三分地。不要满足在别人配置好的环境中开发。了解一些项目架构和上线流程,服务器配置不是坏事。2018年马上过去,今年也承受了我这个年纪不该承受的裁员之苦。明年还要学基本的算法,设计模式,typescript和react。以后的博客也会不定期的增加我的学习笔记。希望大佬们多提意见,联系我的方式很简单,首页有我的知乎账号,发私信即可。