前言:

预览:

开始:

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)开发仿简书的博客项目...相关推荐

  1. 分享Node.js + Koa2 + MySQL + Vue.js 实战开发一套完整个人博客项目网站

    这是个什么的项目? 使用 Node.js + Koa2 + MySQL + Vue.js 实战开发一套完整个人博客项目网站. 博客线上地址:www.boblog.com Github地址:https: ...

  2. 基于SpringBoot+Vue开发的前后端分离博客项目-Java后端接口开发

    文章目录 1. 前言 2. 新建Springboot项目 3. 整合mybatis plus 第一步:导依赖 第二步:写配置文件 第三步:mapper扫描+分页插件 第四步:代码生成配置 第五步:执行 ...

  3. 从0到1编写个人博客项目使用springboot+vue(前后端分离) 到 购买服务器上传项目 到 GitHub开源项目、此过程下所遇问题及解决方法,至少你帮你少走70%弯路

    个人博客编写 后记 2022.12.2.4 : 30.此项目告一段落. ​ 编撰此博客本意里除去对找工作的帮助.更多地是想帮助未走过的人去探探路.总结经验.少走弯路.知识的宝贵不在于无价.而是无私.天 ...

  4. SpringCloud开发个人博客项目(框架搭建)

    1. SpringCloud简介 我们先看看springCloud官网(https://spring.io/projects/spring-cloud#overview)上的介绍: Spring Cl ...

  5. 基于dreamweaver软件设计和开发一网站_基于 abp vNext 和 .NET Core 开发博客项目 Blazor 实战系列(一)...

    系列文章 使用 abp cli 搭建项目 给项目瘦身,让它跑起来 完善与美化,Swagger登场 数据访问和代码优先 自定义仓储之增删改查 统一规范API,包装返回模型 再说Swagger,分组.描述 ...

  6. 实战react技术栈+express前后端博客项目(3)-- 后端路由、代理以及静态资源托管等配置说明...

    项目地址:github.com/Nealyang/Re- 本想等项目做完再连载一波系列博客,随着开发的进行,也是的确遇到了不少坑,请教了不少人.遂想,何不一边记录踩坑,一边分享收获呢.分享当然是好的, ...

  7. 基于 abp vNext 和 .NET Core 开发博客项目 - Blazor 实战系列(三)

    基于 abp vNext 和 .NET Core 开发博客项目 - Blazor 实战系列(三) 转载于:https://github.com/Meowv/Blog 上一篇完成了博客的主题切换,菜单和 ...

  8. 前后端分离_博客项目

    1,前后端分离 1.1 什么是前后端分离 ​ 前端: 即客户端,负责渲染用户显示界面[如web的js动态渲染页面, 安卓, IOS,pc客户端等] ​ 后端:即服务器端,负责接收http请求,处理数据 ...

  9. 基于 abp vNext 和 .NET Core 开发博客项目 - Blazor 实战系列(七)

    基于 abp vNext 和 .NET Core 开发博客项目 - Blazor 实战系列(七) 转载于:https://github.com/Meowv/Blog 上一篇完成了后台分类模块的所有功能 ...

最新文章

  1. 团队分数分配方法——BY 李栋
  2. TCP的3次握手和4次挥手过程
  3. Java类加载机制:双亲委托模型
  4. python跟unicode一样吗_PYTHON编码处理-str与Unicode的区别
  5. 五种常用的异常值检测方法(均方差、箱形图、DBScan 聚类、孤立森林、Robust Random Cut Forest)
  6. TABCTL32.OCX 文件
  7. Android仿网易新闻导航栏PagerSlidingTabStrip
  8. [转帖]AjaxControlToolkit.TabContainer 自定义样式续
  9. ajax在php中使用方法,在项目中如何使用ajax请求
  10. tomcat源码分析--初始化与启动
  11. c语言输入1显示good,求助 无论输入什么输出的结果是个0
  12. 十三、K8s SVC相关操作
  13. matlab函数_常用于连通区域
  14. php++l+函数,关于PHP中usort()函数的解读
  15. 前端常用的JavaScript 库和框架(一)
  16. python更改ip地址_用Python更改IP地址(转)
  17. AI 技术本身的一些优势,比如它能够从大量数据里去总结背后的规律
  18. 模电_热敏PTC电阻_NTC电阻-区别与作用-20190507
  19. 图书管理系统课程设计
  20. oracle发生20001,Oracle IMP数据时报20001错误

热门文章

  1. 想聊天?自己搭建个聊天机器人吧!
  2. mysql router测试_MySQL 主从复制配置 + MySQL Router 部署使用测试
  3. Android监听button按钮的click事件
  4. discuz php如何开发,Discuzx2开发标准流程
  5. python history没有定义_python AttributeError:'Tensor'对象没有属性'_keras_history'_python_酷徒编程知识库...
  6. react state成员
  7. JZOJ5906 传送门
  8. POJ--2104 K-th Number (主席树模版题)
  9. C# 按部门拆分excel文件
  10. android-侧滑菜单