相似于知乎的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