The last article was about the back-end rendering project – egg.js testing the waters – weather forecasts. But no database was introduced. The trial project of this time is to add, delete, change and check the article, introduce the database, and realize the separation of the front and back ends.

The github address of the project is egg-demo/article-project.

Let’s get straight to the point ~🙆

The project structure

Article - Project ├── School Exercises ─ Service └.mdCopy the code

Since the project is separated from the front and back ends, we store the client in the folder client and the server in the folder Service. Readme. md is the project description file.

Client initialization

For a quick demonstration, we used vue-CLI scaffolding to help us generate the project and introduced vue-ant-Design.

Project initialization

Yarn is recommended for package management.

$ npm install -g @vue/cli
# or
$ yarn global add @vue/cli
Copy the code

Then create a new project.

$ vue create client
Copy the code

Then we went into the project and started.

$ cd client
$ npm run serve
# or
$ yarn run serve
Copy the code

At this point, we go to the browser address http://localhost:8080/ and see the welcome page.

Finally, we introduce ant-design-vue.

$ npm install ant-design-vue
# or
$ yarn add ant-design-vue
Copy the code

Here, we introduce the components of Ant-Design-Vue globally. In real development, on-demand is friendlier, especially if only part of the UI framework’s functional components are used.

// src/main.js

import Vue from 'vue'
import App from './App.vue'
import Antd from 'ant-design-vue'
import 'ant-design-vue/dist/antd.css'

Vue.use(Antd)
Vue.config.productionTip = false;

new Vue({
  render: h= > h(App),
}).$mount('#app');
Copy the code

Of course, there are several NPM packages involved in this project, and then just write YARN or NPM command line operations.

Routing setting

The vue-Router is required to redirect routes.

# routing
$ yarn add vue-router

# the progress bar
$ yarn add nprogress
Copy the code

Only the login page, the home page, the article list page and the New/edit page are used here. So my route configuration is as follows:

// src/router/index.js

import Vue from 'vue'
import Router from 'vue-router'
import Index from '@/views/index'
import { UserLayout, BlankLayout } from '@/components/layouts'
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css' // progress bar style

const whiteList = ['login'] // no redirect whitelist

import { getStore } from "@/utils/storage"

Vue.use(Router)

