【Geek议题】合理的VueSPA架构议论(上)

媒介

web前端生长到当代,已不再是严厉意义上的后端MVC的V层,它愈来愈向相似客户端开辟的方向生长,已自力具有了本身的MVVM设想模子。前后端的星散也使前端职员具有更大的自在,能够自力设想客户端部份的架构。

【科普】MVVM是Model-View-ViewModel的简写。它本质上就是MVC 的改进版。MVVM 就是将个中的View 的状况和行动抽象化,让我们将视图 UI 和营业逻辑离开。固然这些事 ViewModel 已帮我们做了,它能够掏出 Model 的数据同时帮助处置惩罚 View 中由于须要展现内容而触及的营业逻辑。

Vue作为如今盛行的MVVM框架,也是本人寻常营业顶用得最多的框架。怎样才更合理、文雅的写VueSPA,是本人一向研讨的课题,经由一年摆布的思索和实践总结出本文。
本文属于中高级实践议论,不适合新手。
本人个人的看法,不代表是最好实践,迎接大牛一同议论,批评指正。

工程搭建

秉着不反复造轮子的准绳(实在就是懒),工程直接应用Vue2.0官方脚手架天生,应用最新webpack模板。与范例模板的重要差别:

  1. 增添了Sass预编译器
  2. 增添了Vuex状况治理
  3. 增添了Axios基本Ajax东西库

新增部份的装置请参考他们各自的文档,这里不赘述。

项目构造

模仿需求

议论架构前我们须要一个项目需求,这里简朴模仿一个。
需求点:3个一级页面,2个二级页面,底部的tabbar只在一级页面涌现,首页、个人中间和登录页面是未登录也能够进入;财务和编辑个人信息是只要登录用户可见,简朴原型以下:

《【Geek议题】合理的VueSPA架构议论(上)》

开辟目次

下面不议论脚手架天生的部份目次,只聚焦src开辟目次,依据原型我们能够大抵计划出下面的目次:

├── build
├── config
├── dist
├── src  开辟目次
│   ├── api  大众api集
│   │   ├── axiosConfig.js  axios实例设置
|   |   └── index.js  大众api集进口
│   ├── assets  资本目次
│   │   ├── images  图片
│   │   ├── scripts  第三方剧本
|   |   └── styles  基本款式库
│   ├── components  大众组件
│   │   ├── common  平常通用组件
│   │   ├── form  表单通用组件
│   │   └── popup  弹出类通用组件
│   │── config  项目设置
│   │   ├── dev.env.js  开辟情势设置
│   │   ├── env.js  平常设置
│   │   ├── modules.js  模块设置
│   │   └── prod.env.js  临盆情势设置
│   │── mixin  用于vue文件夹杂的模板
│   │── modules  模块
│   │   ├── finance  财务模块
│   │   │   ├── components  财务模块私有组件
│   │   │   │   └── FinanceIndexItem.vue  财务模块首页里的条目项
│   │   │   ├── pages  财务模块页面
│   │   │   │   └── FinanceIndex.vue  财务模块首页
│   │   │   ├── api.js  模块api集
│   │   │   ├── index.js  模块进口
│   │   │   ├── Layout.vue  模块承载页
│   │   │   └── router.js  模块内路由
│   │   ├── home  首页模块(子目次同上)
│   │   └── user  用户模块(子目次同上)
│   │── pages  大众页面
│   │   ├── Success.vue  大众状况治理模块
│   │   └── NotFound.vue  用户模块(子目次同上)
│   ├── router  路由治理
│   ├── store  大众状况治理
│   │   ├── modules  大众状况治理模块
│   │   │   ├── com.js  通用状况
│   │   │   └── user.js  用户状况
│   │   └── index.js  大众状况治理进口
│   └── utils  基本东西
└── static

一些范例商定

依据本人个人开辟经验总结的范例,不代表必需这么做。

  1. 一切vue组件都以大写字面开首的驼峰定名法定名,如许坚持到模板代码上,能够便于区离开html的原生标签;
  2. 工资分别vue组件为“页面”和“页面上的组件”,准绳上“页面上的组件”不发请求,不转变大众状况,悉数经由历程事宜交由“页面”完成,本人更偏向用˙集合治理。(实在vue中并没有页面观点);
  3. 各个模块,包括路由治理、大众状况治理、接口集等都在目次下有个index.js的进口文件,轻易援用;
  4. 基本东西内的东西应用函数式编程,做到可移植,不要对本项目发生依靠;
  5. 资本图片只在项目中保存小图(就是会被webpack处置惩罚成base64那些),大图应应用cdn,能够动态猎取也能够把地点写到一个剧本里;
  6. 应用eslint使js代码相符Airbnb范例。

低耦合模块化开辟

