Vue+Express+Mysql 全栈初体验

媒介

原文地点

曾几何时,你有无想过一个前端工程师的将来是什么样的?这个时刻你是否是会想到了一个词”前端架构师“,那末一个及格的前端架构只会前端OK吗?那固然不可,你必需具有全栈的才,如许才扩展个人的抽象力,才升职加薪,才迎娶白富美,才走向人生顶峰…

近来我在写一些后端的项目,发明反复事情太多,尤其是框架部份,然后这就抽闲整理了前后端的架子,主如果用的Vue,Express,数据存储用的Mysql,固然如果有其他须要,也能够直接切换到sqlite、postgres或许mssql。

先献上项目源码地点

项目

项目以todolist为🌰,简朴的完成了前后端的CURD。

后端手艺栈

前端手艺栈

项目组织

先看项目架构,client为前端组织,server为后端组织

|-- express-vue-web-slush
    |-- client
    |   |-- http.js   // axios 要求封装
    |   |-- router.js  // vue-router
    |   |-- assets  // 静态资本
    |   |-- components  // 公用组件
    |   |-- store  // store
    |   |-- styles // 款式
    |   |-- views // 视图
    |-- server
        |-- api    // controller api文件
        |-- container  // ioc 容器
        |-- daos  // dao层
        |-- initialize  // 项目初始化文件
        |-- middleware  // 中间件
        |-- models  // model层
        |-- services // service层

代码引见

前端代码就不多说,一眼就可以看出是vue-cli天生的组织,不一样的处所就是前端编写的代码是以Vue Class的情势编写的,详细细节请见从react转职到vue开辟的项目预备

然后这里重要形貌一下后端代码。

热更新

开辟环境必需品,我们运用的是nodemon,在项目根目次增加nodemon.json

{
  "ignore": [
    ".git",
    "node_modules/**/node_modules",
    "src/client"
  ]
}

ignore疏忽 node_modules 和 前端代码文件夹src/client 的js文件变动,ignore之外的js文件变动nodemon.json会重启node项目。

这里为了轻易,我写了一个剧本,同时启动前后端项目,以下:

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'], {
    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();

前端用vue-cli的vue-cli-service敕令启动。

后端用nodemon实行babel-node敕令启动

然后这前后端项目由node子历程启动,然后我们在package.json里增加script。

{
    "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",
    }
}

server.babel.config.js为后端的bable编译设置。

项目设置

所谓的项目设置呢,说的就是与营业没有关系的系统设置,比方你的日记监控设置、数据库信息设置等等

起首,在项目内里新建设置文件,config.properties,比方我这里运用的是Mysql,内容以下:

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

在项目启动之前,我们运用properties对其举行剖析,在我们的server/initialize新建properties.js,对设置文件举行剖析:

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 {};
  });
}

然后在项目启动之前,初始化mysql,在server/initialize文件夹新建文件index.js

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)
  });
}

这里我们数据耐久化用的sequelize,依靠注入用的awilix,我们下文形貌。

初始化一切设置后,我们在项目启动之前实行initialize,以下:

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('*', (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();

数据耐久化

作为前端,对数据耐久化这个词没什么观点,这里简朴引见一下,起首数据分为两种状况,一种是瞬时状况,一种是耐久状况,而瞬时状况的数据平常是存在内存中,还没有永远保留的数据,一旦我们效劳器挂了,那末这些数据将会丧失,而耐久状况的数据呢,就是已落到硬盘上面的数据,比方mysql、mongodb的数据,是保留在硬盘里的,就算效劳器挂了,我们重启效劳,照样能够获取到数据的,所以数据耐久化的作用就是将我们的内存中的数据,保留在mysql或许其他数据库中。

我们数据耐久化是用的sequelize,它能够帮我们对接mysql,让我们疾速的对数据举行CURD。

下面我们在server/initialize文件夹新建sequelize.js,轻易我们在项目初始化的时刻衔接:

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;

initSequelize的入参config,来源于我们的config.properties,在项目启动之前实行衔接。

然后,我们须要对应数据库的每一个表竖立我们的Model,以todolist为例,在service/models,新建文件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;
}

然后在service/models,新建index.js,用来导入models文件夹下的一切model:

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;

这个installModel也是在我们项目初始化的时刻实行的。

model初始化完了以后,我们就可以够定义我们的Dao层,运用model了。

依靠注入

依靠注入(DI)是反转掌握(IOC)的最经常使用的体式格局。最早据说这个观点的置信大多数都是来源于Spring,反转掌握最大的作用的帮我们建立我们所需如果实例,而不须要我们手动建立,而且实例的建立的依靠我们也不须要体贴,全都由IOC帮我们治理,大大的降低了我们代码之间的耦合性。

这里用的依靠注入是awilix,起首我们建立容器,在server/container,下新建index.js

import * as awilix from 'awilix';

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

export default container;

然后在我们项目初始化的时刻,用awilix-express初始化我们后端的router,以下:

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
}));

