一個nuxt(vue)+mongoose全棧專案聊聊我粗淺的專案架構

Qymh發表於2018-08-07

這是一篇求職文章 年齡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
複製程式碼

專案目錄

前端

一個nuxt(vue)+mongoose全棧專案聊聊我粗淺的專案架構

assets 資原始檔和js邏輯存放處
components 元件目錄 (因為引用了element-ui 專案不大 沒單獨構造元件)
layouts 佈局目錄(此專案沒用上)
middleware 中介軟體目錄
pages 頁面目錄
plugins 外掛目錄
static 靜態檔案目錄
store vuex狀態數目錄

後端

一個nuxt(vue)+mongoose全棧專案聊聊我粗淺的專案架構

actions js事件目錄
config 配置目錄
lib js模版目錄
middleware express中介軟體目錄
model mongoose.model 目錄
plugins 外掛目錄
schmea mongoose.Schema 目錄
app.js 主app
router.js 路由

圖片

一個nuxt(vue)+mongoose全棧專案聊聊我粗淺的專案架構
一個nuxt(vue)+mongoose全棧專案聊聊我粗淺的專案架構
一個nuxt(vue)+mongoose全棧專案聊聊我粗淺的專案架構

架構思路

前端

首先我們大致瞭解一下我們這個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 名稱空間限定

    一個nuxt(vue)+mongoose全棧專案聊聊我粗淺的專案架構
    一定要在每個模組上加入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掛載的

    一個nuxt(vue)+mongoose全棧專案聊聊我粗淺的專案架構

  • 2 專案的node後端和前端都引用了ssl證照

現在專案已經掛到線上了但我的伺服器太差,之前阿里雲買的9.9元的學生機現在續費了只能拿來測試玩玩

之後要做的

這個專案斷斷續續寫了20來天,很多功能沒有完善,之後我會做的

  • 1 前端傳入引數加密
  • 2 api屬性加入型別判斷前端傳入後端,後端schema新增,比如mongoose的幾個型別string boolean schema.types.mixed
  • 3 後端密碼加鹽
  • 4 更過的功能點,比如不止製造json,製造xml,引入echarts加入資料視覺化之類的

相關文章