Today I will introduce the background and foreground part of our full-stack CMS system. If you don’t know much about the project background and technical stack, you can check out my previous article

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

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

Implement server startup details for a CMS full stack project from 0 to 1 based on nodeJS

Abstract

This paper will mainly introduce the following contents:

  • 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

Since there are many details of the implementation of each technical point, I suggest you learn relevant content first, and you can communicate with me if you don’t understand. If you just want to learn more about Vue or React, skip to Section 4.

The body of the

1. Implement custom KOA middleware and restful apis

A Koa application is an object containing a set of middleware functions that are organized and executed in a stack-like manner. We can use the use interface and async functions provided by KOA to customize some of the middleware. The middleware used to print logs is as follows:

// logger
app.use(async (ctx, next) => {
  await next();
  const rt = ctx.response.get('X-Response-Time');
  console.log(`${ctx.method} ${ctx.url} - ${rt}`);
});
Copy the code

More information about KOA can be learned on the official website, and we are officially entering the process of implementing middleware.

In the first chapter of my introduction to CMS, we found the middlewares directory in the source code. First, let’s take a look at common.js. This file is where we store our common middleware.

The source code is as follows:

import logger from 'koa-logger';
import koaBody from 'koa-body';
import session from 'koa-session';
import cors from 'koa2-cors';
import sessionStore from '.. /lib/sessionStore';
import redis from '.. /db/redis';
import statisticsSchema from '.. /db/schema/statistics';

// Set the log
export const Logger = app= > app.use(logger())
// Process the request body
export const KoaBody = app= > app.use(koaBody())

// Configure cross-domain resource sharing
export const Cors = app= > app.use(cors({
    origin: function(ctx) {
      if (ctx.url.indexOf('/api') > - 1) {
        return false;
      }
      return The '*';
    },
    exposeHeaders: ['WWW-Authenticate'.'Server-Authorization'].maxAge: 5.credentials: true.allowMethods: ['GET'].allowHeaders: ['Content-Type'.'Authorization'.'Accept'.'X-Requested-With'],}))/ / set the session
export const Session = app= > {
    app.keys = ['xujiang']
    const SESSION_CONFIG = {
        key: 'zxzkCMS'.maxAge: 12 * 60 * 60 * 1000.// Set the expiration time of the session to half a day
        store: new sessionStore(redis),
        signed: true
    }

    app.use(session(SESSION_CONFIG, app));
}

// Statistics of the website
export const siteStatistics = app= > app.use(async (ctx, next) => {
  if(ctx.url.indexOf('articleList? iSaJAx=isAjax') > - 1) {
    const views = await statisticsSchema.hget('views')
    statisticsSchema.hmset('views', +views + 1)}await next()
})
Copy the code

In fact, it is very simple to implement a middleware, we only need to create their own async business function in the parameters of app.use, such as siteStatistics, you can refer to this method to do custom middleware.

For the implementation of restful apis, we do it at the infrastructure layer. You can see the source code under the lib descorator.js file. Roughly divided into several pieces:

This implementation will involve more ES6 + knowledge, including decorators, symbol and so on. If you don’t understand, please communicate with me.

2. Koa routing and Service layer implementation

This part mainly adopts the MVC pattern, we defined the basic routing class before, so that we can formally handle the service side business, we can define different business interfaces by module, unified management through the route controller.

  1. The router layer
// router/statistics
import { controller, get } from '.. /lib/decorator'
import {
    getSiteStatistics
} from '.. /service/statistics'

@controller('/api/v0/siteStatistics')
class statisticsController {
    Obtain all statistics * / * * * @ param {*} CTX * @ param {*} the next * /
    @get('/all')
    async getSiteStatistics(ctx, next) {
        const res = await getSiteStatistics()
        if(res && !Array.isArray(res)) {
            ctx.status = 200
            ctx.body = {
                data: res,
                state: 200}}else {
            ctx.status = 500
            ctx.body = {
                data: res ? res.join(', ') : 'Server error'.state: 500}}}}export default statisticsController
