Preface:

Preview:

  • Preview of this site: Tencent Cloud IP, domain name Zhanglijian.top
  • Github:github.com/huoguozhang…

Start:

  1. npm i
  2. Configure mysql
  3. npm run server or npm run dev

Functions:

  • User: login, registration, user information modification, details page, similar to the number of Jane book articles, the total number of words, like the total number of harvest, the article deleted.

  • Article: article details page, view, comment, like and step on, the number of times the article read statistics

  • Articles: all articles, support pagination and search by keyword, time
  • Article writing: support markdown and dragging and uploading pictures

  • Home page: article recommendation, author recommendation, home page rotation, top search for articles and users

  • SSR effect preview: similar to Zhihu

  • Seo effect: to be added

1 Technology Stack:

  • Front end: AXIos, Element-UI, nuxt.js, TS
  • The backend: Node.js, hapi.js, Sequelize (ORm), hapi-auth(token), Hapi-swagger (online API documentation), Hapi-Pagination, JOI (Element-like form validation for front-end request data), mysql, and other plug-ins

2. Technical Details:

Note: this paper mainly focuses on the back-end, the final effect is similar to our backend

Directory structure:

├─ Exercises - Assets // Static Resources, CSS, Pictures etc. // Vue component Directory ├─ Config // Default Settings ├─ Layouts // Nuxt View ├─ middleware // Middleware ├─ ├─ Nuxt.config.js ├─ Nuxt.config.ts ├─ Package-Lock. json ├─ Package. json ├─ pages // Hapi And NuxT ├─ routes // Seeders ├─ server // App. Js ├ ─ ─ the static / / static resources ├ ─ ─ store / / nuxt ├ ─ ─ tsconfig. Json ├ ─ ─ uploads / / file upload the target directory └ ─ ─ utils / / auxiliary functionCopy the code

Foreword: Why hapi.js?

The expresstohapi documentation already says a lot, but the most interesting thing for me here is that you don’t have to install a lot of plug-ins (expres has a lot of XX-Parse plug-ins…). And HAPI is already in commercial use.

Note:

These codes of mine can run normally in my current version of package.json, but sometimes there are incompatibilities in the larger version of HAPI. Different versions of HAPI correspond to different plug-in versions, so they need to be consistent with my version. I also encountered @Component unrecognized when adding ts to nuxt.js v2.9.

2.1 Sequelize modeling

The first thought of development background is to build a data model (table), default you have installed mysql before I use the database, do not know the ORM tool, will choose their own navicat such graphical chemical tools to build tables or directly use SQL statements to build tables. There are several disadvantages to this:

  1. Orm data migration can be used to do these things, similar to Git.
  2. Migrate new environment, SQL operation is very troublesome, directly execute ORM command to build tables automatically.
  3. The data model, when the daemon contacted mysql before, we only established the connection pool, data relationship, table structure and so on.
  4. Add, delete, change and check the code more concise and clear
  5. other

Note: Using ORM to perform SQL operations is equivalent to using jquery to perform DOM operations. The API is simpler, but you still need to understand the original

sequelize

Sequelize is the Promise ORm tool for Node.js, and also supports other databases.

use

  1. Install plug-in:
npm i sequelize-cli -D
npm i sequelize
npm i mysql2
Copy the code
  1. Sequelize init sequelize init sequelize cli init sequelize cli init sequelize cli
NPX node_modules/.bin/sequelize initCopy the code
├ ─ ─ the configProject configuration directory| ├ ─ ─ config. JsonConfigure the database connection├ ─ ─ models# database model| ├ ─ ─ index. JsDatabase connection boilerplate code├ ─ ─ migrations# Data migration directory├ ─ ─ seeders# data-populated directory
Copy the code

config/config.json

The default build file is a config.json file, which has three default boilerplate environments for development, test, and production. We can add more Settings as needed. Here, I use config.js instead of config.json, so that the configuration is more flexible. The modified config/config.js is as follows, and only two environments, development and production, are reserved. The configuration parameters of the development environment and production environment can be separated into.env and.env.prod files, which are dynamically distinguished by the environment variable parameter process.env.node_env.

// config.js
if (process.env.NODE_ENV === 'production') {
  require('env2') ('./.env.prod')}else {
  require('env2') ('./.env.dev')
}