项目历程中常碰到要把本来的项目离开布置,或是组件间耦合、或是多人开辟时组件争执等题目。本人提出的解决办法是将项目细分红模块举行开辟,每一个模块由多少相干“页面”构成,具有私有组件、路由、api等,如示例所示:分别了三个模块,首页模块、财务模块、用户模块。

【小结】这类计划的中心就是要将太甚零星的组件(页面)聚合成模块,每一个模块都有肯定迁移性,互不耦合,完成按需打包,并且在代码支解上比纯真的分页面加载越发天真可控。

Layout模块承载页

这个是为了让开辟这个模块的程序员有相似根组件<App>的大众空间。从路由的角度来讲,一切的模块内页面都是它的子路由,如许断绝了对全局路由的影响,最少途径定义能够随便些。
平常来讲它只是个空的路由跳转页,固然你把模块的大众数据放这里也能够的,在子路由就可以this.$parent拿到数据,能够当做子路由间的bus应用,以下以示例的user模块为例:

<template>
  <router-view/>
</template>
<script>
export default {
  name: 'user',
  data(){
    return {
      name: '明白',
      age: 12,
    };
  },
};
</script>

模块内路由

模块内路由末了都邑被导入总路由中,不要认为只是简朴兼并了文件,这里的设想也跟Layout模块承载页有关,
下面以user模块为例,我们把个人中间、登录和修正个人信息这三个页面归为user模块,路由计划以下。

  • 个人中间:/user
  • 登录:/user/login
  • 修正个人信息:/user/userInfo

个中由于“个人中间”是一级页面,需求请求底部有tabBar,所以使它只能是一级路由。
接下来你会发明Layout模块承载页的路由路劲也是’/user’,这里不必忧郁会乱,由于路由治理是按递次婚配的,至于为何要途径一样,这只是为了满足路由计划,让途径悦目罢了。

// 通用的tabbar
import IndexTabBar from '@/components/common/IndexTabBar';
// 模块内的页面
import UserIndex from './pages/UserIndex';
import UserLogin from './pages/UserLogin';
import UserInfo from './pages/UserInfo';

export default [
  // 一级路由
  {
    name: 'userIndex',
    path: '/user',
    meta: {
      title: '个人中间',
    },
    components: {
      default: UserIndex,
      footer: IndexTabBar,
    },
  },
  {
    path: '/user',
    // 这里支解子路由
    component: () => import('./layout.vue'),  
    children: [
      // 二级路由
      {
        name: 'userLogin',
        path: 'login',
        meta: {
          title: '登录',
        },
        component: UserLogin,
      },
      {
        name: 'userInfo',
        path: 'info',
        meta: {
          title: '修正个人信息',
          requiresAuth: true,
        },
        component: UserInfo,
      },
    ],
  },
];

模块承载页以懒加载的情势component: () => import('./layout.vue')引入,这会使webpack在此处支解代码,也就是说进入模块内是须要再此请求的,能够削减初次加载的数据量,进步速率。
官方关于懒加载的文档
这里你会发明后续的子路由,又是以直接引入的体式格局加载,也就是说全部模块会一同加载,完成了分模块加载
这与简朴的分页面加载差别,分页面加载一向有个难点,就是支解的量比较难把握(太多会增添请求次数,太少又降低了速率),而分模块能够将相干页面一同加载(跟进步缓存命中率很像),能够更天真的计划我们的加载,终究结果:

  1. 用户进入应用,首页的三个页面(有tabbar的)就已加载终了,这时刻点击哪一个tabbar按钮都能流通;
  2. 当用户进入某个页面内的子页面,会发生一次请求;
  3. 这时刻全部模块的页面都加载完(不肯定要悉数),用户在这个模块内又能流通接见。

模块api集

这个设想跟模块内路由相似,目的也是为了按需加载和断绝全局。
下面也是以user模块的模块api集为例,能够发明和路由有一些差别就是这里为了防备模块跟全局耦合,应用函数式编程头脑(相似于依靠注入),将全局的axios实例作为函数参数传入,再返回出一个包括api的对象,这个导出的对象将会被以模块名定名,兼并到全局的api集合。

export default function (axios) {
  return {
    postHeadImg(token, userId, data) {
      const options = {
        method: 'post',
        name: '换头像',
        url: '/data/user/updateHeadImg',
        headers: {
          token,
          userId,
        },
        data,
      };
      return axios(options);
    },
    postProduct(token, userId, data) {
      const options = {
        method: 'post',
        name: '提交产物挑选',
        url: '/product/opt',
        headers: {
          token,
          userId,
        },
        data,
      };
      return axios(options);
    },
  };
}

模块进口

为了轻易援用,每一个模块目次下都有一个index.js,引入模块的时刻能够省略,node会自动读这个文件。
还是以user模块为例,这里重假如引入模块专属api和模块内路由,并定义了模块的名字,这个名字是背面挂载专属api是时刻用的。

