Introduction to the

This article starts from the perspective of a novice (who knows Vue by default, Koa or Express by default) and builds a complete front-end project from scratch that gets data in the form of an API provided by Koa and renders pages through Vue. Learn about how Vue builds single pages and uses front-end routing, how Koa provides APIS, how to perform access filtering (routing), validation (JSON-Web-token), and how Sequelize operates MySQL databases. Hope to be able to serve as an introduction to full stack development of the article.

Writing in the front

I wrote an article about Nodejs front-end and back-end development with Express and mongodb. In this article, I made a simple demo that allows you to read and write mongodb databases and display data from the database. Is a simple read and write small demo bar, is also a first attempt to render the server. I’ve also written about using NodeJS to write simple crawlers that retrieve data to write to a database. Through the above method, I wrote a small website with Express to record and display the top ten contents of the Bupt forum every day. It’s fun, isn’t it, to code what you want to do.

Then I got into Koa and started learning, moving from Express to Koa with a relatively smooth curve. In Koa, however, the server renders the page. Moreover, I found that there are few articles on the Internet about the application and website constructed with Koa. In a recent project, I need to use Vue to build pages, and all the data acquisition is in the form of back-end API, which is the so-called front and back end separation. Just in this process to go a lot of pits, including the use of database is also a novice, so write an article to record, with the same idea and method to build a simple Todolist, welcome to discuss, tap ~

The project architecture

.├── LICENSE├─ readme.md ├─.env // Environment Variables Config File ├─ app.js // Koa import File ├─ build // vue │ ├─ Build.js │ ├─ Check-versions. Js │ ├─ dev-client.js│ ├── Utils.js │ ├── Webpack. Base. Conf. Js │ ├ ─ ─ webpack. Dev. Conf., js │ └ ─ ─ webpack. Prod. Conf., js ├ ─ ─ the config / / vue - cli generates & yourself with some of the configuration file │ ├ ─ ─ . Default. Conf │ ├ ─ ─ dev env. Js │ ├ ─ ─ index. The js │ └ ─ ─ prod. The env. Js ├ ─ ─ dist / / Vue folder after the build │ ├ ─ ─ index. The HTML / / entry documents │ └ ─ ─ Static // Static Resources ├─ index.html // vue- GENERATED by the CLI to hold the main HTML file of vUE components. Single page application just one HTML ├─ Package. json // NPM dependency, Project info file ├─ server // Koa Used to provide the Api │ ├ ─ ─ the config / / configuration folder │ ├ ─ ─ controllers / / controller - controller │ ├ ─ ─ models/model/model - │ ├ ─ ─ routes / / route - routing │ └ ─ ─ │ ├─ App. Vue // Main files │ ├─ Assets │ ├─ Components ├─ ├─ lock, ├─ ├─ lock, ├─ lock, ├─ lock, ├─ lock, ├─ lockCopy the code

It seems to be very complicated, in fact, a large part of the folder structure is vue-CLI tool to help us generate. What we need to add are mainly Koa entry files and a Server folder for the Koa provisioning API. This way, Koa provides apis for fetching data, and Vue only needs to worry about rendering the data to the page.

Some of the key dependencies used by the project

The following versions are the versions at the time of writing, or older

  • Vue. Js (v2.1.8)
  • Vue – the Router (v2.1.1)
  • Axios (v0.15.3)
  • Element (v1.1.2)
  • Koa.js(v1.2.4) // Koa2 is not adopted
  • [email protected]\ KOA-jwt \ koa-static and a series of Koa middleware
  • Mysql(v2.12.0) // Nodejs Mysql driver, not Mysql itself (project using mysql5.6)
  • Sequelize(V3.28.0) // Operate the DATABASE ORM
  • Yarn(v0.18.1) // Is faster than NPM

You can refer to the project demo repository at the end of this article for the rest of the dependencies.

Project start

Nodejs and NPM installation are no longer described. If es6 is not installed on Nodejs, you will need to add the — Harmony flag. NPM I vue-cli -g is installed globally. Of course, this project basically uses YARN, so yarn global add vue-cli can also be used.

Tips: can give yarn taobao source, faster: yarn config set registry “https://registry.npm.taobao.org”

Then we initialize a webpack template for Vue2:

vue init webpack demo

Tips: In the demo above you can fill in your own project name

Then after making some basic configuration choices, you should have a basic VUE-CLI-generated project structure.

Next we go to vue-CLI-generated directory, install vUE’s project dependencies and install Koa’s project dependencies: Yarn && yarn add koa [email protected] koa-logger koa-json koa-bodyParser Because the 7.x version supports Koa2) then do some basic directory setup:

In the demo directory generated by vuE-CLI, create the server folder and subfolders:

├─ Flag school - // Flag School, ├─ Api ├─ config config ├─ Controllers ├─ Models ├─ routes route ├─ schema // schema- Database table structureCopy the code

Then in the Demo folder we create an app.js file as the Koa startup file.

To start Koa, write the following basic content:

const app = require('koa')() , koa = require('koa-router')() , json = require('koa-json') , logger = require('koa-logger'); Use (require(' koa-bodyParser ')()); app.use(json()); app.use(logger()); app.use(function* (next){ let start = new Date; yield next; let ms = new Date - start; console.log('%s %s - %s', this.method, this.url, ms); // display execution time}); app.on('error', function(err, ctx){ console.log('server error', err); }); app.listen(8889,() => { console.log('Koa is listening in 8889'); }); module.exports = app;Copy the code

Nodeapp.js: Koa is listening in port 8889. Nodeapp.js: Koa is listening in port 8889.

Front-end page building

This DEMO is to make a todo-list, so let’s start with a login page.

Tips: For ease of page building and aesthetics, the front-end UI framework of Vue2 used in this article is Element-UI. Install: YARN add element-ui

I am used to using PUG for template engine and CSS preprocessing for stylus. Of course, each person’s habits and preferences are different, so you can just use your own preferences.

To make it easier for you to see the code, you don’t need puG, which is relatively expensive to learn. However, the CSS stylus is easy to write in stylus and not difficult to look at, which is my own habit, so I need to install yarn Add Stylus Stylus-Loader.

Tips: Stylus-Loader is installed to enable WebPack to render stylus

Then you introduce element-UI into your project. Open SRC /main.js and rewrite the file as follows:

Import Vue from 'Vue' import App from './App' import ElementUI from 'element-ui' // import element-ui import 'element-ui/lib/theme-default/index.css' vue. use(ElementUI) // Vue global use new Vue({el: '#app', template: '<App/>', components: { App } })Copy the code

We then type NPM run dev in the project root directory to start development mode, which has webpack hot loading, meaning that the browser responds to changes immediately after you write the code.

To implement responsive pages, add the following meta to the head tag of index.html in the project directory:

<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, Minimum - scale = 1.0 ">

Login screen

Go to the SRC/Components directory and create a login. vue file. Then let’s write the first page:

<template> <el-row class="content"> <el-col :xs="24" :sm="{span: 6,offset: 8}"> <span class="title"> <span > <el-row> <el-input v-model="account" placeholder=" placeholder "type="text"> </el-input> <el-input V-model ="password" placeholder=" password" type="password"> </el-input> <el-button type="primary"> </el-row> </el-col> </el-row> </template> <script> export default { data () { return { account: '', password: '' }; }}; </script> <style lang="stylus" scoped> .el-row.content padding 16px .title font-size 28px .el-input margin 12px 0 .el-button width 100% margin-top 12px </style>Copy the code

There are a few things worth noting here. The first is that you can mount at most one direct child within the template tag. So you can’t write:


<template>
  <el-row></el-row>
  <el-row></el-row>
    </template>
Copy the code

Template syntax error Component template should contain exactly one root element template syntax error Component template should contain exactly one root element But to write multiple elements, you can do this:


<template>
  <div>
    <el-row></el-row>
    <el-row></el-row>
  </div>
    </template>
Copy the code

Note also that there is a scoped attribute in the style tag of login. vue, which makes these styles only apply to this component (because Webpack automatically marks elements in this component with attributes like data-V-62a7F97e during rendering). Title [data-v-62a7f97e]{font-size: 28px; } to ensure that it does not conflict with the styles of other components.

After the page is written, it will not display unless the component is registered with the Vue. Therefore, we need to rewrite the app. vue file:

<template> <div id="app"> <img src="./assets/logo.png"> <Login></Login> <! </div> </template> <script> import Login from './components/Login' </script> <style> #app {font-family: 'Avenir', Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; margin-top: 60px; } </style>Copy the code

The Login component is registered with Vue, and when you look at the browser, it is no longer the Hello welcome interface generated by vuE-CLI by default.

Login

Next, let’s write the interface after successful login.

TodoList page

Again, in the SRC/Components directory, write a file called todolist. vue.

And then we start writing a TodoList:

< the template > < el - row class = "content" > < el - col: xs = "{2} span: 20, offset:" : sm = "{span: 8, offset: 8}" > < span > welcome: {{name}} instead! Your to-do list is: </span> <el-input placeholder=" Please input your to-do list "V-model ="todos" @keyup.enter.native="addTodos"></el-input> <el-tabs V-model ="activeName"> < El-Tab-Pane Label =" To-do "name="first"> < EL-col :xs="24"> < Template V-if ="! Done"> <! -- V-if and v-for cannot be used in the same element. <template v-for="(item, index) in list"> <div class="todo-list" v-if="item.status == false"> <span class="item"> {{ index + 1 }}. {{ item.content }} </span> <span class="pull-right"> <el-button size="small" type="primary" @click="finished(index)"> complete </el-button> <el-button size="small" :plain="true" type="danger" </el-button> </span> </div> </template> </template> <div v-else-if="Done"> </el-col> </el-tab-pane> < El-tab-pane Label =" Completed item "name="second"> <template V-if ="count > 0"> <template V-for ="(item, index) in list"> <div class="todo-list" v-if="item.status == true"> <span class="item finished"> {{ index + 1 }}. {{ item.content }} </span> <span class="pull-right"> <el-button size="small" type="primary" @click="restore(index)"> Restore </el-button> </span> </div> </template> </template> <div v-else> Nothing completed </div> </el-tab-pane>  </el-tabs> </el-col> </el-row> </template> <script> export default { data () { return { name: 'Molunerfinn', todos: '', activeName: 'first', list:[], count: 0 }; }, computed: {// Compute attributes used to calculate whether all tasks have been completed Done(){let count = 0; let length = this.list.length; for(let i in this.list){ this.list[i].status == true ? count += 1 : ''; } this.count = count; if(count == length || length == 0){ return true }else{ return false } } }, methods: { addTodos() { if(this.todos == '') return let obj = { status: false, content: this.todos } this.list.push(obj); this.todos = ''; }, finished(index) {this.$set(this.list[index],'status',true) })}, remove(index) {this.list.splice(index,1); this.$message({ type: 'info', message: $set(this.list[index],'status',false) this.$message({type: 'info', message: 'Task restore'})}}}; </script> <style lang="stylus" scoped> .el-input margin 20px auto .todo-list width 100% margin-top 8px padding-bottom 8px border-bottom 1px solid #eee overflow hidden text-align left .item font-size 20px &.finished text-decoration line-through color #ddd .pull-right float right </style>Copy the code

There’s really nothing special to say about page building, but I’ll do it because I’ve done it myself:

  1. V-if and v-for are used together in the same element, because Vue always executes v-for first, so v-if is not executed. Instead, you can use an extra template element to place v-if or V-for for the same purpose. This is the relevant issue.

  2. Calculation attributes can be directly detected for direct data such as a: 2 -> a: 3. However, if the status of one of the list items in this example changes, Vue will not be able to detect the change if we use list[index]. Status = true. Instead, you can use the set method (vue.set () globally, this.$set() in our case) to make changes to the data detectable. This allows computational properties to capture changes. Refer to the official documentation for the description of the reactive principle.

Todolist

After writing TodoList, we need to combine it with vue-Router to enable the single-page application to jump to and from pages.

Page routing

Because server-side rendering is not used, the page routes are front-end routes. Install vue-router: yarn add vue-router.

Once installed, let’s mount the route. Open the main.js file and rewrite it as follows:

// src/main.js import Vue from 'vue' import App from './App' import ElementUI from 'element-ui' import 'element-ui/lib/theme-default/index.css' import VueRouter from 'vue-router' Vue.use(ElementUI); Vue.use(VueRouter); import Login from `./components/Login` import TodoList from `./components/TodoList` const router = new VueRouter({ mode: 'history', // Enable the HTML5 history mode to make the url of the address bar look like the normal page jump URL. Base: __dirname, routes: [{path: '/', // '/todolist', Component: todolist}, {path: '*', redirect: '/'}]}) const app = new Vue({router: Router: h => h(App)}).$mount('# App 'Copy the code

This has the route mounted, but when you open the page, nothing seems to have changed. This is because we didn’t put the routing view on the page. Now let’s rewrite app.vue:

<! -- APP.vue --> <template> <div id="app"> <img src="./assets/logo.png"> <router-view></router-view> <! </div> </template> <script> export default {name: } </script> <style> #app {font-family: arial, helvetica, sans-serif; 'Avenir', Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; margin-top: 60px; } </style>Copy the code

Then look at your page. If you add /todolist to the address bar, you will be redirected to the Todolist page.

But how do we jump to TodoList by clicking the login button? Rewrite login. vue to jump.

Just add a method to the logged button:

<! -- login. vue --> ···· <! Add keyboard events to input <el-input V-model ="password" placeholder=" password" placeholder=" password" placeholder=" password" placeholder=" password" placeholder=" password" placeholder=" password" placeholder=" password" placeholder=" password" placeholder=" password" placeholder @keyup.enter.native="loginToDo"> </el-input> <! <el-button type="primary" @click="loginToDo"> </el-button> ····· <script> export default { data () { return { account: '', password: '' }; }, methods: {loginToDo() {this.$router. Push ('/todolist'); }}}; </script>Copy the code

You can then jump to the page by clicking the login button. And you can see that the page address changes from localhost:8080 to localhost:8080/todolist, which looks like a normal URL jump. (But actually we are a single page app, just jumping to and from the page within the app, no extra requests to the back end.)

login2todolist

At this point, we have a pure front-end, single-page application that can jump to and from pages and do simple ToDoList additions and deletions and restores. Of course, it’s a viewable thing that doesn’t work — the login system is nominal and ToDoList is gone with a page refresh.

So we can put the front end down a little bit. Start our journey to the back end.

Setting up the back-end environment

MySQL

The reason why I didn’t use Mongodb, which is popular in the Node world, was because I had used it before and not MySQL. In the spirit of learning, I decided to use MySQL. Express + Mongodb tutorials have been around for a long time. So if you think Mongodb is more to your liking, you can build a similar application with Mongodb by the end of this article.

Go to the official website of MySQL to download and install MySQL Community Server of the corresponding platform.

Generally speaking, the installation steps are relatively simple. For the basic installation and startup steps of MySQL, please refer to this article, which is for Windows. Of course, the installation of other platforms is also very convenient, there are corresponding package management tools can be obtained. Note that you need to set the password of the root account after installing MySQL. Ensure safety. If you missed the Settings, or if you don’t know how to set them, you can refer to this article. Okay

Since I am not very familiar with MySQL SQL statements, I need a visual tool to operate MySQL. I use HediSQL on Windows and Sequel Pro on macOS. They’re all free.

We can then use these visualization tools to connect to MySQL server (default port is 3306) and create a new database called Todolist. (Of course, you can also use an SQL statement :CREATE DATABASE todolist, which I won’t talk about later).

Now we can start creating tables.

We need to create two tables, one for users and one for to-do lists. The user table is used for login, authentication, and the to-do list is used to display our to-do list.

Create a user table where the password will be md5 encrypted later (32-bit).

field type instructions
id Int (increment) The user id
user_name CHAR(50) User name
password CHAR(32) User password

Create a list (id, user_id, Content, status);

field type instructions
id Int (increment) The list of id
user_id int(11) The user id
content CHAR(255) The list of the content
status tinyint(1) The list of state

That’s pretty much the part where you’re dealing directly with the database.

Sequelize

When dealing with the database, we all need a good operation database tool, which can let us use a relatively simple method to add, delete, change and check the database. Mongoose is familiar to Mongodb and I used Monk which is a little bit simpler. For MySQL, I chose Sequelize, which supports multiple relational databases (Sqlite, MySQL, Postgres, etc.). Its operations almost always return a Promise object, which makes it easy to “synchronize” in Koa.

For more information on the use of Sequelize, see the official documentation and the Sequelize API documentation, Sequelize, and MySQL

Before connecting to the database using Sequelize we need to export the table structure of the database using sequelize-Auto.

For more information on the use of Sequelize-Auto, see the official introduction or this article

Install the following dependencies: YARN Global add Sequelize -auto && YARN add Sequelize mysql.

Note: Mysql installed using YARN is the mysql driver in nodeJS environment.

Sequelize -auto -o “./schema” -d todolist -h 127.0.0.1 -u root -p 3306 -x XXXXX -e mysql, -o parameter is the output folder directory, -d parameter is the database name, -h parameter is the database address, -u parameter is the database user name, -p parameter is the port number, -x parameter is the database password, this should be based on their own database password! Mysql > select mysql as database

Then two files are automatically generated in the schema folder:

// user.js module.exports = function(sequelize, DataTypes) { return sequelize.define('user', { id: { type: DataTypes.INTEGER(11), // allowNull: false, // whether NULL primaryKey: true, // primaryKey autoIncrement: True // whether to increment}, user_name: {type: DataTypes.CHAR(50), allowNull: false}, password: {type: DataTypes.CHAR(32), allowNull: false}}, {tableName: 'user' // tableName}); };Copy the code
// list.js
module.exports = function(sequelize, DataTypes) {
  return sequelize.define('list', {
    id: {
      type: DataTypes.INTEGER(11),
      allowNull: false,
      primaryKey: true,
      autoIncrement: true
    },
    user_id: {
      type: DataTypes.INTEGER(11),
      allowNull: false
    },
    content: {
      type: DataTypes.CHAR(255),
      allowNull: false
    },
    status: {
      type: DataTypes.INTEGER(1),
      allowNull: false
    }
  }, {
    tableName: 'list'
  });
    };
Copy the code

Automated tools save us a lot of time defining table structures manually. Also note that the generated database table structure files automatically help us out of module.exports, so it is very convenient for us to import later.

In the config directory of the server directory, create db.js to initialize the connection between Sequelize and the database.

// db.js const Sequelize = require('sequelize'); // Sequelize // use the url connection form to connect, note that root: Const Todolist = new Sequelize('mysql://root:XXXX@localhost/ Todolist ',{define: {timestamps: }}) module.exports = {Todolist // Todolist exposes Todolist interface for Model calls}Copy the code

Next we go to the Models folder and join the database and table structure files. Create a new user.js file in this folder. So let’s first write something that looks up the user ID.

To do this, we can add a random entry to the database:

test

If you want to query for user ID 1, it’s natural to write something like this:

const userInfo = User.findOne({ where: { id: 1} }); / / query console. The log (the userInfo); // Output the resultCopy the code

But that doesn’t actually work. Because of the nature of JS, its IO operations are asynchronous. As written above, userInfo will be a Promise object returned, not the final userInfo. If you want to use synchronous writing method to obtain asynchronous IO operation data, usually can not be directly obtained. But in Koa, the presence of CO makes it much easier. Rewrite as follows:

// models/user.js const db = require('.. /config/db.js'), userModel = '.. /schema/user.js'; Const TodolistDb = db.todolist; // import database const User = todolistdb.import (userModel); // Use sequelize's import method to introduce the table structure and instantiate User. Const getUserById = function* (id){const getUserById = function* (id){const getUserById = function* (id){const getUserById = function* (id){const getUserById = function* (id){const getUserById = function* (id){ Const userInfo = yield user. findOne({yield controls asynchronous operations that return data from a Promise object. Where: {id: id}}); Module. exports = {getUserById} // exports = {getUserById, which will be called in controller}Copy the code

We then write a user controller in controllers to execute the method and return the result.

// controllers/user.js const user = require('.. /models/user.js'); const getUserInfo = function* (){ const id = this.params.id; Const result = yield user.getUserById(id); const result = yield user.getUserById(id); Returned in / / pass yield "synchronous" query results enclosing body = the result / / will be the result of the request on the response of the body to return to} module. Exports = {auth: (router) => { router.get('/user/:id', getUserInfo); // Define url with id}}Copy the code

After writing this, we can’t request it directly, because we haven’t defined the route yet, and the request will not respond if it can’t find the path through Koa.

Create an auth.js file in the Routes folder. (Actually the user table is used for login, so go to auth)

// routes/auth.js const user = require('.. /controllers/user.js'); const router = require('koa-router')(); user.auth(router); Exports = router module. Exports = router; // Expose the router ruleCopy the code

At this point we are close to completing our first API, but we need the final step to “mount” the routing rule to Koa.

Go back to the root directory of app.js and rewrite as follows:

const app = require('koa')() , koa = require('koa-router')() , json = require('koa-json') , logger = require('koa-logger') , auth = require('./server/routes/auth.js'); Use (require(' koa-bodyParser ')()); app.use(json()); app.use(logger()); app.use(function* (next){ let start = new Date; yield next; let ms = new Date - start; console.log('%s %s - %s', this.method, this.url, ms); }); app.on('error', function(err, ctx){ console.log('server error', err); }); koa.use('/auth', auth.routes()); // Mount to the koa-router and prefix all auth request paths with '/auth' request paths. app.use(koa.routes()); // Mount routing rules to Koa. app.listen(8889,() => { console.log('Koa is listening in 8889'); }); module.exports = app;Copy the code

Open your console, type in Node app.js, and if everything works fine, we’re done! Our first API has been built!

How do you test that?

API Test

Before the interface is connected to the front end, we should test it to prevent any problems. As a tool for testing interfaces, I recommend Postman, which does a good job of simulating incoming requests and viewing the results of the responses, for testing purposes.

The test was successful, I sent the correct URL request, and returned the results I wanted to see. We see that the result returned is actually JSON, which is a very convenient data format for both our front and back ends to work with.

But if something goes wrong with our code and we return error how do we test it? If the console can give you some feedback, it’s definitely not enough, and we probably don’t know which step went wrong to make the end result go wrong.

So I recommend VSCode to debug the nodeJS backend. It adds breakpoints and makes it easy to view the requested information. And with tools like Nodemon, debugging could not be more comfortable.

For details on VSCode nodeJS debugging, see the official article

I myself write code in Sublime and debug in VSCode, haha.

The realization of login system

Just realized but is a simple user information query interface, but we want to realize is a login system, so still need to do some work.

JSON-WEB-TOKEN

Login authentication based on cookies or sessions has become quite common, but jSON-Web-token has become quite popular recently. With its introduction, it is possible to implement truly stateless requests rather than stored stateful authentication based on sessions and cookies.

The jSON-Web-token description can be found in this simple article, but I also recommend an article that makes it very clear how to use JSON-Web-token.

Also check out the jSON-web-token website.

Json-web-token consists of three parts, header information + main body information + key information. The information transmitted by the main body (which is the part where we store the information we need) is encoded in BASE64, so it is easy to decode, and must not store the key information such as plaintext password! Instead, you can store information that is not particularly critical, such as a user name, to make a distinction.

In a nutshell, a jSON-Web-token login system would look like this:

  1. The user enters the account password on the login page and sends a request for the account password (encrypted by MD5) to the backend
  2. The backend verifies the user’s account and password. If they match, the backend sends a TOKEN back to the client. If the TOKEN does not match, the TOKEN is not sent back and the verification error message is returned.
  3. If the login succeeds, the client saves the TOKEN in some way (SessionStorage, LocalStorage), and then requests other resources with the TOKEN in the Header.
  4. After receiving the request message, the backend verifies whether the TOKEN is valid. If the TOKEN is valid, the backend sends the requested resource. If the TOKEN is invalid, a verification error is returned.

In this way, the access between client and server is stateless: that is, the server does not know whether you are still online, as long as you send the TOKEN in the request header is correct, I will return to you the resources you want. This does not take up valuable space resources on the server side, and if a server cluster is involved, the stateless design is obviously cheaper to maintain if the server is being maintained or migrated or CDN node allocation is required.

Without further ado, let’s apply jSON-Web-token to our project.

Yarn add koa-jWT: install the JSON-Web-token library of KOA.

We need to add a method to user.js in models to find the user by user name:

// models/user.js // ...... Const getUserByName = function* (name){const userInfo = yield user.findone ({where: const getUserByName = function* (name){const userInfo = yield user.findone ({where: { user_name: Name}}) return userInfo} module.exports = {getUserById, exports = {getUserById, exports = {getUserById}Copy the code

Then we write user.js in the controllers:

// controllers/user.js const user = require('.. /models/user.js'); const jwt = require('koa-jwt'); Koa-jwt const getUserInfo = function* (){const id = this.params.id; Const result = yield user.getUserById(id); const result = yield user.getUserById(id); } const postUserAuth = function* (){const data = this.request.body; // Request. Body const userInfo = yield user.getUserByName(data.name); if(userInfo ! If (userinfo.password!){if(userinfo.password! = data.password){this.body = {success: false, // success flag is to help the front end of the return is correct or not info: 'password error! '}}else{// If password is correct const userToken = {name: userinfo.user_name, id: userinfo.id} const secret = 'vue-koa-demo'; Const token = jwt.sign(userToken,secret); const token = jwt.sign(userToken,secret); Body = {success: true, token: token, // Return token}}}else{this.body = {success: false, info: 'User does not exist! Module. exports = {auth: (router) => {router. Get ('/user/:id', getUserInfo); Router.post ('/user', postUserAuth); }}Copy the code

So we finished writing the user authentication section. Next we are going to rewrite the front-end login method.

The introduction of Axios

Vue has been using vue-Resource, but after Vue2 came out, Vue no longer recommends it as the official Ajax web request library by default. Instead, some other libraries are recommended, such as axios, which we will use today. Promise is a Promise based HTTP client for the Browser and Node. js, which supports both Node and browser-side Ajax requests. I think it will be necessary to use it.

Yarn add axios. Install axios. Then we introduce axios in SRC /main.js:

// scr/main.js // ... Import Axios from 'Axios' vue.prototype.$HTTP = Axios // this.$http.get();Copy the code
// login.vue methods: {loginToDo() {let obj = {name: this.account, password: this.password } this.$http.post('/auth/user', Then ((res) => {// axios returns data in res.data if(res.data.success){// If successful sessionStorage.setItem('demo-token',res.data.token); This.$message({// login succeeded, type: 'success', message: 'login succeeded! '}); $router. Push ('/todolist')}else{this.$message.error(res.data.info); Sessionstorage. setItem('demo-token',null); }}, (err) => {this.$message.error(' Request error! ') sessionStorage.setItem('demo-token',null); // Empty the token})}}Copy the code

Password MD5 Encryption

In addition, passwords sent from the front end to the back end should be encrypted with MD5.

Therefore, we need to install the MD5 library yarn Add MD5

Then change the loginToDo method under login. vue:

import md5 from 'md5' export default { data () { return { account: '', password: '' }; }, methods: { loginToDo() { let obj = { name: this.account, password: $http.post('/auth/user', obj) // Send the message to the backend. Then ((res) => {console.log(res); If (res.data.success){// SessionStorage.setitem ('demo-token',res.data.token); This.$message({// login succeeded, type: 'success', message: 'login succeeded! '}); $router. Push ('/todolist')}else{this.$message.error(res.data.info); Sessionstorage. setItem('demo-token',null); }}, (err) => {this.$message.error(' Request error! ') sessionStorage.setItem('demo-token',null); // Empty token})}}};Copy the code

Because we are still in plain text database, 123 as a password, it is now the md5, 32-bit md5 encrypted into: 202 cb962ac59075b964b07152d234b70, replace its database of 123. We won’t be able to log in without this step.

We’re not done yet, because our interface runs on port 8080, but the Koa API runs on port 8889, so you can’t request a post via /auth/user. Even localhost:8889/auth/user will fail due to cross-domain problems.

There are two most convenient solutions at this point:

  1. If it is cross-domain, the server simply adds CORS to the request header, and the client can send cross-domain requests.
  2. Become co-domain, can solve the cross-domain request problem.

The first is also very convenient, using KCORS can be solved. However, for ease of deployment later, we use the second type, which becomes the same-domain request.

Open the root directory config/index.js, find the dev proxyTable, with this proxyTable we can forward external requests to the local via webpack, also can change the cross-domain request into the same-domain request.

Rewrite proxyTable as follows:

 proxyTable: {
  '/auth':{
    target: 'http://localhost:8889',
    changeOrigin: true
  },
  '/api':{
    target: 'http://localhost:8889',
    changeOrigin: true
  }
    }
Copy the code

Above the mean, we request address if it is in the component/API/XXXX actually request is http://localhost:8889/api/xxxx, but the localhost on port 8889 as webpack help our agent service, So we can call a request that is actually cross-domain as if it were an interface in the same domain.

Restart webPack: CTRL + C to exit the current process, then NPM run dev.

When all is said and done, we can see the following exciting images:

login2todolist

Jump to intercept

Although we can successfully log in to the system now, there is still a problem: I manually change the address in the address bar to localhost:8080/todolist and I can still successfully jump to the interface after logging in. This then requires a jump intercept, so that when there is no login, whatever address is entered in the address bar is eventually redirected back to the login page.

This is where the token from the back end comes in handy. Having a token means our identity is verified, otherwise it is illegal.

Vue-router provides a hook to jump to the page. We can verify before the router jumps to the page. If the token exists, we jump to the page; if not, we return to the login page.

Navigation hooks for reference routes

Open SRC /main.js and modify as follows:

// src/main.js // ... const router = new VueRouter({.... }) // omit router. BeforeEach ((to,from,next) =>{const token = sessionstorage.getitem ('demo-token'); beforeEach((to,from,next) =>{const token = sessionstorage.getitem ('demo-token'); If (to.path == '/'){// If (token! = 'null' && token ! = null){next('/todolist') // Next (); }else{if(token! = 'null' && token ! Else {next('/') // Otherwise jump to login page}}}) const app = new Vue({... }) / / omittedCopy the code

Note: Make sure the next() method is called otherwise the hook will not be resolved. Calling a method like next(path) will always end up back in the.beforeeach () hook. If the condition is not written correctly there is always the possibility of an endless loop and stack overflow.

Then we can see the following effect:

login2todolist

Tips: This kind of verification is very insecure, this example is just a demonstration, in fact, we should make a further judgment, for example, the unpacked information from the token contains the information we want can be regarded as a valid token, can log in. And so on. This article is just a brief introduction.

Parsing the token

Note that when we issue the token, we write the following sentences:

// server/controllers/user.js // ... const userToken = { name: userInfo.user_name, id: userInfo.id } const secret = 'vue-koa-demo'; Const token = jwt.sign(userInfo,secret); // Issue token //...Copy the code

We package the user name and ID into the body of JWT, and the key we decrypt is VUE-koa-demo. So we can use this information to display the user name after login, and to distinguish who the user is and what Todolist the user has.

Token resolution is then performed on the Todolist page so that the user name is displayed as the login user name.

// src/components/TodoList.vue // ... Import JWT from 'jsonWebToken' // The dependency export default {created(){const userInfo = is called when the component is created this.getUserInfo(); // Add a method to get user information if(userInfo! = null){ this.id = userInfo.id; this.name = userInfo.name; }else{ this.id = ''; This.name = ''}}, data () {return {name: '', activeName: '', list:[], count: 0, id: '// Add user ID attribute to distinguish users}; }, computed: {... }, // omit methods: {addTodos() {... }, // omit finished(index) {... }, remove(index) {... }, restore(index) {... }, getUserInfo(){// getUserInfo const token = sessionstorage.getitem ('demo-token'); if(token ! = null && token ! = 'null'){ let decode = jwt.verify(token,'vue-koa-demo'); {name: XXX,id: XXX}}else {return null}}}}; / /...Copy the code

So you can see:

todolist

The user name is no longer the default Molunerfinn but the login name is Molunerfinn.

Todolist add delete change check implementation

This part is the front and back end collaboration. We’re going to implement what we did in the pure front end. I’ll give you two basic examples: get Todolist and add Todolist. The rest is pretty much the same idea. I’ll provide the code and comments, which I’m sure will be easy to understand.

The Token to send

As mentioned earlier, with jSON-Web-token, the validation of the system is entirely TOKEN dependent. If the token is correct, the resource is sent; if the resource is incorrect, an error message is returned.

Since we are using KOA-JWT, we simply put the Authorization attribute on each request as Bearer {token value} and have KOA verify the token before accepting the request. But if you have to manually write this for every request, it’s too tiring. So we can do global headers.

Open SRC /main.js and add this line to the redirect hook:

// scr/main.json router.beforeEach((to,from,next) =>{ const token = sessionStorage.getItem('demo-token'); if(to.path == '/'){ if(token ! = 'null' && token ! = null){ next('/todolist') } next(); }else{ if(token ! = 'null' && token ! = null){ Vue.prototype.$http.defaults.headers.common['Authorization'] = 'Bearer ' + token; // Global header token validation protocol is Bearer of false space next()}else{next('/')}})Copy the code

This completes the client sending setup of the token.

Token verification at the Koa end

Then we implement two simple apis that request paths not /auth/ XXX but/API/XXX. We also need to implement that all requests to access the/API /* path need to be authenticated by KOA-JWT, while requests to /auth/* do not.

Start by creating a todolist.js file in the models directory:

// server/models/todolist.js const db = require('.. /config/db.js'), todoModel = '.. /schema/list.js'; Const TodolistDb = db.todolist; Const Todolist = todolistdb.import (todoModel); Const getTodolistById = function* (id){const todolist = yield todolist.findAll ({const getTodolistById = function* (id){const todolist = yield todolist Where: {user_id: id}, attributes: ['id','content','status']}; } const createTodolist = function* (data){// Create a todolist yield todolist. create({user_id: Data. id, // The user ID that determines which user to create content: data.content, status: data.status }) return true } module.exports = { getTodolistById, createTodolist }Copy the code

Create a todolist.js file in the controllers directory:

// server/controllers/todolist const todolist = require('.. /models/todolist.js'); Const getTodolist = function* (){// getTodolist const id = this.params.id; Const result = yield todolist.getTodolistById(id); Const createTodolist = function* (){this.body = result} const createTodolist = function* (){this.body = result Todolist const data = this.request.body; Const result = yield todolist.createTodolist(data); this.body = { success: true } } module.exports = (router) => { router.get('/todolist/:id', getTodolist), router.post('/todolist', createTodolist) }Copy the code

Then go to the Routes folder and create an api.js file:

// server/routes/api.js const todolist = require('.. /controllers/todolist.js'); const router = require('koa-router')(); todolist(router); Exports = exports; // exports = exports; // Export router rulesCopy the code

Finally, go to app.js in the root directory and add a new routing rule to KOA:

// app.js const app = require('koa')() , koa = require('koa-router')() , json = require('koa-json') , logger = require('koa-logger') , auth = require('./server/routes/auth.js') , api = require('./server/routes/api.js') , jwt = require('koa-jwt'); / /... Use (function* (next){let start = new Date; yield next; let ms = new Date - start; console.log('%s %s - %s', this.method, this.url, ms); }); App. use(function *(next){// If JWT fails, try {yield next; } catch (err) { if (401 == err.status) { this.status = 401; this.body = { success: false, token: null, info: 'Protected resource, use Authorization header to get access' }; } else { throw err; }}}); app.on('error', function(err, ctx){ console.log('server error', err); }); koa.use('/auth', auth.routes()); // Mount to the koa-router and prefix all auth request paths with '/auth' request paths. Koa. use("/ API ", JWT ({secret: 'vue-koa-demo'}),api.routes()) // All requests starting with/API/need to be verified by the JWT middleware. Secret The secret must be the same as the secret we issued app.use(koa.routes()); // Mount routing rules to Koa. / /... omitCopy the code

At this point, the two apis on the back end have been built.

The initial configuration is relatively complex, involving models, controllers, routes, and app.js, and can be daunting. In fact, after the first build, to add the API, you basically just need to write the methods in the Model and controllers and specify the interface, which is very convenient.

Front-end docking ports

Now that the back-end interface is open, you need to connect the front end to the back end. There are two main docking ports:

  1. Gets all of a user’s Todolist
  2. Create a todolist for a user

Here’s how to rewrite todolist. vue:

// todolist.js // ... Omit created(){const userInfo = this.getUserInfo(); if(userInfo ! = null){ this.id = userInfo.id; this.name = userInfo.name; }else{ this.id = ''; this.name = '' } this.getTodolist(); // New: todolist} on component creation, //... Omit methods: {addTodos() {if(this.todos == ") return let obj = {status: false, content: this.todos, id: this.id } this.$http.post('/api/todolist', Then ((res) => {if(res.status == 200){this.$message({type: 'success', message: 'success! ' }) this.getTodolist(); Todolist}else{this.$message.error(' failed to create! }}, (err) => {this.$message.error(' failed to create! Console. log(err)}) this.todos = "; // Empty todos}, //... GetTodolist (){this.$http.get('/ API /todolist/' + this.id) // Send a request to the backend for todolist. Then ((res) => {if(res.status == $message.error(' failed to get list! ') $message.error(' failed to get list! ')}}, (err) => {this.$message.error(' Failed to get list! ') console.log(err) }) } }Copy the code

At this point, the front and back ends are completely built. Let’s take a look at the results:

todolist

By the time we do this, we’re almost done with our application. Final touches. Let’s wrap it up.

In the original front-end version, there are three states: Complete, delete, and restore. Complete and restore are only state switches (updates), so they can be regarded as an API, and then delete is a separate API. So we can be added, deleted, changed, check. The next part of the code is provided, in fact, the idea is the same as before, but the operation of the function is not the same.

Todolist’s modification and deletion

// server/models/todolist.js // ... Const removeTodolist = function* (id,user_id){yield Todolist. Destroy ({where: const removeTodolist = function* (id,user_id){yield Todolist. Destroy ({where: { id, user_id } }) return true } const updateTodolist = function* (id,user_id,status){ yield Todolist.update( { status }, { where: { id, user_id } } ) return true } module.exports = { getTodolistById, createTodolist, removeTodolist, updateTodolist }Copy the code
// server/controllers/todolist.js // ... Const removeTodolist = function* (){const id = this.params.id; const user_id = this.params.userId; const result = yield todolist.removeTodolist(id,user_id); this.body = { success: true } } const updateTodolist = function* (){ const id = this.params.id; const user_id = this.params.userId; let status = this.params.status; status == '0' ? status = true : status = false; Const result = yield todolist.updateTodolist(id,user_id,status); this.body = { success: true } } module.exports = (router) => { router.get('/todolist/:id', getTodolist), router.post('/todolist', createTodolist), router.delete('/todolist/:userId/:id', removeTodolist), router.put('/todolist/:userId/:id/:status', updateTodolist) }Copy the code
<! -- src/components/TodoList.vue --> .... <! <el-button size="small" type="primary" @click="update(index)"> complete </el-button>.... <el-button size="small" type="primary" @click="update(index)"> restore </el-button>.... <script> // .... {//... Update (index) {this.$http.put('/ API /todolist/'+ this.id + '/' + this.list[index].id + '/' + this.list[index].status); Then ((res) => {if(res.status == 200){this.$message({type: 'success', message: 'Task status update succeeded! ' }) this.getTodolist(); }else{this.$message.error(' Task status update failed! ')}}, (err) => {this.$message.error(' Task status update failed! ') console.log(err) }) }, remove(index) { this.$http.delete('/api/todolist/'+ this.id + '/' + this.list[index].id) .then((res) => { if(res.status $this.$message({type: 'success', message: 'task deleted successfully! ' }) this.getTodolist(); }else{this.$message.error(' Task deletion failed! ')}}, (err) => {this.$message.error(' Task deletion failed! ') console.log(err) }) }, } // ... Omit < / script >...Copy the code

Let’s take a look at the final 99% :

todolist

Project deployment

Many tutorials end up where I left off. But what we really want when we do a project is to deploy it for everybody, right?

There are some holes in the deployment, and we need to let you know about them. This project is a full stack project (although a very simple one…). , so it involves the problem of communication between the front and back end, which will involve whether the same-domain request or cross-domain request.

As we said, there are two convenient solutions to this problem. First, the server side plus CORS allows clients to make arbitrary cross-domain requests. However, there is a problem with this, because we are developing in the same domain, and the requested address is also written relative address: / API /*, auth/*, etc., the access path is naturally the same domain. To add cORS to the server, we also need to change all of our request addresses to localhost:8889/ API /*, localhost:8889/auth/*, so that if the server port number changes, we also need to change all of the front-end request addresses. It’s inconvenient and unscientific.

Therefore, the best solution is to make the request co-domain – regardless of the server port number, as long as the co-domain can be requested.

So to combine Vue and Koa into a complete project (webpack actually broached the request for us in development mode, so it looked like a co-domain request, while Vue and Koa were not fully integrated), we had to “host” the static files from Vue in production mode. All requests to access the front-end go to Koa, including requests for static file resources, also go to Koa, using Koa as a static resource server for Vue projects, so that requests in Vue go to the same domain. (In development mode, Webpack started a server hosting Vue resources and requests. Now Koa hosts Vue resources and requests in production mode.)

Changing different managed servers in development and production mode is as simple as hosting the built Vue files in production mode with Koa’s static resource services middleware.

Webpack packaging

We will package our front-end project with Webpack before deploying. However, if you use NPM run build directly, you will find that the packaged file is too large:

Asset Size Chunks Chunk Names static/css/app.d9034fc06fd57ce00d6e75ed49f0dafe.css 120 kB 2, 5 kB specifies the world currently reserved for the world's most important actors and actresses. Currently, the world's most important actors and actresses are currently emitted by the world's most important actors and actresses. Currently, 0 [emitted] app static/fonts/element-icons.a61be9c.eot 13 kB [emitted] static/img/element-icons.09162bc.svg 17 kB [emitted] static/js/manifest.8ea250834bdc80e4d73b.js 832 bytes 0 [emitted] manifest The static/js/vendor. 75 bbe7ecea37b0d4c62d. Js 623 kB, 1 0 [emitted] vendor static/js/app. E2d125562bfc4c57f9cb. 2, js 16.5 kB 0 [emitted] app static/fonts/element - the ICONS. B02bdc1. The vera.ttf 13.2 kB/emitted static/js/manifest. 8 ea250834bdc80e4d73b. Js. The map 8.86 kB 0 [emitted] manifest the static/js/vendor. 75 bbe7ecea37b0d4c62d. Js. Map 1, 3.94 MB 0 [emitted] vendor static/js/app e2d125562bfc4c57f9cb. Js. 2, map 64.8 kB 0 [emitted] app static/css/app.d9034fc06fd57ce00d6e75ed49f0dafe.css.map 151 kB 2, 0 [emitted] app index.html 563 bytes [emitted]Copy the code

A 3.94MB map file. This is certainly not acceptable. Change the Settings of the webpack output and cancel the map output.

Change productionSourceMap: true to productionSourceMap: false. Then run the NPM run build again.

Asset Size Chunks Chunk Names static/fonts/element - the ICONS. A61be9c. Eot 13.5 kB [emitted] 5 kB [emitted by a wizard/a wizard/a wizard/a wizard/a wizard/a wizard static/js/manifest.3ba218c80028a707a728.js 774 bytes 0 [emitted] manifest static/js/vendor.75bbe7ecea37b0d4c62d.js 623 KB, 1 0 [emitted] vendor static/js/app b6acaca2531fc0baa447. 2, js 16.5 kB 0 [emitted] app static/css/app.d9034fc06fd57ce00d6e75ed49f0dafe.css 120 kB 2, 0 [emitted] app index.html 563 bytes [emitted]Copy the code

After sourceMap was removed, the size was reduced. While 600+ KB is still a bit large, 150+ KB after gzip is barely acceptable on the server. Of course, optimizing webpack output is beyond the scope of this article, and there are many better articles that cover it, so I won’t go into detail here.

When packaged, you output a bunch of static files that need to be on the server to access. We are going to host this heap of static resources with Koa.

Koa Serve Static resources

yarn add koa-static

Open app.js and introduce two new dependencies, where Path is native to NodeJS.

// app.js // .... const path =require('path') , serve = require('koa-static'); / /... App. use(serve(path.resolve('dist'))); // Use webpack-packed project directory as the Koa static file service directory. Koa. use('/auth', auth.routes()); koa.use("/api",jwt({secret: 'vue-koa-demo'}),api.routes()) // ...Copy the code

Koa is listening in 8889. Open your browser to localhost:8889 and you can see the following:

We’re almost done here, but there’s a problem: If we log in and refresh the Todolist page, we’ll see:

404

Why is this happening? The simple reason is that we are using front-end routing, using HTML5 History mode, and if we refresh the page without doing anything else, then the browser will go to the server to access the page address, because the server has Not configured the route for this address, so it will return 404 Not Found.

Refer to the vue-Router documentation for details

How to solve it? In fact, it is easy to add a middleware: koa-history-API-fallback.

yarn add koa-history-api-fallback

/ /... Omit const historyApiFallback = require('koa-history-api-fallback'); Use (require(' koa-bodyParser ')()); app.use(json()); app.use(logger()); app.use(historyApiFallback()); // Join at this location. Must be added before static file serve, otherwise it will fail. / /...Copy the code

At this point, you can restart KOA again, log in and refresh the page, and 404 Not Found will no longer appear.

API Test

Originally wrote the above basic article has been concluded. But I had a few problems along the way, so I needed to do some fine-tuning.

We know that koA’s use method is sequential only.

const app = require('koa');
app.use(A);
app.use(B);
Copy the code
const app = require('koa');
app.use(B);
app.use(A);
Copy the code

There is a difference between the two. The rule that is used first is executed first.

So if we put the static file serve and historyApiFallback before the API request, we will always return the full page when testing the API with Postman:

So the right thing to do is to put them after the rules of the API we wrote:

// app.js // ... koa.use('/auth', auth.routes()); // Mount to the koa-router and prefix all auth request paths with '/auth' request paths. Koa. use("/ API ", JWT ({secret: 'vue-koa-demo'}),api.routes()) // All requests that start with/API/must be verified by JWT. app.use(koa.routes()); // Mount routing rules to Koa. app.use(historyApiFallback()); App.use (serve(path.resolve('dist'))); // Use the webpack-wrapped project directory as the directory for the Koa static file serviceCopy the code

This will return the data normally.

Nginx configuration

When we actually deploy to the server, we definitely don’t ask people to type in the domain name :8889 and let people access it. So we need to use Nginx to listen on port 80 and direct requests to our designated domain to the Koa server.

Nginx.conf looks like this:

http { # .... Upstream koa. Server {server 127.0.0.1:8889; } server { listen 80; server_name xxx.xxx.com; location / { proxy_pass http://koa.server; proxy_redirect off; } #... } #... }Copy the code

If you have the energy, you can also configure Nginx Gzip, which can make the request JS\CSS\HTML and other static files smaller, faster response.

Write in the last

At this point, we have completed a complete project from front end to back end, local to server. Although it is really a very simple little thing, it has been written rotten in other ways (such as using localStorage for storage). However, as a complete front-end and back-end DEMO, I think it is relatively easier for everyone to get started, to realize that full-stack development is not as “difficult” as imagined (the difficulty of getting started is acceptable). There’s so much we can do with Nodejs!

Of course, due to the limited space, this article can not tell enough things, and it is impossible to cover everything, many things are point to stop, so that you can play. In fact, I also want to talk about the simple use of the Event Bus, and the basic implementation of paging, etc. There are too many things to digest at a time.

In fact, I had no idea how to combine Vue and Koa when I was working on the project some time ago. I don’t even know how to use Koa to provide the API, I only use Koa for server-side rendering, such as those pages rendered by template engines such as JADE\EJS. So I learned a lot from that project, so I share it with you.

In fact, the Koa API provided in this article is as close to RESTful as possible, so you can also learn how to provide RESTful apis through Koa.

Finally, put the Github address of this project. If this project is helpful to you, I hope you can fork and give me suggestions. If you have time again, you can click Star, that would be better

The last article before the New Year, wish everyone a happy New Year in advance.

Note: permission is required for reprinting and signature is required


Did this article help you? Welcome to join the front End learning Group wechat group: