HJM's Blog

基于vue2、koa2、mongodb的个人博客

2017年4月19日 22:06

博客整体架构
博客终于差不多写完了,虽然还是可能有一堆bug, 不过我迫不及待要写一篇博文来分享了= =

博客前台展示

博客前台

博客后台展示

博客后台
博客后台编辑

项目地址

大家一起来star、fork呀,欢迎提出各种改进意见,我知道肯定还有一堆问题😵
域名还没有备案,后面就变成imhjm.com😆,敬请期待

项目起源

其实我一直在使用hexo搭建的博客,也一直用得还挺顺手,但是它只是个静态站点,当我需要一些定制化的需求,我没办法做更多的改变,并且搭在github page上有时不稳定,而且觉得搭建一个网站然后自己来做各种优化是一件cool的事情,可以尝试各种新技术,可以接触到各个层面上的优化,所以写一个博客的念头就开始了。
其实vue-blog这个项目开了好久了,但是因为断断续续开发,过一段时间就觉得之前的代码写得不好就开始重写,而且也是边写来边学习新技术,vue2全家桶,koa2, webpack2,这些都是之前我没怎么了解的技术,但是至少通过这个博客项目,我对它们也有了新的认识
这里也要感谢一下@Ma63d(@chuckliu),博客搭建初期学习了很多他的kov-blog代码,学习到很多东西,也让自己的后期实现得比较顺利

项目技术细节

就是这个图啦~
博客整体架构
但是当然还有更多细节

client端admin部分

这是博客后台,其实我的想法就是实现一个文本编辑器,有个左边栏,然后右边栏编辑,为了避免复杂,我只用了两个页,一个登录页,然后一个就是带有编辑器的列表页

鉴权

统一使用axios来通信,鉴权使用jwt,login页中登录成功存入token,然后进入编辑页,通过vue-router的beforeEach钩子加入

Axios.defaults.headers.common['Authorization'] = 'Bearer ' + store.state.auth.token;

然后服务端接收时验证token来决定是否鉴权成功,失败时Axios统一拦截,删除store里存取的token, 通过vue-router再回到登录页

状态管理

列表和编辑器分成了两个组件,用vuex统一管理状态,通过action取/存然后mutaion修改状态即可,但是这部分逻辑较多,具体实现就不展开描述了

// editor部分state
const state = {
  articleList: [],
  tagList: [],
  currentArticle: {
    id: -1,
    index: -1,
    content: '',
    title: '',
    tags: [],
    save: true,
    publish: false
  },
  allPage: 1,
  curPage: 1,
  selectTagArr: []
};
// auth部分state
const state = {
  token: sessionStorage.getItem('token')
};

编辑器/markdown

受@chuckliu安利同样使用了simplemde-markdown-editor,然后解析和高亮部分使用了marked.jshighlight.js

client端front部分

event bus

因为考虑到前台需要更快的加载速度,而且逻辑也比较简单,就放弃使用vuex,采用vue event bus来实现非父子组件间的通信

// 在main.js定义全局event bus,不使用vuex管理
var EventBus = new Vue();
Object.defineProperties(Vue.prototype, {
  $eventBus: {
    get: function() {
      return EventBus;
    }
  }
});
// 然后在组件内可以这样使用
this.$eventBus.$on('filterListByTag', this.filterListByTag);
this.$eventBus.$emit('filterListByTag', this.filterListByTag);
this.$eventBus.$off('filterListByTag', this.filterListByTag);

keep-alive

排除article组件,其他则保留组件状态或避免重新渲染。

<transition name="fade" mode="out-in">
    <!-- keep-alive排除article -->
    <keep-alive exclude="article">
        <router-view>
        </router-view>
    </keep-alive>
</transition>

css小坑/小技巧

本来的transition是使用到transform的,因为移动端我的侧边栏是fixed,发现在切换的过程中侧边栏抖动,才发现是因为fixed是会跟随transform的,可以具体参考CSS3 transform对普通元素的N多渲染影响,于是便删去了切换时的移动,保留opacity

在布局上学习了下vue官网的两栏,两栏超出部分都可以滑动,并且不互相影响,这是怎么实现的?其实还是挺简单的,不过用到一个小技巧,设置

// 这样可以实现一个元素100%width以及100%height
div {
 position: absolute;
 top: 0;
 right: 0;
 bottom: 0;
 left: 0;
}

其他就是经典两栏布局和加个overflow-y:auto了

还有一个经典的布局问题,就是foot部分如何实现在页面没什么东西的,固定在页面底部,页面出现滚动条,然后foot部分跟随在页面主体后面

server端

server端直接上koa2了,支持async/await, 异步部分用起来真的很舒服,现在node7+也支持了,所以小伙伴们赶紧用吧~

Json Web Token

再讲讲jwt鉴权吧
其实并不复杂
在验证登录时,后端取出数据库已有的密码验证,成功则用配置好的secret生成一个signed jwt,通俗点说你将它加密了再传给客户端

const token = jwt.sign({
        uid: user._id,
        name: user.name,
        exp: Math.floor(Date.now() / 1000) + 24 * 60 * 60 //1 hours
      }, config.jwt.secret);

http无状态,所以客户端需要存下这个token, 然后之后请求时带上这个token服务端验证通过后返回资源即可

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');
  }

如果想更深层次地了解jwt是什么,可以自行去搜索网上的文章

RESTful API

Representational State Transfer简单来理解就是每个URL都是资源,通过不同的http method去操作这些资源就行,设计上就可以这样,然后统一加上prefix:'/api'

router.get('/articles', verify, $.getAllArticles) //获取所有文章
    .post('/articles', verify, $.createArticle) //创建文章
    .patch('/articles/:id', verify, $.modifyArticle) //修改特定文章
    .get('/articles/:id', $.getArticle)  //获取特定文章
    .delete('/articles/:id', verify, $.deleteArticle) //删除特定文章

webpack相关

本来打算直接使用vue-cli,这部分就非常省心了,vue-cli这个脚手架做得确实很友好,我觉得vue比较好上手开发的一部分原因也是因为它吧,不过因为这个博客项目就是学习的过程,不想这么轻易地逃过这部分的学习😸,直接上webpack2,然后参考vue-cli和webpack官网来写,觉得也学习到很多东西,并且因为front和admin是分开的,所以也实现了多页配置

开发环境

hot-reload是必备的

// ...
entry: {
    'modules/admin': [
      'webpack-hot-middleware/client?reload=true',
      CLIENT_FOLDER + '/src/modules/admin/main'
    ],
    'modules/front': [
      'webpack-hot-middleware/client?reload=true',
      CLIENT_FOLDER + '/src/modules/front/main'
    ]
  },
// ...
plugins: [
    new webpack.HotModuleReplacementPlugin()
// ..

用CleanWebpackPlugin清空下目录,HtmlWebpackPlugin自动生成html,然后该写的loader

生产环境

生产环境下改动就大了,先得删去hot-reload和devtool部分,然后提取css

//...
styl: ExtractTextPlugin.extract({
      use: [{
        loader: 'css-loader',
        options: {
          minimize: true,
          sourceMap: true
        }
      }, {
        loader: 'stylus-loader',
        options: {
          sourceMap: true
        }
      }],
      fallback: 'vue-style-loader'
    }),
//...

UglifyJsPlugin压缩代码,然后提取公有代码vendor、manifest,manifest用来防止vendor的hash在vendor部分没有变化时不被修改

// ...
// 分别提取vendor、manifest
    new webpack.optimize.CommonsChunkPlugin({
      name: 'modules/vendor_admin',
      chunks: ['modules/admin'],
      minChunks: function(module, count) {
        return (
          module.resource &&
          /\.js$/.test(module.resource) &&
          module.resource.indexOf(
            join(__dirname, './node_modules')
          ) === 0
        )
      }
    }),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'modules/manifest_admin',
      chunks: ['modules/vendor_admin']
    }),
//...

然后用CopyWebpackPlugin将static拷进dist就好了

与koa配合

这部分就可以看vue-cli是怎么实现的了,vue-cli使用的是express通过webpack-dev-middleware和webpack-hot-middleware实现开发模式下热重载,当然koa也可以

  const koaWebpack = require('koa-webpack');
  const webpack = require('webpack');
  const webpackConfig = require('../webpack.config');
  let compiler = webpack(webpackConfig);
  app.use(koaWebpack({
    compiler: compiler,
    dev: {
      //noInfo: true,
      stats: {
        colors: true,
        chunks: false
      },
      publicPath: webpackConfig.output.publicPath,
    }
  }));