import api from './api';
import router from './router';

export default {
  name: 'user',
  api,
  router,
};

按需打包

示例中config目次下有个modules.js文件是指定打包须要的模块,测试一下打包差别数目的模块,会发明产物文件大小会转变,这就证明了已完成按需打包。
至于路由和api集的子模块整合完成,背面会提到。

import home from '@/modules/home';
import finance from '@/modules/finance';
import user from '@/modules/user';

export default [
  home,
  finance,
  user
]

api集的设置

【背景】示例项目模仿罕见的接口商定,服务器与应用交互有两个自定义头部:token和userId。token是权限标识符,险些悉数api都须要带上,为了防CSRF;userId是登录状况标识符,有些须要登录状况才应用的接谈锋须要带上,这两个标识符都有有效期。本示例暂不斟酌自动续期的机制。

在api治理方面本人比较喜好集合治理接口和设置,但提议请乞降请求回调偏向与每一个接口零丁处置惩罚。

导出axios实例

axios是比较盛行的ajax的promise封装。axios官方文档
本人引荐在全局保存唯一的axios实例,一切的请求都应用这个大众实例提议,完成设置的一致。
示例项目的在api文件夹下的axiosConfig.js就是axios的设置,重假如导出一个相符项目设置的实例,并举行一些拦截器设置。

【PS】至于为何到导出实例而不是直接修正axios默许值?

这是为了防备某些惯例状况下大众实例没法满足需求,须要零丁设置axios的状况,所认为了不污染原始的axios默许值,不引荐修正默许值。

// 引入axios包
import axios from 'axios';
// 引入环境设置
import env from '../config/env';
// 引入大众状况治理
import store from '../store/index';

// 全局默许设置
const myAxios = axios.create({
  // 跨域带cookie
  withCredentials: true,
  // 基本url
  baseURL: `${env.apiUrl}/${env.apiVersion}`,
  // 超时时候
  timeout: 12000,
});

// 请求提议前拦截器
myAxios.interceptors.request.use((_config) => {
  // ...
  return config;
}, () => {
  // 非常处置惩罚
});

// 相应拦截器
myAxios.interceptors.response.use((response) => {
  // ...
}, (error) => {
  // 非常处置惩罚
  return Promise.reject(error);
});

export default myAxios;

大众api集

项目的一切大众api都邑编写到这里,完成集合化治理,末了大众api聚会会议挂载到vue根实例下,应用this.$api就可以够轻易的接见。
由于token和userId不是必需头部,这里我引荐每一个接口函数都零丁处置惩罚,按需传入,如许api函数也能越发清楚。
给每一个接口起名字,是为了后续作废请求所设想的。
团体思绪:先定义大众api,再将模块内api(按需)挂载进来,末了导出api集。

// 引入已设置好的axios实例
import axios from './axiosConfig';
// 引入模块
import modules from '../config/modules';

const apiList = {
  // 猎取token不须要
  getToken() {
    const options = {
      method: 'post',
      name: '猎取token',
      url: '/token/get',
    };
    return axios(options);
  },
  loginWithName(token, data) {
    const options = {
      method: 'post',
      name: '用户名暗码登录',
      url: '/data/user/login4up',
      headers: {
        token,
      },
      data,
    };
    return axios(options);
  },
  postHeadImg(token, userId, data) {
    const options = {
      method: 'post',
      name: '换头像',
      url: '/data/user/updateHeadImg',
      headers: {
        token,
        userId,
      },
      data,
    };
    return axios(options);
  },
};
// 使每一个模块里的api集挂载到以模块名为名的定名空间下
modules.forEach((i) => {
  Object.assign(apiList, {
    [i.name]: i.api(axios),
  });
});

export default apiList;

路由治理设置

导入模块内路由

应用示例顶用router文件夹下的index.js设置全局路由,api集相似完成集合化治理,导出路由实例会挂载到vue根实例下,应用this.$router就可以够轻易的接见。
设置参考官方文档,这里重要提的一点是,模块内路由的整合,见实例代码段。

Vue.use(Router);
// 路由设置
const routerConfig = {
  routes: [
    {
      path: '/',
      meta: {
        title: env.appName,
      },
      redirect: { name: 'home' },
    },
    {
      name: 'success',
      path: '/success',
      meta: {
        title: '胜利',
      },
      component: Success,
    },
    {
      path: '*',
      component: NotFound,
    },
  ],
};
// 将模块内的路由拼接到全局
modules.forEach((i) => {
  routerConfig.routes = routerConfig.routes.concat(i.router);
});
const router = new Router(routerConfig);

在路由钩子函数中处置惩罚题目和权限