然后,我们能够在server/api下新建我们的controller,这里新建一个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('效劳端非常');
      return;
    }
    res.successPrint('查询胜利', todolist);
  }

  //  ...
}

这里能够看到组织函数的入参注入了Service层的todoService实例,然后能够直接运用。

然后,我们要搞定我们的Service层和Dao层,这也是在项目初始化的时刻,通知IOC我们一切Service和Dao文件:

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

// 依靠注入设置service层和dao层
container.loadModules(['services/*.js', 'daos/*.js'], {
  formatName: 'camelCase',
  register: asClass,
  cwd: path.resolve(__dirname)
});

然后我们能够在services和daos文件夹下毫无所惧的新建service文件和dao文件了,这里我们新建一个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('效劳端非常'), null];
    }
  }

  // ...
}

然后,新建一个Dao,ItemDao.js,用来对接ItemModel,也就是mysql的Item表:

import BaseDao from './base';

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

    constructor(modules) {
      super(modules);
    }

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

然后搞一个BaseDao,封装一些数据库的经常使用操纵,代码太长,就不贴了,概况见代码库

关于事件

所谓事件呢,简朴的比较好明白,比方我们实行了两条SQL,用来新增两条数据,当第一条实行胜利了,第二条没实行胜利,这个时刻我们实行事件的回滚,那末第一条胜利的纪录也将会被作废。

然后呢,我们这里为了也满足事件,我们能够按需运用中间件,为要求注入事件,然后所以在这个要求下实行的增编削的SQL,都运用这个事件,以下中间件:

import { asValue } from 'awilix';

export default function () {
  return function (req, res, next) {
    const sequelize = container.resolve('sequelize');
    sequelize.transaction({  // 开启事件
      autocommit: false
    }).then(t => {
      req.container = req.container.createScope(); // 为当前要求新建一个IOC容器作用域
      req.transaction = t;
      req.container.register({  // 为IOC注入一个事件transaction
        transaction: asValue(t)
      });
      next();
    });
  }
}

然后当我们须要提交事件的时刻,我们能够运用IOC注入transaction,比方,我们在TodoService.js中运用事件


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

  async addItem(item) {
    // TODO: 增加item数据
    const success = await this.itemDao.addItem(item);
    if (success) {
      this.transaction.commit(); // 实行事件提交
    } else {
      this.transaction.rollback(); // 实行事件回滚
    }
  }

  // ...
}

其他

当我们须要在Service层或许Dao层运用到当前的要求对象怎么办呢,这个时刻我们须要在IOC中为每一条要求注入request和response,以下中间件:

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;

    // 注入request、response
    req.container = req.container.createScope();
    req.container.register({
      request: asValue(req),
      response: asValue(res)
    });
    next();
  }
}

然后在项目初始化的时刻,运用该中间件:

import express from 'express';

const app = express();
app.use(baseMiddleware(app));

关于布置

运用pm2,简朴完成布置,在项目根目次新建pm2.json

{
  "apps": [
    {
      "name": "vue-express",  // 实例名
      "script": "./dist/server/main.js",  // 启动文件
      "log_date_format": "YYYY-MM-DD HH:mm Z",  // 日记日期文件夹花样
      "output": "./log/out.log",  // 其他日记
      "error": "./log/error.log", // error日记
      "instances": "max",  // 启动Node实例数
      "watch": false, // 封闭文件监听重启
      "merge_logs": true,
      "env": {
        "NODE_ENV": "production"
      }
    }
  ]
}

这个时刻,我们须要把客户端和效劳端编译到dist目次,然后将效劳端的静态资本目次指向客户端目次,以下:

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

增加vue-cli的设置文件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: {
        '@': clientPath
      }
    },
    devServer: {
      proxy: {
        '/api': { // 开辟环境将API前缀设置到后端端口
          target: 'http://localhost:9001'
        }
      }
    }
  },
  outputDir: './dist/client/'
};

在package.json中增加以下script:

{
  "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"
  }
}

实行build敕令,清算dist目次,同时编译前后端代码到dist目次下,然后npm run start,pm2启动dist/server/main.js;

到此为止,布置完成。

完毕

发明本身挂羊头卖狗肉,居然全在写后端。。。好吧,我认可我原本就是想写后端的,然则我照样以为作为一个前端工程师,Nodejs应该是在这条路上走下去的必备妙技,加油~。

项目源码地点

    原文作者:_杨溜溜
    原文地址: https://segmentfault.com/a/1190000019294514
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