顺便讲到项目中用的是history模式,这里也需要后端配合

app.use(convert(historyApiFallback({
  verbose: process.env.NODE_ENV == 'production' ? false : true,
  index: '/front.html',
  rewrites: [
    { from: /^\/admin$/, to: '/admin.html' },
    { from: /^\/admin\/login/, to: '/admin.html' },
    { from: /^\/$/, to: '/front.html' },
    { from: /^\/article/, to: '/front.html' }
  ]
})))

但是这里有个坑是要注意书写use的顺序,放在router api后面就行,这里需要理解下koa的洋葱结构
koa

webpack优化

我觉得这部分除了区分开发环境和生产环境以外,还要注意到对webpack打包的过程和模块的分析,不要吝啬webpack打包的输出

# 显示颜色,耗时长的都有颜色区分 --colors
# 可以看到每一步的耗时 --profile
# 显示模块 --display-modules
# 并且按size大小排序  --sort-modules-by size
webpack.config.js --colors --profile --display-modules --sort-modules-by size

这样就能看出打包什么耗去你大量的时间,占据了大量空间,还有是否是重复打包

还有一个直观而又酷炫的方式https://github.com/alexkuz/webpack-chart

直接看占比就能看出哪部分需要你去优化了

我通过alias和external优化了下,效果还是挺明显的,最后本地生产环境打包大概14-20s,也还可以接受,但是估计还有优化的空间,其实我也试过happypack和并行的uglify,不过发现没什么效果= =

//...
resolve: {
    extensions: ['.js', '.vue', '.json'],
    modules: [join(__dirname, './node_modules')],
    alias: {
      'vue$': 'vue/dist/vue.esm.js',
      'vuex$': 'vuex/dist/vuex.esm.js',
      'vue-router$': 'vue-router/dist/vue-router.esm.js',
      'simplemde$': 'simplemde/dist/simplemde.min.js',
      'highlight.js$': 'highlight.js/lib/highlight.js',
      'fastclick': 'fastclick/lib/fastclick.js',
      'lib': resolve(__dirname, './client/src/lib'),
      'api': resolve(__dirname, './client/src/api'),
      'publicComponents': resolve(__dirname, './client/src/components'),
    }
  },
//...
// html template引入simplemde cdn
externals: {
    'simplemde': 'SimpleMDE'
 },
//...

还有一些缓存优化的问题,就是js/css的hash问题,js chunkhash然后提取css使用contenthash,为了避免改动vendor的hash,commonChunk额外提取出manifest(extract the webpack bootstrap logic into a separate file),这样当vendor没有被修改的时候,重新运行webpack不会再生产新的hash,变动的只有manifest,具体可以看https://webpack.js.org/plugins/commons-chunk-plugin/#manifest-file

关于多页配置

其实只需要根据模块划分然后多写几行配置就行,要做类似脚手架的话可以使用glob这些工具
具体看代码吧,会生成以下目录

dist
 |---css
     |---modules
             |---admin.xxx.css
             |---front.xxx.css
 |---fonts
 |---modules
     |---admin.xxx.min.js
     |---front.xxx.min.js
     |---vendor_admin.xxx.min.js
     |---vendor_front.xxx.min.js
     |---manifest_admin.xxx.min.js
     |---manifest_front.xxx.min.js
 |---static
 |---admin.html
 |---front.html

线上部署及优化

pm2

表示之前我在玩耍node服务的时候都是使用screen命令的,pm2确实很赞,监控/日志管理这些都很完善,日志方面我还使用了pm2-logrotate

pm2 install pm2-logrotate
pm2 set pm2-logrotate:retain 100 //控制日志数量
pm2 set pm2-logrotate:size 1M //控制日志切割大小

nginx

nginx基本是上线必备,应该也不用多说了,开启gzip效果确实很显著,本来前台的vendor_front从227k直接被压缩到84k,简直cool!😆

最后

谢谢阅读~
欢迎follow我哈哈https://github.com/BUPT-HJM
看到这里,不star不行了😋
https://github.com/BUPT-HJM/vue-blog
欢迎继续观光我的博客~

欢迎关注