基于Vue的多项目整合实践

10,487 阅读13分钟

在笔者所在的前端开发团队中,采用前后端分离方案是在整个业务线稳定后进行的。业务前期主要采用后端套模板的方式,现阶段是采用基于Vue的单页开发模式。

这会出现一种情形,产品在不断迭代过程中,由于之前线上前端代码并非工程化项目,后面新需求多是另起Vue项目来进行编码上线,前端在整个业务线上没有统一的工程,项目过多分布散乱并且不易优化管理。(项目指根据新需求创建的项目代码,工程指一套代码下包含多个项目。后文以此约定。)针对这种情况下我们做出一些尝试,将目前存在的多个项目整合成一个工程,统一入口。

当然我们更希望整合成统一工程后可以实现后续新需求接入无痛化。下文则主要围绕项目整合过程中遇到的些许问题,分享一些可行解决方案。简要从几个方向,代码层级化分、组件处理、路由处理、数据状态维护、其他优化等来简述。

1. 代码层级划分

如何合理划分整个工程目录?

在前端开发中我们首先会面对如何将代码及静态资源划分目录层级放置问题。比如在前端洪荒时代通常会以img、css、js命名不同目录。那么在结合Vue相关技术栈以及多项目整合场景下,如何划分目录才能保证代码层次合理呢?

谈到这个问题的时候,我们可以首先思考下整个工程具体需要哪些相关功能。在抛开具体源码内部结构情况下,主要有构建脚本、构建配置、Mock数据、项目文档、项目源码等。在不结合具体技术栈的情况下,这也是前端在工程化方面大致目录。

具体到项目源码内部目录结构,按照不同功能模块,主要做了以下层次划分:

  1. 静态资源按项目拆分目录
  2. 路由组件按项目拆分目录
  3. 子组件按项目及所属父路由组件拆分目录
  4. 路由层按项目拆分不同文件
  5. 数据层按项目拆分不同目录
  6. mixins混合代码拆分不同文件
  7. 配置层按项目拆分不同文件

以项目src源码下组件相关目录为例,如下图所示:

├── pages                       // 路由组件目录
│   ├── README.md
│   ├── base                    // 全局基础路由组件目录
│   │   └── SuccessPage.vue
│   ├── period_process          // 项目A路由级别组件目录
│   │   ├── ChooseTime.vue      // 项目A选择时间路由组件
│   │   ├── xxx
│   └── period_suborder         // 项目B路由级别组件目录
│       └── xxx
|
├── components                  // 子组件目录
│   ├── README.md
│   ├── base                    // 全局基础子组件目录
│   │   ├── AddressInfo.vue  
│   │   └── xxx
│   ├── period_process          // 项目A子组件目录
│   │   ├── base                // 项目基础A子组件目录
│   │   │   ├── Picker.vue
│   │   │   └── xxx
│   │   ├── choose_time         // 项目A选择时间路由组件子组件目录
│   │   │   ├── Cleaner.vue
│   │   │   └── xxx
│   │   ├── xxx
│   └── period_suborder         // 项目B子组件目录
│       └── xxx

当然项目目录结构不是一层不变的,可根据业务场景及技术栈灵活处理。但原则上避免一个文件从头写到尾出现绵长代码情况,造成后续迭代可阅读性差、不好维护问题。良好的代码层次可以简述为将相同功能模块聚合同一目录并拆分出独立文件。

2. 组件维护相关

如何控制组件拆分粒度?

谈及组件部分,组件拆分粒度永远是一个绕不开的话题。首先大的方面分为路由组件(页面组件)和相应页面子组件。路由组件为配置路由时组件,组件内部拆分不同子组件进行引用。路由组件及子组件分别拆分相应业务组件和基础组件。

页面组件拆分过程中,我们采用将相关功能模块代码拆分为子组件。将页面划分若干子组件(功能模块),每个子组件完成一个子功能。如下图所示:

如何方便业务组件提升为基础组件?

在组件划分时,我们将子组件拆分业务组件和基础组件。在项目整合的过程中,遇到业务子组件因被后续多个项目使用需要提升为基础组件情况。但起初编码过程中仅考虑作为业务子组件,内部数据源多依赖Vuex,在将其提升为基础组件时需要做大量工作将数据来源改为props对象,修改数据源操作改为emit触发事件机制。

这里更好的处理方式则是希望在编写子组件时,内部数据源尽可能依赖于父组件传递的props对象。将需要修改数据源行为通过emit方式提升至父组件内操作。

3. 路由相关处理

如何处理多入口路由配置?

单页web应用在处理不同view时提出前端router概念。对应单一项目需求时,我们可以很从容设置默认路由入口来解决。但是对于多入口的多项目工程,则需要一些思量。同时由于笔者所处公司APP在处理URL跳转时默认不带hash,那么在URL访问上则没有办法通过附加hash路由来跳转相应视图。

我们采用的解决方式是,在URl访问时不携带hash,后端同学会在不同项目的入口html文件中放置PAGE_TYPE变量,前端根据PAGE_TYPE变量跳转相应路由组件。PAGE_TYPE变量对应于当前待访问的路由。通过结合后端在页面中渲染的路由标志量,解决访问时路径问题。

在此基础上,还需要考虑页面刷新以及跳转外链后退情况。在非入口路由页面刷新会根据PAGE_TYPE变量重置进入入口路由页。这种情况需要判断当前URL是否存在hash路由标志,优先获取当前链接hash值进行跳转。具体伪代码如下所示:

let routeType = window.PAGE_TYPE  // 获取初始化路由变量
let routeName = getHashRouter()   // 获取当前路由名称

if (routeName) {
  router.push({path: `/${routeName}`})
} else {
  switch (parseInt(routeType)) {
    case 0:
      router.push({path: '/index'})   
      break
    case 1:
      router.push({path: '/projectB'})   
      break
    default:
      router.push({path: '/index'})  // 默认路由入口
  }
}

如何优化异步组件加载?

另外一个值得考虑的问题是,随着项目不断增加在打包构建应用时,js文件会变得越来越大,影响页面加载速度。将非入口路由组件异步加载是一种比较高效的解决方案。结合Vue的异步组件和Webpack的代码分割(code splitting)特性可以轻松实现路由组件懒加载。具体语法可参考vue-router官方文档。

非入口路由组件异步加载,可减少首次加载JS文件大小。但随之而来的问题是,假如用户选择点击按钮进行路由跳转时,需要异步获取JS文件,等待异步组件加载完成后再跳转。特别在跳转之前还需要异步调用接口校验,用户等待时间无疑增加。我们更希望可以在用户空闲时间预加载后续跳转的异步组件。

具体预加载操作可以在组件生命周期mounted中手动触发后续异步组件加载。还可将预加载操作聚合成mixin文件,注册成全局mixin。埋点数据显示后续路由组件跳转时间约在300ms左右,属于秒开范围。示例如下:

mounted () {
  // 预加载后续异步路由组件
  import(/* webpackChunkName: 'chooseTime' */ '@/pages/period_process/ChooseTime.vue')
  
  // 或使用 webpack 特定的 require.ensure 语法
  // require.ensure(['@/pages/period_process/ChooseTime.vue'], null, 'chooseTime')
}

将非入口路由组件异步加载,并在用户空闲时间实现预加载接下来路由组件。降低用户等待时间,用户体验自然也就更好。

如何优雅处理路由跳转效果?

这里说到的路由跳转效果指在不同路由组件跳转时所添加的过渡效果。Vue默认的router-view跳转不存在动效,略感生硬。为router-view添加transition是不错的选择。

不过在处理transition过程中,起初是将过渡效果添加至路由组件根节点上,但在某些安卓机型下跳转会出现明显的闪屏现象。解决方式是将transition组件移至router-view组件外统一处理。

4. 数据状态维护

如何维护项目中的数据状态?

可以预料到的数据交互行为主要有:

  • 父子组件数据共享
  • 兄弟组件数据共享
  • 用户行为数据存储
  • 后端接口数据缓存

另外还希望所有数据层model和异步接口可以抽取进行统一维护,为此我们引入了Vuex

VuexVue应用程序的状态管理模式,采用集中式存储管理应用的所有组件状态。未引入Vuex下常见的问题多个视图依赖与同一状态,多个视图需要变更同一状态。两种情形下多通过组件间参数传递或采用事件同步状态。引入Vuex后,将组件共享状态抽取成单例模式管理。定义并约定遵守一定的状态管理的规则,代码结构化更清晰更容易维护。当然如果不开发复杂单页应用,使用Vuex可能是繁琐冗余。

