preface

The original address

Have you ever wondered what the future of a front-end engineer looks like? Do you think of the word “front-end architect” at this time, so a qualified front-end architecture only front-end OK? Of course not, you must have the full stack of ability, so as to expand the personal image power, in order to promote salary, in order to marry bai Fu Mei, in order to the peak of life…

Recently, I was writing some back-end projects and found that there was too much duplication of work, especially in the framework part. Then I took some time to tidy up the front and back shelves, mainly using Vue, Express and Mysql for data storage. Of course, if there is other need, I can also directly switch to SQLite, Postgres or MSSQL.

Present the source code address of the project first

project

The project takes Todolist as 🌰 and simply implements CURD at the front and back ends.

Back-end technology stack

  • Framework Express
  • Hot update nodemon
  • Dependency injection Awilix
  • Data persistence sequelize
  • The deployment of pm2

Front-end technology stack

  • vue-router
  • vuex
  • axios
  • vue-class-component
  • vue-property-decorator
  • vuex-class

The project structure

First look at the project architecture, client is the front-end structure, server is the back-end structure

| - express - vue web - slush | - client | | - HTTP. Js / / axios request package | | -- the router. Js / / vue - the router | | | - assets / / static resources | - components / / utility components | | - store / / store | | - styles / / style | | - views / / view | -- - | server API / / controller API documentation | - container / / ioc container | - | daos / / dao layer - the initialize / / project initialization file | - middleware / / middleware | | -- - models / / model layer Services / / service layerCopy the code

The code is introduced

The front-end code is written in the form of vue Class. For details, please refer to the preparation of the Project from react to VUE development

Then I’ll focus on the back-end code here.

Hot update

Json to the root directory of the project:

{
  "ignore": [
    ".git"."node_modules/**/node_modules"."src/client"]}Copy the code

Ignore Ignores node_modules and js file changes in the front-end code folder SRC /client. If js files other than ignore are changed, nodemon.json restarts the Node project.

Here, for convenience, I write a script that starts the front and back end projects as follows:

import * as childProcess from 'child_process';

function run() {
  const client = childProcess.spawn('vue-cli-service'['serve']);
  client.stdout.on('data', x => process.stdout.write(x));
  client.stderr.on('data', x => process.stderr.write(x));

  const server = childProcess.spawn('nodemon'['--exec'.'npm run babel-server'] and {env: Object.assign({
      NODE_ENV: 'development'
    }, process.env),
    silent: false
  });
  server.stdout.on('data', x => process.stdout.write(x));
  server.stderr.on('data', x => process.stderr.write(x));

  process.on('exit', () => {
    server.kill('SIGTERM');
    client.kill('SIGTERM');
  });
}
run();
Copy the code

The vue-cli-service command of vue-cli is used to start the front-end service.

Run the babel-node command to start the back-end system.

The backend project is then started by the Node child process, and we add script to package.json.

{
    "scripts": {
        "dev-env": "cross-env NODE_ENV=development"."babel-server": "npm run dev-env && babel-node --config-file ./server.babel.config.js -- ./src/server/main.js"."dev": "babel-node --config-file ./server.babel.config.js -- ./src/dev.js",}}Copy the code

Server.babel.config. js compiles the configuration for the backend bable.

Project configuration

By project configuration, we mean non-business related system configuration, such as your log monitoring configuration, database information configuration, etc

First, create a new configuration file, config.properties, in your project. For example, I’m using Mysql.

[mysql]
host=127.0.0.1
port=3306
user=root
password=root
database=test
Copy the code

Before starting the project, we parse it using Properties. In our server/initialize, we create properties.js to parse the configuration file:

import properties from 'properties';
import path from 'path';

const propertiesPath = path.resolve(process.cwd(), 'config.properties');

export default function load() {
  return new Promise((resolve, reject) = > {
    properties.parse(propertiesPath, { path: true.sections: true }, (err, obj) => {
      if (err) {
        reject(err);
        return;
      }
      resolve(obj);
    });
  }).catch(e= > {
    console.error(e);
    return {};
  });
}
Copy the code

Then, before the project starts, initialize mysql and create a new file index.js in the server/initialize folder

import loadProperties from './properties';
import { initSequelize } from './sequelize';
import container from '.. /container';
import * as awilix from 'awilix';
import { installModel } from '.. /models';

export default async function initialize() {
  const config = await loadProperties();
  const { mysql } = config;
  const sequelize = initSequelize(mysql);
  installModel(sequelize);
  container.register({
    globalConfig: awilix.asValue(config),
    sequelize: awilix.asValue(sequelize)
  });
}
Copy the code

Here we use Sequelize for data persistence and Awilix for dependency injection, which we describe below.

After initializing all configurations, we perform Initialize before the project starts as follows:

import express from 'express';
import initialize from './initialize';
import fs from 'fs';

const app = express();

