Первый опыт полного стека Vue+Express+Mysql

полный стек

предисловие

исходный адрес

Когда-то вы когда-нибудь задумывались, как будет выглядеть будущее фронтенд-инженера? В настоящее время вы думаете о слове «дизайнер внешнего интерфейса», поэтому квалифицированная архитектура внешнего интерфейса будет в порядке только во внешнем интерфейсе? Конечно нет, у вас должны быть возможности полного стека, чтобы вы могли расширить свой личный имидж, получить продвижение по службе и повысить свою зарплату, выйти замуж за Бай Фумей и достичь вершины жизни...

Недавно я писал несколько back-end проектов и обнаружил, что слишком много дублирования работы, особенно часть framework.Потом я потратил время на то, чтобы разобраться с полками front-end и back-end, в основном используя Vue, Express, и Mysql для хранения данных.Конечно, если есть другие потребности, Вы также можете переключиться напрямую на sqlite, postgres или mssql.

дай первымАдрес исходного кода проекта

проект

В проекте используется todolist как 🌰, который просто реализует CURD на переднем и заднем концах.

Стек серверных технологий

Стек передовых технологий

Структура проекта

Сначала посмотрите на структуру проекта, клиент — это интерфейсная структура, сервер — это внутренняя структура.

|-- 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 Класс Подробнее см.Подготовка проекта к переходу с react на vue development

Тогда вот основное описание внутреннего кода.

Горячее обновление

Необходимость среды разработки, мы используем nodemon, добавляем в корневую директорию проектаnodemon.json:

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

ignoreИзменения в js-файлах в node_modules и в папке внешнего кода src/client игнорируются, а изменения в nodemon.json в js-файлах, кроме игнорирования, перезапускают проект узла.

Для удобства я написал скрипт для одновременного запуска front-end и back-end проектов следующим образом:

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-clivue-cli-serviceкоманда запускается.

для бэкендаnodemonвоплощать в жизньbabel-node命令启动.

Затем подпроцессом узла запускаются проекты front-end и back-end, после чего мы добавляем скрипт в 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",
    }
}

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

Здесь мы используем sequenceize для сохранения данных и awilix для внедрения зависимостей, которые мы опишем ниже.

После инициализации всех конфигураций мы выполняем инициализацию перед запуском проекта следующим образом:

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.properties, подключение выполняется до запуска проекта.

Затем нам нужно построить нашу модель, соответствующую каждой таблице в базе данных, взяв в качестве примера 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, используемый для импорта всех моделей в папку моделей:

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Он также выполняется при инициализации нашего проекта.

После того, как модель инициализирована, мы можем определить наш уровень Dao и использовать модель.

внедрение зависимости

Внедрение зависимостей (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Инициализируйте наш внутренний маршрутизатор следующим образом:

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Далее создаем наш контроллер, а здесь создаем новый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);
  }

  //  ...
}

Здесь вы можете видеть, что параметры конструктора внедряются в сервисный слой.todoServiceэкземпляр, который затем можно использовать напрямую.

Затем мы должны получить наш сервисный слой и уровень 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)
});

Затем мы можем недобросовестно создавать новые служебные файлы и файлы dao в папках services и daos, здесь мы создаем новый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];
    }
  }

  // ...
}

Затем создайте новое Дао,ItemDao.js, используемый для стыковки ItemModel, которая является таблицей Item в mysql:

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 для внедрения транзакции, например, мы используем транзакцию в 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(); // 执行事务回滚
    }
  }

  // ...
}

разное

Что нам делать, когда нам нужно использовать текущий объект запроса на уровне службы или уровне Dao?В настоящее время нам нужно вводить запрос и ответ для каждого запроса в IOC, например следующее промежуточное программное обеспечение:

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-clivue.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": {
    "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"
  }
}

Выполните команду сборки, очистите каталог dist, скомпилируйте внешний и внутренний код в каталог dist, а затемnpm run start, pm2 начинаетсяdist/server/main.js;

На этом развертывание завершено.

конец

Я обнаружил, что продаю собачье мясо на голову овцы, но писал задницу. . . Ну, я признаю, что изначально хотел писать бэкенд, но я все еще думаю, что как фронтенд-инженер, Nodejs должен быть обязательным навыком на этом пути, да ладно~.

Адрес исходного кода проекта