今天給你們介紹的主要是咱們全棧CMS系統的後臺部分,因爲後臺部分涉及的點比較多,我會拆解成幾部分來說解,若是對項目背景和技術棧不太瞭解,能夠查看個人上一篇文章javascript
這篇文章除了會涉及node的知識,還會涉及到redis(一個高性能的key-value數據庫),前端領域的javascript大部分高級技巧以及ES6語法,因此在學習以前但願你們對其有所瞭解。前端
本文主要介紹CMS服務端部分的實現,具體包括以下內容:vue
因爲每個技術點實現的細節不少,建議先學習相關內容,若是不懂的能夠和我交流。java
最新的node雖然已經支持大部分es6+語法,可是對於import,export這些模塊化導入導出的API尚未完全支持,因此咱們能夠經過babel去編譯支持,若是你習慣使用commonjs的方式,也能夠直接使用。這裏我直接寫出個人配置:node
"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"
},
複製代碼
// .babelrc
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"node": "current"
}
}
]
],
"plugins": [
["@babel/plugin-proposal-decorators", { "legacy": true }],
["@babel/plugin-proposal-class-properties", { "loose" : true }]
]
}
複製代碼
"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
首先來看看咱們完成後的目錄設計:webpack
項目參考了不少經典資料和MDN的文檔,採用經典的MVC模式,爲了方便理解,筆者特地作了一個大體的導圖: 這種模式用於應用程序的分層開發,方便後期的管理和擴展,並提供了清晰的設計架構。在項目開發前,咱們須要根據業務結構和內容設計數據模型,數據庫部分我這裏採用的是redis+json-schema,原本想使用mongodb來實現主數據的存儲,可是考慮到本身對新方案的研究和想本身經過二次封裝redis實現類mongoose的客戶端管理框架,因此這裏會採用此方案,關於mongoDB的實現,我以前也有項目案例,感興趣能夠一塊兒交流優化。css3
咱們來先看看CMS設計的視圖和內容,咱們分管理端和客戶端,管理端主要的模塊有:
根據以上的展現,咱們大體知道了咱們須要設計哪些數據庫模型,接下來我先帶你們封裝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,對於其餘的數據庫設計,也能夠參考此方式。
因爲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操做,咱們主要用來處理用戶的登陸信息。
文件上傳的方案我是在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全棧的管理後臺和客戶端部分的實現。包括:
項目完整源碼地址我會在十一以前告訴你們,歡迎在公衆號《趣談前端》加入咱們一塊兒討論。