export default async function run() {
  await initialize(app);

  app.get(The '*', (req, res) => {
    const html = fs.readFileSync(path.resolve(__dirname, '.. /client'.'index.html'), 'utf-8');
    res.send(html);
  });

  app.listen(9001, err => {
    if (err) {
      console.error(err);
      return;
    }
    console.log('Listening at http://localhost:9001');
  });
}

run();
Copy the code

Data persistence

The word as a front end for data persistence nothing concept, here a brief introduction, the first data is divided into two kinds of state, is a transient state, is a persistent state, and the instantaneous state of data is generally exists in memory, there’s no permanent data, once we server hang up and then the data will be lost, and persistent state data? The data that has fallen to the hard disk, such as the data of mysql and mongodb, is stored in the hard disk. Even if the server is down, we can restart the service and still get the data. Therefore, the function of data persistence is to save the data in our memory in mysql or other databases.

We use Sequelize for data persistence, which helps us connect to mysql and allows us to CURD data quickly.

Create sequelize.js in the server/initialize folder, so that we can connect to sequelize.js during project initialization.

import Sequelize from 'sequelize';

let sequelize;

const defaultPreset = {
  host: 'localhost'.dialect: 'mysql'.operatorsAliases: false.port: 3306.pool: {
    max: 10.min: 0.acquire: 30000.idle: 10000}};export function initSequelize(config) {
  const { host, database, password, port, user } = config;
  sequelize = new Sequelize(database, user, password, Object.assign({}, defaultPreset, {
    host,
    port
  }));
  return sequelize;
};

export default sequelize;
Copy the code

The input parameter config to initSequelize, from our config.properties, performs the connection before the project starts.

Then, we need to create our Model for each table in the database. Todolist for example, in service/ Models, create a file itemModel.js:

export default function(sequelize, DataTypes) {
    const Item = sequelize.define('Item', {
        recordId: {
            type: DataTypes.INTEGER,
            field: 'record_id'.primaryKey: true
        },
        name: {
            type: DataTypes.STRING,
            field: 'name'
        },
        state: {
            type: DataTypes.INTEGER,
            field: 'state'}}, {tableName: 'item'.timestamps: false
    });
    return Item;
}
Copy the code

Then, under service/ Models, create index.js to import all models in the models folder:

import fs from 'fs';
import path from 'path';
import Sequelize from 'sequelize';

const db = {};

export function installModel(sequelize) {
  fs.readdirSync(__dirname)
    .filter(file= > (file.indexOf('. ')! = =0 && file.slice(- 3) = = ='.js'&& file ! = ='index.js'))
    .forEach((file) = > {
      const model = sequelize.import(path.join(__dirname, file));
      db[model.name] = model;
    });
  Object.keys(db).forEach((modelName) = > {
    if(db[modelName].associate) { db[modelName].associate(db); }}); db.sequelize = sequelize; db.Sequelize = Sequelize; }export default db;
Copy the code

This installModel is also executed during our project initialization.

Once the Model is initialized, we can define our Dao layer and use the Model.

Dependency injection

Dependency injection (DI) is the most common form of inversion of control (IOC). First heard of the concept that most are from the Spring, inversion of control the biggest role to help us create instances, all we need is not we need to manually create, dependence and instance creation we don’t need to care about, all managed by the IOC to help us, greatly reduces the coupling between the code.

The dependency injection used here is awilix. First we create the container, under server/container, create index.js:

import * as awilix from 'awilix';

const container = awilix.createContainer({
  injectionMode: awilix.InjectionMode.PROXY
});

export default container;
Copy the code

Then, during the initialization of our project, we used awilix-Express to initialize our back-end router as follows:

import { loadControllers, scopePerRequest } from 'awilix-express';
import { Lifetime } from 'awilix';

const app = express();

app.use(scopePerRequest(container));

app.use('/api', loadControllers('api/*.js', {
  cwd: __dirname,
  lifetime: Lifetime.SINGLETON
}));
Copy the code

Then, we can create our controller under server/ API. Here we create todoapi.js:

import { route, GET, POST } from 'awilix-express';

@route('/todo')
export default class TodoAPI {

  constructor({ todoService }) {
    this.todoService = todoService;
  }

  @route('/getTodolist')
  @GET()
  async getTodolist(req, res) {
    const [err, todolist] = await this.todoService.getList();
    if (err) {
      res.failPrint('Server exception');
      return;
    }
    res.successPrint('Query successful', todolist);
  }

  / /...
}
Copy the code

Here you can see that the constructor’s input parameter injects the todoService instance of the Service layer and can then be used directly.

Then, we need to fix our Service and Dao layers. This also tells IOC all of our Service and Dao files at project initialization:

import container from './container';
import { asClass } from 'awilix';

Dependency injection configures the Service layer and dao layer
container.loadModules(['services/*.js'.'daos/*.js'] and {formatName: 'camelCase'.register: asClass,
  cwd: path.resolve(__dirname)
});
Copy the code

