如何管理大型前端单页面应用

浅谈使用 Vue 构建前端 10w+ 代码量的单页面应用开发底层

Posted by Li Yucang on June 13, 2019

如何管理大型前端单页面应用

前言

在单页面前端项目的开发中,随着业务的不断累积,业务前端代码量往往会达到几万行~几十万行,业务复杂度类比 Excel 等桌面应用。管理好 10 万行级甚至百万行级代码的前端应用,已经成为了越来越多前端团队的核心挑战之一。

本文会在主要描述以 Vue 技术栈为技术主体,ToB 端项目业务主体,在构建过程中,遇到或者总结的点,可能并不适合你的业务场景。我会尽可能多的描述问题与其中的思考,最大可能的帮助到需要的同学。

单页面 VS 多页面

首先要思考我们的项目最终的构建主体是单页面,还是多页面,还是单页 + 多页,通过他们的优缺点来分析。

单页面(SPA)

基本概念:在项目应用中,以单个 html 页面作为外壳页面,并在外壳页面一次性加载项目所依赖的资源(CSS、JS)。项目中其他页面作为页面片段在外壳页面中进行无感切换(例如:利用 H5 的 History 监听到 URL 的变化,对页面片段进行切换(删除和添加))。

优点:体验好,路由之间跳转流畅,可定制转场动画,使用了懒加载可有效减少首页白屏时间,相较于多页面减少了用户访问静态资源服务器的次数等。

缺点:初始会加载较大的静态资源,并且随着业务增长会越来越大,懒加载也有他的弊端,不做特殊处理不利于 SEO 等。

多页面(MPA)

这里我觉得多页面应用可以分为两种形式:

  1. 传统形式:这种技术方案就是最初搭建页面的实现方式,每个页面都有各自的 HTML 文件,每个页面所依赖的资源都在各自 HTML 文件中引入,并且页面之间的跳转通过 URL 跳转。架构图如下所示:

  1. 单页面形式:这种技术方案根据实际项目出发,本质上是将单页面应用拆分成多个单页面应用,多个单页面应用之间的调整通过传统形式来实现;架构图如下所示:

总结:传统形式结构是完全符合多页面的思想。单页面结构是单页面应用和多页面应用的互补方案;解决了首屏加载慢的问题。相对于传统形式结构,它可以像单页面应用一样实现转场动画(多页面跳转之间还是不能实现)。

缺点:资源请求较多,整页刷新体验较差,页面间传递数据只能依赖 URL,cookie,storage 等方式,较为局限。

SPA + MPA

这种方式常见于较老 MPA 项目迁移至 SPA 的情况,缺点结合两者,两种主体通信方式也只能以兼容 MPA 为准。

不过这种方式也有他的好处,假如你的 SPA 中,有类似文章分享这样(没有后端直出,后端返 HTML 串的情况下),想保证用户体验在 SPA 中开发一个页面,在 MPA 中也开发一个页面,去掉没用的依赖,或者直接用原生 JS 来开发,分享出去是 MPA 的文章页面,这样可以加快分享出去的打开速度,同时也能减少静态资源服务器的压力,因为如果分享出去的是 SPA 的文章页面,那 SPA 所需的静态资源至少都需要去进行协商请求,当然如果服务配置了强缓存就忽略以上所说。

如何选择页面结构

还是那句话,具体根据项目分析,对于官网、电商类这种对 SEO 和首屏加载速度要求比较高的项目,可以采用多页面应用结构。或者优秀“服务器端渲染方案”;如果是后台管理系统页面,不对外开放的系统,用单页面应用,这样可以利用第三方框架(Vue、React 等)对系统进行组件化,如果系统过大,导致首屏加载缓慢,可以将系统拆分成单页面形式的多页面应用;

我们首先根据业务所需,来最终确定构建主体,而我们选择了体验至上的 SPA,并选用 Vue 技术栈。

目录结构

其实我们看开源的绝大部分项目中,目录结构都会差不太多,我们可以综合一下来个通用的 src 目录:

src
├── assets          // 资源目录 图片,样式,iconfont
├── components      // 全局通用组件目录
├── config          // 项目配置,拦截器,开关
├── plugins         // 插件相关,生成路由、请求、store 等实例,并挂载 Vue 实例
├── directives      // 拓展指令集合
├── routes          // 路由配置
├── service         // 服务层
├── utils           // 工具类
└── views           // 视图层