const { env } = process
module.exports = {
  'development': {
    'username': env.MYSQL_USERNAME,
    'password': env.MYSQL_PASSWORD,
    'database': env.MYSQL_DB_NAME,
    'host': env.MYSQL_HOST,
    'port': env.MYSQL_PORT,
    dialect: 'mysql',
    logging: false// mysql execution log timezone:'+ 08:00'
    // "operatorsAliases": false},},},},},},}'production': {
    'username': env.MYSQL_USERNAME,
    'password': env.MYSQL_PASSWORD,
    'database': env.MYSQL_DB_NAME,
    'host': env.MYSQL_HOST,
    'port': env.MYSQL_PORT,
    dialect: 'mysql',
    timezone: '+ 08:00'
    // "operatorsAliases": false}}}}}}}}}}}}Copy the code

.env.dev

# Service startup name and port, but you can leave the default value blank, the default value is only to reduce the initial data configuration work
HOST = 127.0.0.1
PORT = 80
The port should be 80, otherwise the AXIos URL should be absolute
MySQL database link configurationMYSQL_HOST = 111.111.111.111 MYSQL_PORT = 3306 MYSQL_DB_NAME = database name MYSQL_USERNAME = database user name MYSQL_PASSWORD = database password JWT_SECRET = Token keyCopy the code
  1. Creating a database
npx sequelize db:create
Copy the code
  1. Create migration files
npx migration:create --name user
Copy the code

In the migrations directory, a timestamp-user. js migration file is added. The automatically generated file contains two empty functions: up is used to define the details of the forward change of the table structure, and Down is used to define the rollback logic of the table structure. For example, if up has the createTable creation action, down has a corresponding dropTable deletion action. This is equivalent to an operation log. The modified user migration file is as follows:

'use strict'
module.exports = {
  up: (queryInterface, Sequelize) => queryInterface.createTable(
    'user',
    {
      uid: {
        type: Sequelize.UUID,
        primaryKey: true
      },
      nickname: {
        type: Sequelize.STRING,
        allowNull: false,
        unique: true
      },
      avatar: Sequelize.STRING,
      description: Sequelize.STRING,
      username: {
        type: Sequelize.STRING,
        allowNull: false,
        unique: true
      },
      password: {
        type: Sequelize.STRING,
        allowNull: false
      },
      created_time: Sequelize.DATE,
      updated_time: Sequelize.DATE
    },
    {
      charset: 'utf8'
    }
  ),

  down: queryInterface => queryInterface.dropTable('user')}Copy the code
  1. Perform the migration
npx sequelize db:migrate
Copy the code

Sequelize db: Migrate The sequelize db: Migrate command will enable you to define migrations in the Migrations directory and perform migrations one by one in time-stamped order. Also, by default, a table named SequelizeMeta is created in the database to record the historical versions of migrations that have been run on the current database. You can run the sequelize db:migrate:undo command to run the down command of the last migrated file.

  1. The seed data

perform

sequelize seed:create --name init-user
Copy the code

Similarly generate a file in the seeders directory with timestamp -init-user.js modified

'use strict'
const uuid = require('uuid')
const timeStamp = {
  created_time: new Date(),
  updated_time: new Date()
}
const users = []
for (let i = 1; i < 5; i++) {
  users.push(
    {
      uid: uuid(), username: 'zlj' + i, password: '123', nickname: 'hot pot' + 1, ...timeStamp
    }
  )
}
module.exports = {
  up: queryInterface => queryInterface.bulkInsert('user', users, { charset: 'utf-8' }),
  down: (queryInterface, Sequelize) => {
    const { Op } = Sequelize
    return queryInterface.bulkDelete('user', { uid: { [Op.in]: users.map(v => v.uid) } }, {})
  }
}


Copy the code

Executing the fill command

sequelize db:seed:all
Copy the code

See the database user table for more records. Other operations are similar to migrations. For more, see the document 7 Defining model User table Models /user.js

const moment = require('moment')
module.exports = (sequelize, DataTypes) => sequelize.define(
  'user',
  {
    uid: {
      type: DataTypes.UUID,
      primaryKey: true
    },
    avatar: DataTypes.STRING,
    description: DataTypes.STRING,
    nickname: {
      type: DataTypes.STRING,
      unique: true,
      allowNull: false
    },
    username: {
      type: DataTypes.STRING,
      allowNull: false,
      unique: true
    },
    password: {
      type: DataTypes.STRING,
      allowNull: false
    },
    created_time: {
      type: DataTypes.DATE,
      get () {
        return moment(this.getDataValue('created_time')).format('YYYY-MM-DD HH:mm:ss')
      }
    },
    updated_time: {
      type: DataTypes.DATE,
      get () {
        return moment(this.getDataValue('updated_time')).format('YYYY-MM-DD HH:mm:ss')
      }
    }
  },
  {
    tableName: 'user'})Copy the code
  1. Instantiate seqlize modes/index.js
'use strict'
const fs = require('fs')
const path = require('path')
const uuid = require('uuid')
const Sequelize = require('sequelize')
const basename = path.basename(__filename) // eslint-disable-line
const configs = require(path.join(__dirname, '.. /config/config.js'))
const db = {}
const env = process.env.NODE_ENV || 'development'const config = { ... configs[env], define: { underscored:true,
    timestamps: true,
    updatedAt: 'updated_time',
    createdAt: 'created_time',
    hooks: {
      beforeCreate (model) {
        model.uid = uuid()
      }
    }
  }
}
let sequelize
if (config.use_env_variable) {
  sequelize = new Sequelize(process.env[config.use_env_variable], config)
} else {
  sequelize = new Sequelize(config.database, config.username, config.password, config)
}
fs
  .readdirSync(__dirname)
  .filter((file) => {
    return (file.indexOf('. ')! == 0) && (file ! == basename) && (file.slice(-3) ==='.js')
  })
  .forEach((file) => {
    const model = sequelize.import(path.join(__dirname, file))
    db[model.name] = model
  })
Object.keys(db).forEach((modelName) => {
  ifAssociate (db[modelName]. Associate (DB)}}) db.Sequelize = Sequelize db.Sequelize = Sequelize // Foreign key association relationship Db.user. hasMany(db.article, {foreignKey:'uid' })
db.article.belongsTo(db.user, { foreignKey: 'author' })
db.user.hasMany(db.comment, { foreignKey: 'uid' })
db.comment.belongsTo(db.user, { foreignKey: 'author' })
db.user.hasMany(db.article_like, { foreignKey: 'uid' })
db.article_like.belongsTo(db.user, { foreignKey: 'author' })
db.article.hasMany(db.comment)
db.comment.belongsTo(db.article)
db.article.hasMany(db.article_like)
db.article_like.belongsTo(db.article)
module.exports = db
Copy the code
  1. The functions used in this project

    Multi-table query, single table add, delete, change, search, model unified configuration, migration and seed filling, transaction (when deleting articles, the article related data: comments, reading, like data are also deleted.) And so on.

2.2 Verification of Joi request parameters

Joi validates the request parameters

Use:

  1. The installation
Install joI plugin for HAPI V16
npm i joi@14
Copy the code
  1. See 2.3 config.validate for more information

2.3 Using HAPI to Write interfaces

Post: login interface: routes/user.js

const models = require('.. /models')
const Joi = require('@hapi/joi')
{
    method: 'POST',
    path: '/api/user/login',
    handler: async (request, h) => {
      const res = await models.user.findAll({
        attributes: {
          exclude: ['password'.'created_time'.'updated_time']},where: {username: request. Content. The username, / / libraries commonly existing password encryption, such as md5 password: request. Content. The password}}) const data = res [0]if (res.length > 0) {
        return h.response({
          code: 0,
          message: 'Login successful! ', data: {// Write token Token: generateJWT(data.uid)... data.dataValues } }) }else {
        return h.response({
          code: 10,
          message: 'Wrong username or password'
        })
      }
    },
    config: {
      auth: false,
      tags: ['api'.'user'],
      description: 'User Login',
      validate: {
        payload: {
          username: Joi.string().required(),
          password: Joi.string().required()
        }
      }
    }
  },

Copy the code

2.4 Interface Document Swagger

  1. Installation:
npm i hapi-swagger@10
npm i inert@5
npm i vision@5
npm i package@1
Copy the code
  1. use
├ ─ ─ the plugins# haPI plugin configuration| ├ ─ ─ hapi - swagger. JsCopy the code

hapi-swagger.js

// plugins/hapi-swagger.js
const inert = require('@hapi/inert')
const vision = require('@hapi/vision')
const package = require('package')
const hapiSwagger = require('hapi-swagger')
module.exports = [
  inert,
  vision,
  {
    plugin: hapiSwagger,
    options: {
      documentationPath: '/docs',
      info: {
        title: 'my-blog interface document ', version: package.version}, // Define the interface using the tags attribute as grouping:'tags',
      tags: [
        { name: 'user', description: 'User interface' },
        { name: 'article', description: 'Article interface'}]}}]Copy the code

server/index.js

const pluginHapiSwagger = require('.. /plugins/hapi-swagger'// Register plugins... Await server.register([// use hapi-swagger...pluginHapiSwagger]...Copy the code

Open your dev.host:dev.port/docs to see what’s on my site

2.5 Token Authentication Hapi-auth-JWT2

Cookie Hapi has resolved this for you, as has file uploads

  1. Install: NPM I hapi-auth-jwt2@8
  2. Configuration: Documentation
├ ─ ─ the plugins# haPI plugin configuration│ ├ ─ ─ hapi - auth - jwt2. Js# JWT config plugin
Copy the code

hapi-auth-jwt2.js

const validate = (decoded) => {
  // eslint disable// decoded = JWT payload decoded const {exp} = decodedif (new Date(exp * 1000) < new Date()) {
    const response = {
      code: 4,
      message: 'Login expired',
      data: 'Login expired'
    }
    return { isValid: true, response }
  }
  return { isValid: true }
}
module.exports = (server) => {
  server.auth.strategy('jwt'.'jwt', {// Add the jwtSecret configuration in config/index.js and manage it outside the.git repository via process.env.jwt_secret. key: process.env.JWT_SECRET, validate, verifyOptions: { ignoreExpiration:true
    }
  })
  server.auth.default('jwt')}Copy the code
  1. Register the plug-in server/index.js
const hapiAuthJWT2 = require('hapi-auth-jwt2')... await server.register(hapiAuthJWT2) ...Copy the code
  1. Effect: By default, all interfaces require token authentication. You can disable an interface (such as the login interface)config. Auth = false to return to the login interface
const generateJWT = (uid) => {
  const payload = {
    userId: uid,
    exp: Math.floor(new Date().getTime() / 1000) + 24 * 60 * 60
  }
  return JWT.sign(payload, process.env.JWT_SECRET)
}
handler () {
      const res = await models.user.findAll({
        attributes: {
          exclude: ['password'.'created_time'.'updated_time']},where: {
          username: request.payload.username,
          password: request.payload.password
        }
      })
      const data = res[0]
      if (res.length > 0) {
        return h.response({
          code: 0,
          message: 'Login successful! ', data: { token: generateJWT(data.uid), ... data.dataValues } }) }else {
        return h.response({
          code: 10,
          message: 'Wrong username or password'}}})Copy the code

Client/API /index.ts

request.interceptors.request.use((config: AxiosRequestConfig): AxiosRequestConfig => {
  const token = getToken()
  if (token) { config.headers.authorization = token }
  return config
})
Copy the code
  1. Add Joi validation to the request header
Const jwtHeaderDefine = {headers: Joi.object({authorization: Joi.string().required()}).unknown()} // Some interface... validate: { ... jwtHeaderDefine, params: { uid: Joi.string().required() } } ...Copy the code

The change can be seen in Swagger’s online documentation

2.6 Hapi-pagination

  1. Install NPM I hapi-pagination@3
  2. Configure plugins/hapi – pagination. Js
const hapiPagination = require('hapi-pagination')
const options = {
  query: {
    page: {
      name: 'the_page' // The page parameter will now be called the_page
    },
    limit: {
      name: 'per_page', // The limit will now be called per_page
      default: 10 // The default value will be 10
    }
  },
  meta: {
    location: 'body', // The metadata will be put in the response body
    name: 'metadata', // The meta object will be called metadata
    count: {
      active: true,
      name: 'count'
    },
    pageCount: {
      name: 'totalPages'
    },
    self: {
      active: false // Will not generate the self link
    },
    first: {
      active: false // Will not generate the first link
    },
    last: {
      active: false // Will not generate the last link
    }
  },
  routes: {
    include: ['/article'}} module.exports = {plugin: hapiPagination, options}Copy the code
  1. To register the plugin
const pluginHapiPagination = require('./plugins/hapi-pagination');
await server.register([
  pluginHapiPagination,
])
Copy the code
  1. Add parameter check
const paginationDefine = {
  limit: Joi.number().integer().min(1).default(10)
    .description('Items per page'),
  page: Joi.number().integer().min(1).default(1)
    .description('Page number'),
  pagination: Joi.boolean().description('Whether paging is enabled, default is true'} // joI check... validate: { query: { ... paginationDefine } } ...Copy the code
  1. Database query
   const { rows: results, count: totalCount } = await models.xxxx.findAndCountAll({
      limit: request.query.limit,
      offset: (request.query.page - 1) * request.query.limit,
    });
Copy the code

3 the last

Welcome to the online address to experience the full function

1 Step pit summary:

  • In the case of interface 500, you can catch errors after model operations, such as models.findall ().catch(e => console.log(e))
  • Note version compatibility issues. Plug-ins are compatible with haPI or NUXT versions
  • The configuration of nuxt.config.ts does not take effect. You can manually compile TSC nuxt.config.ts
  • In asyncData request data, do not write absolute address, will default to request port 80

2 Development and Harvest

  • Familiarize yourself with the basic back-end development process
  • In the case of incompatible plug-ins or other requirements, you must look at the English document yourself and feel comfortable with it
  • There is a lot of work to be done in back-end development, from interface to deployment and so on, so we need to understand each other in the future

3 reference

“Hapi-based Node.js Applet Backend Development Practice Guide” by Ye Shengfei

Ps: welcome to like star ^_^ github: github.com/huoguozhang…