Copy the code
  1. The service layer
// Service is used to handle business logic and database operations
import statisticsSchema from '.. /db/schema/statistics'

export const getSiteStatistics = async() = > {const result = await statisticsSchema.hgetall()
    return result
}
Copy the code

Here we give a simple example for you to understand, as for admin and Config module development is similar, can be combined with their own business needs to deal with. The code for the other modules is already written and can be found on my Github. If you don’t understand, you can communicate with me.

3. Basic use and skills of template engine PUG

The template engine was not the focus of the project, and there were no template engines such as Jade and EJS involved in the project, but as a front end, it is good to know more about them. Here’s a brief introduction to PUG, an updated version of Jade.

In order to use the template engine in a KOA project, we can use KOA-views for rendering as follows:

/***** Koa-view basic use *****/
 import views from 'koa-views';
 app.use(views(resolve(__dirname, './views'), { extension: 'pug' }));
 app.use(async (ctx, next) => {
     await ctx.render('index', {
         name: 'xujiang'.years: '248'})});Copy the code

Pug file for specific pages:

  1. index.pug

  1. layout/default

Pug uses the indent method to specify the code level, can use the inheritance syntax, you can refer to the puG website to learn. I won’t go into details here.

4. Vue management background page implementation and source code sharing

First, let’s look at the organizational structure of vUE management background:

// type.ts
export interface State {
    name: string;
    isLogin: boolean;
    config: Config;
    [propName: string]: any;  // Used to define optional additional attributes
}

export interface Config {
    header: HeaderType,
    banner: Banner,
    bannerSider: BannerSider,
    supportPay: SupportPay
}

export interface HeaderType {
    columns: string[],
    height: string,
    backgroundColor: string,
    logo: string
}

export interface Banner {
    type: string,
    label: string[],
    bgUrl: string,
    bannerList: any[]
}

export interface BannerSider {
    tit: string,
    imgUrl: string,
    desc: string
}

export interface SupportPay {
    tit: string,
    imgUrl: string
}

// Handle the corresponding type
export interface Response {
    [propName: string]: any;
}
Copy the code

The mutation content is as follows:

//action.ts
import { 
    HeaderType,
    Banner,
    BannerSider,
    SupportPay,
    Response
 } from './type'
import http from '.. /utils/http'
import { uuid, formatTime } from '.. /utils/common'
import { message } from 'ant-design-vue'

export default {
    / * * * /
    setConfig(context: any, paylod: HeaderType) {
        http.get('/config/all').then((res:Response) = > {
            context.commit('setConfig', res.data)
        }).catch((err:any) = > {
            message.error(err.data)
        })
    },

    /**header */
    saveHeader(context: any, paylod: HeaderType) {
        http.post('/config/setHeader', paylod).then((res:Response) = > {
            message.success(res.data)
            context.commit('saveHeader', paylod)
        }).catch((err:any) = > {
            message.error(err.data)
        })  
    },

    /**banner */
    saveBanner(context: any, paylod: Banner) {
        http.post('/config/setBanner', paylod).then((res:Response) = > {
            message.success(res.data)
        }).catch((err:any) = > {
            message.error(err.data)
        })  
    },

    /** List of articles */
    getArticles(context: any) {
        http.get('article/all').then((res:Response) = > {
            context.commit('getArticles', res.data);
        }).catch((err:any) = >{
            message.error(err.data)
        })
    },

    addArticle(context: any, paylod: any) {
        paylod.id = uuid(8.10);
        paylod.time = formatTime(Date.now(), '/');
        paylod.views = 0;
        paylod.flover = 0;
        return new Promise((resolve:any, reject:any) = > {
            http.post('/article/saveArticle', paylod).then((res:Response) = > {
                context.commit('addArticle', paylod)
                message.success(res.data)
                resolve()
            }).catch((err:any) = > {
                message.error(err.data)
                reject()
            })
        })  
    }
    // ...
};
Copy the code

