一个nuxt(vue)+mongoose全栈项目聊聊我粗浅的项目架构

2,310 阅读3分钟

这是一篇求职文章 年龄21 坐标成都 找一份vue.js移动端H5工作
一份没有任何包装纯真实的简历 简历戳这

求职文章一共有两篇 另外一篇请点击一个基于Vue+TypeScript的[移动端]Vue UI

项目简介

名字

JsonMaker

作用

添加api和属性,用于制造JSON

地址

  github

技术栈

前端

pug scss vue vue-router vuex axios nuxt element-ui

后端

node express mongoose mongodb jsonwebtoken

项目目录

前端

assets 资源文件和js逻辑存放处
components 组件目录 (因为引用了element-ui 项目不大 没单独构造组件)
layouts 布局目录(此项目没用上)
middleware 中间件目录
pages 页面目录
plugins 插件目录
static 静态文件目录
store vuex状态数目录

后端

actions js事件目录
config 配置目录
lib js模版目录
middleware express中间件目录
model mongoose.model 目录
plugins 插件目录
schmea mongoose.Schema 目录
app.js 主app
router.js 路由

图片

架构思路

前端

首先我们大致了解一下我们这个nuxt.config.js中的配置,之后会一个一个讲解

nuxt.config.js

nuxt.config.js 配置

