I. Project introduction

This project uses Vue3 to build a single page of H5 mobile terminal, and uses NodeJS to build a RESTful framework template based on MVC.

The front-end is based on Vue3 + WebPack4 + Sass + Vant UI REM adaptation scheme to build the mobile end template.

The back-end restful API template is built based on KOA + Sequelize + mysql.

Node version requirements: Vue CLI 4.x Node.js v8.9 or later (V10 or later is recommended). This example uses Node.js V10.17.0

Source code has been submitted to Github, please move small hands Star.

2. Unified development specifications

The first thing to do before code development is to use VsCode’s ESLint + stylelint to format JS and CSS code uniformly.

ESLint

The ESLint plugin needs to be installed in VScode. Once installed, to make the extension work, you need to install and configure ESLint. EsLint can be installed locally as well as globally, here using part of the current project build system for a local installation:

npm install eslint --save-dev
Copy the code

After the installation is complete, configure the.eslintrc.js file in the root directory of the project:

module.exports = {
  // Specify the environment to enable
  env: {
    browser: true.es6: true.node: true
  },
  // Set language options
  parserOptions: {
    ecmaVersion: 6 
  },
  // Enable the recommendation rule
  extends: 'eslint:recommended'.rules: {
    Absolute equality is always enforced except when comparing with null literals
    eqeqeq: ['error'.'always', { null: 'ignore'}].}}Copy the code

When using rules to create a custom rule, the first value of each rule is the error level displayed after the rule is checked:

  1. offor0Will close the rule
  2. warnor1Think of the rule as a warning
  3. erroror2Treat the rule as a mistake

More complete rules can be accessed:

eslint.cn/docs/rules/

Eslint.vuejs.org/user-guide/…

Finally, enable ESLint in Vscode setting.json:

// VSCode validates the auto-formatting code according to the rules of the.eslintrc.js configuration file under your current project
"editor.codeActionsOnSave": {
  "source.fixAll": true
}
Copy the code

Error vue/no-multiple-template-root: vue/no-multiple-template-root: vue/no-multiple-template-root: vue/no-multiple-template-root: vue/no-multiple-template-root: vue/no-multiple-template-root The solution is to open the setting configuration and uncheck:

// F1>Preferences:Open Settings (JSON)
"vetur.validation.template": false
Copy the code

Another problem is that the Vue project is in a subdirectory, so VsCode will go to the top directory to find eslint-plugin-vue and will report Cannot find module ‘eslint-plugin-vue’ error. The workaround is to customize the ESLint working directory in the setting configuration:

// F1>Preferences:Open Settings (JSON)
"eslint.workingDirectories": [
    "./vue3-template"
]
Copy the code

stylelint

The stylelint plugin is needed in VScode, and then partially installed in the project:

npm install --save-dev stylelint stylelint-config-standard stylelint-order
Copy the code

Stylelint is a running tool, and stylelint-config-Standard is the recommended configuration of stylelint. Stylelint-order is a CSS attribute sorting plug-in (write location first, then box model, then content area style, then CSS3 related attributes).

Configuration file for stylelintrc.json:

{
  // Enable the default recommendation rule
  "extends": "stylelint-config-standard"
  // Rules takes precedence over extends. If you want to change the default rules of your plug-in, you can add rules
  "rules": {
    // Specifies whether to use single or double quotation marks for the string
    "string-quotes": "double"}}Copy the code

Third, the front-end

The front-end is initialized using vuE-cli4, using @vue/ CLI 4.5.13:

vue -V
@vue/cli 4.513.

npm get registry
// I am not using Taobao source here
https://registry.npmjs.org/
Copy the code

Here are my initial Settings for creating the project:

Dart-sass is recommended instead of Node-sass.

Panjiachen.github. IO /vue-element…

Multi-environment Configuration

Generally, a project will have the following three environments:

  • The development environmentdevelopment
  • The test environmentstage
  • The production environmentproduction

Scripts in package.json configure three environments as follows:

"scripts": {
  "serve": "vue-cli-service serve"."build": "vue-cli-service build"."stage": "vue-cli-service build --mode staging"."preview": "vue-cli-service build --report"."lint": "vue-cli-service lint"
}
Copy the code

Create configuration files for different environments in the project root directory:

  • .env.development
NODE_ENV = 'development'

VUE_APP_BASE_API =  '/development-api'
Copy the code
  • .env.staging
NODE_ENV = 'staging'

VUE_APP_BASE_API =  '/staging-api'
Copy the code
  • .env.production
NODE_ENV = 'production'

VUE_APP_BASE_API =  '/production-api'
Copy the code

