Vue + Node + Mongodb 开发一个完整博客流程
前言
前段时间刚把自己的个人网站写完, 于是这段时间因为事情不是太多,便整理了一下,写了个简易版的博客系统
服务端用的是 koa2框架 进行开发
技术栈
Vue + vuex + element-ui + webpack + nodeJs + koa2 + mongodb
目录结构讲解
- build – webpack的配置文件
- code – 放置代码文件
- config – 项目参数配置的文件
- logs – 日志打印文件
- node_modules – 项目依赖模块
- public – 项目静态文件的入口 例如: public下的 demo.html文件, 可通过 localhost:3000/demo.html 访问
- static – 静态资源文件
- .babelrc – babel编译
- postcss.config.js – css后处理器配置
build 文件讲解
- build.js – 执行webpack编译任务, 还有打包动画 等等
- get-less-variables.js – 解析less文件, 赋值less全局变量
- style-loader.js – 样式loader配置
- vue-config.js – vue配置
- webpack.base.conf.js – webpack 基本通用配置
- webpack.dev.conf.js – webpack 开发环境配置
- webpack.prod.conf.js – webpack 生产环境配置
code 文件
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
src - 代码区域 1. components - 组件 2. filters - 过滤器 3. font - 字体/字体图标 4. images - 图片 5. router - 路由 6. store - vuex状态管理 7. styles - 样式表 8. utils - 请求封装 9. views - 页面模块 10. App.vue - app组件 11. custom-components.js - 自定义组件导出 12. main.js - 入口JS index.html - webpack 模板文件 |
2.client – web端界面源码
0 1 |
跟后台管理界面的结构基本一样 |
0 1 2 3 4 5 6 7 8 |
1. controller: 所有接口逻辑代码 2. middleware: 所有的中间件 3. models: 数据库model 4. router: 路由/接口 5. app.js: 入口 6. config.js: 配置文件 7. index.js: babel编译 8. mongodb.js: mongodb配置 |
config – 项目参数配置的文件
logs – 日志文件
public – 项目静态文件的入口
static – 静态资源文件
.babelrc – babel编译
postcss.config.js – css后处理器配置
后台管理
开发中用的一些依赖模块
- vue/vue-router/vuex – Vue全家桶
- axios – 一个现在主流并且很好用的请求库 支持Promise
- qs – 用于解决axios POST请求参数的问题
- element-ui – 饿了么出品的vue2.0 pc UI框架
- babel-polyfill – 用于实现浏览器不支持原生功能的代码
- highlight.js / marked- 两者搭配实现Markdown的常用语法
- js-md5 – 用于登陆时加密
- nprogress – 顶部加载条
components
这个文件夹一般放入常用的组件, 比如 Loading组件等等
views
所有模块页面
store
vuex用来统一管理公用属性, 和统一管理接口
1. 登陆
登陆是采用 jsonwebtoken方案 来实现整个流程的
jwt.sign(payload, secretOrPrivateKey, [options, callback])
生成TOKENjwt.verify(token,secretOrPublicKey,[options,callback])
验证TOKEN- 获取用户的账号密码
- 通过
jwt.sign
方法来生成token
01234567891011121314151617181920//server端import jwt from 'jsonwebtoken'let data = { //用户信息username,roles,...}let payload = { // 可以把常用信息存进去id: data.userId, //用户IDusername: data.username, // 用户名roles: data.roles // 用户权限},secret = 'admin_token'// 通过调用 sign 方法, 把 **用户信息**、**密钥** 生成token,并设置过期时间let token = jwt.sign(payload, secret, {expiresIn: '24h'})// 存入cookie发送给前台ctx.cookies.set('Token-Auth', token, {httpOnly: false }) - 每次请求数据的时候通过
jwt.verify
检测token的合法性jwt.verify(token, secret)
2. 权限
通过不同的权限来动态修改路由表
- 通过 vue的 钩子函数 beforeEach 来控制并展示哪些路由, 以及判断是否需要登陆
0123456789101112131415161718192021222324252627282930313233343536373839404142import store from '../store'import { getToken } from 'src/utils/auth'import { router } from './index'import NProgress from 'nprogress' // Progress 进度条import 'nprogress/nprogress.css' // Progress 进度条样式const whiteList = ['/login'];router.beforeEach((to, from, next) => {NProgress.start()if (getToken()) { //存在tokenif (to.path === '/login') { //当前页是登录直接跳过进入主页next('/')}else{if (!store.state.user.roles) { //拉取用户信息store.dispatch('getUserInfo').then( res => {let roles = res.data.rolesstore.dispatch('setRoutes', {roles}).then( () => { //根据权限动态添加路由router.addRoutes(store.state.permission.addRouters)next({ ...to }) //hash模式 确保路由加载完成})})}else{next()}}}else{if (whiteList.indexOf(to.path) >= 0) { //是否在白名单内,不在的话直接跳转登录页next()}else{next('/login')}}})router.afterEach((to, from) => {document.title = to.nameNProgress.done()})export default router- 通过调用
getUserInfo
方法传入 token 获取用户信息, 后台直接解析 token 获取里面的 信息 返回给前台
01234567891011getUserInfo ({state, commit}) {return new Promise( (resolve, reject) => {axios.get('user/info',{token: state.token}).then( res => {commit('SET_USERINFO', res.data)resolve(res)}).catch( err => {reject(err)})})}
- 通过调用
- 通过调用
setRoutes
方法 动态生成路由
012345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152import { constantRouterMap, asyncRouterMap } from 'src/router'const hasPermission = (roles, route) => {if (route.meta && route.meta.role) {return roles.some(role => route.meta.role.indexOf(role) >= 0)} else {return true}}const filterAsyncRouter = (asyncRouterMap, roles) => {const accessedRouters = asyncRouterMap.filter(route => {if (hasPermission(roles, route)) {if (route.children && route.children.length) {route.children = filterAsyncRouter(route.children, roles)}return true}return false})return accessedRouters}const permission = {state: {routes: constantRouterMap.concat(asyncRouterMap),addRouters: []},mutations: {SETROUTES(state, routers) {state.addRouters = routers;state.routes = constantRouterMap.concat(routers);}},actions: {setRoutes({ commit }, info) {return new Promise( (resolve, reject) => {let {roles} = info;let accessedRouters = [];if (roles.indexOf('admin') >= 0) {accessedRouters = asyncRouterMap;}else{accessedRouters = filterAsyncRouter(asyncRouterMap, roles)}commit('SETROUTES', accessedRouters)resolve()})}}}export default permission
axios 请求封装, 统一对请求进行管理
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
import axios from 'axios' import qs from 'qs' import { Message } from 'element-ui' axios.defaults.withCredentials = true // 发送时 axios.interceptors.request.use(config => { // 开始(LLoading动画..) return config }, err => { return Promise.reject(err) }) // 响应时 axios.interceptors.response.use(response => response, err => Promise.resolve(err.response)) // 检查状态码 function checkStatus(res) { // 结束(结束动画..) if (res.status === 200 || res.status === 304) { return res.data } return { code: 0, msg: res.data.msg || res.statusText, data: res.statusText } return res } // 检查CODE值 function checkCode(res) { if (res.code === 0) { Message({ message: res.msg, type: 'error', duration: 2 * 1000 }) throw new Error(res.msg) } return res } const prefix = '/admin_demo_api/' export default { get(url, params) { if (!url) return return axios({ method: 'get', url: prefix + url, params, timeout: 30000 }).then(checkStatus).then(checkCode) }, post(url, data) { if (!url) return return axios({ method: 'post', url: prefix + url, data: qs.stringify(data), timeout: 30000 }).then(checkStatus).then(checkCode) }, postFile(url, data) { if (!url) return return axios({ method: 'post', url: prefix + url, data }).then(checkStatus).then(checkCode) } } |
面包屑 / 标签路径
- 通过检测路由来把当前路径转换成面包屑
- 把访问过的路径储存在本地,记录下来,通过标签直接访问
01234567891011121314151617181920// 面包屑getBreadcrumb() {let matched = this.$route.matched.filter(item => item.name);let first = matched[0],second = matched[1];if (first && first.name !== '首页' && first.name !== '') {matched = [{name: '首页', path: '/'}].concat(matched);}if (second && second.name === '首页') {this.levelList = [second];}else{this.levelList = matched;}}// 检测路由变化watch: {$route() {this.getBreadcrumb();}}
上面介绍了几个主要以及必备的后台管理功能,其余的功能模块 按照需求增加就好
前台
前台展示的页面跟后台管理界面差不多, 也是用vue+webpack搭建,基本的结构都差不多,具体代码实现的可以直接在github下载便行
server端
权限
主要是通过 jsonwebtoken
的verify方法检测cookie 里面的token 验证它的合法性
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
import jwt from 'jsonwebtoken' import conf from '../../config' export default () => { return async (ctx, next) => { if ( conf.auth.blackList.some(v => ctx.path.indexOf(v) >= 0) ) { // 检测是否在黑名单内 let token = ctx.cookies.get(conf.auth.tokenKey); try { jwt.verify(token, conf.auth.admin_secret); }catch (e) { if ('TokenExpiredError' === e.name) { ctx.sendError('token已过期, 请重新登录!'); ctx.throw(401, 'token expired,请及时本地保存数据!'); } ctx.sendError('token验证失败, 请重新登录!'); ctx.throw(401, 'invalid token'); } console.log("鉴权成功"); } await next(); } } |
日志
日志是采用 log4js
来进行管理的,
log4js
算 nodeJs 常用的日志处理模块,用起来额也比较简单
- log4js 的日志分为九个等级,各个级别的名字和权重如下:
图
- 设置 Logger 实例的类型
logger = log4js.getLogger('cheese')
- 通过
Appender
来控制文件的 名字、路径、类型 - 配置到
log4js.configure
- 便可通过 logger 上的打印方法 来输出日志了
logger.info(JSON.stringify(currTime:
当前时间为${Date.now()}s))
0123456789101112131415161718192021222324//指定要记录的日志分类let appenders = {}appenders.all = {type: 'dateFile', //日志文件类型,可以使用日期作为文件名的占位符filename: `${dir}/all/`, //日志文件名,可以设置相对路径或绝对路径pattern: 'task-yyyy-MM-dd.log', //占位符,紧跟在filename后面alwaysIncludePattern: true //是否总是有后缀名}let logConfig = {appenders,/*** 指定日志的默认配置项* 如果 log4js.getLogger 中没有指定,默认为 cheese 日志的配置项*/categories: {default: {appenders: Object.keys(appenders),level: logLevel}}}log4js.configure(logConfig)
定制书写规范(API)
- 设计思路
当应用程序启动时候,读取指定目录下的 js 文件,以文件名作为属性名,挂载在实例 app 上,然后把文件中的接口函数,扩展到文件对象上
0123456789101112131415161718192021222324//other.jsconst path = require('path');module.exports = {async markdown_upload_img (ctx, next) {console.log('----------------添加图片 markdown_upload_img-----------------------');let opts = {path: path.resolve(__dirname, '../../../../public')}let result = await ctx.uploadFile(ctx, opts)ctx.send(result)},async del_markdown_upload_img (ctx, next) {console.log('----------------删除图片 del_markdown_upload_img-----------------------');let id = ctx.request.query.idtry {ctx.remove(musicModel, {_id: id})ctx.send()}catch(e){ctx.sendError(e)}// console.log(id)}}
读取出来的便是以下形式:
app.controller.admin.other.markdown_upload_img
便能读取到markdown_upload_img
方法
01234567async markdown_upload_img (ctx, next) {console.log('----------------添加图片 markdown_upload_img-----------------------');let opts = {path: path.resolve(__dirname, '../../../../public')}let result = await ctx.uploadFile(ctx, opts)ctx.send(result)}
在把该形式的方法 赋值过去就行
router.post('/markdown_upload_img', app.controller.admin.other.markdown_upload_img)
通过 mongoose
链接 mongodb
0 1 2 3 4 5 6 7 8 9 10 11 12 13 |
import mongoose from 'mongoose' import conf from './config' // const DB_URL = `mongodb://${conf.mongodb.address}/${conf.mongodb.db}` const DB_URL = `mongodb://${conf.mongodb.username}:${conf.mongodb.pwd}@${conf.mongodb.address}/${conf.mongodb.db}`; // 账号登陆 mongoose.Promise = global.Promise mongoose.connect(DB_URL, { useMongoClient: true }, err => { if (err) { console.log("数据库连接失败!") }else{ console.log("数据库连接成功!") } }) export default mongoose |
封装返回的send函数
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
export default () => { let render = ctx => { return (json, msg) => { ctx.set("Content-Type", "application/json"); ctx.body = JSON.stringify({ code: 1, data: json || {}, msg: msg || 'success' }); } } let renderError = ctx => { return msg => { ctx.set("Content-Type", "application/json"); ctx.body = JSON.stringify({ code: 0, data: {}, msg: msg.toString() }); } } return async (ctx, next) => { ctx.send = render(ctx); ctx.sendError = renderError(ctx); await next() } } |
通过 koa-static
管理静态文件入口
注意事项:
cnpm run server
启动服务器- 启动时,记得启动mongodb数据库,账号密码 可以在 server/config.js 文件下进行配置
db.createUser({user:"cd",pwd:"123456",roles:[{role:"readWrite",db:'test'}]})
(mongodb 注册用户)cnpm run dev:admin
启动后台管理界面- 登录后台管理界面录制数据
- 登录后台管理时需要在数据库 创建 users 集合注册一个账号进行登录
0123456789db.users.insert({"name" : "cd","pwd" : "e10adc3949ba59abbe56e057f20f883e","username" : "admin","roles" : ["admin"]})// 账号: admin 密码: 123456 cnpm run dev:client
启动前台页面
参考文章