基於nodeJS從0到1實現一個CMS全棧項目(中)(含源碼)

今天給你們介紹的主要是咱們全棧CMS系統的後臺部分,因爲後臺部分涉及的點比較多,我會拆解成幾部分來說解,若是對項目背景和技術棧不太瞭解,能夠查看個人上一篇文章javascript

基於nodeJS從0到1實現一個CMS全棧項目(上)css

這篇文章除了會涉及node的知識,還會涉及到redis(一個高性能的key-value數據庫),前端領域的javascript大部分高級技巧以及ES6語法,因此在學習以前但願你們對其有所瞭解。前端

摘要

本文主要介紹CMS服務端部分的實現,具體包括以下內容:vue

  • 如何使用babel7讓node支持更多es6+語法以及nodemon實現項目文件熱更新和自動重啓
  • node項目的目錄結構設計和思想
  • 如何基於ioredis和json-schema本身實現一個類schema的基礎庫
  • 基於koa-session封裝一個sessionStore庫
  • 基於koa/multer封裝文件處理的工具類
  • 實現自定義的koa中間件和restful API
  • 模版引擎pug的基本使用及技巧

因爲每個技術點實現的細節不少,建議先學習相關內容,若是不懂的能夠和我交流。java

正文

1.如何使用babel7讓node支持更多es6+語法以及nodemon實現項目文件熱更新和自動重啓

最新的node雖然已經支持大部分es6+語法,可是對於import,export這些模塊化導入導出的API尚未完全支持,因此咱們能夠經過babel去編譯支持,若是你習慣使用commonjs的方式,也能夠直接使用。這裏我直接寫出個人配置:node

  1. package.json安裝babel模塊和nodemon熱重啓
"devDependencies": {
    "@babel/cli": "^7.5.5",
    "@babel/core": "^7.5.5",
    "@babel/node": "^7.5.5",
    "@babel/plugin-proposal-class-properties": "^7.5.5",
    "@babel/plugin-proposal-decorators": "^7.4.4",
    "@babel/preset-env": "^7.5.5",
    "nodemon": "^1.19.1"
  },
複製代碼
  1. 配置.babelrc文件,讓node支持import,export,class以及裝飾器:
// .babelrc
{
    "presets": [
      [
        "@babel/preset-env",
        {
          "targets": {
            "node": "current"
          }
        }
      ]
    ],
    "plugins": [
      ["@babel/plugin-proposal-decorators", { "legacy": true }],
      ["@babel/plugin-proposal-class-properties", { "loose" : true }]
    ]
  }
複製代碼
  1. 配置啓動腳本。爲了使用npm的方式啓動項目,咱們在package.json裏配置以下腳本:
