前段時間剛把本身的我的網站寫完, 因而這段時間由於事情不是太多,便整理了一下,寫了個簡易版的博客系統
服務端用的是 koa2框架 進行開發css
Vue + vuex + element-ui + webpack + nodeJs + koa2 + mongodbhtml
1.admin - 後臺管理界面源碼
vue
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端界面源碼node
跟後臺管理界面的結構基本同樣
3.server - 服務端源碼
webpack
1. controller: 全部接口邏輯代碼 2. middleware: 全部的中間件 3. models: 數據庫model 4. router: 路由/接口 5. app.js: 入口 6. config.js: 配置文件 7. index.js: babel編譯 8. mongodb.js: mongodb配置
這個文件夾通常放入經常使用的組件, 好比 Loading組件等等ios
全部模塊頁面git
vuex用來統一管理公用屬性, 和統一管理接口github
登錄是採用 jsonwebtoken方案 來實現整個流程的web
jwt.sign(payload, secretOrPrivateKey, [options, callback])
生成TOKENjwt.verify(token,secretOrPublicKey,[options,callback])
驗證TOKEN經過 jwt.sign
方法來生成tokenvue-router
//server端 import jwt from 'jsonwebtoken' let data = { //用戶信息 username, roles, ... } let payload = { // 能夠把經常使用信息存進去 id: data.userId, //用戶ID username: 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)
經過不一樣的權限來動態修改路由表
經過 vue的 鉤子函數 beforeEach 來控制並展現哪些路由, 以及判斷是否須要登錄
import 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()) { //存在token if (to.path === '/login') { //當前頁是登陸直接跳過進入主頁 next('/') }else{ if (!store.state.user.roles) { //拉取用戶信息 store.dispatch('getUserInfo').then( res => { let roles = res.data.roles store.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.name NProgress.done() }) export default router
經過調用 getUserInfo
方法傳入 token 獲取用戶信息, 後臺直接解析 token 獲取裏面的 信息 返回給前臺
getUserInfo ({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
方法 動態生成路由
import { 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
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) } }
把訪問過的路徑儲存在本地,記錄下來,經過標籤直接訪問
// 麪包屑 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下載便行
主要是經過 jsonwebtoken
的verify方法檢測cookie 裏面的token 驗證它的合法性
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 = log4js.getLogger('cheese')
Appender
來控制文件的 名字、路徑、類型 log4js.configure
即可經過 logger 上的打印方法 來輸出日誌了 logger.info(JSON.stringify(currTime:
當前時間爲${Date.now()}s))
//指定要記錄的日誌分類 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)
設計思路
當應用程序啓動時候,讀取指定目錄下的 js 文件,以文件名做爲屬性名,掛載在實例 app 上,而後把文件中的接口函數,擴展到文件對象上
//other.js const 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.id try { 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
方法
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) }
在把該形式的方法 賦值過去就行 router.post('/markdown_upload_img', app.controller.admin.other.markdown_upload_img)
mongoose
連接 mongodb 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
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
啓動服務器db.createUser({user:"cd",pwd:"123456",roles:[{role:"readWrite",db:'test'}]})
(mongodb 註冊用戶)cnpm run dev:admin
啓動後臺管理界面登陸後臺管理時須要在數據庫 建立 users 集合註冊一個帳號進行登陸
db.users.insert({ "name" : "cd", "pwd" : "e10adc3949ba59abbe56e057f20f883e", "username" : "admin", "roles" : [ "admin" ] }) // 帳號: admin 密碼: 123456
cnpm run dev:client
啓動前臺頁面參考文章
我的博客
github
基於Koa2搭建Node.js實戰項目教程
手摸手,帶你用vue擼後臺