const router = new Router({
  routes: [{path: '/'.name: 'index'.redirect: '/dashboard/workplace'.component: Index,
        children: [{path: 'dashboard/workplace'.name: 'dashboard'.component: () = > import('@/views/dashboard')}, {path: 'article/list'.name: 'article_list'.component: () = > import('@/views/article/list')}, {path: 'article/info'.name: 'article_info'.component: () = > import('@/views/article/info'}]}, {path: '/user'.component: UserLayout,
      redirect: '/user/login'.// hidden: true,
      children: [{path: 'login'.name: 'login'.component: () = > import(/* webpackChunkName: "user" */ '@/views/user/login'}]}, {path: '/exception'.component: BlankLayout,
      redirect: '/exception/404'.children: [{path: '404'.name: '404'.component: () = > import(/* webpackChunkName: "user" */ '@/views/exception/404'}]}, {path: The '*'.component: () = > import(/* webpackChunkName: "user" */ '@/views/exception/404')},// base: process.env.BASE_URL,
  scrollBehavior: () = > ({ y: 0 }),
})

router.beforeEach((to, from, next) = > {
  NProgress.start() // start progress bar
  if(getStore('token'.false)) { / / a token
    if(to.name === 'index' || to.path === '/index' || to.path === '/') {
      next({ path: '/dashboard/workplace'})
      NProgress.done()
      return false
    }
    next()
  } else {
    if(to.path ! = ='/user/login') {(new Vue()).$notification['error'] ({message: 'Authentication failed, please log in again! '})}if(whiteList.includes(to.name)) {
      // Enter the whitelist directly
      next()
    } else {
      next({
        path: '/user/login'.query: {
          redirect: to.fullPath
        }
      })
      NProgress.done()
    }
  }
  next()
})

router.afterEach(route= > {
  NProgress.done()
})

export default router
Copy the code

Interface request Setting

The interface request uses AXIOS, so let’s integrate.

# axios
$ yarn add axios
Copy the code

The address of the back-end service we are going to proxy is 127.0.0.1:7001, so our configuration is as follows:

// vue.config.js.devServer: {
    host: '0.0.0.0'.port: '9008'.https: false.hotOnly: false.proxy: { // Configure cross-domain
      '/api': {
        // The domain name of the cross-domain API to access
        target: 'http://127.0.0.1:7001/'.ws: true.changOrigin: true}},},...Copy the code

We encapsulate the request 🙂

// src/utils/request.js

import Vue from 'vue'
import axios from 'axios'
import store from '@/store'
import notification from 'ant-design-vue/es/notification'
import { ACCESS_TOKEN } from '@/store/mutation-types'
import { notice } from './notice';

const err = (error) = > {
  if (error.response) {}
  return Promise.reject(error)
}

function loginTimeOut () {
  notification.error({ message: 'Invalid login information'.description: 'Please log in again' })
  store.dispatch('user/logout').then(() = > {
    setTimeout(() = > {
      window.location.reload()
    }, 1500)})}// Create an auth Axios instance
const auth = axios.create({
  headers: {
    'Content-Type': 'application/json; charset=UTF-8'.'X-Requested-With': 'XMLHttpRequest'
  },
  baseURL: '/'.// api base_url
  timeout: 10000 // Request timeout is 10 seconds
})

// request interceptor
auth.interceptors.request.use(config= > {
  const token = Vue.ls.get(ACCESS_TOKEN)
  if (token) {
    config.headers[ 'Authorization' ] = 'JWT '+ token // Allow each request to carry a user-defined token. Change the token based on the actual situation
  }
  return config
}, err)

// response interceptor
auth.interceptors.response.use(
  response= > {
    if (response.code === 10140) {
      loginTimeOut()
    } else {
      return response.data
    }
  }, 
  error= > { // Error handling
    console.log(error.response, 'come here')
    if(error.response && error.response.status === 403) {
      notice({
          title: 'Not authorized, you do not have access, please contact the administrator! ',},'notice'.'error'.5)
      return
    }
    notice({
        title: (error.response && error.response.data && error.response.data.msg) || (error.response && `${error.response.status} - ${error.response.statusText}`),},'notice'.'error'.5)})export {
  auth
}

Copy the code

Style preprocessor

Of course, in order to better manage your page style, it is recommended to add a CSS preprocessor. Here I chose the less preprocessor.

# less and less - loader
$ yarn add less --dev
$ yarn add less-loader --dev
Copy the code

You can’t just install it. Let’s configure it.

// vue.config.js.css: {
    loaderOptions: {
      less: {
        modifyVars: {
          blue: '#3a82f8'.'text-color': '# 333'
        },
        javascriptEnabled: true}}},...Copy the code

Layout the article page

Skeleton of the article list page:

<! --src/views/article/list.vue--> <template> <div class="article-list"> <a-table style="border: none;" bordered :loading="loading" :rowKey="row => row.id" :columns="columns" :data-source="data" :pagination="pagination" @change="change"/> </div> </template>Copy the code

Article editing/new page skeleton:

<! --src/views/article/info.vue--> <template> <div class="article-info"> <a-spin :spinning="loading"> <a-row style="display: flex; justify-content: flex-end; margin-bottom: 20px;" <a-form :form="form" V-bind ="formItemLayout"> <a-form :form="form" V-bind ="formItemLayout"> <a-form-item label=" title "> < A-input placeholder=" please enter the title "V-decorator ="['title', {rules: [{required: true, message: 'Please enter a title '}]}]"/> </a-form-item> < A-form-item label=" grouping "> < A-select showSearch V-decorator ="['group', {rules: [{ required: true, message: }]}]" placeholder=" placeholder "> <a-select-option value=" placeholder "> <a-select-option > <a-select-option Value =" grouping 2"> Grouping 2</a-select-option> < A-select-option value=" grouping 3"> Grouping 3</ A-select-option > <a-select-option Value =" placeholder 4"> placeholder 4</a-select > </ A-select > </ A-form-item > </ A-form-item label=" placeholder "> < A-input placeholder=" placeholder" v-decorator="[ 'author', {rules: [{ required: true, message: }]}]"/> </a-form-item> <a-form-item label=" content "> <a-textarea :autosize="{minRows: 10, maxRows: 12}" placeholder=" v-decorator="['content', {rules: [{required: true, message: 'please enter the content'}}]] "/ > < / a - form - item > < / a - form > < a - row style =" margin - top: 20 px; display: flex; justify-content: space-around;" <a-button type="primary" icon="upload" @click="submit"> <a-button @click="$router. Go (-1)"> </a-row> </a-spin> </div> </template>Copy the code

The front-end project has a prototype, the following to build the next server project.

The server is initialized

The EggJS framework is used directly to implement the server. You can consider using typescript to initialize projects, but we use javascript instead of its super typescript to initialize projects.

Initialize the project

$ mkdir service
$ cd service
$ npm init egg --type=simple
$ npm i
Copy the code

Launch project:

$ npm run dev
Copy the code

By opening the localhost:7001 address in your browser, you can see the welcome page for EggJS. Of course, we basically won’t touch browser pages here, because we’re developing an API. More debugging is done using the Postman tool.

Importing the database

The database used here is mysql, but instead of using it directly, we’ll install wrapped mysql2 and egg-sequelize.

In the Node.js community, Sequelize is a widely used ORM framework that supports multiple data sources such as MySQL, PostgreSQL, SQLite, and MSSQL. This will help us load our defined Model objects into our app and CTX.

# mysql installation
$ yarn add mysql2

# installation sequelize
$ yarn add egg-sequelize
Copy the code

Of course, we need a database to connect to, so we need to install a database. If you are using MAC OS, you can do this by:

brew install mysql
brew services start mysql
Copy the code

For Windows, you can download the relevant installation package and execute it.

Once the database is installed, we manage the database, either through the console command line or through graphical tools. We recommend the latter. We downloaded a tool called Navicat Premiun.

Navicat Premiun is a database management tool.

You can also download PHPStudy for development assistance.

Connecting to a Database

Configure the basic information of the database, assuming that we have created the database. Suppose we create a database named Article with user reng and password 123456. So, we can connect like this.

// config/config.default.js. config.sequelize = {dialect: 'mysql'.host: '127.0.0.1'.port: 3306.database: 'article'.username: 'reng'.password: '123456'.operatorsAliases: false}; .Copy the code

Of course, this is handled through the egg-sequelize package, which we will also introduce to tell EggJS to use this plug-in.

// config/plugin.js.sequelize: {
    enable: true.package: 'egg-sequelize',},...Copy the code

Create a database table

You can create mysql statements directly from the console command line. However, we do it directly using the migration operation.

In the project, we wanted to put all migrations-related content under the Database directory, so we created a new one under the root directory.

// .sequelizerc

'use strict';

const path = require('path');

module.exports = {
  config: path.join(__dirname, 'database/config.json'),
  'migrations-path': path.join(__dirname, 'database/migrations'),
  'seeders-path': path.join(__dirname, 'database/seeders'),
  'models-path': path.join(__dirname, 'app/model'),};Copy the code

Initializes Migrations configuration files and directories.

npx sequelize init:config
npx sequelize init:migrations
Copy the code

For more details, see the eggJS Sequelize section.

We initialized the articles database table according to the operation on the official website. The corresponding model contents are as follows:

// app/model/article.js

'use strict';

module.exports = app= > {
  const { STRING, INTEGER, DATE, NOW, TEXT } = app.Sequelize;

  const Article = app.model.define('articles', {
    id: {type: INTEGER, primaryKey: true.autoIncrement: true},/ / record id
    title: {type: STRING(255)},/ / title
    group: {type: STRING(255)}, / / group
    author: {type: STRING(255)},/ / the author
    content: {type: TEXT}, / / content
    created_at: {type: DATE, defaultValue: NOW},// Create time
    updated_at: {type: DATE, defaultValue: NOW}// Update time
  }, {
    freezeTableName: true // Table names are not automatically added to the complex number
  });

  return Article;
};
Copy the code

APIçš„CRUD

The above work on the server side has already helped us prepare for writing interfaces. So, the following combined with the database, we will realize the operation of adding, deleting, changing and checking the article.

We are using an MVC architecture, so our existing code logic naturally flows like this:

App/router. Js access route to articles – > app/controller/article. The corresponding method in js – > to app/service/article. The method of js. So, let’s focus on what we do at the Controller and Service layers. After all, the Router layer is not much to talk about.

Get the list of articles

[get] /api/get-article-list

// app/controller/article.js.async getList() {
    const { ctx } = this
    const { page, page_size } = ctx.request.query
    let lists = await ctx.service.article.findArticle({ page, page_size })
    ctx.returnBody(200.'Succeeded in getting the list of articles! ', {
      count: lists && lists.count || 0.results: lists && lists.rows || []
    }, '00000')}...Copy the code