"scripts": {
    "start": "export NODE_ENV=development && nodemon -w src --exec \"babel-node src\"",
    "build": "babel src --out-dir dist",
    "run-build": "node dist",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
複製代碼

有關babel7和nodemon以及npm的一些配置問題和使用方式,不過有不懂的能夠在文章末尾和我交流。這裏提供幾個學習連接:react

至此,咱們node項目的基礎設施基本搭建完成了,接下來咱們繼續深刻服務端設計底層。jquery

2.node項目的目錄結構設計和思想

首先來看看咱們完成後的目錄設計:webpack

項目參考了不少經典資料和MDN的文檔,採用經典的MVC模式,爲了方便理解,筆者特地作了一個大體的導圖:

這種模式用於應用程序的分層開發,方便後期的管理和擴展,並提供了清晰的設計架構。

  • Model層咱們管理數據對象,它也能夠帶有邏輯,在數據變化時更新控制器。
  • View層主要用來展現數據的視圖。
  • Controller控制器做用於模型和視圖上。它控制數據流向模型對象,並在數據變化時更新視圖,使視圖與模型分離開。

3.基於ioredis和json-schema本身實現一個類schema的基礎庫

在項目開發前,咱們須要根據業務結構和內容設計數據模型,數據庫部分我這裏採用的是redis+json-schema,原本想使用mongodb來實現主數據的存儲,可是考慮到本身對新方案的研究和想本身經過二次封裝redis實現類mongoose的客戶端管理框架,因此這裏會採用此方案,關於mongoDB的實現,我以前也有項目案例,感興趣能夠一塊兒交流優化。css3

咱們來先看看CMS設計的視圖和內容,咱們分管理端和客戶端,管理端主要的模塊有:

  1. 登陸模塊

2. 首頁配置管理模塊

配置頁主要包括header頭部,banner位,bannerSider側邊欄和文章讚揚設置,咱們對對它作一個單獨的config數據庫。 3. 文章管理模塊

這裏咱們須要對文章數據進行存儲,包括文章分類,文章首圖,文章內容等信息,以下:
4. 圖片管理

圖片管理主要是方便博主管理圖片信息,定位圖片的來源,方便後期作埋點跟蹤。

  1. 網站統計

網站統計只是一個雛形,博主能夠根據本身需求作統計分析,提升更大的自定義。

  1. 管理員模塊

這裏用來管理系統的管理員,能夠分配管理員權限等。關於權限的設計,能夠有更復雜的模式,後面有須要也能夠相互交流。

根據以上的展現,咱們大體知道了咱們須要設計哪些數據庫模型,接下來我先帶你們封裝redis-schema,也是咱們用到的數據庫的底層工具:

// lib/schema.js
import { validate } from 'jsonschema'
import Redis from 'ioredis'

const redis = new Redis()

class RedisSchema {
    constructor(schemaName, schema) {
        this.schemaName = schemaName
        this.schema = schema
        this.redis = redis
    }

    validate(value, schema, cb) {
        const { valid, errors } = validate(value, schema);
        if(valid) {
            return cb()
        }else {
            return errors.map(item => item.stack)
        }
    }

    get() {
        return this.redis.get(this.schemaName)
    }

    // 獲取整個hash對象
    hgetall() {
        return this.redis.hgetall(this.schemaName)
    }

    // 獲取指定hash對象的屬性值
    hget(key) {
        return this.redis.hget(this.schemaName, key)
    }

    // 經過索引獲取列表中的元素
    lindex(index) {
        return this.redis.lindex(this.schemaName, index)
    }

    // 獲取列表中指定範圍的元素
    lrange(start, end) {
        return this.redis.lrange(this.schemaName, start, end)
    }

    // 獲取列表的長度
    llen() {
        return this.redis.llen(this.schemaName)
    }

    // 檢測某個schemaName是否存在
    exists() {
        return this.redis.exists(this.schemaName)
    }

    // 給某個schemaName設置過時時間,單位爲秒
    expire(time) {
        return this.redis.expire(this.schemaName, time)
    }

    // 移除某個schemaName的過時時間
    persist() {
        return this.redis.persist(this.schemaName)
    }

    // 修改schemaName名
    rename(new_schemaName) {
        return this.redis.rename(this.schemaName, new_schemaName)
    }


    set(value, time) {
        return this.validate(value, this.schema, () => {
            if(time) {
                return this.redis.set(this.schemaName, value, "EX", time)
            }else {
                return this.redis.set(this.schemaName, value)
            }
        })
    }

    // 將某個schema的值自增指定數量的值
    incrby(num) {
        return this.redis.incrby(this.schemaName, num)
    }

    // 將某個schema的值自增指定數量的值
    decrby(num) {
        return this.redis.decrby(this.schemaName, num)
    }

    hmset(key, value) {
        if(key) {
            if(this.schema.properties){
                return this.validate(value, this.schema.properties[key], () => {
                    return this.redis.hmset(this.schemaName, key, JSON.stringify(value))
                })
            }else {
                return this.validate(value, this.schema.patternProperties["^[a-z0-9]+$"], () => {
                    return this.redis.hmset(this.schemaName, key, JSON.stringify(value))
                })
            }
            
        }else {
            return this.validate(value, this.schema, () => {
                // 將第一層鍵值json化,以便redis能正確存儲鍵值爲引用類型的值
                for(key in value) {
                    let v = value[key];
                    value[key] = JSON.stringify(v);
                }
                return this.redis.hmset(this.schemaName, value)
            })
        }
    }

    hincrby(key, num) {
        return this.redis.hincrby(this.schemaName, key, num)
    }

    lpush(value) {
        return this.validate(value, this.schema, () => {
            return this.redis.lpush(this.schemaName, JSON.stringify(value))
        })
    }

    lset(index, value) {
        return this.redis.lset(this.schemaName, index, JSON.stringify(value))
    }

    lrem(count, value) {
        return this.redis.lrem(this.schemaName, count, value)
    }

    del() {
        return this.redis.del(this.schemaName)
    }

    hdel(key) {
        return this.redis.hdel(this.schemaName, key)
    }
}

export default RedisSchema
複製代碼

這個筆者本身封裝的庫還有跟多可擴展的地方,好比增長類事物處理,保存前攔截器等等,我會在第二版改進,這裏只供參考。關於json-schema更多的知識,若有不懂,能夠在咱們的交流區溝通學習。 咱們定義一個管理員的schema:

/db/schema/admin.js
import RedisSchema from '../../lib/schema'

// 存放管理員數據
const adminSchema = new RedisSchema('admin', {
    id: "/admin",
    type: "object",
    properties: {
        username: {type: "string"},
        pwd: {type: "string"},
        role: {type: "number"}   // 0 超級管理員 1 普通管理員
      }
  })

export default adminSchema
複製代碼

由上能夠知道,管理員實體包含username用戶名,密碼pwd,角色role,對於其餘的數據庫設計,也能夠參考此方式。

4.基於koa-session封裝一個sessionStore庫

因爲session的知識網上不少資料,這裏就不耽誤時間了,這裏列出個人方案:

function getSession(sid) {
    return `session:${sid}`
}

class sessionStore {
    constructor (client) {
        this.client = client
    }

    async get (sid) {
        let id = getSession(sid)
        let result = await this.client.get(id)
        if (!result) {
            return null
        } else {
            try{
                return JSON.parse(result)
            }catch (err) {
                console.error(err)
            }
        }
    }

    async set (sid, value, ttl) {
        let id = getSession(sid)

        try {
            let sessStr = JSON.stringify(value)
            if(ttl && typeof ttl === 'number') {
                await this.client.set(id, sessStr, "EX", ttl)
            } else {
                await this.client.set(id, sessStr)
            }
        } catch (err) {
            console.log('session-store', err)
        }
    }

    async destroy (sid) {
        let id = getSession(sid)
        await this.client.del(id)
    }
}

module.exports = sessionStore
複製代碼

這裏主要實現了session的get,set,del操做,咱們主要用來處理用戶的登陸信息。

5.基於koa/multer封裝文件處理的工具類

文件上傳的方案我是在github上看的koa/multer,基於它封裝文件上傳的庫,但凡涉及到文件上傳的操做都會使用它。

import multer from '@koa/multer'
import { resolve } from 'path'
import fs from 'fs'

const rootImages = resolve(__dirname, '../../public/uploads')
//上傳文件存放路徑、及文件命名
const storage = multer.diskStorage({
    destination: function (req, file, cb) {
        cb(null, rootImages)
    },
    filename: function (req, file, cb) {
        let [name, type] = file.originalname.split('.');
        cb(null, `${name}_${Date.now().toString(16)}.${type}`)
    }
})
//文件上傳限制
const limits = {
    fields: 10,//非文件字段的數量
    fileSize: 1024 * 1024 * 2,//文件大小 單位 b
    files: 1//文件數量
}

export const upload = multer({storage,limits})

// 刪除文件
export const delFile = (path) => {
    return new Promise((resolve, reject) => {
        fs.unlink(path, (err) => {
            if(err) {
                reject(err)
            }else {
                resolve(null)
            }
        })
    }) 
}

// 刪除文件夾
export function deleteFolder(path) {
    var files = [];
    if(fs.existsSync(path)) {
        files = fs.readdirSync(path);
        files.forEach(function(file,index){
            var curPath = path + "/" + file;
            if(fs.statSync(curPath).isDirectory()) { // recurse
                deleteFolder(curPath);
            } else { // delete file
                fs.unlinkSync(curPath);
            }
        });
        fs.rmdirSync(path);
    }
}

export function writeFile(path, data, encode) {
    return new Promise((resolve, reject) => {
        fs.writeFile(path, data, encode, (err) => {
            if(err) {
                reject(err)
            }else {
                resolve(null)
            }
        })
    })
}
複製代碼

這套方案包含了上傳文件,刪除文件,刪除目錄的工具方法,能夠拿來當輪子使用到其餘項目,也能夠基於個人輪子作二次擴展。

關於實現自定義的koa中間件和restful API和模版引擎pug的基本使用及技巧部分,因爲時間緣由,我會在明天繼續更新,以上部分若有不懂的,能夠和筆者交流學習。

最後

接下來的兩天將推出服務端剩下的部分,CMS全棧的管理後臺和客戶端部分的實現。包括:

  • 實現自定義的koa中間件和restful API
  • koa路由和service層實現
  • 模版引擎pug的基本使用及技巧
  • vue管理後臺頁面的實現及源碼分享
  • react客戶端前臺的具體實現及源碼分享
  • pm2部署以及nginx服務器配置

項目完整源碼地址我會在十一以前告訴你們,歡迎在公衆號《趣談前端》加入咱們一塊兒討論。

更多推薦

相關文章
相關標籤/搜索