一、项目介绍
本项目使用Vue3
搭建h5
移动端单页,使用NodeJS
基于MVC
搭建RESTful
框架模版。
前端基于Vue3 + webpack4 + sass + vant ui
的rem
适配方案,搭建手机端模板。
后端基于koa + sequelize + mysql
搭建restful api
的模版。
Node
版本要求:Vue CLI 4.x
需要Node.js v8.9
或更高版本 (推荐v10
以上)。本示例使用的Node.js v10.17.0
源码已经提交到github了,麻烦大家动动小手Star一下。
二、统一开发规范
代码开发之前首先要做的就是使用VsCode
的ESLint + stylelint
统一格式化JS
和CSS
代码。
ESLint
VScode
里需要安装ESLint
插件,安装完成后,想要让扩展进行工作,我们还需要先进行ESLint
的安装配置。EsLint
可以局部安装和全局安装,这里使用当前项目构建系统的一部分进行局部安装:
npm install eslint --save-dev
安装完成后在项目根目录下配置.eslintrc.js
文件:
module.exports = {
// 指定要启用的环境
env: {
browser: true,
es6: true,
node: true
},
// 设置语言选项
parserOptions: {
ecmaVersion: 6
},
// 启用推荐规则
extends: 'eslint:recommended',
rules: {
// 除了与 null 字面量进行比较时,总是强制使用绝对相等
eqeqeq: ['error', 'always', { null: 'ignore' }],
}
}
当使用rules
新增自定义规则的时候,每个规则的第一个值都是代表该规则检测后显示的错误级别:
off
或0
将关闭规则warn
或1
将规则视为一个警告error
或2
将规则视为一个错误
更完整的规则可以访问:
最后在Vscode
的setting.json
里启用ESLint
:
// VSCode 会根据你当前项目下的.eslintrc.js配置文件的规则来验证自动格式化代码
"editor.codeActionsOnSave": {
"source.fixAll": true
}
如果在Vscode
安装了vetur
插件,在使用Vue3
的时候报vue/no-multiple-template-root
的错误,vetur
会提示说不能有多个根元素。解决办法是打开setting
配置,取消检查:
// F1>Preferences:Open Settings (JSON)
"vetur.validation.template": false
还有一个问题就是Vue
项目是放在子目录下的,VsCode
会去顶层目录下找eslint-plugin-vue
,会报Cannot find module 'eslint-plugin-vue'
错误。解决方案是在setting
配置里自定义ESLint
工作目录:
// F1>Preferences:Open Settings (JSON)
"eslint.workingDirectories": [
"./vue3-template"
]
stylelint
VScode
里需要stylelint
插件,然后在项目里局部安装:
npm install --save-dev stylelint stylelint-config-standard stylelint-order
stylelint
是运行工具,stylelint-config-standard
是stylelint
的推荐配置。stylelint-order
是CSS
属性排序插件(先写定位,再写盒模型,再写内容区样式,最后写CSS3
相关属性)。
安装完成后在项目下配置.stylelintrc.json
的配置文件:
{
// 启用默认推荐规则
"extends": "stylelint-config-standard"
// rules优先级大于extends,如果想修改插件的默认规则,可以增加rules
"rules": {
// 指定字符串使用单引号还是双引号
"string-quotes": "double"
}
}
三、前端
前端使用Vue-cli4
来初始化,使用的是@vue/cli 4.5.13
版本:
vue -V
@vue/cli 4.5.13
npm get registry
// 这里我没有使用淘宝源
https://registry.npmjs.org/
下面是我创建项目的初始设置:
Sass
建议使用dart-sass
,不要使用node-sass
。
具体可以参考:panjiachen.github.io/vue-element…
多环境配置
一般一个项目都会有以下3种环境:
- 开发环境
development
- 测试环境
stage
- 生产环境
production
在package.json
里的scripts
配置三种环境,如下:
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"stage": "vue-cli-service build --mode staging",
"preview": "vue-cli-service build --report",
"lint": "vue-cli-service lint"
}
在项目根目录新建不同环境的配置文件:
.env.development
NODE_ENV = 'development'
VUE_APP_BASE_API = '/development-api'
.env.staging
NODE_ENV = 'staging'
VUE_APP_BASE_API = '/staging-api'
.env.production
NODE_ENV = 'production'
VUE_APP_BASE_API = '/production-api'
vue-cli
官方已经集成了webpack-bundle-analyzer
插件分析webpack
构建产物。只需要执行npm run preview
就可以构建打包并生成report.html
帮助分析包内容了。打开文件后,如下:
rem适配
因为Vant
默认使用px
作为样式单位,如果需要使用rem
单位,官方推荐使用以下两个工具:
postcss-pxtorem
是一款PostCSS
插件,用于将px
单位转化为rem
单位lib-flexible
用于设置rem
基准值
// 安装
npm install vant@next -S
npm install amfe-flexible -S
npm install postcss -D
npm install postcss-pxtorem@5.1.1 -D
安装最新版本的postcss 8.3.0
和postcss-pxtorem 6.0.0
不兼容,需要使用postcss-pxtorem@5.1.1
版本。
在main.js
文件中引入lib-flexible
:
import 'amfe-flexible'
然后在根目录下新建postcss.config.js
文件:
module.exports = {
plugins: {
// postcss-pxtorem 插件的版本需要 >= 5.0.0
'postcss-pxtorem': {
rootValue({ file }) {
// vant组件rootValue使用37.5
return file.indexOf('vant') !== -1 ? 37.5 : 75
},
propList: ['*']
}
}
}
目前
postcss-pxtorem
默认版本是6.0.0,与vue-cli4不兼容。所以postcss-pxtorem
版本最好降到5.1.1。
Vant UI
设置rootValue: 37.5
,我们使用了lib-flexible
适配,所以iPhone6
下1rem
等于37.5
px。
因为Vant
默认使用px
作为样式单位,假如说Vant
组件有一个元素宽度是375px
。
通过插件设置rootValue
是37.5
,转换rem
为10rem
。如果插件rootValue
改为75
,转换rem
为5rem
。但是实际的font-size
还是37.5px
。所以Vant
实际css
像素是187.5px
,整体缩小了一倍。
所以就可以加一个判断是否是vant
文件,如果是vant
文件就已rootValue: 37.5
为基准。假如UI设计稿是750px
,我们就不用拿到设计稿计算了,直接1:1
来写css
样式了。
vant 按需加载
推荐使用官方文档的按需来引入组件,安装babel-plugin-import
插件:
npm install babel-plugin-import -D
然后在根目录配置babel.config.js
:
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
],
plugins: [
[
'import',
{
libraryName: 'vant',
libraryDirectory: 'es',
style: true
}
]
]
}
可以在src/plugins/vant.js
下统一管理组件,把引入组件的工作单独写入到一个js文件中,如下:
import { Button, Tabbar, TabbarItem } from 'vant'
export default function(app) {
app.use(Button).use(Tabbar).use(TabbarItem)
}
然后在main.js
文件中引入就可以了:
import { createApp } from 'vue'
import useVant from '@/plugins/vant'
const app = createApp(App)
// 注册vant组件
useVant(app)
Vuex 状态管理
Vuex
使用模块化方式管理来编写一个计数器demo
。如下目录结构:
├── store
│ ├── modules
│ │ └── counter.js
│ ├── index.js
index.js
如下:
import { createStore } from 'vuex'
const modulesFiles = require.context('./modules', true, /\.js$/)
// 自动装配模块
const modules = modulesFiles.keys().reduce((modules, modulePath) => {
const moduleName = modulePath.replace(/^\.\/(.*)\.\w+$/, '$1')
const value = modulesFiles(modulePath)
modules[moduleName] = value.default
return modules
}, {})
export default createStore({
modules
})
counter.js
如下:
const state = function() {
return {
count: 0
}
}
const mutations = {
increment(state, count) {
state.count++
}
}
const actions = {
incrementAsync({ commit }) {
return new Promise((resolve, reject) => {
setTimeout(() => {
commit('increment')
resolve()
}, 1000)
})
}
}
const getters = {
evenOrOdd(state) {
return state.count % 2 === 0 ? 'even' : 'odd'
}
}
export default {
namespaced: true,
state,
getters,
actions,
mutations
}
在main.js
文件中引入:
import { createApp } from 'vue'
import store from './store'
app.use(store)
使用方式:
<template>
<div>{{ count }} is {{ evenOrOdd }}.</div>
<button @click="incrementAsync">Increment async</button>
</template>
<script>
import { computed } from '@vue/runtime-core'
import { useStore } from 'vuex'
export default {
name: 'App',
setup() {
const store = useStore()
return {
count: computed(() => store.state.counter.count),
evenOrOdd: computed(() => store.getters['counter/evenOrOdd']),
incrementAsync: () => store.dispatch('counter/incrementAsync')
}
}
}
</script>
Vue-Router 路由
本模版采用hash模式来进行开发,也使用模块化进行管理。
├── router
│ ├── modules
│ │ └── tabbar.js
│ ├── index.js
index.js
如下:
import { createRouter, createWebHashHistory } from 'vue-router'
import tabBar from './modules/tabBar'
// 静态路由
export const constantRoutes = [tabBar]
// 动态路由
export const asyncRoutes = []
const router = createRouter({
// 新的 history 配置取代 mode
history: createWebHashHistory(),
routes: constantRoutes
// Vue3 中scrollBehavior返回的对象 x 改名为 left,y 改名为 top
// scrollBehavior: () => ({ top: 0 })
})
export default router
定义底部导航modules/tabBar.js
,然后在相应的目录新建layout
、Home
、my
组件:
import Layout from '@/layout'
export default {
path: '/',
component: Layout,
meta: { title: '首页', keepAlive: true },
redirect: '/home',
children: [
{
path: 'home',
name: 'home',
component: () => import('@/views/home'),
meta: { title: '首页', keepAlive: true }
},
{
path: 'my',
name: 'my',
component: () => import('@/views/my'),
meta: { title: '我的', keepAlive: true }
}
]
}
在main.js
文件中引入:
import { createApp } from 'vue'
import router from './router'
app.use(router)
axios 封装
安装axios
:
npm install axios -S
在src/utils/request.js
封装axios
:
import axios from 'axios'
import store from '@/store'
const servie = axios.create({
baseURL: process.env.VUE_APP_BASE_API,
timeout: 10000,
headers: {
'Content-Type': 'application/json;charset=UTF-8'
}
})
// 添加请求拦截器
servie.interceptors.request.use(config => {
if (store.getters.accessToken) {
config.headers.accessToken = ''
}
return config
}, error => {
console.log('err:' + error)
return Promise.reject(error)
})
// 添加响应拦截器
servie.interceptors.response.use(response => {
const res = response.data
if (res.code !== 200) {
return Promise.reject(res.msg || 'error')
} else {
return res
}
}, error => {
console.log('err:' + error)
return Promise.reject(error)
})
export default servie
然后在src/api
文件夹下统一管理接口就好了,如下:
// common.js
export function getInfo(data) {
return request({
url: '/info',
method: 'get',
data
})
}
vue.config.js 配置
const path = require('path')
const resolve = dir => path.join(__dirname, dir)
module.exports = {
publicPath: '/',
outputDir: 'dist', // 生产环境构建文件的目录
assetsDir: 'static', // outputDir的静态资源(js、css、img、fonts)目录
lintOnSave: false,
productionSourceMap: false, // 如果不需要生产环境的 source map,可以将其设置为 false 以加速生产环境构建。
// 跨域配置
devServer: {
disableHostCheck: true,
proxy: {
// /api如果网络请求中有这个字符串,那么就开始匹配代理
'/api': {
target: 'http://xx.xxx.com', // 指向开发环境 API 服务器
changeOrigin: true, // 如果设置成true,发送请求头中host会设置成target
ws: true, // 开启webSocket
// 重写路径,替换成target中已/api开头的地址为空字符串,根据业务进行修改
pathRewrite: {
'^/api': ''
}
}
},
hot: true, // 设置为true的时候,如果编译报错,会抛出错误。重新改成正确的,又会触发重新编译,整个浏览器会重新刷新!
port: 8999, // 端口号
open: true, // 启动后打开浏览器
// 当出现编译器错误或警告时,在浏览器中显示全屏覆盖层
overlay: {
warnings: true,
errors: true
}
},
chainWebpack: config => {
// 别名 alias
config.resolve.alias
.set('@', resolve('src'))
config.plugin('preload').tap(() => [
{
rel: 'preload',
fileBlacklist: [/\.map$/, /hot-update\.js$/, /runtime\..*\.js$/],
include: 'initial'
}
])
config.plugins.delete('prefetch')
config.when(process.env.NODE_ENV !== 'development', config => {
config
.plugin('ScriptExtHtmlWebpackPlugin')
.after('html')
.use('script-ext-html-webpack-plugin', [
{
// 将 runtime 作为内联引入不单独存在
inline: /runtime\..*\.js$/
}
])
.end()
config.optimization.splitChunks({
chunks: 'all',
cacheGroups: {
// cacheGroups 下可以可以配置多个组,每个组根据test设置条件,符合test条件的模块
commons: {
name: 'chunk-commons',
test: resolve('src/components'),
minChunks: 3, // 被至少用三次以上打包分离
priority: 5, // 优先级
reuseExistingChunk: true // 表示是否使用已有的 chunk,如果为 true 则表示如果当前的 chunk 包含的模块已经被抽取出去了,那么将不会重新生成新的。
},
node_vendors: {
name: 'chunk-libs',
chunks: 'initial', // 只打包初始时依赖的第三方
test: /[\\/]node_modules[\\/]/,
priority: 10
},
vantUI: {
name: 'chunk-vantUI', // 单独将 vantUI 拆包
priority: 20,
test: /[\\/]node_modules[\\/]_?vant(.*)/
}
}
})
config.optimization.runtimeChunk('single')
})
}
}
四、Node后端
一开始想用egg.js
来开发,但是对node.js
对于我来说对自己理解整个项目不太友好。所以自己就看了很多文章在加上动手写了一个基于koa
的MVC
的后端模板,自己在这个基础上编写代码目前感觉也挺好用的。
需要解决的问题
自己手动搭建一个NodeJS
后端模板,需要考虑的问题还挺多的,这里我列举一下我想到的:
- 快速调试
- 错误处理
- 日志记录
- 文件目录分层
- 数据合法性校验
- 区分开发环境和生产环境 ....
然后接下来就一步一步解决这些问题。
项目目录结构
首先先把项目的基础的目录结构列出来,有个大致的了解:
├── bin // 项目启动目录
├── www // 配置项
├── logs // 日志文件
├── src // 源代码
├── config // 公共配置文件
└── index.js
└── config-development.js // 开发环境
└── config-production.js // 生产环境
└── config-staging.js // 测试环境
├── controllers // 路由层
└── userController.js
├── db // 数据库连接配置
└── index.js
└── init-db.js // 自动生成数据库表
├── lib // 第三方依赖库
└── WXBizDataCrypt.js // 如微信小程序的加解密
├── middlewares // 中间件
└── errorHandle.js // 错误处理中间件
└── getTokenData.js // 获取token中间件
└── myLogger.js // 写入日志中间件
├── models // 数据库模型
└── index.js // 自动加载models
└── user.js // user用户模型
├── services // 服务层
└── index.js // 自动加载services
└── userService.js // 用户服务层
├── utils // 工具库
└── response.js // 自定义response
└── exception.js // 自定义异常
├── .eslintrc.js // eslint配置
├── app.js // 应用入口
├── ecosystem.config.js // pm2配置文件
├── logs.config.js // 日志配置
├── package-lock.json
├── package.json // 项目依赖
nodemon、cross-env介绍
nodemon
用来监视node.js
应用程序中的任何更改并自动重启服务。
cross-env
用来处理windows
和其他unix
系统在设置环境变量的写法上不一致的问题。
需要安装下面依赖:
npm install cross-env -D
npm install nodemon -D
下文会在package.json
中使用nodemon
和cross-env
。
Node 开发调试
--inspect
命令可以启动调试模式。
然后在Chrome
浏览器的地址栏,输入 chrome://inspect
回车后就可以看到下面的界面。
在Target
部分,点击inspect
链接,就可以进入调试了。
第二种方法就是打开"开发者工具",顶部左上角有一个Node
的绿色标志,点击就可以进入调试了。
具体更多使用细节可以看这篇文章。
pm2和开发环境配置
因为本项目是基于koa2
的,所以环境配置首先需要安装一下:
npm install koa -S
1. 新建www文件
在bin
文件夹下面新建www
可执行文件,用来运行、关闭、重启服务。
#!/usr/bin/env node
/**
* Module dependencies.
*/
var app = require('../app')
var http = require('http')
var port = normalizePort(process.env.PORT || '3000')
/**
* Create HTTP server.
*/
var server = http.createServer(app.callback())
/**
* Listen on provided port, on all network interfaces.
*/
server.listen(port)
server.on('error', onError)
server.on('listening', onListening)
/**
* Normalize a port into a number, string, or false.
*/
function normalizePort(val) {
var port = parseInt(val, 10)
if (isNaN(port)) {
// named pipe
return val
}
if (port >= 0) {
// port number
return port
}
return false
}
/**
* Event listener for HTTP server "error" event.
*/
function onError(error) {
if (error.syscall !== 'listen') {
throw error
}
var bind = typeof port === 'string'
? 'Pipe ' + port
: 'Port ' + port
// handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES':
console.error(bind + ' requires elevated privileges')
process.exit(1)
}
}
/**
* Event listener for HTTP server "listening" event.
*/
function onListening() {
var addr = server.address()
var bind = typeof addr === 'string'
? 'pipe ' + addr
: 'port ' + addr.port
console.log('Listening on ' + bind)
}
这里是koa-generator
程序启动的代码,根据我们的具体情况简单的修改直接使用就可以了。然后新建app.js
,导入koa2
:
const Koa = require('koa')
const app = new Koa()
app.use(async(ctx, next) => {
ctx.body = 'Hello World'
})
module.exports = app
2. 安装全局PM2
$ npm install pm2@latest -g
# or
$ yarn global add pm2
全局安装完成后,在package.json
的script
配置和pm2
默认的读取的配置文件ecosystem.config.js
进行多环境运行的配置。
// ecosystem.config.js
module.exports = {
// 应用程序列表,pm2可以管理多个程序
apps: [
{
name: 'www', // 应用进程名称
script: './bin/www', // 启动脚本路径
ignore_watch: ['node_modules', 'logs'], // 忽视监听的文件
args: '', // 传递给脚本的参数
instances: 1, // 应用启动实例个数,仅在cluster模式有效,默认为fork
autorestart: true,
watch: true, // 监听重启,启用情况下,文件夹或子文件夹下变化应用自动重启
max_memory_restart: '1G', // 最大内存限制数,超出自动重启
// 测试环境
env_staging: {
'PORT': 8002,
'NODE_ENV': 'staging'
},
// 生产环境
env_production: {
'PORT': 80,
'NODE_ENV': 'production'
}
}
]
}
3. 在package.json添加环境
"scripts": {
"dev": "cross-env NODE_ENV=development PORT=8001 nodemon --inspect bin/www",
"stage": "cross-env pm2 start ecosystem.config.js --env staging",
"prod": "cross-env pm2 start ecosystem.config.js --env production",
"logs": "pm2 logs",
"stop": "pm2 stop ecosystem.config.js"
}
下面对相关环境进行解释:
npm run dev // 开发环境
npm run stage // 测试环境
npm run prod // 生产环境
npm run logs // 查看pm2日志
npm run stop // 停止pm2服务
自定义异常
当编写代码的时候,我们自己可能会遇到一些未知的错误要抛出异常。比如说http
错误,参数错误等。我们自己可以写一个工具类来进行处理。
// exception.js
/**
* http 异常处理
*/
class HttpException extends Error {
constructor(customError = {}) {
super()
const defaultError = { message: '参数错误', state: 500, errorCode: 10000 }
const { message, status, errorCode } = Object.assign(defaultError, customError)
this.message = message
this.status = status
this.errorCode = errorCode
}
}
/**
* request params 异常处理
* example: throw new ParamsException({ message: '参数错误', status: 400,errorCode: 10001 })
*/
class ParamsException extends Error {
constructor(customError = {}) {
super()
const defaultError = { message: '参数错误', status: 400, errorCode: 10001 }
const { message, status, errorCode } = Object.assign(defaultError, customError)
this.message = message // 返回的错误信息
this.status = status // http status code 2xx 4xx 5xx
this.errorCode = errorCode // 自定义的错误码,例如:10001
}
}
module.exports = {
HttpException,
ParamsException
}
自定义response
自定义response来对返回前端的响应进行处理,把响应提取出来进行封装,返回的通用的格式:
/**
* 响应成功格式
* @param {*} data 数据
* @param {*} code 响应码
* @param {*} msg 消息
* @returns {}
*/
const responseFormat = (data = {}, msg = 'success',code = 200) => {
return {
code,
msg,
data
}
}
module.exports = {
responseFormat
}
koa中间件的使用
中间件使用了常用的开源中间件和一些自定义中间件。
1. koa-bodyparser、koa-static、koa2-cors
安装后直接使用就可以了:
npm install koa-bodyparser -S
npm install koa-static -S
npm install koa2-cors -S
const static = require('koa-static')
const bodyparser = require('koa-bodyparser')
const path = require('path')
const cors = require('koa2-cors')
// post请求参数解析
app.use(bodyparser({
enableTypes: ['json', 'form', 'text', 'xml']
}))
// 处理静态文件
app.use(static(path.join(__dirname, './src/public')))
// 跨域处理
app.use(cors())
2. 自定义logger中间件
为了线上更好地监控或排查问题,要记录请求的参数或者报错等信息。没有日志的话就无法定位问题。一个好的日志系统,在项目开发中是十分重要的。项目中使用log4js
来进行项目日志的配置。首先进行安装:
npm install log4js -S
在根目录下新建logs.config.js
进行测试环境和生产环境的日志输出配置:
/**
* 日志配置文件
*/
const log4js = require('log4js')
const path = require('path')
let developmentLogConfig = {}
// 测试环境增加标准输出流
if (process.env.NODE_ENV !== 'production') {
developmentLogConfig = {
STDOUT: {
type: 'stdout'
}
}
}
// 保存日志的文件名
const fileAllName = path.join(__dirname, './logs/all.log')
const fileErrorName = path.join(__dirname, './logs/error.log')
log4js.configure({
/**
* 如果生产环境在cluster模式下,pm2需要设置为true, 否则日志不生效
* pm2: process.env.NODE_ENV === 'production'
*/
appenders: {
...developmentLogConfig,
FILE_ALL: {
type: 'datefile', // log4js 会按照日期分日志,一天一个文件,每过一天都会把前一天的 all.log 重命名为 all.2021-06-03.log
filename: fileAllName,
backups: 10, // 日志最多保留10个
maxLogSize: 10485760, // 文件最大值10M
daysToKeep: 10, // 最多保留10天的日志,如果为0则永久保存
keepFileExt: true // 是否保持日志文件后缀名
},
FILE_ERROR: {
type: 'datefile',
filename: fileErrorName,
daysToKeep: 30,
keepFileExt: true
}
},
categories: {
default: {
appenders: process.env.NODE_ENV !== 'production' ? ['STDOUT', 'FILE_ALL'] : ['FILE_ALL'],
level: 'debug'
},
error: {
appenders: ['FILE_ERROR'],
level: 'error'
}
}
})
const defaultLogger = log4js.getLogger()
const errorLogger = log4js.getLogger('error')
// 导出对应的log
module.exports = {
defaultLogger,
errorLogger
}
在middlewares
文件夹下新建myLogger.js
中间件,并在app.js
里注册中间件记录接口请求时间,写入日志:
// 文件目录:src/middlewares/myLogger.js
const { defaultLogger } = require('../../logs.config')
module.exports = function() {
return async(ctx, next) => {
const start = new Date()
await next()
const ms = new Date() - start
const logText = `${ctx.method} ${ctx.status} ${ctx.url} 响应时间:${ms}ms`
defaultLogger.info(logText)
}
}
最后在app.js
使用中间件
const myLogger = require('./src/middlewares/myLogger')
// 需要放在所有中间件的最顶部
app.use(myLogger())
3. jwt验证中间件
项目使用jsonwebtoken
生成token
,koa-jwt
来进行jwt
验证。
npm install jsonwebtoken -S
npm install koa-jwt -S
const jwt = require('jsonwebtoken')
// 可以写入配置文件中
const config = {
// 自定义jwt加密的私钥
PRIVATE_KEY: 'private_key',
// 过期时间秒为单位
JWT_EXPIRED: 60 * 60
}
// 生成token
const token = jwt.sign({ data: 'testToken', exp: config.JWT_EXPIRED }, config.PRIVATE_KEY)
ctx.body = {
token
}
然后通过koa-jwt
配置中间件实现对token
的验证:
// app.js
const koaJwt = require('koa-jwt')
app.use(koaJwt({ secret: 'private_key' }).unless({
// 设置login、register接口,可以不需要认证访问
path: [
/^\/user\/login/,
/^\/user\/register/
]
}))
4. 统一进行异常处理中间件
良好的编码习惯必然离不开异常处理,通过对异常处理,不仅仅要返回给客户端进行友好的提示,还要把错误的堆栈信息写入日志。
// errorHandles.js
const { errorLogger } = require('../../logs.config')
module.exports = function() {
return async(ctx, next) => {
try {
await next()
} catch (err) {
errorLogger.error(err.stack)
// 异常处理
ctx.status = err.statusCode || err.status || 500
ctx.body = {
msg: err.message || '服务器错误',
errorCode: err.errorCode || err.statusCode || err.status || 500
}
}
}
}
// 文件目录:app.js
const Koa = require('koa')
const app = new Koa()
const errorHandler = require('./src/middlewares/errorHandler.js')
// 在jwt中间件验证之前使用
app.use(errorHandler())
数据校验
在定义接口的时候我们不能信任用户提交过来的数据,需要对数据进行最基本的校验。验证是一件麻烦的事情,很有可能你需要验证数据类型,长度,特定规则等等。joi
是hapijs
自带的数据校验模块,他已经高度封装常用的校验功能,这里将使用joi
来进行数据校验和错误的处理:
// 安装
npm install joi -S
下面是一个demo
,具体API可以看joi的官方文档:
const Joi = require('joi')
const { ParamsException } = require('../utils/exception')
router.get('/vali', async ctx => {
const schema = Joi.object({
username: Joi.string().min(1).max(30).required(),
password: Joi.string().trim().pattern(new RegExp('^[a-zA-Z0-9]{3,30}$')),
})
// allowUnknown: true 代表对多余传进来的变量不要理会
const { error } = schema.validate({ username: 'bob', age: 10 }, { allowUnknown: true })
if (error) {
// 使用自定义异常抛出错误
throw new ParamsException({ message: error.details[0].message, state: 400 })
}
ctx.body = {msg:'success'}
})
当抛出ParamsException
异常后,自定义全局异常处理捕获到异常,返回给用户提示并写入日志。
controllers路由层
这里使用koa-router
来搭建模板的路由部分。首先需要安装koa-router
:
npm install koa-router -S
在controllers
目录新建userController.js
,如下:
const Router = require('koa-router')
const router = new Router()
router.prefix('/user')
router.get('/getInfo', ctx => {
ctx.body = { data: 'getInfo' }
})
module.exports = router
然后就可以在app.js
里通过程序读取controllers
目录下的所有路由文件,来统一注册路由,这样就不用每次新建一个文件就要重新导入了。
// app.js
const path = require('path')
const fs = require('fs')
const Koa = require('koa')
const app = new Koa()
/* loader router */
const jsFiles = fs.readdirSync(path.join(__dirname, './src/controllers')).filter(file => file.indexOf('.') !== 0 && file.endsWith('.js') !== -1)
jsFiles.forEach(file => {
const mapping = require(path.join(__dirname, './src/controllers', file))
app.use(mapping.routes(), mapping.allowedMethods())
})
services层和models层也同理。
models层
定义services
层之前,需要定义models
层模型。该项目中是使用mysql
对数据库进行操作。需要初始化数据库配置和定义。项目使用的是Node
的ORM框架Sequelize
来操作数据库。首先需要安装:
npm install mysql2 -S
npm install sequelize -S
定义数据库连接的基础信息配置:
// config-development.js
const config = {
myEnv: 'development',
mysql: {
dialect: 'mysql',
host: 'xxx.xx.xx.x',
port: 3306,
database: 'template_db',
username: 'root', // 数据库用户名
password: '123456', // 数据库密码
// 连接池配置
max: 20,
min: 10,
// 当前连接多久没有操作就断开
idle: 10000,
// 多久没有获取到连接就断开
acquire: 30000
}
}
module.exports = config
sequelize
新建model
的语法有好几种,这里使用sequelize.define
来定义model
。因为我们要自定义一些自己的内容,不要直接使用Sequelize
的API
,通过新建db
文件夹下面的index.js
文件加入我们自定义内容来间接的定义Model
。如下:
const { v4: uuidv4 } = require('uuid')
const { mysql } = require('../config')
const { Sequelize, DataTypes } = require('sequelize')
const { dialect, host, port, database, username, password, max, min, idle, acquire } = mysql
// 初始化对象
const sequelize = new Sequelize({
dialect,
host,
port,
database,
username,
password,
pool: {
max,
min,
idle,
acquire
},
define: {
freezeTableName: true, // 取消表名复数形式
underscored: true // 驼峰下划线转换
},
query: {
raw: true
}
})
/*
* uuid
* @returns uuid
*/
function generateId() {
return uuidv4().replace(/-/g, '')
}
/*
* 统一定义model
* 每个model必须有createAt,updateAt。
* 主键使用uuid生成,名称必须是id。
*/
function defineModel(name, attributes, options = {}) {
const attrs = {
...attributes,
id: {
type: DataTypes.STRING(50),
primaryKey: true
},
// createAt和updateAt以BIGINT存储时间戳,无需处理时区问题。
createAt: {
type: DataTypes.BIGINT
},
updateAt: {
type: DataTypes.BIGINT
}
}
// console.log(`model->${name} is create`)
return sequelize.define(name, attrs, {
...options,
timestamps: false,
hooks: {
// create前的hook,插入id,createAt,updateAt
beforeCreate(instance, options) {
if (!instance.id) {
instance.id = generateId()
}
const now = Date.now()
instance.createAt = now
instance.updateAt = now
},
// update前的hook,更新updateAt
beforeUpdate(instance) {
instance.updateAt = Date.now()
}
}
})
}
const mysqlDB = {
defineModel,
// 测试环境下生成表结构功能
sync() {
return new Promise((resolve, reject) => {
if (process.env.NODE_ENV !== 'production') {
sequelize.sync({ force: true }).then(() => {
console.log('创建成功!')
resolve()
}).catch(err => {
reject(err)
})
} else {
reject('不能在生产环境下使用sync()')
}
})
}
}
module.exports = {
DataTypes,
mysqlDB,
sequelize
}
可以在开发模式下,通过node init-db.js
自动生成数据库表:
const { sync } = require('../models')
/**
* 初始化数据库
*/
sync().then(() => {
console.log('init db ok!')
process.exit(0)
}).catch((e) => {
console.log('failed with: ' + e)
process.exit(0)
})
这里通过自定义defineModel
统一主键生成方式,统一createAt
和updateAt
为时间戳。然后所有的Model
存放在models
文件夹内,并且以model
的实际名称命名。如下面的user.js
:
const { mysqlDB, DataTypes } = require('../db')
module.exports = mysqlDB.defineModel('user',
{
username: {
type: DataTypes.STRING(255),
allowNull: false,
comment: '用户名'
},
password: {
type: DataTypes.STRING(255),
allowNull: false,
comment: '密码'
}
},
{
tableName: 'user' // 自定义表名
}
)
最后在models
文件夹下面新建index.js
来自动扫描所有model
。
// models/index.js
const fs = require('fs')
const path = require('path')
const { sequelize, mysqlDB } = require('../db')
const jsFiles = fs.readdirSync(__dirname).filter((file) => {
return file.endsWith('.js') && (file !== 'index.js') && (file.indexOf('.') !== 0)
})
const models = {}
jsFiles.forEach((file) => {
const name = file.substring(0, file.length - 3)
models[name] = require(path.join(__dirname, file))
})
models.sequelize = sequelize
// 生成数据库表
models.sync = async() => {
return await mysqlDB.sync()
}
module.exports = models
这样就可以通过const { user } = require('../models/index')
来引入不同的model
了。
services层
services
主要负责操作数据库逻辑。和上面models
文件一样,首先新建index.js
文件进行自动注册每个service
。
// services/index.js
const fs = require('fs')
const path = require('path')
const jsFiles = fs.readdirSync(__dirname).filter(file => {
return file.endsWith('.js') && (file !== 'index.js') && (file.indexOf('.') !== 0)
})
const service = {}
jsFiles.forEach((file) => {
const name = file.substring(0, file.length - 3)
service[name] = require(path.join(__dirname, file))
})
module.exports = service
这样就可以通过const { userService } = require('../services')
来动态导入所有的service
了。然后新建userService.js
。进行数据库的操作:
const { user, sequelize } = require('../models')
module.exports = {
// 登录
async login({ username, password }) {
return await user.findOne({ where: { username, password }})
},
// 编辑用户
async editUser(user) {
// 更新数据的时候要开启`individualHooks:true`选项,否则不会触发`beforeUpdate`的`hooks`。
const result = await sequelize.transaction(async(t) => {
const res = await user.update(user, { where: { id: user.id }, transaction: t, individualHooks: true })
return res
})
return result
},
// 增加用户
async addUser({ username, password }) {
// 开启事务
const result = await sequelize.transaction(async(t) => {
const result = await user.create({ username, password }, { transaction: t })
return result
})
return result
}
}
遇到多条SQL
语句执行的时候,一定要开启事务。sequelize.transaction
是框架对事务的操作,默认使用数据库的隔离级别。
sequelize日志
sequelize
输出日志默认是在控制台输出,需要修改默认配置,开发环境下在控制台输出,生产环境下输出到日志文件里,如下:
const sequelize = new Sequelize({
// 省略其他一些配置...
logging: process.env.NODE_ENV === 'production' ? sqlLogger : console.log
})
function sqlLogger(msg) {
defaultLogger.info(msg)
}
五、总结
使用常用的技术和框架,通过一步一步的搭建,完成了Vue3
和Node
前后端项目的结合。
项目github源码,源码有些细节没有写在文章里,喜欢的可以再去了解下。