Vue-cli has officially integrated the Webpack-bundle-Analyzer plugin to analyze webpack build artifacts. Simply execute NPM Run Preview to build the package and generate the report.html help to analyze the package contents. Open the file as follows:

Rem adaptation

Since Vant uses PX as the default style unit, the following two tools are recommended for rem use:

  • postcss-pxtoremIs aPostCSSA plug-in that willpxUnit conversion toremunit
  • lib-flexibleUsed to set theremAt baseline
/ / installation
npm install vant@next -S
npm install amfe-flexible -S
npm install postcss -D
npm install postcss-pxtorem@5.11. -D
Copy the code

Postcss 8.3.0 is incompatible with PostCSS-pxtorem 6.0.0. Use [email protected].

Introduce lib-flexible in main.js:

import 'amfe-flexible'
Copy the code

Then create a new postcss.config.js file in the root directory:

module.exports = {
  plugins: {
    Postcss-pxtorem plugin version requires >= 5.0.0
    'postcss-pxtorem': {
      rootValue({ file }) {
        // The vant component rootValue uses 37.5
        return file.indexOf('vant')! = = -1 ? 37.5 : 75
      },
      propList: [The '*']}}}Copy the code

The current postCSS-Pxtorem default is 6.0.0, which is incompatible with VUE-CLI4. So postCSS-PxtoREM had better be 5.1.1.

Vant UI set rootValue: 37.5, we used lib-flexible adaptation, so 1rem on iPhone6 equals 37.5px.

Since Vant uses PX as a style unit by default, let’s say a Vant component has an element that is 375px wide. Set rootValue to 37.5 via the plugin and convert REM to 10REM. If the plugin rootValue is changed to 75, convert REM to 5REM. But the actual font size is 37.5px. So Vant’s actual CSS is 187.5px, doubling its size.

Therefore, we can add a vant file to determine whether it is a vant file. If it is a vant file, the base is rootValue: 37.5. If the UI design is 750px, we don’t need to take the design to calculate, and write CSS styles 1:1.

Vants are loaded on demand

It is recommended to import components on demand using the official documentation and install the babel-plugin-import plug-in:

npm install babel-plugin-import -D
Copy the code

Then configure babel.config.js in the root directory:

module.exports = {
  presets: [
    '@vue/cli-plugin-babel/preset'].plugins: [['import',
      {
        libraryName: 'vant'.libraryDirectory: 'es'.style: true}}]]Copy the code

It is possible to unify the management of components under SRC /plugins/ vuant.js and write the work of the imported components into a separate JS file as follows:

import { Button, Tabbar, TabbarItem } from 'vant'

export default function(app) {
  app.use(Button).use(Tabbar).use(TabbarItem)
}
Copy the code

Then add it to the main.js file:

import { createApp } from 'vue'
import useVant from '@/plugins/vant'
const app = createApp(App)
// Register the vant component
useVant(app)
Copy the code

Vuex status management

Vuex uses modular management to write a counter demo. The following directory structure:

├ ─ ─ store │ ├ ─ ─ modules │ │ └ ─ ─ counter. Js │ ├ ─ ─ index, jsCopy the code

Index. Js as follows:

import { createStore } from 'vuex'

const modulesFiles = require.context('./modules'.true./\.js$/)

// Automatic assembly module
const modules = modulesFiles.keys().reduce((modules, modulePath) = > {
  const moduleName = modulePath.replace(/^\.\/(.*)\.\w+$/.'$1')
  const value = modulesFiles(modulePath)
  modules[moduleName] = value.default
  return modules
}, {})

export default createStore({
  modules
})
Copy the code

Counter. Js as follows:

const state = function() {
  return {
    count: 0}}const mutations = {
  increment(state, count) {
    state.count++
  }
}
const actions = {
  incrementAsync({ commit }) {
    return new Promise((resolve, reject) = > {
      setTimeout(() = > {
        commit('increment')
        resolve()
      }, 1000)}}}const getters = {
  evenOrOdd(state) {
    return state.count % 2= = =0 ? 'even' : 'odd'}}export default {
  namespaced: true,
  state,
  getters,
  actions,
  mutations
}
Copy the code

In the main.js file:

import { createApp } from 'vue'
import store from './store'
app.use(store)
Copy the code

Usage:

<template>
  <div>{{ count }} is {{ evenOrOdd }}.</div>
  <button @click="incrementAsync">Increment async</button>
</template>
<script>
import { computed } from '@vue/runtime-core'
import { useStore } from 'vuex'
export default {
  name: 'App'.setup() {
    const store = useStore()
    return {
      count: computed(() = > store.state.counter.count),
      evenOrOdd: computed(() = > store.getters['counter/evenOrOdd']),
      incrementAsync: () = > store.dispatch('counter/incrementAsync')}}}</script>
Copy the code

Vue – the Router routing

This template is developed in hash mode and managed using modularity.

├ ─ ─ the router │ ├ ─ ─ modules │ │ └ ─ ─ tabbar. Js │ ├ ─ ─ index, jsCopy the code

Index. Js as follows:

import { createRouter, createWebHashHistory } from 'vue-router'
import tabBar from './modules/tabBar'
// Static route
export const constantRoutes = [tabBar]

// Dynamic routing
export const asyncRoutes = []

const router = createRouter({
  // The new history configuration replaces mode
  history: createWebHashHistory(),
  routes: constantRoutes
  // In Vue3, the object x returned by scrollBehavior is renamed left and y is renamed top
  // scrollBehavior: () => ({ top: 0 })
})

export default router
Copy the code

Define the bottom navigation modules/ tabbar.js, and create layout, Home, and my components in the corresponding directory:

import Layout from '@/layout'
export default {
  path: '/'.component: Layout,
  meta: { title: 'home'.keepAlive: true },
  redirect: '/home'.children: [{path: 'home'.name: 'home'.component: () = > import('@/views/home'),
      meta: { title: 'home'.keepAlive: true}}, {path: 'my'.name: 'my'.component: () = > import('@/views/my'),
      meta: { title: 'I'.keepAlive: true}}}]Copy the code

In the main.js file:

import { createApp } from 'vue'
import router from './router'

app.use(router)
Copy the code

Axios encapsulation

Install axios:

npm install axios -S
Copy the code

Encapsulate axios in SRC /utils/request.js:

import axios from 'axios'
import store from '@/store'
const servie = axios.create({
  baseURL: process.env.VUE_APP_BASE_API,
  timeout: 10000.headers: {
    'Content-Type': 'application/json; charset=UTF-8'}})// Add request interceptor
servie.interceptors.request.use(config= > {
  if (store.getters.accessToken) {
    config.headers.accessToken = ' '
  }
  return config
}, error= > {
  console.log('err:' + error)
  return Promise.reject(error)
})

// Add a response interceptor
servie.interceptors.response.use(response= > {
  const res = response.data
  if(res.code ! = =200) {
    return Promise.reject(res.msg || 'error')}else {
    return res
  }
}, error= > {
  console.log('err:' + error)
  return Promise.reject(error)
})

export default servie
Copy the code

SRC/API: SRC/API: SRC/API

// common.js

export function getInfo(data) {
  return request({
    url: '/info'.method: 'get',
    data
  })
}
Copy the code

Vue. Config. Js configuration

const path = require('path')
const resolve = dir= > path.join(__dirname, dir)

module.exports = {

  publicPath: '/'.outputDir: 'dist'.// Directory of the production environment build files
  assetsDir: 'static'.// outputDir static resources (js, CSS, img, fonts) directory
  lintOnSave: false.productionSourceMap: false.// If you don't need the source map for production, you can set it to false to speed up production builds.

  // Cross-domain configuration
  devServer: {
    disableHostCheck: true.proxy: {
      // / API If there is this string in the network request, then match the proxy
      '/api': {
        target: 'http://xx.xxx.com'.// Point to the development environment API server
        changeOrigin: true.// If set to true, host is set to target in the send request header
        ws: true./ / open the webSocket
        // Rewrite the path to replace the target address with an empty string that starts with/API
        pathRewrite: {
          '^/api': ' '}}},hot: true.// When set to true, an error will be thrown if compilation fails. Changing to correct will trigger a recompilation and the entire browser will refresh again!
    port: 8999./ / the port number
    open: true.// Open the browser after startup
    Display full screen overlay in the browser when compiler errors or warnings occur
    overlay: {
      warnings: true.errors: true}},chainWebpack: config= > {
    / / alias alias
    config.resolve.alias
      .set(The '@', resolve('src'))

    config.plugin('preload').tap(() = >[{rel: 'preload'.fileBlacklist: [/\.map$/./hot-update\.js$/./runtime\.. *\.js$/].include: 'initial'
      }
    ])
    config.plugins.delete('prefetch') config.when(process.env.NODE_ENV ! = ='development'.config= > {
      config
        .plugin('ScriptExtHtmlWebpackPlugin')
        .after('html')
        .use('script-ext-html-webpack-plugin'[{// The runtime is introduced as inline and does not stand alone
            inline: /runtime\.. *\.js$/
          }
        ])
        .end()
      config.optimization.splitChunks({
        chunks: 'all'.cacheGroups: {
          // Multiple groups can be configured under cacheGroups. Each group is conditional on test and the module that matches the test condition
          commons: {
            name: 'chunk-commons'.test: resolve('src/components'),
            minChunks: 3.// Have been used at least three times to pack and separate
            priority: 5./ / priority
            reuseExistingChunk: true // Indicates whether to use the existing chunk. True indicates that if the chunk contains modules that have been extracted, no new chunk will be generated.
          },
          node_vendors: {
            name: 'chunk-libs'.chunks: 'initial'.// Package only the third party that you originally relied on
            test: /[\\/]node_modules[\\/]/,
            priority: 10
          },
          vantUI: {
            name: 'chunk-vantUI'.// Unpack vantUI separately
            priority: 20.// The number is heavily weighted to the one with the highest weight when multiple cacheGroups are met
            test: /[\\/]node_modules[\\/]_? vant(.*)/
          }
        }
      })
      config.optimization.runtimeChunk('single')}}}Copy the code

4. Node back-end

Originally I wanted to use egg.js for development, but node.js was not very friendly for me to understand the whole project. So I read a lot of articles in addition to start to write a KOA-based MVC back-end template, their own code on this basis is also good to use.

Problems that need to be solved

There are quite a few things to consider when creating a NodeJS back-end template by hand, but here are some of the things that come to mind:

  • Rapid debugging
  • Error handling
  • logging
  • File directory hierarchy
  • Data validity check
  • Distinguish between development and production environments

.

And then it’s a step by step process.

Project directory structure

First of all, list the basic directory structure of the project, have a general understanding:

├ ─ ─ bin// Project startup directory├ ─ ─ the WWW/ / configuration items├ ─ ─ logs// Log file├ ─ ─ the SRC/ / the source code├ ─ ─ the config// Public configuration file└ ─ ─ index. Js └ ─ ─ the config - development. Js// Development environment└ ─ ─ the config - production. Js// Production environment└ ─ ─ the config - the staging. Js// Test the environment├ ─ ─ controllers/ / the routing layer└ ─ ─ userController. Js ├ ─ ─ the db// Database connection configuration└ ─ ─ index. Js └ ─ ─ init - db. Js// Automatically generate database tables├ ─ ─ lib// Third-party dependency libraries└ ─ ─ WXBizDataCrypt. Js// Such as wechat small program encryption and decryption├ ─ ─ middlewares/ / middleware└ ─ ─ errorHandle. Js// Error handling middleware└ ─ ─ getTokenData. Js// Get token middleware└ ─ ─ myLogger. Js// Write to logging middleware├ ─ ─ models// Database model└ ─ ─ index. Js// Automatically load models└ ─ ─ user. Js// user user model├ ─ ─ services/ / service layer└ ─ ─ index. Js// Automatically load services└ ─ ─ userService. Js// User services layer├ ─ ─ utils/ / tool library└ ─ ─ response. Js// Customize response└ ─ ─ exception. Js// Custom exception├ ─ ─. Eslintrc. Js/ / eslint configuration├ ─ ─ app. Js// Application entry├ ─ ─ ecosystem. Config. Js// PM2 configuration file├ ─ ─ logs. Config. Js// Log configuration├ ─ ─ package - lock. Json ├ ─ ─ package. The json// Project dependencies

Copy the code

Nodemon and cross-env are introduced

Nodemon is used to monitor any changes in node.js applications and automatically restart the service.

Cross-env is used to deal with inconsistencies in how Windows and other Unix systems write environment variables.

The following dependencies need to be installed:

npm install cross-env -D
npm install nodemon -D
Copy the code

Nodemon and cross-env are used in package.json below.

Node development and Debugging

The inspect command starts debug mode.

Enter Chrome ://inspect in the Chrome address bar to see the screen below.

In the Target section, click the Inspect link to start debugging.

The second way is to open developer Tools, which has a Green Node logo in the top left corner. Click to debug.

See this article for more details.

Pm2 and development environment configuration

Because this project is based on KOA2, the environment configuration needs to be installed first:

npm install koa -S
Copy the code

1. Create a WWW file

Create a WWW executable file under the bin folder to run, shut down, and restart the service.

#! /usr/bin/env node

/** * Module dependencies. */
var app = require('.. /app')
var http = require('http')

var port = normalizePort(process.env.PORT || '3000')

/** * Create HTTP server. */
var server = http.createServer(app.callback())

/** * Listen on provided port, on all network interfaces. */
server.listen(port)
server.on('error', onError)
server.on('listening', onListening)

/** * Normalize a port into a number, string, or false. */
function normalizePort(val) {
  var port = parseInt(val, 10)
  if (isNaN(port)) {
    // named pipe
    return val
  }
  if (port >= 0) {
    // port number
    return port
  }
  return false
}

/** * Event listener for HTTP server "error" event. */
function onError(error) {
  if(error.syscall ! = ='listen') {
    throw error
  }
  var bind = typeof port === 'string'
    ? 'Pipe ' + port
    : 'Port ' + port

  // handle specific listen errors with friendly messages
  switch (error.code) {
    case 'EACCES':
      console.error(bind + ' requires elevated privileges')
      process.exit(1)}}/** * Event listener for HTTP server "listening" event. */
function onListening() {
  var addr = server.address()
  var bind = typeof addr === 'string'
    ? 'pipe ' + addr
    : 'port ' + addr.port
  console.log('Listening on ' + bind)
}
Copy the code

Here is the koA-generator program start code, according to our specific situation simple modification direct use can be. Then create app.js and import koa2:

const Koa = require('koa')
const app = new Koa()

app.use(async(ctx, next) => {
  ctx.body = 'Hello World'
})

module.exports = app
Copy the code

2. Install the global PM2

$ npm install pm2@latest -g
# or
$ yarn global add pm2
Copy the code

After the global installation is complete, configure the multi-environment running in the script configuration of package.json and the configuration file file.config. js read by pM2 by default.

// ecosystem.config.js
module.exports = {
  Pm2 can manage multiple applications
  apps: [{name: 'www'.// Application process name
      script: './bin/www'.// Start the script path
      ignore_watch: ['node_modules'.'logs'].// Ignore listening files
      args: ' '.// Parameters passed to the script
      instances: 1.// Number of application instances. This parameter is valid only in cluster mode. The default value is fork
      autorestart: true.watch: true.// Listen for restart. With this enabled, applications under folders or subfolders will automatically restart
      max_memory_restart: '1G'.// The maximum memory limit is exceeded and the system restarts automatically
      // Test the environment
      env_staging: {
        'PORT': 8002.'NODE_ENV': 'staging'
      },
      // Production environment
      env_production: {
        'PORT': 80.'NODE_ENV': 'production'}}}]Copy the code

3. Add the environment to package.json


"scripts": {
  "dev": "cross-env NODE_ENV=development PORT=8001 nodemon --inspect bin/www"."stage": "cross-env pm2 start ecosystem.config.js --env staging"."prod": "cross-env pm2 start ecosystem.config.js --env production"."logs": "pm2 logs"."stop": "pm2 stop ecosystem.config.js"
}
Copy the code

The following explains the relevant environment:

npm run dev    // Development environment
npm run stage  // Test the environment
npm run prod   // Production environment
npm run logs   // View PM2 logs
npm run stop   // Stop the PM2 service
Copy the code

Custom exception

When writing code, we ourselves may encounter some unknown error to throw an exception. Such as HTTP errors, parameter errors and so on. We can write our own utility class to handle this.

// exception.js

/** * HTTP exception handling */
class HttpException extends Error {
  constructor(customError = {}) {
    super(a)const defaultError = { message: 'Parameter error'.state: 500.errorCode: 10000 }
    const { message, status, errorCode } = Object.assign(defaultError, customError)
    this.message = message
    this.status = status
    this.errorCode = errorCode
  }
}

Example: throw new ParamsException({message: 'parameter error ', status: 400,errorCode: 10001}) */
class ParamsException extends Error {
  constructor(customError = {}) {
    super(a)const defaultError = { message: 'Parameter error'.status: 400.errorCode: 10001 }
    const { message, status, errorCode } = Object.assign(defaultError, customError)
    this.message = message // Error message returned
    this.status = status // http status code 2xx 4xx 5xx
    this.errorCode = errorCode // Custom error code, for example, 10001}}module.exports = {
  HttpException,
  ParamsException
}
Copy the code

Custom response

Customize response to process the response returned from the front end, extract the response for encapsulation, and return the general format:

/** * Response success format *@param {*} The data data *@param {*} Code Response code *@param {*} MSG message *@returns {}* /
const responseFormat = (data = {}, msg = 'success',code = 200) = > {
  return {
    code,
    msg,
    data
  }
}

module.exports = {
  responseFormat
}
Copy the code

Use of KOA middleware

The middleware uses common open source middleware and some custom middleware.

1. Koa-bodyparser, KOA-static, KOA2-CORS

After installation, it can be used directly:

npm install koa-bodyparser -S
npm install koa-static -S
npm install koa2-cors -S
Copy the code
const static = require('koa-static')
const bodyparser = require('koa-bodyparser')
const path = require('path')
const cors = require('koa2-cors')

// Post request parameter parsing
app.use(bodyparser({
  enableTypes: ['json'.'form'.'text'.'xml']}))// Process static files
app.use(static(path.join(__dirname, './src/public')))
// cross-domain processing
app.use(cors())
Copy the code

2. Customize logger middleware

To better monitor or troubleshoot problems online, record information such as request parameters or errors. Without logs, problems cannot be located. A good log system is very important in project development. Log4js is used in the project to configure project logs. Install first:

npm install log4js -S
Copy the code

Create logs.config.js in the root directory to configure the log output for the test environment and production environment:

/** * Log configuration file */
const log4js = require('log4js')
const path = require('path')

let developmentLogConfig = {}
// Add standard output stream to test environment
if(process.env.NODE_ENV ! = ='production') {
  developmentLogConfig = {
    STDOUT: {
      type: 'stdout'}}}// The name of the file to save the log
const fileAllName = path.join(__dirname, './logs/all.log')
const fileErrorName = path.join(__dirname, './logs/error.log')

log4js.configure({
  /** * If the production environment is in cluster mode,pm2 must be set to true. Otherwise, logs will not take effect. * pm2: process.env.node_env === 'production' */
  appenders: {
    ...developmentLogConfig,
    FILE_ALL: {
      type: 'datefile'.// Log4js will divide the log by date, one file per day, each day will rename the previous all.log to all.2021-06-03.log
      filename: fileAllName,
      backups: 10.// A maximum of 10 logs can be saved
      maxLogSize: 10485760.// Maximum file size is 10 MB
      daysToKeep: 10.// Logs can be saved for a maximum of 10 days. If the value is 0, logs are saved permanently
      keepFileExt: true // Whether to keep the log file name extension
    },
    FILE_ERROR: {
      type: 'datefile'.filename: fileErrorName,
      daysToKeep: 30.keepFileExt: true}},categories: {
    default: {
      appenders: process.env.NODE_ENV ! = ='production' ? ['STDOUT'.'FILE_ALL'] : ['FILE_ALL'].level: 'debug'
    },
    error: {
      appenders: ['FILE_ERROR'].level: 'error'}}})const defaultLogger = log4js.getLogger()
const errorLogger = log4js.getLogger('error')

// Export the corresponding log
module.exports = {
  defaultLogger,
  errorLogger
}
Copy the code

Middlewares: Create middleware myLogger.js in middlewares and register middleware in app.js to log interface request times:

/ / file directory: / SRC/middlewares myLogger. Js
const { defaultLogger } = require('.. /.. /logs.config')

module.exports = function() {
  return async(ctx, next) => {
    const start = new Date(a)await next()
    const ms = new Date() - start
    const logText = `${ctx.method} ${ctx.status} ${ctx.url}Response time:${ms}ms`
    defaultLogger.info(logText)
  }
}

Copy the code

Finally, use middleware in app.js

const myLogger = require('./src/middlewares/myLogger')

// Need to be placed at the top of all middleware
app.use(myLogger())
Copy the code

3. JWT validation middleware

The project uses jSONWebToken to generate tokens, koA-JWT for JWT validation.

npm install jsonwebtoken -S
npm install koa-jwt -S
Copy the code
const jwt = require('jsonwebtoken')
// Can be written to the configuration file
const config = {
  // Custom JWT encrypted private key
  PRIVATE_KEY: 'private_key'.// Expiration time is in seconds
  JWT_EXPIRED: 60 * 60
}

/ / token is generated
const token = jwt.sign({ data: 'testToken'.exp: config.JWT_EXPIRED }, config.PRIVATE_KEY)

ctx.body = {
  token
}
Copy the code

Token verification is then implemented through the KOA-JWT configuration middleware:

// app.js
const koaJwt = require('koa-jwt')
app.use(koaJwt({ secret: 'private_key' }).unless({
  // Set the login and register interfaces without authentication
  path: [
    /^\/user\/login/./^\/user\/register/]}))Copy the code

4. Unified exception handling middleware

Good coding practices are inseparable from exception handling, which not only returns friendly prompts to the client, but also logs the wrong stack information.

// errorHandles.js

const { errorLogger } = require('.. /.. /logs.config')

module.exports = function() {
  return async(ctx, next) => {
    try {
      await next()
    } catch (err) {
      errorLogger.error(err.stack)
      // Exception handling
      ctx.status = err.statusCode || err.status || 500
      ctx.body = {
        msg: err.message || 'Server error'.errorCode: err.errorCode || err.statusCode || err.status || 500}}}}Copy the code
// File directory: app.js
const Koa = require('koa')
const app = new Koa()
const errorHandler = require('./src/middlewares/errorHandler.js')
// Used before JWT middleware validation
app.use(errorHandler())
Copy the code

Data validation

When defining the interface, we cannot trust the data submitted by the user. We need to perform basic verification on the data. Validation is a hassle, and it’s likely that you’ll need to validate data types, lengths, specific rules, and so on. Joi is the data checksum module of HapiJS, which has been highly encapsulated with common checksum functions. Here, JOI will be used to deal with data checksum errors:

/ / installation
npm install joi -S
Copy the code

Here is a demo, the API can be seen in the official joI documentation:

const Joi = require('joi')
const { ParamsException } = require('.. /utils/exception')
router.get('/vali'.async ctx => {
  const schema = Joi.object({
    username: Joi.string().min(1).max(30).required(),
    password: Joi.string().trim().pattern(new RegExp('^ [a zA - Z0-9] {30} 3 $')),})// allowUnknown: true means to ignore extra variables passed in
  const { error } = schema.validate({ username: 'bob'.age: 10 }, { allowUnknown: true })
  if (error) {
    Throw an error with a custom exception
    throw new ParamsException({ message: error.details[0].message, state: 400 })
  }
  ctx.body = {msg:'success'}})Copy the code

When a ParamsException is thrown, the custom global exception handler catches the exception, returns a message to the user, and writes the exception to the log.

Controllers routing layer

Here the KOa-Router is used to build the routing part of the template. First you need to install the KOA-router:

npm install koa-router -S
Copy the code

Create userController.js in the controllers directory as follows:

const Router = require('koa-router')
const router = new Router()

router.prefix('/user')

router.get('/getInfo'.ctx= > {
  ctx.body = { data: 'getInfo'}})module.exports = router
Copy the code

You can then use app.js to read all the controllers routing files so that you don’t have to import them every time you create a new file.

// app.js
const path = require('path')
const fs = require('fs')
const Koa = require('koa')
const app = new Koa()

/* loader router */
const jsFiles = fs.readdirSync(path.join(__dirname, './src/controllers')).filter(file= > file.indexOf('. ')! = =0 && file.endsWith('.js')! = = -1)

jsFiles.forEach(file= > {
  const mapping = require(path.join(__dirname, './src/controllers', file))
  app.use(mapping.routes(), mapping.allowedMethods())
})
Copy the code

The same goes for the Services layer and the Models layer.

The models layer

Before defining the Services layer, you need to define the Models layer model. In this project, mysql is used to operate the database. You need to initialize the database configuration and definitions. The project uses Sequelize, Node’s ORM framework, to manipulate the database. First you need to install:

npm install mysql2 -S
npm install sequelize -S
Copy the code

Define basic information configuration for database connection:

// config-development.js

const config = {
  myEnv: 'development'.mysql: {
    dialect: 'mysql'.host: 'xxx.xx.xx.x'.port: 3306.database: 'template_db'.username: 'root'.// Database user name
    password: '123456'.// Database password
    // Connection pool configuration
    max: 20.min: 10.// How long is the current connection disconnected without operation
    idle: 10000.// How long does it take to get a connection
    acquire: 30000}}module.exports = config
Copy the code

There are several syntax options for creating a model. Here we use sequelize.define to define the model. Because we need to customize some of our own content, instead of using the Sequelize API directly, we can indirectly define the Model by adding our own content to the index.js file under the db folder. As follows:

const { v4: uuidv4 } = require('uuid')
const { mysql } = require('.. /config')
const { Sequelize, DataTypes } = require('sequelize')
const { dialect, host, port, database, username, password, max, min, idle, acquire } = mysql


// Initialize the object
const sequelize = new Sequelize({
  dialect,
  host,
  port,
  database,
  username,
  password,
  pool: {
    max,
    min,
    idle,
    acquire
  },
  define: {
    freezeTableName: true.// Remove the plural form of the table name
    underscored: true // Hump underline conversion
  },
  query: {
    raw: true}})/* * uuid * @returns uuid */
function generateId() {
  return uuidv4().replace(/-/g.' ')}Each model must have createAt,updateAt. * Primary keys are generated using uUID, and the name must be ID. * /
function defineModel(name, attributes, options = {}) {
  constattrs = { ... attributes,id: {
      type: DataTypes.STRING(50),
      primaryKey: true
    },
    // createAt and updateAt use BIGINT to store timestamps.
    createAt: {
      type: DataTypes.BIGINT
    },
    updateAt: {
      type: DataTypes.BIGINT
    }
  }
  // console.log(`model->${name} is create`)
  returnsequelize.define(name, attrs, { ... options,timestamps: false.hooks: {
      // Insert id,createAt,updateAt before create hook
      beforeCreate(instance, options) {
        if(! instance.id) { instance.id = generateId() }const now = Date.now()
        instance.createAt = now
        instance.updateAt = now
      },
      // Update hook before updateAt
      beforeUpdate(instance) {
        instance.updateAt = Date.now()
      }
    }
  })
}

const mysqlDB = {
  defineModel,
  // Generate table structure function in test environment
  sync() {
    return new Promise((resolve, reject) = > {
      if(process.env.NODE_ENV ! = ='production') {
        sequelize.sync({ force: true }).then(() = > {
          console.log('Created successfully! ')
          resolve()
        }).catch(err= > {
          reject(err)
        })
      } else {
        reject('Sync () cannot be used in production')}})}}module.exports = {
  DataTypes,
  mysqlDB,
  sequelize
}
Copy the code

Nodeinit-db.js automatically generates database tables in development mode:

const { sync } = require('.. /models')
/** * Initialize the database */
sync().then(() = > {
  console.log('init db ok! ')
  process.exit(0)
}).catch((e) = > {
  console.log('failed with: ' + e)
  process.exit(0)})Copy the code

Define defineModel (createAt and updateAt); define defineModel (createAt and updateAt); All models are then stored in the Models folder and named after the actual name of the Model. User.js looks like this:

const { mysqlDB, DataTypes } = require('.. /db')

module.exports = mysqlDB.defineModel('user',
  {
    username: {
      type: DataTypes.STRING(255),
      allowNull: false.comment: 'Username'
    },
    password: {
      type: DataTypes.STRING(255),
      allowNull: false.comment: 'password'}}, {tableName: 'user' // Customize the table name})Copy the code

Finally, create index.js under the Models folder to automatically scan all models.

// models/index.js
const fs = require('fs')
const path = require('path')
const { sequelize, mysqlDB } = require('.. /db')

const jsFiles = fs.readdirSync(__dirname).filter((file) = > {
  return file.endsWith('.js') && (file ! = ='index.js') && (file.indexOf('. ')! = =0)})const models = {}

jsFiles.forEach((file) = > {
  const name = file.substring(0, file.length - 3)
  models[name] = require(path.join(__dirname, file))
})

models.sequelize = sequelize

// Create a database table
models.sync = async() = > {return await mysqlDB.sync()
}

module.exports = models

Copy the code

Const {user} = require(‘.. /models/index’) to introduce different models.

Services layer

Services is responsible for manipulating the database logic. As with the models file above, start by creating an index.js file to automatically register each service.

// services/index.js
const fs = require('fs')
const path = require('path')

const jsFiles = fs.readdirSync(__dirname).filter(file= > {
  return file.endsWith('.js') && (file ! = ='index.js') && (file.indexOf('. ')! = =0)})const service = {}

jsFiles.forEach((file) = > {
  const name = file.substring(0, file.length - 3)
  service[name] = require(path.join(__dirname, file))
})

module.exports = service
Copy the code

Const {userService} = require(‘.. /services’) to dynamically import all services. Then create userService.js. Perform database operations:

const { user, sequelize } = require('.. /models')

module.exports = {
  / / login
  async login({ username, password }) {
    return await user.findOne({ where: { username, password }})
  },
  // Edit the user
  async editUser(user) {
    // Update data with 'individualHooks:true' enabled otherwise 'hooks' of' beforeUpdate 'are not triggered.
    const result = await sequelize.transaction(async(t) => {
      const res = await user.update(user, { where: { id: user.id }, transaction: t, individualHooks: true })
      return res
    })
    return result
  },
  // Add a user
  async addUser({ username, password }) {
    // Start the transaction
    const result = await sequelize.transaction(async(t) => {
      const result = await user.create({ username, password }, { transaction: t })
      return result
    })
    return result
  }
}
Copy the code

Ensure that transactions are enabled when multiple SQL statements are executed. Sequelize.transaction is the framework’s operations on transactions, using the database isolation level by default.

Sequelize log

Sequelize Logs are generated on the console by default. You need to modify the default Settings. The logs are generated on the console in the development environment and in the log file in the production environment, as follows:

const sequelize = new Sequelize({

// Omit some other configurations...


logging: process.env.NODE_ENV === 'production' ? sqlLogger : console.log
})

function sqlLogger(msg) {
  defaultLogger.info(msg)
}
Copy the code

Five, the summary

Using common technologies and frameworks, Vue3 and Node front-end projects are combined step by step.

Project github source code, source code some details are not written in the article, like you can go to understand.