Hybird App多个轻应用工程开发实践

前言

为期两个半月的分销App一期开发任务在已进入了SIT测试阶段, 本文主要记录本次项目最开始的设计思路和一些项目中遇到问题的解决方法, 是对一期阶段性总结.

项目介绍

分销App主要业务围绕着品牌商-服务商-零售商这条线展开的, 基于我司移动端底座的 Hybrid App 技术架构, Web 端是 vue 全家桶.

相较于之前的开发的轻应用有所不同的是, 在分销App中几乎所有的业务都是由 Web 开发完成, 包括了商城, 积分, 物流, 数据罗盘, 资讯等十几个轻应用组成. 客户端只提供登录, 底部tab 和工作台.

App原型

应用模块化工程

我们本身有一个轻应用的脚手架模板工程, 提供了一些基础的功能, 已经应用于美信中多个H5轻应用. 然而对于分销App客户项目来说, 如果延续旧模式进行开发的话, 会面临着:

  • 十几个应用需要建十几个项目工程, 对应十几个 git repo, 代码管理复杂度.
  • 项目自身业务组件共享方式.
  • 基础功能根据项目定制化, 同步到各个应用的问题.

基于上述的一些问题, 对原有的脚手架工程进行了一些改造, 主体思路:

  • 延续原有的 app 初始化功能
  • 不破坏性地修改原有的项目目录结构
  • 模块应用间的业务不耦合
  • 编写新的构建方式来完成当前需求

整体的工程结构如下

分销宝架构

在原有的项目工程下(也就是主工程), 增加了 modules 用于管理分销App的各个轻应用, 演变为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
|- libraries
|- modules
||-- config
||-- libraries
||-- pages
|||--- app
||-- services
||-- index.js
|- pages
||-- app.vue
||-- error
|- services
|- main.js
|- Module.js

主工程下:

  • libraries: 通用的基础库
  • modules: 模块应用代码
  • pages: 相关根组件和异常页面组件
  • services: 通用的基础服务
  • main.js: 主入口
  • Module.js: 当前使用的模块应用

modules 下:

  • config: 模块应用配置
  • libraries: 模块应用基础库
  • pages: 模块应用视图组件
  • services: 模块应用服务
  • index.js: 模块应用入口文件

模块应用与主工程

依赖关系

模块与主工程依赖关系

对于SPA来说, 往往只有一个入口 main.js, 那么如何在主工程中构建某一个模块应用? 在这里通过动态生成 module.js 作为中介, 来暴露当前构建的模块应用.

实现

假如有一个模块应用叫 example, 那么在 modules 下, 有:

1
2
3
4
5
6
7
8
9
10
11
|-modules
||--example
|||---services
||||----pages
|||||-----app
||||----http(接口配置)
||||----i18n(国际化配置)
||||----native(底座配置)
||||----router(路由配置)
||||----store(vuex配置)
|||---index.js
  1. index.js 中, 暴露一个方法 bootstrap 用于 Vue 实例化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import Vue from 'vue'
import App from './pages/app'
import router from './service/router'
import store from './service/vuex'
import i18n from './service/i18n'
import http from './service/http'
import native from './service/native'

function bootstrap () {
/* eslint-disable no-new */
new Vue({
router,
store,
i18n,
render: h => h(App)
}).$mount('#app')
}

export default {
bootstrap,
router,
store,
i18n,
http,
native
}
  1. module.js 中, 引用当前构建的模块应用 example
1
2
import Module from '@/modules/example'
export default Module
  1. main.js 中, 初始化 Vue 实例
1
2
3
4
5
import Module from './module

// Some code

Module.bootstrap()

动态生成 module.js

上面说到 module.js 是动态生成的, 用于让主工程感知当前构建的是哪个模块应用, 这里通过一个应用配置项和脚本搭建构建的环境.

动态生成module流程

实现

  1. 在项目根目录中有一个 build-config.json, 用于配置所有模块应用的信息.

build-config.json

1
2
3
4
5
6
7
8
{
"modules": [
{
"name": "example",
"title": "示例模版"
}
]
}
  1. 搭建模块应用的构建环境: 通过 npm script, 执行一个 bash 脚本, 这个 bash 首先会读取 build-config.json, 把所有的模块应用信息打印在 Terminal 中, 用户选择一个要构建的模块应用, 然后执行一个任务生成 module.js, 最后就是 webpack build 的执行.
  • package.json

    1
    2
    3
    4
    5
    6
    7
    {
    "scripts": {
    "build-module": "gulp build-module",
    "build:dev": "bash ./mx-libs/mx-build/build.sh build app-dev",
    "build:prod": "bash ./mx-libs/mx-build/build.sh build app-prod"
    }
    }
  • build-module.js: 生成 module.js 的脚本

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function buildModule () {
    const moduleNmae = process.env.VUE_APP_MODULE
    if (moduleNmae) {
    let filePath = resolve('src/module.js')
    let fileContent = `import Module from '@/modules/${moduleNmae}'\nexport default Module\n`
    writeCubeModule(filePath, fileContent)
    } else {
    console.log('process.env.VUE_APP_MODULE', '没有找到')
    }
    }
  • build.sh

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    #!/bin/bash

    # 运行命令
    CMD="$1"
    # 运行环境
    ENV="$2"
    # 运行模块
    MOD=""
    # 打包模块
    MDOULE_CMD="npm run build-module"
    # 启动服务
    SERVE_CMD="npm run serve"
    # 打包项目
    BUILD_CMD="npm run build"

    #---------------------------- 选择模块 --------------------

    # 模块选项
    MODULES=`grep 'name' build-config.json | awk -F':' '{print $NF}'`

    echo "请选择模块序号 :"

    select module in $MODULES
    do
    MOD=$module

    if [[ "$MOD"x = ""x ]];
    then
    echo "选择模块不存在"
    exit
    fi

    break
    done

    #---------------------------- 执行命令 --------------------

    echo "CMD: ${CMD}"
    echo "ENV: ${ENV}"
    echo "MOD: ${MOD}"

    if [[ "$CMD"x = "serve"x ]];
    then
    cross-env VUE_APP_MODULE=${MOD} VUE_APP_MX_ENV=${ENV} $MDOULE_CMD &&
    cross-env VUE_APP_MODULE=${MOD} VUE_APP_MX_ENV=${ENV} $SERVE_CMD
    fi

    if [[ "$CMD"x = "build"x ]];
    then
    cross-env VUE_APP_MODULE=${MOD} VUE_APP_MX_ENV=${ENV} $MDOULE_CMD &&
    cross-env VUE_APP_MODULE=${MOD} VUE_APP_MX_ENV=${ENV} $BUILD_CMD
    fi

