相似於知乎的
css
待補充html
說明: 本文主要側重後端,最後的效果相似於我司後端前端
├── assets // 靜態資源,css, 圖片等 ├── client // 客戶端目錄,axios請求函數和其餘輔助函數 ├── components // vue組件目錄 ├── config // 默認設置 ├── layouts // nuxt視圖 ├── middleware // nuxt 中間件 ├── migrations // orm 數據遷移 ├── models // orm 數據模型 ├── nuxt.config.js ├── nuxt.config.ts ├── package-lock.json ├── package.json ├── pages // nuxt ├── plugins // hapi插件和nuxt插件 ├── routes // hapi路由 ├── seeders // 種子數據 ├── server // app.js ├── static // 靜態資源 ├── store // nuxt ├── tsconfig.json ├── uploads // 文件上傳目標目錄 └── utils // 輔助函數
hapi官方文檔已經說了不少了(expresstohapi),這裏最吸引個人是,不用安裝不少的插件(expres的話有不少的xx-parse插件...),就能知足個人需求,並且hapi已經應用於商用了。vue
個人這些代碼,在我目前的package.json的版本是能正常運行的,hapi版本大版本有時候會出現不兼容的,不一樣版本的hapi對應着不一樣的插件版本,因此須要和個人版本保持一致,我還遇到過nuxt.js v2.9運行加入ts出現不識別@component的狀況,安裝2.8.x版本就沒有問題。node
開發後臺第一個想到的是創建數據模型(建表),默認你已經安裝好了mysql
以前我本身用數據庫,不知道有orm這個工具的時候,會選擇本身用navicat這樣的圖形化工具建表或者直接用sql語句建表。這樣作有幾個缺點:mysql
注意:用orm在執行sql操做時,至關於咱們用jquery執行dom操做,api簡單了,但仍是須要對原來的有點了解jquery
sequelize就是node.js的promise orm工具,同時也支持其餘數據庫.ios
npm i sequelize-cli -D npm i sequelize npm i mysql2
經過 sequelize-cli 初始化 sequelize,咱們將獲得一個好用的初始化結構:git
// 能夠安裝npx node_modules/.bin/sequelize init
├── config # 項目配置目錄 | ├── config.json # 數據庫鏈接的配置 ├── models # 數據庫 model | ├── index.js # 數據庫鏈接的樣板代碼 ├── migrations # 數據遷移的目錄 ├── seeders # 數據填充的目錄
config/config.jsongithub
默認生成文件爲一個 config.json 文件,文件裏配置了開發、測試、生產三個默認的樣板環境,咱們能夠按需再增長更多的環境配置。這裏我用config.js替代config.json,這樣配置更加靈活
修改後的 config/config.js 以下,僅預留了 development(開發) 與 production(生產) 兩個環境,開發環境與生產環境的配置參數能夠分離在 .env 和 .env.prod 兩個不一樣的文件裏,經過環境變量參數 process.env.NODE_ENV 來動態區分。
// config.js if (process.env.NODE_ENV === 'production') { require('env2')('./.env.prod') } else { require('env2')('./.env.dev') } const { env } = process module.exports = { 'development': { 'username': env.MYSQL_USERNAME, 'password': env.MYSQL_PASSWORD, 'database': env.MYSQL_DB_NAME, 'host': env.MYSQL_HOST, 'port': env.MYSQL_PORT, dialect: 'mysql', logging: false, // mysql 執行日誌 timezone: '+08:00' // "operatorsAliases": false, // 此參數爲自行追加,解決高版本 sequelize 鏈接警告 }, 'production': { 'username': env.MYSQL_USERNAME, 'password': env.MYSQL_PASSWORD, 'database': env.MYSQL_DB_NAME, 'host': env.MYSQL_HOST, 'port': env.MYSQL_PORT, dialect: 'mysql', timezone: '+08:00' // "operatorsAliases": false, // 此參數爲自行追加,解決高版本 sequelize 鏈接警告 } }
.env.dev
# 服務的啓動名字和端口,但也能夠缺省不填值,默認值的填寫只是必定程度減小起始數據配置工做 HOST = 127.0.0.1 PORT = 80 # 端口最好就爲80,否則axios url要改成絕對地址 # MySQL 數據庫連接配置 MYSQL_HOST = 111.111.111.111 MYSQL_PORT = 3306 MYSQL_DB_NAME = 數據庫名 MYSQL_USERNAME = 數據庫用戶名 MYSQL_PASSWORD = 數據庫密碼 JWT_SECRET = token密鑰
npx sequelize db:create
npx migration:create --name user
在 migrations 的目錄中,會新增出一個 時間戳-user.js 的遷移文件,自動生成的文件裏,包涵有 up 與 down 兩個空函數, up 用於定義表結構正向改變的細節,down 則用於定義表結構的回退邏輯。好比 up 中有 createTable 的建錶行爲,則 down 中配套有一個對應的 dropTable 刪除錶行爲。至關因而一條操做記錄記錄。修改後的用戶遷移文件以下:
'use strict' module.exports = { up: (queryInterface, Sequelize) => queryInterface.createTable( 'user', { uid: { type: Sequelize.UUID, primaryKey: true }, nickname: { type: Sequelize.STRING, allowNull: false, unique: true }, avatar: Sequelize.STRING, description: Sequelize.STRING, username: { type: Sequelize.STRING, allowNull: false, unique: true }, password: { type: Sequelize.STRING, allowNull: false }, created_time: Sequelize.DATE, updated_time: Sequelize.DATE }, { charset: 'utf8' } ), down: queryInterface => queryInterface.dropTable('user') }
npx sequelize db:migrate
sequelize db:migrate 的命令,能夠最終幫助咱們將 migrations 目錄下的遷移行爲定義,按時間戳的順序,逐個地執行遷移描述,最終完成數據庫表結構的自動化建立。而且,在數據庫中會默認建立一個名爲 SequelizeMeta 的表,用於記錄在當前數據庫上所運行的遷移歷史版本。已經執行過的不會再次執行,能夠執行sequelize db:migrate:undo執行上個遷移文件的down命令。
執行
sequelize seed:create --name init-user
相似的在seeders目錄下生成一份文件 時間戳-init-user.js
修改後
'use strict' const uuid = require('uuid') const timeStamp = { created_time: new Date(), updated_time: new Date() } const users = [] for (let i = 1; i < 5; i++) { users.push( { uid: uuid(), username: 'zlj' + i, password: '123', nickname: '火鍋' + 1, ...timeStamp } ) } module.exports = { up: queryInterface => queryInterface.bulkInsert('user', users, { charset: 'utf-8' }), down: (queryInterface, Sequelize) => { const { Op } = Sequelize return queryInterface.bulkDelete('user', { uid: { [Op.in]: users.map(v => v.uid) } }, {}) } }
執行填充命令
sequelize db:seed:all
查看數據庫user表就多了一些記錄,其餘的操做相似於遷移,更多的操做能夠看文檔
7 定義模型
user表 models/user.js
const moment = require('moment') module.exports = (sequelize, DataTypes) => sequelize.define( 'user', { uid: { type: DataTypes.UUID, primaryKey: true }, avatar: DataTypes.STRING, description: DataTypes.STRING, nickname: { type: DataTypes.STRING, unique: true, allowNull: false }, username: { type: DataTypes.STRING, allowNull: false, unique: true }, password: { type: DataTypes.STRING, allowNull: false }, created_time: { type: DataTypes.DATE, get () { return moment(this.getDataValue('created_time')).format('YYYY-MM-DD HH:mm:ss') } }, updated_time: { type: DataTypes.DATE, get () { return moment(this.getDataValue('updated_time')).format('YYYY-MM-DD HH:mm:ss') } } }, { tableName: 'user' } )
modes/index.js
'use strict' const fs = require('fs') const path = require('path') const uuid = require('uuid') const Sequelize = require('sequelize') const basename = path.basename(__filename) // eslint-disable-line const configs = require(path.join(__dirname, '../config/config.js')) const db = {} const env = process.env.NODE_ENV || 'development' const config = { ...configs[env], define: { underscored: true, timestamps: true, updatedAt: 'updated_time', createdAt: 'created_time', hooks: { beforeCreate (model) { model.uid = uuid() } } } } let sequelize if (config.use_env_variable) { sequelize = new Sequelize(process.env[config.use_env_variable], config) } else { sequelize = new Sequelize(config.database, config.username, config.password, config) } fs .readdirSync(__dirname) .filter((file) => { return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.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 // 外鍵關聯關係 // 假設你全部表創建好了 db.user.hasMany(db.article, { foreignKey: 'uid' }) db.article.belongsTo(db.user, { foreignKey: 'author' }) db.user.hasMany(db.comment, { foreignKey: 'uid' }) db.comment.belongsTo(db.user, { foreignKey: 'author' }) db.user.hasMany(db.article_like, { foreignKey: 'uid' }) db.article_like.belongsTo(db.user, { foreignKey: 'author' }) db.article.hasMany(db.comment) db.comment.belongsTo(db.article) db.article.hasMany(db.article_like) db.article_like.belongsTo(db.article) module.exports = db
多表查詢、單表增刪改查、模型統一配置、遷移和種子填充、事務(刪除文章的時候,把文章相關的數據:評論,閱讀,點贊數據也一塊兒刪了。)等。
joi能夠對請求參數進行校驗
# 安裝適配 hapi v16 的 joi 插件 npm i joi@14
post: 登陸接口:
routes/user.js
const models = require('../models') const Joi = require('@hapi/joi') { method: 'POST', path: '/api/user/login', handler: async (request, h) => { const res = await models.user.findAll({ attributes: { exclude: ['password', 'created_time', 'updated_time'] }, where: { username: request.payload.username, // 通常密碼存庫都會加密的,md5等 password: request.payload.password } }) const data = res[0] if (res.length > 0) { return h.response({ code: 0, message: '登陸成功!', data: { // 寫入token token: generateJWT(data.uid), ...data.dataValues } }) } else { return h.response({ code: 10, message: '用戶名或密碼錯誤' }) } }, config: { auth: false, tags: ['api', 'user'], description: '用戶登陸', validate: { payload: { username: Joi.string().required(), password: Joi.string().required() } } } },
npm i hapi-swagger@10 npm i inert@5 npm i vision@5 npm i package@1
├── plugins # hapi 插件配置 | ├── hapi-swagger.js
hapi-swagger.js
// plugins/hapi-swagger.js const inert = require('@hapi/inert') const vision = require('@hapi/vision') const package = require('package') const hapiSwagger = require('hapi-swagger') module.exports = [ inert, vision, { plugin: hapiSwagger, options: { documentationPath: '/docs', info: { title: 'my-blog 接口 文檔', version: package.version }, // 定義接口以 tags 屬性定義爲分組 grouping: 'tags', tags: [ { name: 'user', description: '用戶接口' }, { name: 'article', description: '文章接口' } ] } } ]
server/index.js
const pluginHapiSwagger = require('../plugins/hapi-swagger') // 註冊插件 ... await server.register([ // 爲系統使用 hapi-swagger ...pluginHapiSwagger ] ...
打開你的dev.host:dev.port/docs
能夠查看我線上的
cookie hapi已經幫你解析好了,文件上傳也是
npm i hapi-auth-jwt2@8
├── plugins # hapi 插件配置 │ ├── hapi-auth-jwt2.js # jwt 配置插件
hapi-auth-jwt2.js
const validate = (decoded) => { // eslint disable // decoded 爲 JWT payload 被解碼後的數據 const { exp } = decoded if (new Date(exp * 1000) < new Date()) { const response = { code: 4, message: '登陸過時', data: '登陸過時' } return { isValid: true, response } } return { isValid: true } } module.exports = (server) => { server.auth.strategy('jwt', 'jwt', { // 須要自行在 config/index.js 中添加 jwtSecret 的配置,而且經過 process.env.JWT_SECRET 來進行 .git 版本庫外的管理。 key: process.env.JWT_SECRET, validate, verifyOptions: { ignoreExpiration: true } }) server.auth.default('jwt') }
server/index.js
const hapiAuthJWT2 = require('hapi-auth-jwt2') ... await server.register(hapiAuthJWT2) ...
默認狀況下全部的接口都須要token認證的
能夠將某個接口(好比登陸接口)config.auth = false不開啓
回到上面的登陸接口,用戶名和密碼檢驗成功就生成token
const generateJWT = (uid) => { const payload = { userId: uid, exp: Math.floor(new Date().getTime() / 1000) + 24 * 60 * 60 } return JWT.sign(payload, process.env.JWT_SECRET) } handler () { const res = await models.user.findAll({ attributes: { exclude: ['password', 'created_time', 'updated_time'] }, where: { username: request.payload.username, password: request.payload.password } }) const data = res[0] if (res.length > 0) { return h.response({ code: 0, message: '登陸成功!', data: { token: generateJWT(data.uid), ...data.dataValues } }) } else { return h.response({ code: 10, message: '用戶名或密碼錯誤' }) } }
前端拿到toke塞在頭部就行了
client/api/index.ts
request.interceptors.request.use((config: AxiosRequestConfig): AxiosRequestConfig => { const token = getToken() if (token) { config.headers.authorization = token } return config })
const jwtHeaderDefine = { headers: Joi.object({ authorization: Joi.string().required() }).unknown() } // 某個接口 ... validate: { ...jwtHeaderDefine, params: { uid: Joi.string().required() } } ...
能夠從swagger在線文檔中文看出變化
npm i hapi-pagination@3
plugins/hapi-pagination.js
const hapiPagination = require('hapi-pagination') const options = { query: { page: { name: 'the_page' // The page parameter will now be called the_page }, limit: { name: 'per_page', // The limit will now be called per_page default: 10 // The default value will be 10 } }, meta: { location: 'body', // The metadata will be put in the response body name: 'metadata', // The meta object will be called metadata count: { active: true, name: 'count' }, pageCount: { name: 'totalPages' }, self: { active: false // Will not generate the self link }, first: { active: false // Will not generate the first link }, last: { active: false // Will not generate the last link } }, routes: { include: ['/article'] // 須要開啓的路由 } } module.exports = { plugin: hapiPagination, options }
const pluginHapiPagination = require('./plugins/hapi-pagination'); await server.register([ pluginHapiPagination, ])
const paginationDefine = { limit: Joi.number().integer().min(1).default(10) .description('每頁的條目數'), page: Joi.number().integer().min(1).default(1) .description('頁碼數'), pagination: Joi.boolean().description('是否開啓分頁,默認爲true') } // 某個接口 // joi校驗 ... validate: { query: { ...paginationDefine } } ...
const { rows: results, count: totalCount } = await models.xxxx.findAndCountAll({ limit: request.query.limit, offset: (request.query.page - 1) * request.query.limit, });
歡迎到線上地址體驗完整功能
掘金小冊: 葉盛飛 《基於 hapi 的 Node.js 小程序後端開發實踐指南》
ps:歡迎點贊star ^_^
github: https://github.com/huoguozhang/my-blog