Here are a few typical actions for you to learn and understand, and then further, we can use it to encapsulate baseAction, which can reduce most of the reuse information, here you can try to do a wave of encapsulation. Finally, we uniformly introduce the following in index:

import Vue from 'vue';
import Vuex from 'vuex';
import { state } from './state';
import mutations from './mutation';
import actions from './action';

Vue.use(Vuex);

export default new Vuex.Store({
  state,
  mutations,
  actions,
});
Copy the code

Managing VUEX in this way also helps with scalability and maintainability later.

In the vue page section, we can know the general page module and function point according to the use case and data model in the previous Node section, which will not be discussed in detail here. Let’s look at a few key points:

  • How to ensure that the page refresh navigation is correct
  • How to do custom caching when switching pages
  • How to achieve analog PC terminal, mobile terminal preview
  • How to use vuEX advanced API to implement data listening mechanism
  • How do I authenticate the login

I’m going to go ahead and outline my plan for you to look at.

1. How do I ensure that the page refresh navigation is correct
// layout.vue
// Page routing table
const routeMap: any = {
    '/': '1'.'/banner': '2'.'/bannerSider': '3'.'/article': '4'.'/addArticle': '4'.'/support': '5'.'/imgManage': '6'.'/videoManage': '7'.'/websiteAnalysis': '8'.'/admin': '9'};// Listen for route changes to match the currently selected navigation
@Watch('$route')
private routeChange(val: Route, oldVal: Route) {
  // do something
  if(val.path.indexOf('/preview') < 0) {
    this.curSelected = routeMap[val.path] || routeMap[oldVal.path]; }}Copy the code
2. How do I customize the cache when switching between pages

We use keep-alive for caching, and pass the key in the routing view to determine whether it will be cached next time:

<template>
  <div id="app">
    <keep-alive>
      <router-view :key="key" />
    </keep-alive>
  </div>
</template>

<script lang="ts">
import { Vue } from 'vue-property-decorator';
import Component from 'vue-class-component';

@Component
export default class App extends Vue {
  get key() {
    // Cache all other pages except the preview page
    console.log(this.$route.path)
    if(this.$route.path.indexOf('/preview') > - 1) {
      return '0'
    }else if(this.$route.path === '/login') {
      return '1'
    }else {
      return '2'}}}</script>
Copy the code

Since our business is to preview and manage the page to be updated to the latest data when switching, so we do not go to the cache when switching the two modules, and call the latest data. The same is true for login, and distributed caching is done by setting different keys.

3. How to implement analog PC terminal and mobile terminal preview

I used a width-based simulation to implement previews, defining preview routes to define PC and mobile screens. If you don’t understand, you can talk to me, but of course you can also use iframe and simulation.

4. How to use vuEX advanced API to implement data listening mechanism

Here’s the code:

public created() {
    let { id, label } = this.$route.query;
    this.type = id ? 1 : 0;
    if(id) {
        // Listen for changes in the vuex article data, which triggers an action to display the article data
        // Note: This is done to prevent page refresh data loss
        let watcher = this.$store.watch(
            (state,getter) = > {
                return state.articles
            },
            () => {
                this.getDetail(id, label, watcher)
            }
        )

        if(Object.keys(this.$store.state.articles).length) {
            this.getDetail(id, label, watcher)
        }
    }
  }
Copy the code

We use vuex’s Watch to listen for store changes and act accordingly. The Watch API takes two callback arguments, the first callback returns a value, and if the value changes, the second callback is triggered. This is similar to the Memo and callback of the React hooks.

5. How do I perform login authentication

Login authentication is mainly to negotiate a set of rules with the back-end service. The background checks whether the user has logged in or has the permission to operate a certain module, and generally notifies the front-end through the corresponding data in response. Here we mainly talk about login authentication. So the front end can do the redirection:

// HTTP module encapsulation
import axios from 'axios'
import qs from 'qs'

axios.interceptors.request.use(config= > {
  // loading
  return config
}, error => {
  return Promise.reject(error)
})

axios.interceptors.response.use(response= > {
  return response
}, error => {
  return Promise.resolve(error.response)
})

function checkStatus (response) {
  // loading
  // If the HTTP status code is normal, return the data directly
  if(response) {
    if (response.status === 200 || response.status === 304) {
      return response.data
      // Return response.data if you don't need data other than data
    } else if (response.status === 401) {
      location.href = '/login';
    } else {
      throw response.data
    }
  } else {
    throw {data:'Network error'}}}// Axios default parameter configuration
axios.defaults.baseURL = '/api/v0';
axios.defaults.timeout = 10000;

export default {
  post (url, data) {
    return axios({
      method: 'post',
      url,
      data: qs.stringify(data),
      headers: {
        'X-Requested-With': 'XMLHttpRequest'.'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
      }
    }).then(
      (res) = > {
        return checkStatus(res)
      }
    )
  },
  get (url, params) {
    return axios({
      method: 'get',
      url,
      params, // Get specifies the parameters of the request
      headers: {
        'X-Requested-With': 'XMLHttpRequest'
      }
    }).then(
      (res) = > {
        return checkStatus(res)
      }
    )
  },
  del (url, params) {
    return axios({
      method: 'delete',
      url,
      params, // Get specifies the parameters of the request
      headers: {
        'X-Requested-With': 'XMLHttpRequest'
      }
    }).then(
      (res) = > {
        return checkStatus(res)
      }
    )
  }
}
Copy the code

As for the setup of the specific AXIos request interceptors and response interceptors, we can manipulate and add custom logic depending on the business.

5. React client foreground implementation and source code sharing

For react, I mainly use webpack built by myself to do module packaging. Those who want to learn Webpack can refer to my Webpack configuration. Currently, the packaging file is compatible with IE9 +. React reception desk mainly includes:

import React, { useState, useEffect } from "react"
import { Carousel } from 'antd'
import ArticleItem from '.. /.. /components/ArticleItem'
import { isPC, ajax, unparam } from 'utils/common'

import './index.less'

function Home(props) {
    let [articles, setArticles] = useState([])
    let { search } = props.location

    function getArticles(cate = ' ', num = 10, page = 0) {
        ajax({
            url: '/article/articleList'.method: 'get'.data: { cate, num, page }
        }).then(res= > {
            setArticles(res.data || [])
        }).catch(err= > console.log(err))
    }

    if(search && sessionStorage.getItem('prevCate') !== search) {
        getArticles(unparam(search).cate)
        sessionStorage.setItem('prevCate', search)
    }

    useEffect((a)= > {
        getArticles()
        return (a)= > {
            sessionStorage.removeItem('prevCate')}}, [])return<div className="home-wrap"> <div className="banner-wrap"> { isPC ? <React.Fragment> <div className="banner-sider"> <div className="tit">{ props.bannerSider.tit }</div> <img src={props.bannerSider.imgUrl} alt="" /> <div className="desc">{ props.bannerSider.desc }</div> </div> { +props.banner.type ? <Carousel autoplay className="banner"> { props.banner.bannerList.map((item, i) => ( <div key={i}> <a className="banner-img" href="" style={{ backgroundImage: 'url('+ item.imgUrl +')'}}> <p className="tit">{ item.tit }</p> </a> </div> )) } </Carousel> : <div className="banner"> <div className="banner-img" style={{backgroundImage: 'url('+ props.banner.bgUrl +')'}}> { props.banner.label.map((item, i) => ( <span className="banner-label" style={{left: 80*(i+1) + 'px'}} key={i}> { item } </span> )) } </div> </div> } </React.Fragment> : <Carousel autoplay className="banner"> { props.banner.bannerList.map((item, i) => ( <a className="banner-img" href="" key={i} style={{ backgroundImage: 'url('+ item.imgUrl +')'}}> <p className="tit">{ item.tit }</p> </a> )) } </Carousel> } </div> <div <div className="article "> <div className="tit"> <div className="tit"> {article. Map ((item, I) => (<ArticleItem {... item} key={i} /> )) } </div> </div> } export default HomeCopy the code