接下来我们会详细的介绍各个目录在整个项目架构中起到的作用。

通用组件

components 中我们会存放 UI 组件库中的那些常见通用组件了,在项目中直接通过设置别名来使用,如果其他项目需要使用,就发到 npm 上。

// components 简易结构
components
├── dist
├── build
├── src
    ├── modal
    ├── toast
    └── ...
├── index.js
└── package.json

如果想最终编译成 es5,直接在 html 中使用或者部署 CDN 上,在 build 配置简单的打包逻辑,搭配着 package.json 构建 UI 组件 的自动化打包发布,最终部署 dist 下的内容,并发布到 npm 上即可。

而我们也可直接使用 es6 的代码:

import 'Components/src/modal'

全局配置,插件与拦截器

这个点其实会是项目中经常被忽略的,或者说很少聚合到一起,但同时我认为是整个项目中的重要之一,后续会有例子说道。

全局配置,拦截器目录结构

config
├── index.js             // 全局配置/开关
├── interceptors        // 拦截器
    ├── index.js        // 入口文件
    ├── axios.js        // 请求/响应拦截
    ├── router.js       // 路由拦截
    └── ...
└── ...

全局配置

我们在 config/index.js 可能会有如下配置:

// 当前宿主平台
export const HOST_PLATFORM = 'WEB';

// 环境变量
export const NODE_ENV = process.env.NODE_ENV || 'production';

// 区分jenkins打包
export const JENKINS_BUILD = process.env.BUILD_BY === 'jenkins';

// 是否开启监控
export const MONITOR_ENABLE = true;

// 路由默认配置,路由表并不从此注入
export const ROUTER_DEFAULT_CONFIG = {
  waitForData: true,
  transitionOnLoad: true,
};

// axios 默认配置
export const AXIOS_DEFAULT_CONFIG = {
  timeout: 0,
  maxContentLength: Infinity,
  headers: {},
  baseURL: JENKINS_BUILD ? '/web/data-platform' : '',
};

// vuex 默认配置
export const VUEX_DEFAULT_CONFIG = {
  strict: process.env.NODE_ENV !== 'production',
};

// API 默认配置
export const API_DEFAULT_CONFIG = {
  mockBaseURL: '',
  mock: false,
  sep: '/',
};

// CONST 默认配置
export const CONST_DEFAULT_CONFIG = {
  sep: '/',
};

// 还有一些方便开发的配置
export const CONSOLE_REQUEST_ENABLE = true; // 开启请求参数打印
export const CONSOLE_RESPONSE_ENABLE = true; // 开启响应参数打印
export const CONSOLE_MONITOR_ENABLE = true; // 监控记录打印

可以看出这里汇集了项目中所有用到的配置,下面我们在 plugins 中实例化插件,注入对应配置,目录如下:

插件目录结构

plugins
├── api.js              // 服务层 api 插件
├── axios.js            // 请求实例插件
├── const.js            // 服务层 const 插件
├── store.js            // vuex 实例插件
├── inject.js           // 注入 Vue 原型插件
└── router.js           // 路由实例插件

实例化插件并注入配置

这里先举出两个例子,看我们是如何注入配置,拦截器并实例化的

实例化 router:

import Vue from 'vue';
import Router from 'vue-router';
import ROUTES from 'Routes';
import { ROUTER_DEFAULT_CONFIG } from 'Config/index';
import { routerBeforeEachFunc, routerAfterEachFunc } from 'Config/interceptors/router';

Vue.use(Router);

// 注入默认配置和路由表
const routerInstance = new Router({
  ...ROUTER_DEFAULT_CONFIG,
  routes: ROUTES,
});

// 注入拦截器
routerInstance.beforeEach(routerBeforeEachFunc);
routerInstance.afterEach(routerAfterEachFunc);

export default routerInstance;

实例化 axios:

import axios from 'axios';
import { AXIOS_DEFAULT_CONFIG } from 'Config/index';
import {
  requestSuccessFunc, requestFailFunc, responseSuccessFunc, responseFailFunc,
} from 'Config/interceptors/axios';

const axiosInstance = axios.create(AXIOS_DEFAULT_CONFIG);

// 注入请求拦截
axiosInstance
  .interceptors.request.use(requestSuccessFunc, requestFailFunc);

// 注入失败拦截
axiosInstance
  .interceptors.response.use(responseSuccessFunc, responseFailFunc);

