vue+node+mongodb 搭建一個完整博客

Vue + Node + Mongodb 開發一個完整博客流程

前言

前段時間剛把本身的我的網站寫完, 因而這段時間由於事情不是太多,便整理了一下,寫了個簡易版的博客系統
服務端用的是 koa2框架 進行開發css

技術棧

Vue + vuex + element-ui + webpack + nodeJs + koa2 + mongodbhtml


目錄結構講解

圖片描述

  • 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 文件

圖片描述

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配置

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組件等等ios

views

全部模塊頁面git

store

vuex用來統一管理公用屬性, 和統一管理接口github

1. 登錄

登錄是採用 jsonwebtoken方案 來實現整個流程的web

  • jwt.sign(payload, secretOrPrivateKey, [options, callback]) 生成TOKEN
  • jwt.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)

2. 權限

經過不一樣的權限來動態修改路由表

  • 經過 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

axios 請求封裝, 統一對請求進行管理

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下載便行


server端

權限

主要是經過 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 實例的類型 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)

定製書寫規範(API)

  • 設計思路
    當應用程序啓動時候,讀取指定目錄下的 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

封裝返回的send函數

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 管理靜態文件入口

注意事項:

  1. cnpm run server 啓動服務器
  2. 啓動時,記得啓動mongodb數據庫,帳號密碼 能夠在 server/config.js 文件下進行配置
  3. db.createUser({user:"cd",pwd:"123456",roles:[{role:"readWrite",db:'test'}]}) (mongodb 註冊用戶)
  4. cnpm run dev:admin 啓動後臺管理界面
  5. 登陸後臺管理界面錄製數據
  6. 登陸後臺管理時須要在數據庫 建立 users 集合註冊一個帳號進行登陸

    db.users.insert({
        "name" : "cd",
        "pwd" : "e10adc3949ba59abbe56e057f20f883e",
        "username" : "admin",
        "roles" : [ 
            "admin"
        ]
    })
    
    // 帳號: admin  密碼: 123456
  7. cnpm run dev:client 啓動前臺頁面

參考文章

我的博客
github
基於Koa2搭建Node.js實戰項目教程
手摸手,帶你用vue擼後臺
相關文章
相關標籤/搜索