组件优化

Header 组件优化

在基础组件库中有同事之前写的页面头部组件 c-header, 对于 header 右侧只提供了菜单栏的选择, 然而在分销App中有一些场景右侧需要渲染搜索按钮或提交按钮等等, 原有的 c-header 组件无法满足需求, 因此对该组件进行优化, 步骤为:

  1. 抽取原有的右侧下拉菜单为独立的组件 c-dropdown
  2. 保留原有的 c-header 接口设计, 增加一个渲染函数 rightRender 的参数.

header优化

代码实现:

c-header

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
export default {
props: {
rightRender: {
type: Function
}
},
render (h) {
return (
<header>
// some code
/* 右侧容器 */
<div class="c-header-right">
{ this.rightRender.call(this) }
</div>
/* 右侧容器 */
</header>
)
}
}

调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
<div>
<c-header right-render="rightRender"></c-header>
</div>
</template>

<script>
export default {
methods: {
rightRender () {
return (
<span>提交</span>
)
}
}
}
</script>

图片和视频选择组件优化

图片选择和视频选择需要调用底座cordova, 对于页面交互逻辑大致是一样的, 图片提供拍照和相册获取, 视频提供录制和相册获取. 在基础库中只提供了图片相关的组件, 视频需要重新开发.

原有的图片选择组件通过 actionSheet 进行选择不同来源的图片, 为了达到更多的场景的复用效果, 例如点击头像进行拍照上传等等, 主要的思路有:

  1. 拆分 actionSheet UI交互逻辑和底座cordova逻辑
  2. 对于上层组件, 通过引用UI组件和 mixin 底座接口进行一层封装
  3. 对于底座逻辑公共接口逻辑复用

优化后这个模块的目录结构为:

1
2
3
4
5
6
7
8
|- native-camera
||-- service
|||--- base.js(基类)
|||--- picture.js(图片与底座逻辑)
|||--- video.js(视频与底座逻辑)
||-- camera.js(camera插件配置常量值)
||-- index.js(模块暴露文件)
||-- index.vue(UI逻辑)

base.js 为公共的接口配置和一些逻辑复用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
export default {
props: {
list: {
type: Array,
default: () => {
return []
}
},
max: {
type: Number,
default: 9
},
...
},
computed: {
btnVisible () {
// some code
}
}
}
}

对于 picture.jsvideo.js, 通过 extends 引入 base

1
2
3
4
5
import Base from './base'

export default {
extends: Base
}

对于上层组件, 例如添加图片组件 c-picture, 需要引入UI组件和逻辑组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<template>
<div>
// some code
<native-camera :options="menus"></native-camera>
</div>
</template>

<script>
import { NativeCamera, PirtureService } from 'native-camera'

export default {
mixins: [PirtureService],
components: {
NativeCamera
},
data () {
return {
// actionSheet值
menus: []
}
}
}
</script>

商品详情优化

接口请求封装了统一的loading, 然而在商品详情中, 接口返回在涉及到查询复杂的情况下时间很长, 导致全局的loading体验不好, 对于这种情况, 和后端同事讨论之后, 把接口进行拆分为基础信息一块, 规格和价钱和活动等一块, 详情信息一块.

通过配置关闭默认的loading, 通过引入三段页面骨架(skeleton)组件的方式, 优化了整体的交互体验.

优化前

优化前.gif

优化后

优化后.gif

开发规范

  • 强制开启 eslint
  • 组件风格参考官方风格
  • git commit 参考 commitizen, 规范 scope 为哪个应用模块, 例如:
    feat(积分商品): 完成积分商品商品列表开发

一些其它的想法

目前产品研发和项目实施而言, 开发流程往往为标准化产品开分支给项目实施进行定制化, 或者直接 clone 代码之后另起 git repo, 对于客户来说, 一般会出现需要A应用定制化, B应用跟着产品迭代更新, 这中间的过程都是通过人为去进行管理的.

我和后端的同事讨论过, 他们每个人负责着几个服务, 通过 devops 选择需要的功能模块服务进行构建. 由此我也有个想法, 能否通过本次项目这种方式, 将主工程的一些逻辑插件化, 每个应用模块独立, 通过 git submodule 或者 npm private 的方式, 用 cli 封装这一部分的操作, 对于项目定制化, 锁死某个 tag 版本.

这个想法已经跟团队的同事分享了, 后面还需要进一步讨论和demo的实践之后摸索出更多的可行性.

最后

本文主要是对分销app的一次阶段性总结, 有项目功能的思路, 一些常用的组件优化方法和规范以及对未来进一步实践的想法, 总体来说收获还是比较大的, 无论是在任务的分配, 代码编写, 还是带团队开发方面都是宝贵的经历.