export default axiosInstance;

我们在 main.js 注入插件:

import Vue from 'vue';
import ZhuiyiUI from 'zhuiyi-ui';

// 根据实际生成的主题文件路径进行引入
import '../theme/index.css';

// import 'Components'// 全局组件注册
import 'Directives'; // 指令

// 引入插件
import router from 'Plugins/router';
import inject from 'Plugins/inject';
import store from 'Plugins/store';

// 引入根组件
import App from './App';

Vue.use(ZhuiyiUI);

GLOBAL.vbus = new Vue();

Vue.use(inject);

new Vue({
  el: '#app',
  router,
  store,
  template: '<App/>',
  components: { App },
});

axios 实例我们并没有直接引用,相信你也猜到他是通过 inject 插件引用的,我们看下 inject:

import axios from './axios';
import api from './api';
import consts from './const';

GLOBAL.ajax = axios;

export default {
  install: (Vue, options) => {
    Vue.prototype.$api = api;
    Vue.prototype.$ajax = axios;
    Vue.prototype.$const = consts;
    // 需要挂载的都放在这里
  },
};

这里可以挂载你想在业务中( vue 实例中)便捷访问的 api,除了 $ajax 之外,api 和 const 两个插件是我们服务层中主要的功能,后续会介绍,这样我们插件流程大致运转起来,下面写对应拦截器的方法。

请求,路由拦截器

在 ajax 拦截器中(config/interceptors/axios.js):

const errCodeMsg = {
  12000: '当前账号无该分类上传权限,请联系管理员开通',
  12010: '每日累积下载FAQ数量不可大于2000条,请联系管理员开通大量数据下载权限',
};

export function requestSuccessFunc(requestObj) {
  // 自定义请求拦截逻辑,可以处理权限,请求发送监控等
  return requestObj;
}

export function requestFailFunc(requestError) {
  // 自定义发送请求失败逻辑,断网,请求发送监控等
  return Promise.reject(requestError);
}

export function responseSuccessFunc(responseObj) {
  // 自定义响应成功逻辑,全局拦截接口,根据不同业务做不同处理,响应成功监控等
  // 假设我们请求体为
  // {
  //     code: 1010,
  //     msg: 'this is a msg',
  //     data: null
  // }

  const resData = responseObj.data;
  const { code } = resData;

  // 特殊特殊 code 逻辑下放业务层
  const isErrCode = Object.keys(errCodeMsg).includes(`${code}`);
  if (isErrCode) {
    return {
      isErrCode: true,
      errMsg: errCodeMsg[`${code}`],
      errData: resData.data,
    };
  }

  switch (code) {
    case 0: // 如果业务成功,直接进成功回调
      return resData.data;
    default:
      return Promise.reject(resData);
  }
}

export function responseFailFunc(responseError) {
  // 响应失败,可根据 responseError.message 和 responseError.response.status 来做监控处理
  if (responseError.response.status === 401) {
    window.location.href = `${window.location.origin}/#/noPermission`;
  }
  return Promise.reject(responseError);
}

定义路由拦截器(config/interceptors/router.js):

export function routerBeforeEachFunc(to, from, next) {
  // 这里可以做页面拦截,很多后台系统中也非常喜欢在这里面做权限处理
  next();
}

export function routerAfterEachFunc(to, from) {
}

最后在入口文件(config/interceptors/index.js)中引入并暴露出来即可:

import {
  requestSuccessFunc, requestFailFunc, responseSuccessFunc, responseFailFunc,
} from './axios';
import { routerBeforeEachFunc, routerAfterEachFunc } from './router';

export default {
  requestSuccessFunc,
  requestFailFunc,
  responseSuccessFunc,
  responseFailFunc,
  routerBeforeEachFunc,
  routerAfterEachFunc,
};

路由配置与懒加载

directives 里面没什么可说的,不过很多难题都可以通过他来解决,要时刻记住,我们可以再指令里面操作虚拟 DOM。

路由配置

而我们根据自己的业务性质,最终根据业务流程来拆分配置:

routes
├── index.js              // 入口文件
├── modules
    ├── common.js         // 公共路由,登录,提示页等
    ├── data-platform.js  // 数据平台
    └── ...
└── ...

最终通过 index.js 暴露出去给 plugins/router 实例使用,这里的拆分配置有两个注意的地方:

需要根据自己业务性质来决定,有的项目可能适合业务线划分,有的项目更适合以 功能 划分。