module.exports = {
  // html
  head: {
    title: 'JsonMaker一个JSON制造器',
    meta: [
      { charset: 'utf-8' },
      { name: 'author', content: 'Qymh' },
      { name: 'keywords', content: 'Json,JSON,JsonMaker' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      {
        hid: 'description',
        name: 'description',
        content:
          'JsonMaker用户制造JSON,一个全栈项目,前端基于Nuxt Vuex Pug Scss Axios element-ui 后端基于 Node Express mongoose mongodb jsonwebtoken'
      }
    ],
    link: [
      {
        rel: 'icon',
        type: 'image/x-icon',
        href: 'https://nav.qymh.org.cn/static/images/q.ico'
      }
    ]
  },
  // 全局css
  css: [
    // reset css
    '~/assets/style/normalize.css',
    // common css
    '~/assets/style/common.css',
    // element-ui css
    'element-ui/lib/theme-chalk/index.css'
  ],
  // 加载颜色
  loading: { color: '#409EFF' },
  // 插件
  plugins: [
    // element-ui
    { src: '~/plugins/element-ui' },
    // widget
    { src: '~/plugins/widget' },
    // 百度统计
    { src: '~/plugins/baiduStatistics', ssr: false },
    // 百度站长平台
    { src: '~/plugins/baiduStation', ssr: false }
  ],
  // webpack配置
  build: {
    extend(config, { isDev, isClient }) {
      // eslint
      if (isDev && isClient) {
        config.module.rules.push({
          enforce: 'pre',
          test: /\.(js|vue)$/,
          loader: 'eslint-loader',
          exclude: /(node_modules)/
        })
      }
      config.module.rules.push(
        // pug
        {
          test: /\.pug$/,
          loader: 'pug-plain-loader'
        },
        // scss
        {
          test: /\.scss$/,
          use: [
            'vue-style-loader',
            'css-loader',
            'sass-loader',
            'postcss-loader'
          ]
        }
      )
    },
    // postcss配置
    postcss: [require('autoprefixer')()],
    // 公用库
    vendor: ['axios', 'element-ui']
  },
  router: {
    // 认证中间件
    middleware: 'authenticate'
  }
}

解析nuxt.config.js中的插件

插件中我引用了4个

  • 1 element-ui 插件
  • 2 widget 这里面包装了cookie的操作方法
    通过Vue.use()引入插件,直接通过vue环境下的this调用
    这个位置有一个坑,服务器端是没有document这个属性的,所以没法获取通过这种方式获取cookie
    所以我们还需要构造一个从req获取token的函数,我写在了assets/lib/utils
    cookie是从req.headers.cookie中读取的
  • 3 引入百度统计
  • 4 引入百度站长平台

解析 nuxt.config.js 中的 middleware

middleware目中就一个文件,这个文件包含了验证用户登陆和自动登陆的功能
这个位置也有一个坑,与非nuxt项目不同,我们平常的vue项目这个操作
是在router.beforeEach全局钩子里进行验证,而且在nuxt中你不光要验证客户端也要验证服务器端
大体思路就几点

  • 1 在需要登陆的页面设置meta: { auth: true },不需要的页面设置meta: { notAuth: true }
  • 2 当处于需要登陆的页面如果有token直接退出,没有则分两部获取token,一个客户端,一个服务器端,最后如果token存在
    则执行全局系统参数的api调用然后写入vuex,如果不存在则返回登陆界面
  • 3 在某些notAuth auth 都不存在时,检查存放的userName属性存在不,存在就跳到用户首页,不存在则跳到登陆界面

全局参数配置

每个人对这个全局配置理解不一样,看习惯,有人喜欢把很多配置都往全局放,比如vue-router的配置,我觉得没必要
我一般在全局配置中放一些配置没那么复杂的,诸如项目名字啊还有各类插件的配置,这个项目不大,所以全局配置也不太多 assets/lib/appconfig.js

const isDev = process.env.NODE_ENV === 'development'

// app
export const APPCONFIG = {
  isDebug: true
}

// cookie 设置
export const COOKIECONFIG = {
  expiresDay: 7
}

// server 设置
export const SERVERCONFIG = {
  domain: isDev ? 'http://127.0.0.1:5766' : 'https://api.qymh.org.cn',
  timeout: 10000
}

全局还有一个配置就是api接口的配置,我喜欢把api接口放在一个文件里面,然后引入,这个项目不大,一共15个接口 assets/lib/api

// 获取全局属性
export const system = '/api/system'

// 注册
export const register = '/api/register'
// 登陆
export const login = '/api/login'

// 添加api
export const addApi = '/api/addApi'
// 获取api
export const getApi = '/api/getApi'
// 删除api
export const deleteApi = '/api/deleteApi'
// 修改api
export const putApi = '/api/putApi'

// 添加属性
export const addProperty = '/api/addProperty'
// 获取属性
export const getProperties = '/api/getProperties'
// 删除属性
export const deleteProperty = '/api/deleteProperty'
// 修改属性
export const putProperty = '/api/putProperty'

// 添加集合
export const addCollections = '/api/addCollections'
// 获取集合
export const getCollections = '/api/getCollections'
// 删除集合
export const deleteCollections = '/api/deleteCollections'
// 修改集合
export const putCollections = '/api/putCollections'

ajax函数请求架构

nuxt.config.js聊完了,我们来聊聊前后端分离的一个大点,就是请求,我的习惯的一层一层从底部往上抽离

  • 1 第一步,封装拦截器
    拦截器就几个部分,一个axios基础参数配置,一个请求request拦截,一个响应response拦截
    一般在请求拦截就是构造参数,比如参数加密 请求头的发送 之类的,这个项目暂时还没做前端参数加密吗,同时我也会在请求输出log日志
    响应拦截也是一样的,输出接收到的参数日志并处理出错的情况,我们来看看代码
    assets/lib/axios.js
import axios from 'axios'
import Vue from 'vue'
import { SERVERCONFIG, APPCONFIG } from './appconfig'

const isClient = process.client
const vm = new Vue()

const ax = axios.create({
  baseURL: SERVERCONFIG.domain,
  timeout: SERVERCONFIG.timeout
})

// 请求拦截
ax.interceptors.request.use(config => {
  const token = isClient ? vm.$cookie.get('token') : process.TOKEN
  if (token) {
    config.headers.common['authenticate'] = token
  }
  const { data } = config
  if (APPCONFIG.isDebug) {
    console.log(`serverApi:${config.baseURL}${config.url}`)
    if (Object.keys(data).length > 0) {
      console.log(`request data ${JSON.stringify(data)}`)
    }
  }
  return config
})

// 响应拦截
ax.interceptors.response.use(response => {
  const { status, data } = response
  if (APPCONFIG.isDebug) {
    if (status >= 200 && status <= 300) {
      console.log('---response data ---')
      console.log(data)
      if (data.error_code && isClient) {
        vm.$message({
          type: 'error',
          message: data.error_message,
          duration: 1500
        })
      }
    } else {
      console.log('--- error ---')
      console.log(data)
      if (isClient) {
        vm.$message({
          type: 'error',
          message:
            status === 0 ? '网络链接异常' : `网络异常,错误代码:${status}`,
          duration: 1500
        })
      }
    }
  }
  return {
    data: response.data
  }
})

export default ax

  • 2 第二部构造http请求底层
    底层分装了4个方法,get post put delete, 增删改查,用promise实现,一层一层往上套,我们来看看代码

assets/lib/http.js

import ax from './axios'
import Vue from 'vue'

export default {
  /**
   * ajax公用函数
   * @param {String} api api接口
   * @param {Object} data 数据
   * @param {Boolean} isLoading 是否需要加载
   */
  ajax(method, api, data, isLoading = false) {
    return new Promise((resolve, reject) => {
      let vm = ''
      let loading = ''
      if (isLoading) {
        vm = new Vue()
        loading = vm.$loading()
      }
      ax({
        method,
        url: api,
        data
      }).then(res => {
        let { data } = res
        if (data.error_code) {
          isLoading && loading.close()
          reject(data)
        } else {
          isLoading && loading.close()
          resolve(data)
        }
      })
    })
  },

  /**
   * post函数
   * @param {String} api api接口
   * @param {Object} data 数据
   * @param {Boolean} isLoading 是否需要加载
   */
  post(api, data, isLoading = false) {
    return new Promise((resolve, reject) => {
      this.ajax('POST', api, data, isLoading)
        .then(data => {
          resolve(data)
        })
        .catch(err => {
          reject(err)
        })
    })
  },

  /**
   * delete函数
   * @param {String} api api接口
   * @param {Object} data 数据
   * @param {Boolean} isLoading 是否需要加载
   */
  delete(api, data, isLoading = false) {
    return new Promise((resolve, reject) => {
      this.ajax('DELETE', api, data, isLoading)
        .then(data => {
          resolve(data)
        })
        .catch(err => {
          reject(err)
        })
    })
  },

  /**
   * put函数
   * @param {String} api api接口
   * @param {Object} data 数据
   * @param {Boolean} isLoading 是否需要加载
   */
  put(api, data, isLoading = false) {
    return new Promise((resolve, reject) => {
      this.ajax('PUT', api, data, isLoading)
        .then(data => {
          resolve(data)
        })
        .catch(err => {
          reject(err)
        })
    })
  }
}

  • 3 第三部分就是事件的逻辑代码,我放在了assets/actions里面,同样用promise实现,一步一步往上套,通过调用底层封装的4个方法,调用封装的全局api参数,这里举一个关于api首页获取的操作事件的列子
    assets/actions/api.js
import http from '../lib/http'
import * as api from '../lib/api'

export default {

  /**
   * 获取api
   */
  getApi(userName) {
    return new Promise((resolve, reject) => {
      http
        .post(api.getApi, { userName })
        .then(data => {
          resolve(data)
        })
        .catch(err => {
          reject(err)
        })
    })
  }

  • 4 其实一般到第三步,直接在vue中就可以引用 actions里面封装好的事件了,但这个项目还多了一层,是用vuex再次封了一层
    这里仍然举获取api并操作vuex的列子,省略掉了非事件的代码
import api from '~/assets/actions/api'
import Vue from 'vue'
const vm = new Vue()

const actions = {
  // 获取api
  async getApi({ commit }, { userName, redirect }) {
    await api
      .getApi(userName)
      .then(arr => {
        commit('_getApi', arr)
      })
      .catch(() => {
        redirect({
          path: '/login',
          query: {
            errorMessage: '用户不存在,请重新登陆'
          }
        })
      })
  }

  • 5 下面就是在vue中引入actions就可以用了,接下来我们聊聊vuex的规范性

vuex的架构

  • 1 接口暴漏
    vuex中有四个属性,state getters mutations actions
    按我的架构思路,我永远暴漏在vue中可以使用的仅有两个,一个getters,一个actions
    为什么呢?因为state改变后值不会在dom中刷新,mutations无法异步

  • 2 命名
    按官方建议要有一个mutations-type专门用于存放突变事件名字,我觉得没必要,太麻烦了
    按第一点所说的,未暴漏的命名我会直接在前面加一个下划线,就像我上面的代码显示的那样

  • 3 事件和值的改变
    从名字上来讲,actions表事件,mutations表突变,换句话来说,我执行事件逻辑,比如接口请求,我会在actions里面执行, 而改变vuex状态树的值,我会在mutations里面执行

  • 4 命名空间限定

    一定要在每个模块上加入namespaced: true,一个是思路更清晰,第二个避免重复命名

后端

这个项目是我第二次用express写后端,架构思路感觉自己还不太成熟,写完之后发现有很多地方没对.忙着找工作,时间也来不及了,之后改改

先来看看app.js

app.js

app.js干了几件事

  • 1 引入mongoose并连接mongodb
  • 2 设置跨域CORS
  • 3 引入中间件和路由

全局参数

node后端也有全局参数,主要包含了错误代码的集合还有一些常用的配置

config/nodeconfig.js


// token设置
exports.token = {
  secret: 'Qymh',
  expires: '7 days'
}

// 错误code
exports.code = {
  // 用户不存在
  noUser: 10001,
  // 密码错误
  wrongPassword: 10002,
  // token过期
  outDateToken: 10003,
  // 检验不符合规则
  notValidate: 10004,
  // 已存在的数据
  existData: 10005,
  // 未知错误
  unknown: 100099,
  // 未知错误文字
  unknownText: '未知错误,请重新登陆试试'
}

// session
exports.session = {
  secret: 'Qymh',
  maxAge: 10000
}

数据存储架构思路

  • 1 第一步 构建Schema

Schema也是mongoose需要第一个构建的,项目中引用了很多官方提供的验证接口,我将Schema的配置放在了config/schema中,我们来看一下用户的Schema是什么样的

schema/user.js

const mongoose = require('mongoose')
const Schema = mongoose.Schema
const ApiSchema = require('./api')
const config = require('../config/schema/user').USERSCHEMACONFIG

const UserSchema = new Schema(
  {
    account: config.account,
    password: config.password,
    userName: config.userName,
    token: config.token,
    api: [ApiSchema]
  },
  config.options
)

module.exports = UserSchema

config/schema/user.js

exports.USERSCHEMACONFIG = {
  // 帐号
  account: {
    type: String || Number,
    index: [true, '帐号已经存在'],
    unique: [true, '帐号已经存在'],
    required: [true, '帐号不能为空'],
    minlength: [5, '帐号长度需要大于等于5'],
    maxlength: [18, '帐号长度需要小于等于18'],
    trim: true
  },
  // 密码
  password: {
    type: String || Number,
    required: [true, '密码不能为空'],
    minlength: [8, '密码长度需要大于等于8'],
    maxlength: [18, '密码长度需要小于等于18'],
    trim: true
  },
  // 名字
  userName: {
    type: String || Number,
    index: [true, '用户名已经存在'],
    unique: [true, '用户名已经存在'],
    required: [true, '用户名不能为空'],
    minlength: [2, '姓名长度需要大于等于2'],
    maxlength: [8, '姓名长度需要小于等于8'],
    trim: true
  },
  // token
  token: {
    type: String
  },
  // schema配置
  options: {
    versionKey: 'v1.0',
    timestamps: {
      createdAt: 'createdAt',
      updatedAt: 'updatedAt'
    }
  }
}
    
  • 2 第二步构建model

model放在model文件夹中,接收传来的Schema,然后传出Model,我们来看看用户的model

model/user.js

const mongoose = require('mongoose')
const UserSchema = require('../schema/user')

const UserModel = mongoose.model('UserModel', UserSchema)

module.exports = UserModel

  • 3 第三步构建数据存储lib

这个存储其实是为了actions文件服务的,actions接受路由事件,而lib则负责储存,包含了注册和登陆功能,然后在这个lib操作里面,我将对最后获得数据的处理进行封装,封装到了plugins目录,里面就包括了,对用户的token处理,对用于注册失败成功和登陆失败成功的回调参数处理,我们来看看用户的lib

lib/user.js

const UserModel = require('../model/user')
const UserPlugin = require('../plugins/user')

/**
 * 注册
 * @param {String | Number} account 帐号
 * @param {String | Number} password 密码
 * @param {String | Number} userName 名字
 */
exports.register = (account, password, userName) => {
  return new Promise((resolve, reject) => {
    const User = new UserModel({
      account,
      password,
      userName
    })

    User.save((err, doc) => {
      if (err) {
        err = UserPlugin.dealRegisterError(err)
        reject(err)
      }
      resolve(doc)
    })
  })
}

/**
 * 登陆
 * @param {String | Number} account 帐号
 * @param {String | Number} password 密码
 */
exports.login = (account, password) => {
  return new Promise((resolve, reject) => {
    UserModel.findOne({ account }).exec((err, user) => {
      err = UserPlugin.dealLoginError(user, password)
      if (err.error_code) {
        reject(err)
      } else {
        user = UserPlugin.dealLogin(user)
        resolve(user)
      }
    })
  })
}

  • 4 第四步 构建路由actions

actions目录用于处理路由的接收,然后引入lib进行数据的存储,我们来看看用户的actions

actions/user.js


const user = require('../lib/user')

// 注册
exports.register = async (req, res) => {
  const data = req.body
  const { account, password, userName } = data
  await user
    .register(account, password, userName)
    .then(doc => {
      res.json(doc)
    })
    .catch(err => {
      res.json(err)
    })
}

// 登陆
exports.login = async (req, res) => {
  const data = req.body
  const { account, password } = data
  await user
    .login(account, password)
    .then(doc => {
      res.json(doc)
    })
    .catch(err => {
      res.json(err)
    })
}

  • 5 构建路由

router.js就是所有api的挂载处,最后在app.js里面引用即可挂载,这个项目不大,一共提供了16个api

数据储存这5步就基本结束了,下面我们聊聊express的中间件

middleware中间件

这里的中间件主要就验证token过期没,过期了则直接返回,然后不进行任何操作

middleware/authenticate.js

const userPlugin = require('../plugins/user')
const nodeconfig = require('../config/nodeconfig')

// 验证token是否过期
exports.authenticate = (req, res, next) => {
  const token = req.headers.authenticate
  res.locals.token = token
  if (token) {
    const code = userPlugin.verifyToken(token)
    if (code === nodeconfig.code.outDateToken) {
      const err = {
        error_code: code,
        error_message: 'token过期'
      }
      res.json(err)
    }
  }
  next()
}

我的出错

后端的架构就上面这些了,在这次的后端架构中我出了一个错误,你可以看见我上面的userSchema是把apiSchema放在里面了,然后 apiSchema里面我有包含了两个schema,一个propertSchema,一个collectionsSchema
为什么我会这么做呢,因为刚开始写的时候想的是如果要从一个数据库去搜索一个信息,这个信息是属于用户的,有两个方法

  • 1 直接构造这个数据库的model然后存储,存储中带一个userId指向当前这个信息所属的用户
  • 2 将这个数据放在userModel用户model里,查找的时候先查找当前用于然后再读取这个信息

最后我选择了第二个....因为我想的是如果数据10w条,用户只有100个,去找100个总比找10w个好,我这么选择带来的几个问题

  • 1 mongoose储存的时候如果对象里面嵌套过多你想储存是没有api接口提供的.我看了几遍文档,只能通过$set $push 去存储对象的最多第二属性 比如下面的对象,是没有直接的api提供去修改collections的值的,需要用其他的方法绕一圈
   [
       {
           userName: 'Qymh',
           id: 'xxxxx',
           api: [
               {
                   id: 'xxxx',
                   apiName: 'test',
                   collections:[
                       {
                           id: 'xxxx',
                           age: 21,
                           sex: man
                       }
                   ]
               }
           ]
       }
   ]
  • 2 查找的时候挺麻烦的,比如我要查找到collections,我需要提供两个参数,一个用户的id先找到用户,再一个就是api的id再找到api最后再去提取collections,如果选择第一种只需要用户id就行了

所以我感觉自己在这一步上出错了

项目的挂载

  • 1 最后项目的挂载是通过pm2挂载的

  • 2 项目的node后端和前端都引用了ssl证书

现在项目已经挂到线上了但我的服务器太差,之前阿里云买的9.9元的学生机现在续费了只能拿来测试玩玩

之后要做的

这个项目断断续续写了20来天,很多功能没有完善,之后我会做的

  • 1 前端传入参数加密
  • 2 api属性加入类型判断前端传入后端,后端schema添加,比如mongoose的几个类型string boolean schema.types.mixed
  • 3 后端密码加盐
  • 4 更过的功能点,比如不止制造json,制造xml,引入echarts加入数据可视化之类的