路由的钩子函数有许多妙用,这里列举了一些例子。
路由元信息meta能够自定义须要的数据,相当于给路由一个标记,然后在router.afterEach钩子函数中能够读取到并举行处置惩罚。
回忆上面示例的模块内路由,meta中定义了title(题目)和requiresAuth(是不是要登录状况),这就会在这里体现出用途。把登录权限设置在这里推断是为了防备用户进入某些须要权限的“页面”。

router.beforeEach((to, from, next) => {
  // 封闭大众弹框
  if (window.loading) {
    window.loading.close();
  }
  // 设置微信分享(假如有)
  wxShare({
    title: '哇哈哈',
    desc: '在路由钩子函数中处置惩罚题目和权限',
    link: env.shareBaseUrl,
    imgUrl: env.shareBaseUrl + '/images/shareLogo.png'
  });
  // 设置题目
  document.title = to.meta.title ? to.meta.title : '示例';
  // 搜检登录状况
  if (to.meta.requiresAuth) {
    // 目的路由须要登录状况
    // ...
  }
  next();
});

自动化治理权限标识符(token)

权限标识符的特性就是险些每一个链接都要带上,须要保护有效期,为了不糟蹋服务器资本还须要耐久化并保证请求唯一。
本人比较引荐应用大众状况治理vuex举行自动化治理,削减代码编写时的挂念。

妙用大众状况治理猎取token

示例中大众状况中的com模块里有tokenObj和waitToken两个字段,个中tokenObj包括了token和逾期时候,waitToken是一个标记是不是当前在猎取token的布尔值。

【PS】为何要token保证唯一一次请求?

罕见的场景:当用户进入应用,这时刻候token要么没有要么已逾期,这时刻页面须要并发两个ajax请求,由于都没有token,不唯一化处置惩罚的话,会同时先提议两个token请求,如许首先是糟蹋了请求资本,其次由因而异步请求,不能保证两次token的递次,假如服务器对token治理较严厉则会出题目。

由于猎取token是异步操纵,所以getToken写在actions中,把重要历程包裹成马上实行函数,并经由历程waitToken推断是不是要守候,假如要守候就隔一段时候再搜检,如许就保证了并发请求时,token能唯一。

const actions = {
  // needToRegain是为了特别条件下强迫猎取应用
  getToken({ commit, state: _state }, needToRegain) {
    return new Promise((resolve, reject) => {
      (function main() {
        // 假如waitToken为真即示意提议了请求但还未回应
        if (_state.waitToken) {
          console.log('守候token');
          setTimeout(() => {
            main();
          }, 1000);
          return;
        }
        // 是不是逾期标记
        let isExpire = false;
        // 提取现有的tokenObj
        let tokenObj = {
          ..._state.tokenObj,
        };
        // 假如没有token就从当地存储中读取
        if (!tokenObj.token) {
          tokenObj = JSON.parse(localStorage.getItem('tokenObj'));
          // 假如当地有tokenObj会趁便添加到状况治理
          if (tokenObj) {
            commit('setTokenObj', tokenObj);
          }
        }
        // token是不是过期
        if (tokenObj && tokenObj.token) {
          isExpire = new Date().getTime() - tokenObj.expireTime > -10000;
        }
        // 综合推断是不是须要猎取token
        if (!tokenObj || !tokenObj.token || isExpire || needToRegain) {
          commit('setWaitToken', true);
          api.getToken().then((res) => {
            // 搜检返回的数据
            const checkedData = connect.dataCheck(res);
            if (checkedData.isDataReady) {
              const newTokenObj = {
                token: checkedData.data.token,
                expireTime: new Date().getTime() + (checkedData.data.expire_time * 1000),
              };
              // 设置TokenObj会趁便保存一份到当地存储
              commit('setTokenObj', newTokenObj);
              commit('setWaitToken', false);
              console.log('猎取token胜利');
              resolve(newTokenObj.token);
            } else {
              commit('setWaitToken', false);
              console.error('猎取token失利');
              reject(checkedData.msg);
            }
          }).catch((err) => {
            window.toast('收集毛病');
            commit('setWaitToken', false);
            reject(err);
          });
        } else {
          console.log('token已存在,直接返回');
          resolve(tokenObj.token);
        }
      }());
    });
  },
};

token在请求代码中应用

将须要token的api函数套在getToken的回调中,就可以轻易的应用,不必再忧郁token是不是逾期。

const sendData = {
  mobile: this.formData1.mobile,
};
this.$store.dispatch('getToken').then((token) => {
  this.$api.sendSMS(token, sendData).then((res) => {
    const checkedData = this.$connect.dataCheck(res);
    if (checkedData.isDataReady) {
      window.toast('验证码已发送,请查收短信');
    } else {
      window.toast('验证码发送失利');
    }
  }).catch(() => {
    window.toast('收集毛病');
  });
});
    原文作者:calimanco
    原文地址: https://segmentfault.com/a/1190000014086261
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