在多人协作过程中,尽可能避免冲突,或者减少冲突。

懒加载

单页面静态资源过大,首次打开/每次版本升级后都会较慢,可以用懒加载来拆分静态资源,减少白屏时间,但懒加载也有待商榷的地方:

  • 如果异步加载较多的组件,会给静态资源服务器/ CDN 带来更大的访问压力的同时,如果当多个异步组件都被修改,造成版本号的变动,发布的时候会大大增加 CDN 被击穿的风险。
  • 懒加载首次加载未被缓存的异步组件白屏的问题,造成用户体验不好。
  • 异步加载通用组件,会在页面可能会在网络延时的情况下参差不齐的展示出来等。

这就需要我们根据项目情况在空间和时间上做一些权衡。以下几点可以作为简单的参考:

  • 对于访问量可控的项目,如公司后台管理系统中,可以以操作 view 为单位进行异步加载,通用组件全部同步加载的方式。
  • 对于一些复杂度较高,实时度较高的应用类型,可采用按功能模块拆分进行异步组件加载。
  • 如果项目想保证比较高的完整性和体验,迭代频率可控,不太关心首次加载时间的话,可按需使用异步加载或者直接不使用。

打包出来的 main.js 的大小,绝大部分都是在路由中引入的并注册的视图组件。

Service 服务层

服务层作为项目中的另一个核心之一,“自古以来”都是大家比较关心的地方。

不知道你是否看到过如下组织代码方式:

views/
    pay/
        index.vue
        service.js
        components/
            a.vue
            b.vue

在 service.js 中写入编写数据来源:

export const CONFIAG = {
    apple: '苹果',
    banana: '香蕉'
}
// ...

// ① 处理业务逻辑,还弹窗
export function getBInfo ({name = '', id = ''}) {
    return this.$ajax.get('/api/info', {
        name,
        id
    }).then({age} => {
        this.$modal.show({
            content: age
        })
    })
}

// ② 不处理业务,仅仅写请求方法
export function getAInfo ({name = '', id = ''}) {
    return this.$ajax.get('/api/info', {
        name,
        id
    })
}

...

简单分析:

① 就不多说了,拆分的不够单纯,当做二次开发的时候,你还得去找这弹窗到底哪里出来的。

② 看起来很美好,不掺杂业务逻辑,但不知道你与没遇到过这样情况,经常会有其他业务需要用到一样的枚举,请求一样的接口,而开发其他业务的同学并不知道你在这里有一份数据源,最终造成的结果就是数据源的代码到处冗余。

我相信 ② 在绝大多数项目中都能看到。那么我们的目的就很明显了,解决冗余,方便使用,我们把枚举和请求接口的方法,通过插件,挂载到一个大对象上,注入 Vue 原型,方面业务使用即可。

目录层级(仅供参考):

service
├── api
    ├── index.js             // 入口文件
    ├── modules
      ├── order.js             // 订单相关接口配置
      └── ...
├── const
    ├── index.js             // 入口文件
    ├── modules
      ├── order.js             // 订单常量接口配置
      └── ...
├── store                    // vuex 状态管理
    ├── index.js             // 入口文件
    ├── modules
      ├── order             // 订单常量接口配置
      └── ...
├── expands                  // 拓展
    ├── monitor.js           // 监控
    ├── beacon.js            // 打点
    ├── localstorage.js      // 本地存储
    └── ...                  // 按需拓展
└── ...

抽离模型

首先抽离请求接口模型 (service/api/index.js):

{
    user: [{
        name: 'info',
        method: 'GET',
        desc: '测试接口1',
        path: '/api/info',
        mockPath: '/api/info',
        params: {
            a: 1,
            b: 2
        }
    }, {
        name: 'info2',
        method: 'GET',
        desc: '测试接口2',
        path: '/api/info2',
        mockPath: '/api/info2',
        params: {
            a: 1,
            b: 2,
            b: 3
        }
    }],
    order: [{
        name: 'change',
        method: 'POST',
        desc: '订单变更',
        path: '/api/order/change',
        mockPath: '/api/order/change',
        params: {
            type: 'SUCCESS'
        }
    }]
    ...
}

定制下需要的几个功能:

  • 需要命名空间。
  • 通过全局配置开启调试模式。
  • 通过全局配置来控制走本地 mock 还是线上接口等。

插件编写