除此我们还需要考虑的是,由于Vuex数据状态是存储在JS变量中的,当页面刷新时整个应用状态会全部丢失。需要在state、mutations读取、存储中添加本地持久化操作。封装本地持久化存储层Cache.js,可选sessionStorage、localStorage、indexedDB存储方式。依据业务情形在mutations文件中选择相应方式做本地持久化操作。

Vuex + Cache方式做数据状态维护,并将相应代码拆分独立数据层,减少与业务代码耦合程度。业务流程中只需要通过mapState、mapActions方式获取相应数据状态或更新相应数据状态。

如何保证多项目中数据状态不被污染?

多项目整合在一起,各个项目中state难免会出现变量名称冲突,多个状态变量相互污染现象。同时actions、mutations操作也都暴露在全局状态下。随着项目不断整合加入,这将会成为一个定时炸弹。

很开森的是Vuex有相应的解决方案。Vuex中提出模块(module)和命名空间(namespace)概念。Vuex允许我们将store分割成模块,并且可通过添加namespaced: true将其变为命名空间模块。多个项目使用各自state对象,数据耦合程度及被污染的可能性降低。

结合Vuex还可以做哪些好玩的事情?

按照Vuex所约定数据状态存储以及修改的规则,很容易将数据层按照相应层次拆分出来。这时异步请求则可以进行统一聚合,封装成全局状态下的fetchDataaction方法,在统一的异步请求中我们做了以下工作:

  • 防止重复提交
  • 网络异常统一处理
  • 缓存接口数据(可选)
  • 自动处理接口返回数据code不为0情况(可选)
  • 接口请求时间过长显示loading状态(可选)
  • 自动上报接口请求时间(可选)

如此下来,我们业务组件不再需要考虑异步请求中的重复提交、网络异常等事情。单Vue文件组件中中仅聚焦业程逻辑的实现。

5. 其他优化

如何拆分组件中与业务无关逻辑代码?

解释下,这里无关逻辑是指与业务关联性密切性不大的代码。比如,前端做router跳转不同视图时,需要考虑跳转到相应视图下设置当前视图的页面title或发送当前视图的pv埋点。每个路由级别组件几乎都会写类似于这些与业务逻辑无关的代码。那么如何提取拆分呢?

这里提一个混合(mixin)概念,mixin是一种分发组件中可复用功能的灵活方式。通过mixinsVue.mixin()语法接受一个混合对象或混合对象数组。混合对象可以包含任意组件选项,另外同名钩子函数将混合为数组,混合对象的钩子将在组件钩子前调用。

结合Vue中的mixin语法,我们可以很容易做到将基础与业务无关代码拆分成不同mixin文件,通过Vue.mixin()注册为全局混合对象。在不同mixin文件编写相应的组件生命周期函数做预加载组件、设置页面title、发送PV等操作。

如何将雪碧图区分项目进行合并?

一言简述之,在我们整合项目过程中发现之前的构建脚本在处理雪碧图合并的过程中将多个项目图片合并成一张。这就会出现用户访问项目A过程中,会将整个工程的雪碧图资源下载,占用流量并造成资源浪费。那如何分项目合并雪碧图呢?

这里我们使用webpack-spritesmith模块做雪碧图合并。通过实例化webpack-spritesmith对象,传递自定义模板,分别构建出css、scss文件(具体可参考官方文档)。其实分项目构建不同雪碧图与webpack构建多个html页面做法类似。在webpackplugins配置数组中push多个webpack-spritesmith实例,不同实例分别构建不同项目下的雪碧图。这时不同项目业务组件便可分别引用相应的雪碧图样式文件。

总结

将多个相关需求的项目整合到单一工程的过程中,从前期详细设计到后面多个项目接入上线,一直在踩坑填坑。本文也是针对一些我们遇到的比较典型的问题拿出来分享。

在前端越来越追求工程化的今天,如何在工程化的基础上将项目做到层次清晰、代码简洁、耦合更低、性能更优则是我们要去思考的方向。

最后本文主要聚焦于基于Vue的多个相关项目整合统一工程实践解决方案。

仓促成文,如有错误,措辞不当,敬请斧正 :)