// app/service/article.js.async findArticle(obj) {
    const { ctx } = this
    return await ctx.model.Article.findAndCountAll({
      order: [['created_at'.'ASC']],
      offset: (parseInt(obj.page) - 1) * parseInt(obj.page_size), 
      limit: parseInt(obj.page_size)
    })
  }
...
Copy the code

Get article details

[get] /api/get-article

// app/controller/article.js.async getItem() {
    const { ctx } = this
    const { id } = ctx.request.query
    let articleDetail = await ctx.service.article.getArticle(id)
    if(! articleDetail) { ctx.returnBody(400.'This data does not exist! ', {}, '00001')
      return
    }
    ctx.returnBody(200.'Get the article success! ', articleDetail, '00000')}...Copy the code
// app/service/article.js.async getArticle(id) {
    const { ctx } = this
    return await ctx.model.Article.findOne({
      where: {
        id
      }
    })
  }
...
Copy the code

Add the article

[post] /api/post-article

// app/controller/article.js.async postItem() {
    const { ctx } = this
    const { author, title, content, group } = ctx.request.body

    / / new articles
    let newArticle = { author, title, content, group }

    let article = await ctx.service.article.addArticle(newArticle)
    
    if(! article) { ctx.returnBody(400.'Network error, please try again later! ', {}, '00001')
      return
    }
    ctx.returnBody(200.'New article succeeded! ', article, '00000')}...Copy the code