定制好功能,开始编写简单的 plugins/api.js 插件:

import _assign from 'lodash/assign';

import { API_DEFAULT_CONFIG } from 'Config';
import API_CONFIG from 'Service/api';
import axios from './axios';

function _normoalize(options, data) {
  // put请求,替换url地址中的id占位符
  if (data.urlId) {
    options.url = options.url.replace('URLID', data.urlId);
    delete data.urlId;
  }

  if (data.formData) { // 表单
    options.data = data.formData;
  } else if (options.method === 'GET') {
    options.params = data;
  } else { // POST、PUT
    options.data = data;
  }

  return options;
}

class MakeApi {
  constructor(options) {
    this.api = {};
    this.apiBuilder(options);
  }

  apiBuilder({
    sep = '/',
    mock = false,
    mockBaseURL = '',
    config = {},
  }) {
    Object.keys(config).forEach((namespace) => {
      this._apiSingleBuilder({
        sep,
        mock,
        mockBaseURL,
        config: config[namespace],
        namespace,
      });
    });
  }

  _apiSingleBuilder({
    sep = '/',
    mock = false,
    mockBaseURL = '',
    config = {},
    namespace,
  }) {
    config.forEach((api) => {
      const {
        name, params, method, mockEnable, path, mockPath,
      } = api;

      // 在接口中配置mockEnable可mock单个接口
      const isMock = process.env.NODE_ENV === 'production' ? false : mock || mockEnable;
      const url = isMock ? mockPath : path;
      const _innerOptions = isMock ? {
        url, method, baseURL: mockBaseURL,
      } : { url, method };

      Object.defineProperty(this.api, `${namespace}${sep}${name}`, {
        value(outerParams, outerOptions) {
          const _data = _assign({}, params, outerParams);
          const _options = _assign({}, _innerOptions, outerOptions);
          return axios(_normoalize(_options, _data));
        },
      });
    });
  }
}

export default new MakeApi({
  config: API_CONFIG,
  ...API_DEFAULT_CONFIG,
}).api;

挂载到 Vue 原型上,上文有说到,通过 plugins/inject.js

import api from './api'

export default {
    install: (Vue, options) => {
        Vue.prototype.$api = api
        // 需要挂载的都放在这里
    }
}

使用

这样我们可以在业务中愉快的使用业务层代码:

// .vue 中
export default {
    methods: {
        test() {
            this.$api['order/info']({
                a: 1,
                b: 2
            })
        }
    }
}

即使在业务之外也可以使用:

import api from 'Plugins/api'

api['order/info']({
    a: 1,
    b: 2
})

当然对于运行效率要求高的项目中,避免内存使用率过大,我们需要改造 API,用解构的方式引入使用,最终利用 webpack 的 tree-shaking 减少打包体积。

一般来说,多人协作时候大家都可以先看 api 是否有对应接口,当业务量上来的时候,也肯定会有人出现找不到,或者找起来比较费劲,这时候我们完全可以在 请求拦截器中,把当前请求的 url 和 api 中的请求做下判断,如果有重复接口请求路径,则提醒开发者已经配置相关请求,根据情况是否进行二次配置即可。

最终我们可以拓展 Service 层的各个功能:

基础

api:异步与后端交互 const:常量枚举 store:Vuex 状态管理

拓展

localStorage:本地数据,稍微封装下,支持存取对象即可 monitor:监控功能,自定义搜集策略,调用 api 中的接口发送 beacon:打点功能,自定义搜集策略,调用 api 中的接口发送 …

const,localStorage,monitor 和 beacon 根据业务自行拓展暴露给业务使用即可,思想也是一样的,下面着重说下 store(Vuex)。

状态管理与视图拆分

我们是不是真的需要状态管理?

答案是否定的,就算你的项目达到 10 万行代码,那也并不意味着你必须使用 Vuex,应该由业务场景决定。

业务场景

第一类项目:业务/视图复杂度不高,不建议使用 Vuex,会带来开发与维护的成本,使用简单的 vbus 做好命名空间,来解耦即可。

let vbus = new Vue()
vbus.$on('print.hello', () => {
    console.log('hello')
})

vbus.$emit('print.hello')

第二类项目:类似多人协作项目管理,有道云笔记,网易云音乐,微信网页版/桌面版等应用,功能集中,空间利用率高,实时交互的项目,无疑 Vuex 是较好的选择。这类应用中我们可以直接抽离业务领域模型:

store
├── index.js
├── actions.js        // 根级别 action
├── mutations.js      // 根级别 mutation
└── modules
    ├── user.js       // 用户模块
    ├── products.js   // 产品模块
    ├── order.js      // 订单模块
    └── ...

当然对于这类项目,vuex 或许不是最好的选择,有兴趣的同学可以学习下 rxjs。

第三类项目:后台系统或者页面之间业务耦合不高的项目,这类项目是占比应该是很大的,我们思考下这类项目:

全局共享状态不多,但是难免在某个模块中会有复杂度较高的功能(客服系统,实时聊天,多人协作功能等),这时候如果为了项目的可管理性,我们也在 store 中进行管理,随着项目的迭代我们不难遇到这样的情况:

store/
    ...
    modules/
        b.js
        ...
views/
    ...
    a/
        b.js
        ...
  • 试想下有几十个 module,对应这边上百个业务模块,开发者在两个平级目录之间调试与开发的成本是巨大的。
  • 这些 module 可以在项目中任一一个地方被访问,但往往他们都是冗余的,除了引用的功能模块之外,基本不会再有其他模块引用他。
  • 项目的可维护程度会随着项目增大而增大。

如何解决第三类项目的 store 使用问题?

先梳理我们的目标:

  • 项目中模块可以自定决定是否使用 Vuex。(渐进增强)
  • 从有状态管理的模块,跳转没有的模块,我们不想把之前的状态挂载到 store 上,想提高运行效率。(冗余)
  • 让这类项目的状态管理变的更加可维护。(开发成本/沟通成本)

实现

我们借助 Vuex 提供的 registerModule 和 unregisterModule 一并解决这些问题,我们在 service/store 中放入全局共享的状态:

service/
    store/
        index.js
        actions.js
        mutations.js
        getters.js
        state.js

一般这类项目全局状态不多,如果多了拆分 module 即可。

编写插件生成 store 实例:

import Vue from 'vue'
import Vuex from 'vuex'
import {VUEX_DEFAULT_CONFIG} from 'Config'
import commonStore from 'Service/store'

Vue.use(Vuex)

export default new Vuex.Store({
    ...commonStore,
    ...VUEX_DEFAULT_CONFIG
})

对一个需要状态管理页面或者模块进行分层:

views/
    pageA/
        index.vue
        components/
            a.vue
            b.vue
            ...
        children/
            childrenA.vue
            childrenB.vue
            ...
        store/
            index.js
            actions.js
            moduleA.js
            moduleB.js

module 中直接包含了 getters,mutations,state,我们在 store/index.js 中做文章:

import Store from 'Plugins/store'
import actions from './actions.js'
import moduleA from './moduleA.js'
import moduleB from './moduleB.js'

export default {
    install() {
        Store.registerModule(['pageA'], {
            actions,
            modules: {
                moduleA,
                moduleB
            },
            namespaced: true
        })
    },
    uninstall() {
        Store.unregisterModule(['pageA'])
    }
}

最终在 index.vue 中引入使用, 在页面跳转之前注册这些状态和管理状态的规则,在路由离开之前,先卸载这些状态和管理状态的规则:

import store from './store'
import {mapGetters} from 'vuex'
export default {
    computed: {
        ...mapGetters('pageA', ['aaa', 'bbb', 'ccc'])
    },
    beforeRouterEnter(to, from, next) {
        store.install()
        next()
    },
    beforeRouterLeave(to, from, next) {
        store.uninstall()
        next()
    }
}

当然如果你的状态要共享到全局,就不执行 uninstall。

这样就解决了开头的三个问题,不同开发者在开发页面的时候,可以根据页面特性,渐进增强的选择某种开发形式。

跨模块通信

模块粒度逐渐细化,会带来更多的跨模块通信诉求,为避免模块间相互耦合、确保架构长期干净可维护,我们规定:

  • 不允许在一个模块内部直接调用其他模块的方法(写操作、变更其他模块的 state)

  • 不允许在一个模块内部直接读取其他模块的 state 方法(读操作)

我们建议将跨模块通信的逻辑代码放在父模块中,或者做一个通用层来维护跨模块通信。

最后

项目底层构建往往会成为前端忽略的地方,我们既要从一个大局观来看待一个项目或者整条业务线,又要对每一行代码精益求精,对开发体验不断优化,慢慢累积后才能更好的应对未知的变化