原文地址html
曾幾什麼時候,你有沒有想過一個前端工程師的將來是什麼樣的?這個時候你是否是會想到了一個詞」前端架構師「,那麼一個合格的前端架構只會前端OK嗎?那固然不行,你必須具有全棧的能力,這樣才能擴大我的的形象力,才能升職加薪,才能迎娶白富美,才能走向人生巔峯...前端
最近我在寫一些後端的項目,發現重複工做太多,尤爲是框架部分,而後這就抽空整理了先後端的架子,主要是用的Vue,Express,數據存儲用的Mysql,固然若是有其餘須要,也能夠直接切換到sqlite、postgres或者mssql。vue
先獻上項目源碼地址node
項目以todolist爲🌰,簡單的實現了先後端的CURD。mysql
先看項目架構,client爲前端結構,server爲後端結構react
|-- 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開發的項目準備ios
而後這裏主要描述一下後端代碼。git
開發環境必需品,咱們使用的是nodemon,在項目根目錄添加nodemon.json
:github
{ "ignore": [ ".git", "node_modules/**/node_modules", "src/client" ] }
ignore
忽略 node_modules 和 前端代碼文件夾src/client 的js文件變動,ignore之外的js文件變動nodemon.json會重啓node項目。web
這裏爲了方便,我寫了一個腳本,同時啓動先後端項目,以下:
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應該是在這條路上走下去的必備技能,加油~。