Article details:

import React, { useState, useEffect } from "react"
import { Button, Modal, Skeleton, Icon } from 'antd'
import { ajax, unparam } from 'utils/common'
import QTQD from 'images/logo.png'
import './index.less'

function ArticleDetail(props) {
    let [isPayShow, setIsPayShow] = useState(false)
    let [detail, setDetail] = useState(null)
    let [likeNum, setLikeNum] = useState(0)
    let [articleContent, setArticleContent] = useState(null)
    let [isShowLike, setShowLike] = useState(false)

    function toggleModal(flag) {
        setIsPayShow(flag)
    }

    function getcontent(url) {
        ajax({
            url
        }).then(res= > {
            setArticleContent(res.content)
            
        })
    }

    function showLike() {
        if(! isShowLike) { ajax({url: `/article/likeArticle/${unparam(props.location.search).id}`.method: 'post'
            }).then(res= > {
                setShowLike(true)
                setLikeNum(prev= > prev + 1)
            })
        }
    }

    useEffect((a)= > {
        ajax({
            url: `/article/${unparam(props.location.search).id}`
        }).then(res= > {
            setDetail(res.data)
            setLikeNum(res.data.flover)
            getcontent(res.data.articleUrl)
        })
        return (a)= >{}; }, [])return! detail ? <Skeleton active /> : <div className="article-wrap"> <div className="article"> <div className="tit">{ detail.tit }</div> <div className="article-info"> <span className="article-type">{ detail.label }</span> <span className="article-time">{ detail.time }</span> <span className="article-views"><Icon type="eye" />&nbsp; { detail.views }</span> <span className="article-flover"><Icon type="fire" />&nbsp; { likeNum }</span> </div> <div className="article-content" dangerouslySetInnerHTML={{__html: articleContent}}></div> <div className="article-ft"> <div className="article-label"> </div> <div ClassName ="support-author"> <p style =" text-align: center; </p> <div className="support-wrap"> <Button className="btn-pay" type="danger" ghost onClick={() => </Button> <Button className="btn-flover" type="primary" onClick={showLike} disabled={isShowLike}>{ ! isShowLike ? 'Like' : 'has great} ({likeNum}) < / Button > {isShowLike && < Icon type = "like" the className = "like - animation" / >} < / div > < / div > < / div > < / div > <div className="sider-bar"> <div className="sider-item"> <img SRC ={QTQD} Alt =""/> <p> </div> </div> <Modal visible={isPayShow} onCancel={() => toggleModal(false)} width="300px" footer={null} > <div className="img-wrap"> <img src={props.supportPay.imgUrl} alt={props.supportPay.tit} /> <p>{ props.supportPay.tit }</p> </div> </Modal> </div> } export default ArticleDetailCopy the code

Since the foreground implementation is relatively simple, I have written complete comments in the code on how to define the router and how to use the skeleton screen, so you can communicate with me if you are interested.

6. Pm2 deployment and Nginx server configuration

Pm2 server persistence and nGINX do multi-site configuration and how to optimize the content of the code I will use the whole file to do a detailed introduction, I hope you have something to gain, if you want to learn the project source code, you can pay attention to the public number “interesting talk front end” join us to learn and discuss.

More recommended

  • Implementing a CMS full stack project from 0 to 1 based on nodeJS (middle)
  • Implement server startup details for a CMS full stack project from 0 to 1 based on nodeJS
  • 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