// app/service/article.js.async addArticle(data) {
    const { ctx } = this
    return await ctx.model.Article.create(data)
  }
...
Copy the code

Edit the articles

[put] /api/put-article

// app/controller/article.js.async putItem() {
    const { ctx } = this
    const { id } = ctx.request.query
    const { author, title, content, group } = ctx.request.body

    // There are articles
    let editArticle = { author, title, content, group }

    let article = await ctx.service.article.editArticle(id, editArticle)
    
    if(! article) { ctx.returnBody(400.'Network error, please try again later! ', {}, '00001')
      return
    }
    ctx.returnBody(200.'Edit the article successfully! ', article, '00000')}...Copy the code
// app/service/article.js.async editArticle(id, data) {
    const { ctx } = this
    return await ctx.model.Article.update(data, {
      where: {
        id
      }
    })
  }
...
Copy the code

Delete articles

[delete] /api/delete-article

// app/controller/article.js.async deleteItem() {
    const { ctx } = this
    const { id } =  ctx.request.query
    let articleDetail = await ctx.service.article.deleteArticle(id)
    if(! articleDetail) { ctx.returnBody(400.'This data does not exist! ', {}, '00001')
      return
    }
    ctx.returnBody(200.'Deleted article succeeded! ', articleDetail, '00000')}...Copy the code
// app/service/article.js.async deleteArticle(id) {
    const { ctx } = this
    return await ctx.model.Article.destroy({
      where: {
        id
      }
    })
  }
...
Copy the code

After writing the interface, you can use the Postman application to verify that the data is returned.

Front-end docking ports

The next step is to cut back to the client folder. We have simply encapsulated the request method above. Here to write the article CRUD request method, we call it in order to facilitate the unified mount under the Vue instance.

// src/api/index.js

import article from './article'

const api = {
  article
}