Todoservice.js todoservice.js todoservice.js todoservice.js todoservice.js


export default class TodoService {
  constructor({ itemDao }) {
    this.itemDao = itemDao;
  }

  async getList() {
    try {
      const list = await this.itemDao.getList();
      return [null, list];
    } catch (e) {
      console.error(e);
      return [new Error('Server exception'), null]; }}// ...
}
Copy the code

Create a Dao, itemDao.js, that connects to the ItemModel table:

import BaseDao from './base';

export default class ItemDao extends BaseDao {
    
    modelName = 'Item';

    constructor(modules) {
      super(modules);
    }

    async getList() {
      return await this.findAll(); }}Copy the code

Then create a BaseDao that encapsulates some common operations of the database. If the code is too long, I will not paste it. See the code base for details.

About the transaction

The so-called transaction, simple and easy to understand, for example, we execute two SQL, to add two data, when the first execution is successful, the second execution is not successful, this time we execute the transaction rollback, then the first successful record will be canceled.

Then, in order to satisfy the transaction, we can use the middleware as needed to inject a transaction into the request, and then use this transaction for all the SQL added, deleted, or modified under this request:

import { asValue } from 'awilix';

export default function () {
  return function (req, res, next) {
    const sequelize = container.resolve('sequelize');
    sequelize.transaction({  // Start the transaction
      autocommit: false
    }).then(t= > {
      req.container = req.container.createScope(); // Create an IOC container scope for the current request
      req.transaction = t;
      req.container.register({  // Inject a transaction into IOCtransaction: asValue(t) }); next(); }); }}Copy the code

Then when we need to commit a transaction, we can use IOC to inject transaction, for example, we use transaction in todoService.js


export default class TodoService {
  constructor({ itemDao, transaction }) {
    this.itemDao = itemDao;
    this.transaction = transaction;
  }

  async addItem(item) {
    // TODO:Add item data
    const success = await this.itemDao.addItem(item);
    if (success) {
      this.transaction.commit(); // Perform the transaction commit
    } else {
      this.transaction.rollback(); // Perform transaction rollback}}// ...
}
Copy the code

other

What if we need to use the current request object in the Service layer or Dao layer? In this case, we need to inject request and response for each request in IOC, as follows middleware:

import { asValue } from 'awilix';

export function baseMiddleware(app) {
  return (req, res, next) = > {
    res.successPrint = (message, data) = > res.json({ success: true, message, data });

    res.failPrint = (message, data) = > res.json({ success: false, message, data });
    req.app = app;

    // Inject request and response
    req.container = req.container.createScope();
    req.container.register({
      request: asValue(req),
      response: asValue(res) }); next(); }}Copy the code

This middleware is then used during project initialization:

import express from 'express';

const app = express();
app.use(baseMiddleware(app));
Copy the code

About the deployment

Use PM2 to implement simple deployment and create pm2. Json in the project root directory

{" apps ": [{" name" : "vue - express", / / instance name "script" : ". / dist/server/main. Js ", / / boot file "log_date_format" : "Yyyy-mm-dd HH: MM Z", // Log date folder format "output": "./log/out.log", // other logs "error": "./log/error.log", // error log" instances": "Max ", // Start Node instances" watch": false, // stop file listening restart "merge_logs": true, "env": { "NODE_ENV": "production" } } ] }Copy the code

At this point, we need to compile the client and server to the dist directory, and then point the server’s static resource directory to the client directory as follows:

app.use(express.static(path.resolve(__dirname, '.. /client')));
Copy the code

Add vue-CLI configuration file vue.config.js:

const path = require('path');
const clientPath = path.resolve(process.cwd(), './src/client');
module.exports = {
  configureWebpack: {
    entry: [
      path.resolve(clientPath, 'main.js')].resolve: {
      alias: {
        The '@': clientPath
      }
    },
    devServer: {
      proxy: {
        '/api': { // The development environment configures the API prefix to the back-end port
          target: 'http://localhost:9001'}}}},outputDir: './dist/client/'
};
Copy the code

Add the following script to package.json:

{
  "script": {
    "clean": "rimraf dist"."pro-env": "cross-env NODE_ENV=production"."build:client": "vue-cli-service build"."build:server": "babel --config-file ./server.babel.config.js src/server --out-dir dist/server/"."build": "npm run clean && npm run build:client && npm run build:server"."start": "pm2 start pm2.json"."stop": "pm2 delete pm2.json"}}Copy the code

Dist /server/main.js; dist/server/main.js; dist/server/main.js; dist/server/main.js; dist/server/main.js

At this point, deployment is complete.

The end of the

I found myself hanging a sheep’s head selling dog meat, actually all in the back end of writing… Ok, I’ll admit that I wanted to write the back end, but I still think Nodejs is a necessary skill to have as a front-end engineer on this path.

Project source code address