Today I will mainly introduce the background part of our full-stack CMS system. Since the background part involves many points, I will break it into several parts to explain it. If you are not familiar with the project background and technical stack, you can check out my last article

Implementing a CMS full stack project from 0 to 1 based on nodeJS (Part 1)

In addition to node, this article will cover Redis (a high-performance key-value database), most advanced javascript tips for the front end, and ES6 syntax, so hopefully you’ll be familiar with it before you start.

Abstract

This paper mainly introduces the implementation of the CMS server, including the following contents:

  • How to use Babel7 to enable Node to support more ES6 + syntax and nodemon to implement hot update and automatic restart of project files
  • Directory structure design and thinking for the Node project
  • How to implement a class schema base library based on ioredis and JSON-Schema
  • Encapsulates a sessionStore library based on KOA-Session
  • Koa/Multer based package file processing tool class
  • Implement custom KOA middleware and restful apis
  • Basic use and skills of the template engine PUG

Since there are many details of the implementation of each technical point, I suggest you learn relevant content first. If you don’t understand, you can communicate with me.

The body of the

1. How to use Babel7 to enable Node to support more ES6 + syntax and nodemon to implement hot update and automatic restart of project files

Most of the ES6 + syntax is supported in the latest node, but the modular import/export API is not fully supported, so we can compile it in Babel, or if you are comfortable using CommonJS, you can use it directly. Here I’ll just write out my configuration:

  1. Package. json installs the Babel module and hot restarts 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"
  },
Copy the code
  1. Configure.babelrc for node to support import, export, class and decorators:
// .babelrc
{
    "presets": [["@babel/preset-env",
        {
          "targets": {
            "node": "current"}}]],"plugins": [["@babel/plugin-proposal-decorators", { "legacy": true }],
      ["@babel/plugin-proposal-class-properties", { "loose" : true}}]]Copy the code
  1. Configure the startup script. To start the project using NPM, we configure the following script in 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"
  },
Copy the code

Babel7 and Nodemon as well as some configuration problems and use of NPM, but there are not understand can communicate with me at the end of the article. Here are a few learning links:

  • Babel7 documentation tutorial
  • Nodemon manages documents
  • Jquery, React, vue, typescript

So far, the infrastructure of our Node project is basically set up, and we will continue to go into the bottom layer of server design.

2. Directory structure design and thinking for the Node project

First take a look at our finished catalog design:

  • The Model layer manages the data objects, which can also have logic to update the controller as the data changes.
  • The View layer is used to display a View of the data.
  • Controller The Controller acts on the model and view. It controls the flow of data to the model objects and updates the view as the data changes, keeping the view separate from the model.

3. Implement a basic library of class Schema based on Ioredis and JSON-Schema

Before the project development, we need to design the data model according to the business structure and content. For the database part, I used Redis +json-schema. Originally, I wanted to use mongodb to realize the storage of master data. However, considering my research on the new scheme and my plan to implement mongoose-like client management framework through secondary encapsulation of Redis, THIS scheme will be adopted here. As for the implementation of mongoDB, I also have project cases before, so we can exchange and optimize together.

Let’s first take a look at the view and content of CMS design. We are divided into management side and client side. The main modules of management side are:

  1. The login module

  1. Website statistics

  1. Administrator module

According to the above presentation, we have a rough idea of what database models we need to design. Next, I will first take you to encapsulate Redis-Schema, which is also the underlying tool of the database we use:

// 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)
    }

    // Get the entire hash object
    hgetall() {
        return this.redis.hgetall(this.schemaName)
    }

    // Get the attribute value of the specified hash object
    hget(key) {
        return this.redis.hget(this.schemaName, key)
    }

    // Get the list elements by index
    lindex(index) {
        return this.redis.lindex(this.schemaName, index)
    }

    // Get the list of elements in the specified range
    lrange(start, end) {
        return this.redis.lrange(this.schemaName, start, end)
    }

    // Get the length of the list
    llen() {
        return this.redis.llen(this.schemaName)
    }

    // Check whether a schemaName exists
    exists() {
        return this.redis.exists(this.schemaName)
    }

    // Set expiration time for a schemaName in seconds
    expire(time) {
        return this.redis.expire(this.schemaName, time)
    }

    // Remove the expiration time of a schemaName
    persist() {
        return this.redis.persist(this.schemaName)
    }

    // Change the schemaName name
    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)
            }
        })
    }

    // Increment the value of a schema to a specified number of values
    incrby(num) {
        return this.redis.incrby(this.schemaName, num)
    }

    // Increment the value of a schema to a specified number of values
    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, () => {
                // Jsonize the first layer key so that Redis can correctly store the key as a reference value
                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
Copy the code

The author’s own encapsulation of the library with many extensible places, such as the addition of class things, save the interceptor and so on, I will improve in the second version, here only for reference. More knowledge about JSON-Schema, if you don’t understand, you can communicate and learn in our communication area. We define an administrator schema:

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

// Store administrator data
const adminSchema = new RedisSchema('admin', {
    id: "/admin".type: "object".properties: {
        username: {type: "string"},
        pwd: {type: "string"},
        role: {type: "number"}   // 0 Super administrator 1 Common administrator}})export default adminSchema
Copy the code

As shown above, the administrator entity includes username, PWD, and role. This method can also be used for other database designs.

4. Encapsulate a sessionStore library based on KOA-Session

Since there are a lot of information about session on the Internet, I won’t waste time here. Here is my plan:

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
Copy the code

Here the main implementation of the session get, set, del operation, we mainly used to deal with the user’s login information.

5. Koa/Multer based package file processing tool class

The file upload scheme IS koA/Multer, which I saw on Github, because it encapsulates the file upload library and will be used for any operation involving file upload.

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

const rootImages = resolve(__dirname, '.. /.. /public/uploads')
// Upload file saving path and file name
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}_The ${Date.now().toString(16)}.${type}`)}})// File upload limit
const limits = {
    fields: 10.// The number of non-file fields
    fileSize: 1024 * 1024 * 2.// File size unit b
    files: 1// Number of files
}

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

// Delete the file
export const delFile = (path) = > {
    return new Promise((resolve, reject) = > {
        fs.unlink(path, (err) => {
            if(err) {
                reject(err)
            }else {
                resolve(null)}})})}// Delete the folder
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 filefs.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)}})})}Copy the code

This package includes upload file, delete file, delete directory tool method, can be used as a wheel to other projects, can also be based on my wheel to do secondary expansion.

Due to time reasons, I will continue to update the basic use and skills of the customized KOA middleware, restful API and template engine PUG tomorrow. If you don’t understand the above parts, please communicate with the author.

The last

The next two days will introduce the rest of the server side, CMS full stack management back end and client part of the implementation. Include:

  • Implement custom KOA middleware and restful apis
  • Koa routing and Service layer implementation
  • Basic use and skills of the template engine PUG
  • Vue management background page implementation and source sharing
  • React client foreground specific implementation and source sharing
  • Pm2 deployment and nGINx server configuration

Project full source address I will tell you before eleven, welcome in the public number “interesting talk front end” to join us to discuss.

More recommended

  • Implementing a CMS full stack project from 0 to 1 based on nodeJS (Part 1)
  • “Javascript advanced programming” core knowledge summary
  • With CSS3 to achieve stunning interviewers background that background animation (advanced source)
  • Remember an old project with cross-page communication issues and front-end implementation of file downloads
  • Write a mock data server using nodeJS in 5 minutes
  • JavaScript binary tree and binary search tree implementation and application
  • With JavaScript and C3 to achieve a turntable small game
  • Teach you to use 200 lines of code to write a love bean spell H5 small game (with source code)
  • Exploration and summary of front-end integrated solutions based on React/VUE ecology
  • How to write your own JS library in less than 200 lines of code
  • A picture shows you how to play vue-Cli3 quickly
  • 3 minutes teach you to use native JS implementation with progress listening file upload preview component
  • Developing travel List with Angular8 and Baidu Maps API
  • Js basic search algorithm implementation and 1.7 million data under the performance test
  • How to make front-end code 60 times faster
  • Vue Advanced Advanced series – Play with Vue and vuex in typescript