export default api

export const ApiPlugin = {}

ApiPlugin.install = function (Vue, options) {
  Vue.prototype.api = api // Mount the API on the prototype
}
Copy the code

Get the list of articles

// src/api/article.js.export function getList(params) {
    return auth({
      url: '/api/get-article-list'.method: 'get',
      params
    })
  }
...
Copy the code
// src/views/article/list.vue ... getList() { let vm = this vm.loading = true vm.api.article.getList({ page: vm.pagination.current, page_size: vm.pagination.pageSize }).then(res => { if(res.code === '00000'){ vm.pagination.total = res.data && res.data.count || 0 Vm. Data = res. Data && res. Data. The results | | []} else {vm. $message. Warning (res) MSG | | 'failed to get the article list')}}) finally (() = > { vm.loading = false }) } ...Copy the code

Get article details

// src/api/article.js.export function getItem(params) {
    return auth({
      url: '/api/get-article'.method: 'get',
      params
    })
  }
...
Copy the code
// src/views/article/info.vue ... getDetail(id) { let vm = this vm.loading = true vm.api.article.getItem({ id }).then(res => { if(res.code === '00000') { / / data backfill vm. Form. SetFieldsValue ({title: res. Data && res. Data. The title | | undefined, the author: res.data && res.data.author || undefined, content: res.data && res.data.content || undefined, group: Res. Data && res. Data. Group | | undefined,})} else {vm. $message. Warning (res) MSG | | 'failed to get the article details! ') } }).finally(() => { vm.loading = false }) }, ...Copy the code

Add the article

// src/api/article.js.export function postItem(data) {
    return auth({
      url: '/api/post-article'.method: 'post',
      data
    })
  }
...
Copy the code
// src/views/article/info.vue ... submit() { let vm = this vm.loading = true vm.form.validateFields((err, values) => { if(err){ vm.loading = false return } let data = { title: values.title, group: values.group, author: values.author, content: values.content } vm.api.article.postItem(data).then(res => { if(res.code === '00000') { vm.$message.success(res.msg || 'Add success! ') vm. $router. Push ({path: '/ article/list'})} else {vm. $message. Warning (res) MSG | | 'new failure! ') } }).finally(() => { vm.loading = false }) }) }, ...Copy the code

Edit the articles

// src/api/article.js.export function putItem(params, data) {
    return auth({
      url: '/api/put-article'.method: 'put',
      params,
      data
    })
  }
...
Copy the code
// src/views/article/info.vue ... submit() { let vm = this vm.loading = true vm.form.validateFields((err, values) => { if(err){ vm.loading = false return } let data = { title: values.title, group: values.group, author: values.author, content: values.content } vm.api.article.putItem({id: Vm $route. Query. Id}, data). Then (res = > {the if (res) code = = = '00000') {vm. $message. Success (res) MSG | | 'new success! ') vm. $router. Push ({path: '/ article/list'})} else {vm. $message. Warning (res) MSG | | 'new failure! ') } }).finally(() => { vm.loading = false }) }) } ...Copy the code

Delete articles

// src/api/article.js.export function deleteItem(params) {
    return auth({
      url: '/api/delete-article'.method: 'delete',
      params
    })
  }
...
Copy the code
// src/views/article/list.vue ... Delete (text, record, index) {let vm = this vm.$confirm({title: '', content: '', okText: 'sure, okType:' danger 'cancelText:' cancelled 'onOk () {vm. API. Article. Called deleteItem ({id: Record. Id}). Then (res = > {the if (res) code = = = '00000') {vm. $message. Success (res) MSG | | 'deleted successfully! ') vm. HandlerSearch ()} else {vm. $message. Warning (res) MSG | | 'delete failed! ') } }) }, onCancel() {}, }) } ...Copy the code

rendering

In egg-demo/article-project/client/ front-end project, the page contains a login page, a welcome page, and an article page.

The welcome page is ignored

The login page

The article lists

The article editor

The latter

At this point, the whole project is complete. Code repository for egg-demo/article-project/, interested in extension learning.

In the next article, we’ll talk about mysql.

See Jimmy Github for more.