hapi mysql项目实战路由初始化_用hapi.js mysql和nuxt.js(vue ssr)开发仿简书的博客项目...
前言:
預覽:
開始:
npm i
把mysql配置好
npm run server or npm run dev
實現功能:
用戶: 登錄、注冊、用戶資料修改,詳情頁面,類似于簡書的文章數量、總字數、收獲的喜歡總數,文章刪除。
文章:文章詳情頁面,查看,評論,點贊和踩,文章閱讀次數統計
文章: 所有文章,支持分頁和按關鍵詞、時間查找
文章書寫:支持markdown和圖片拖拽上傳
首頁: 文章推薦,作者推薦,首頁輪播,頂部搜索文章和用戶
ssr 效果預覽:
類似于知乎的
seo 效果:
待補充
1 技術棧:
前端:axios、element-ui、nuxt.js、 ts
后端:node.js、hapi.js、sequelize(orm)、 hapi-auth(token)、 hapi-swagger(在線api文檔)、hapi-pagination(分頁)、joi(前端請求數據檢驗類似于element的表單校驗)、 mysql 和其他插件
2 技術細節介紹:
說明: 本文主要側重后端,最后的效果類似于我司后端
目錄結構:
├── 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.js ?
hapi官方文檔已經說了很多了(expresstohapi),這里最吸引我的是,不用安裝很多的插件(expres的話有很多的xx-parse插件...),就能滿足我的需求,而且hapi已經應用于商用了。
注意點:
我的這些代碼,在我目前的package.json的版本是能正常運行的,hapi版本大版本有時候會出現不兼容的,不同版本的hapi對應著不同的插件版本,所以需要和我的版本保持一致,我還遇到過nuxt.js v2.9運行加入ts出現不識別@component的情況,安裝2.8.x版本就沒有問題。
2.1 Sequelize建模
開發后臺第一個想到的是建立數據模型(建表),默認你已經安裝好了mysql
之前我自己用數據庫,不知道有orm這個工具的時候,會選擇自己用navicat這樣的圖形化工具建表或者直接用sql語句建表。這樣做有幾個缺點:
對數據庫的操作記錄不明確,我新建一個表或者新增字段,我后悔了,刪掉,我又后悔了,orm的數據遷移就可以用來做這些事情類似于git。
遷移新環境,用sql操作很麻煩,直接執行orm的命令自動建表。
數據模型,之前后臺程序與mysql聯系的時候,僅僅在建立了連接池,數據的關系,表結構這些我們其實并不知道。
執行增刪改查代碼更簡潔清晰
其他
注意:用orm在執行sql操作時,相當于我們用jquery執行dom操作,api簡單了,但還是需要對原來的有點了解
sequelize
sequelize就是node.js的promise orm工具,同時也支持其他數據庫.
使用
安裝插件:
npm i sequelize-cli -D
npm i sequelize
npm i mysql2
sequelize init
通過 sequelize-cli 初始化 sequelize,我們將得到一個好用的初始化結構:
// 可以安裝npx
node_modules/.bin/sequelize init
├── config # 項目配置目錄
| ├── config.json # 數據庫連接的配置
├── models # 數據庫 model
| ├── index.js # 數據庫連接的樣板代碼
├── migrations # 數據遷移的目錄
├── seeders # 數據填充的目錄
config/config.json
默認生成文件為一個 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'
}
)
實例化seqlize
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
本項目用到的功能
多表查詢、單表增刪改查、模型統一配置、遷移和種子填充、事務(刪除文章的時候,把文章相關的數據:評論,閱讀,點贊數據也一起刪了。)等。
2.2 Joi 請求參數校驗
joi可以對請求參數進行校驗
使用:
安裝
# 安裝適配 hapi v16 的 joi 插件
npm i joi@14
使用見2.3 config.validate,更多參考官方文檔
2.3 用hapi 寫接口
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()
}
}
}
},
2.4 接口文檔swagger
安裝:
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
可以查看我線上的
2.5 token認證hapi-auth-jwt2
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
})
請求頭增加Joi校驗
const jwtHeaderDefine = {
headers: Joi.object({
authorization: Joi.string().required()
}).unknown()
}
// 某個接口
...
validate: {
...jwtHeaderDefine,
params: {
uid: Joi.string().required()
}
}
...
可以從swagger在線文檔中文看出變化
2.6 加入分頁hapi-pagination
安裝
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,
});
3 最后
歡迎到線上地址體驗完整功能
1 踩坑總結:
碰到接口500的情況,可以在model的操作后面捕獲錯誤,比如models.findAll().catch(e => console.log(e))
注意版本兼容問題,插件和hapi或者nuxt版本的兼容
nuxt.config.ts的配置不生效可以執行tsc nuxt.config.ts手動編譯
在asyncData中請數據,不寫絕對地址,會默認請求80端口的
2 開發收獲
熟悉了基本的后端開發流程
插件不兼容或者有其他需求的情況下,必須自己看英文文檔,看到英文文檔能淡定了
后端開發需要做的工作蠻多的,從接口到部署等,以后工作中要相互理解
3 參考
總結
以上是生活随笔為你收集整理的hapi mysql项目实战路由初始化_用hapi.js mysql和nuxt.js(vue ssr)开发仿简书的博客项目...的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: u盘右击没有新建是怎么回事 U盘右键无新
- 下一篇: php 支付宝订单查询_5. PHP接入