5

前言

自 2018 年 4 月福礼上线以来,通过快速迭代和扩展功能模块,整个项目快速发展。2019 年,京东福礼的月均活跃用户同比增长达到 265%,累计为超过 3500 家企业提供了数字化福利管理服务,有近 600 万人次通过京东福礼实现了个性化福利发放。回首过往,从只支持一种活动模式到现今支持六种活动模式,从只支持积分支付到支持混合支付、金额支付、单次支付,从只支持扫码展示 H5 到支持微信小程序内嵌、原生 App 内嵌...福礼正在以飞快的速度完善自己的功能体系,以期给客户更好的服务。伴随着项目功能的快速迭代和完善,是研发同学们在技术上的攻坚克难和不断突破的过程,也是整个团队(产品,后台,测试)一同成长,共同进步的过程。为此特整理此文,记录过往的经验和思考,也期望能给同路人带来启发和帮助。

自2019年至今的主要功能迭代进展图

什么是福礼

福礼是京东为优质企业客户打造的以年节福礼/季度劳保兑换为主的员工福利商城。该产品致力于提升员工福利感知度、降低福利发放和领取成本,为企业客户提供京东品牌保障的海量正品、搭配企业专属优惠价格和极速物流配送服务,进而帮助企业合理规划年度福利方案,完成一站式企业员工福利的管理及采购。

接下来就带大家看一下福礼的庐山真面目吧:

好了,广告宣传部分到此结束,接下来我们将从更好的福礼、开发效率优化、流程优化三个方面来聊聊我们在福礼项目的持续升级中的收获和思考!

前端架构

福礼项目自 18 年立项以来一直使用 Vue 技术栈,使用了团队自行开发的 Gaea 构建工具和 NutUI 组件库,此外引入了 Carefree,SMock,Vuex 等。

Gaea构建工具 ,是我们团队自主开发的一套 Vue 技术栈构建工具,基于 Node.js、Webpack 模版工程等的 Vue 技术栈的整套解决方案,包含了开发、调试、打包上线完整的工作流程。极大的提高了工作效率,目前团队所有 Vue 业务都使用该脚手架。

NutUI组件库 ,是一套京东风格的轻量级移动端 Vue 组件库,由我们团队历时数年打磨,目前有 50+ 京东移动端项目使用,github上得到1.9k+的 star ,176 Used By(不包含私有仓库)、236 fork,NPM 下载量超过 14.5 K。该 Vue 组件库提供大量的可复用的 Vue 基础组件,极大的便利了福礼项目的开发。

Carefree ,一套不依赖 wifi 热点的移动 web 真机测试一站式解决方案,是我们团队在日常开发中发现真机很依赖电脑发出热点才能进行调试的痛点,针对这一问题,旨在摆脱 wifi 热点束缚,让移动 web 真机测试自由自在而自主研发的一套解决方案。

SMock,由团队自主研发,针对项目前期尚无数据的问题,分析需要 mock 的文档,输出相应的 mock 数据,并启动 node 服务,供前端开发时调试使用,提高前端开发效率,支持跨域访问。

更好的福礼

站在巨人的肩膀--组件库助力开发

古语道:“君子性非异也,善假于物也“一套值得信赖的组件库,会使开发事倍功半。

福礼项目自立项开发以来,一直使用 NutUI 组件库。从 1.x 版本中部分的引用组件到 2.x 版本大范围使用,NutUI 活跃的社区互动和及时的响应速度,以及具有连续性的升级,逐渐获得了我们的信赖。

NutUI 组件库是一套京东风格的轻量级移动端 Vue 组件库。通过 JDRD 前端团队 2 年多的迭代升级,目前有 50+ 京东移动端项目使用,外部使用项目达 40+ 项目。GitHub 2k star 、194 Used By(不包含私有仓库)、254 fork,NPM 下载量超过 16.8 K。

除了 NutUI 本身的项目影响力强劲外,最吸引我使用的还有以下几点:

业务组件

除了普通的常用组件,NutUI 基于本身的基础组件,通过分析项目中具有很多共性的业务逻辑,抽象后再次封装,从而开发出了很多业务组件。它们可以省去使用者由基础组件组装并写重复业务代码的过程,真正的解决业务开发中时间紧,任务重的痛点。

就比如经常在商城中使用到的地址组件,一个组件包含了选择自定义地址,选择已有地址,自定义图标,自定义地址与已有地址切换等多种业务的需要。只要产品需要对应的功能,我们就可以快速引入组件并看到对应的效果,基于大量用户使用的业务组件,在功能上更加完善,防止了因业务逻辑没有思考全而带来返工的问题,也减少了开发者从基础组件封装的时间消耗。

地址组件在福礼中的使用

img

电商类组件覆盖率高

NutUI 是一套京东风格的轻量级移动端 Vue 组件库,所以其中收录的组件能更好的覆盖电商类移动应用的开发。
相较于其他经典开源组件库,福礼使用组件覆盖率对比如下:

功能 MintUI VantUI NutUI
上拉加载、下拉刷新 o × o
Dialog 对话框 o o o
Swiper 轮播图 o o o
Tab 选项卡 o o o
Toast 吐司 o o o
回到顶部 × × o
左滑删除 × o o
上传 × o o
Popup 弹出层 o o o
Stepper 步进器 × o o
图片懒加载 o × o
时间轴 × o o
搜索栏 o o o
商品价格 × × o
徽标 o × o

统一的京东设计风格,使整个组件库的组件样式统一,交互符合逻辑。开发者也可以通过主题定制的方式来满足业务多样化的视觉需求。想了解更多主题定制戳这里 主题定制

了解你的用户--数据采集

如何使自己的项目更贴近用户,首先就需要了解用户。作为与用户交互的最前线,为了助力客户作出更优的采购决策,推送给顾客更称心的商品,我们只能迎接挑战,升级自己的数据采集能力。为此,我们使用京东自主研发的数据采集服务--“子午线”来实现交互埋点等用户操作信息的收集功能。一番修改下来,颇有收获,分享于此。

1.动态引入 PV 埋点代码

原本的 PV 埋点代码是默认写死在 html 中的,但为了要在登陆之后,从接口中传入活动信息和员工信息,所以必须改造为登陆之后动态引入 PV 埋点代码。而又因为项目中有外接模块,从外接模块返回到应用的时候也许活动数据有变化,所以还需要清除之前的埋点代码,再动态引入新的埋点代码。
基于此我们的解决思路可以分以下两步:

1-1 调用

分别在根组件和首页中调用。
根组件调用:外接模块返回到本应用的时候,都会触发根组件的生命周期,这时可以重新获取活动本身的信息。
首页调用:在登陆前,后台不会返回对应的活动和用户数据,所以需要在首次登陆的之后进入首页再调用 PV 埋点函数。

this.$JDUnify.JDPV(data.data);

1-2 在动态插入的时候先动态删除之前的插入的内容

 <!-- 动态插入核心方法 -->
 content(html) {
     let cont = document.getElementById('cont');
     cont.innerHTML = html;
     let oldScript = cont.getElementsByTagName('script')[0];
     cont.removeChild(oldScript);
     let newScript = document.createElement('script');
     newScript.type = 'text/javascript';
     newScript.className = "xxx";
     newScript.innerHTML = oldScript.innerHTML;
     document.body.appendChild(newScript);
 };
 JDPV(obj) {
     ...
     let dongtaiTag = document.body.querySelectorAll('.xxx');
     <!-- 在创建的时候先删除这个节点 -->
     if (dongtaiTag.length > 0) {
         document.body.removeChild(document.body.querySelectorAll('.xxx')[0]);
     }
     let japHtml = `<script type="text/javascript">
         var jap = {
           siteId: "xxx",
           autoLogPv: true,
           anchorpvflag: true,
           extParams:{
             xxx:'${obj.xxx}'
           }
         };
         <\/script>`;
     this.content(japHtml);
 };

2.统一埋点方法

因为在点击触发埋点的时候有公共值和不同埋点要求的数据值的差别,并且公共值的内容与 PV 埋点值相同,所以我们构建了一个类来管理两个方法,并将公共数据放在 this.obj 上共享。

point(eventId, enventInfo) {
    <!-- 传入参数校验 -->
    if (JSON.stringify(this.obj) == "{}") {
        return
    }
    <!-- 特殊字段加密 -->
    const xxx = MD5(xxx + '');
    try {
        let click = new MPing.inputs.Click(eventId);
        <!-- 统一传入的值 -->
        click.xxx = this.obj.xxx;
        <!-- enventInfo 不同埋点要求的数据值 -->
        if (enventInfo) {
            <!-- 点击埋点方法传入的值 -->
            click = Object.assign(click, enventInfo); // 上报扩展字段,字段名称和内容均可自己设置
        }
        click.updateEventSeries();
        new MPing().send(click);
    } catch (e) { }
};

最终整体的数据采集代码结构如下:

class JDUnify {
    constructor() {
    // 用于 PV 和点击埋点传递数据
        this.obj = {}
    }
    //埋点方法
    point(eventId, enventInfo) {
    };
    //文档动态插入方法
    content(html) {
    };
    // pv 方法
    JDPV(obj) {
    };
}
export default {
    //挂载到 vue 原型链上,未来可以通过this.$JDUnify.point("xxx");使用
    install: function (vm) {
        vm.prototype.$JDUnify = new JDUnify()
    },
    // 项目中使用到的公用方法
    JDUnify: new JDUnify()
}

多端接入

作为一个 H5 项目,福礼本身具有很强的跨多端能力。但所谓没有限制的自由就不是真正的自由,没有内接规范的束缚,就很难有好的用户体验,甚至还会因为外接容器的不同而产生兼容性问题。基于上一个小节采集的用户数据,我们发现,当前的内嵌诉求主要集中在微信小程序内嵌和原生 App 内嵌两个方面。

对于微信小程序,我们使用了微信小程序原生的 webview 组件。其中从小程序打开内嵌项目页面使用的是 src 的方式:

从微信小程序到 H5

<web-view src="{{url}}" ></web-view>

所有需要传递给 H5 的参数都可以写在 url 的后面拼接过来,然后在 H5 中通过 url 参数获取的方式获得。

如果想从 H5 到小程序通信,我们采用的是 wx.miniProgram.xxx API ,如示例中的 reLaunch API。

从H5到微信小程序

wx.miniProgram.reLaunch({
  url: `/pages/xxx?url=` + encodeURIComponent(url)
})

这里要注意跳转函数间 API 所代表的不同含义,同时需要注意微信小程序中路由层级的限制问题。详情请参考官方文档 web-view,本文就不再赘述。

对于原生 App 内嵌 H5 ,因为不像微信小程序有统一的规范,所以我们制定了一套 JS API 规则,通过 postMessage 方法,实现 H5 与原生 App 的通信。整体思路如下:

  1. 原生 App 确认使用 JS API 规范,通过原生 App 设置 navigator.userAgent 为 fuli/andriod 或者 fuli/ios 来通知 H5 使用规范。
  2. 前端调用不同的原生 API 与 原生 App 通信,其中 andriod 使用 window.callApp.postMessage(jsonstr) ;ios 使用 window.webkit.messageHandlers.callApp.postMessage(jsonstr) ;

两方约定好的规则使用 jsonstr 描述。

  1. 原生 App 接受 json 匹配做对应逻辑。

H5 同原生 App 通信

class NativeApp extends App {
    executed(name, data) {
        let params = { name, data };
        let str = JSON.stringify(params); // 调用 app 参数输出
        const _window= window;
        const _userAgent = navigator.userAgent; //app userAgent 输出
        if (_userAgent.indexOf('fuli/android') !== -1) { // 调用 android
            try {
                _window.callApp.postMessage(str) 
            } 
            catch (error) {
                alert('android error :' + JSON.stringify(error) + 'post android str:' + str) }
        } else if (_userAgent.indexOf('fuli/ios') !== -1) { // 调用 ios
            try {
                _window.webkit.messageHandlers.callApp.postMessage(str);
            } catch (error) { 
                alert('ios error :' + JSON.stringify(error) + 'post ios str :' + str);
            }
        }
    }
    //设置标题
    setTitle(title) {
        this.executed(setTitle, { title })
    }
    //打开新窗口
    newWebView(data) {
        this.executed(newWebView, data)
    }
    //关闭新窗口
    closeWebView(){
        this.executed(closeWebView)
    }
    //打开登陆页
    openLogin(){
        this.executed(openLogin)
    }
    ...
}

开发效率

作为一个由多人开发以及涉及到多端测试的项目,如何提高开发效率和协作效率,一直是我们苦苦思索的方向。

绕不过的坎--真机测试

像许多 H5 项目一样,真机调试是所有开发和测试都绕不过的流程。在开发中我们的方式同大多数开发者一样使用的是手机连接电脑热点的方式。

手机连接本地热点

一般在本地使用 webpack-dev-server 启动项目,再在代理工具中配置上映射关系,测试手机连接电脑发出的热点,就可以在手机上轻松的跑起来本地的测试代码了。但这样的配置给测试也带来了两个问题:

  1. 电脑发出的热点有时候会不稳定,有时候还会跑掉。
  2. 连接不同的热点要配置不同电脑的 https 证书。

这两个问题,对于前端开发还好,但是对于要大量测试各个版本手机的测试们来说可就压力山大了。

重新梳理这两个问题,我们发现问题的核心就在于不同电脑的多个热点上。于是转化思路我们尝试在服务器上布置代理软件,让服务器代替发送热点的电脑。这样一来只需要手机配置访问指定的端口并安装这台服务器上的 https 认证证书,就可以在手机上轻松的访问前端发布在测试服务器上的代码了。

手机连接服务器热点

但好景不长,随着项目外接的 app 增多,以及微信小程序的外接,导致在本身手机代理配置没有问题的情况下,出现了手机就是代理不上的情况。通过浏览代理工具的文档发现了这段话在 android 6.0 之后的一些 app 在成功安装证书后仍然无法对 https 连接进行手抓包,有可能是该 app 没有添加信任用户自定义证书的权限 。改原生 app 的配置,臣妾做不到呀。

重整思路,再次出发,我们发现测试的目标不是为了抓包,而在特殊手机兼容性的测试上。换句话说,我们能不能不配置代理,来测试前端的代码。通过分析,我们发现之前配置代理是因为为了方便打包上线,即上线的静态资源访问路径不变,通过代理工具,将静态资源映射到指定 IP 服务器的模式,如下所示:

192.168.XXX.XX(测试服务器端口) static.360buyimg.com(线上机群域名)

所以只要将静态页面上的静态资源链接以及打包的链接改为对应的IP的域名,就可以不用配置代理了。

为了更好的管理和区分,最终我们选择基于京东商城对象存储来实现特殊手机兼容性测试的工作。想要了解更多内容,请参考 京东商城对象存储文档

这样,对于特殊的手机版本以及内嵌的特殊 App ,我们都可以通过发布到京东商城对象存储平台上来实现兼容性的测试。

薛定谔的猫--恼人的缓存

在开发中经常会遇到这样的情况,自己修改完测试提出的问题,满足的发布到测试服务器,结果测试反馈没有看到效果,并向你甩出了截屏。这样的冲突场景,大多数是因为测试没有访问到新发布的静态资源导致的。那怎么会没有访问到新发布的静态资源呢?这就要我们先看看浏览器获取资源数据的顺序:

  1. 先在内存( from memory cache )中查找,如果有,直接加载;
  2. 如果内存中不存在,则在硬盘( from disk cache )中查找,如果有直接加载;
  3. 如果硬盘中也没有,那么就进行网络请求;
  4. 进行网络请求时,强缓存是优先于协商缓存的,是先进行强缓存( expires 和 cache-control),如果生效则直接使用缓存数据,否则进行协商缓存( Etag/if-none-match ),由服务器来决定是否使用缓存数据;
  5. 请求获取的资源缓存到硬盘和内存。

资源获取流程图

所以,虽然提交了改正后的代码到测试服务器,但因为测试访问频繁,测试看到的是浏览器缓存下来的静态资源。对于真实版本上线,简略的说,前端会发布不同的版本号,同时后端也会发布与之对应的同样版本号的页面。从而在用户访问资源的时候会直接访问到新版本号的资源,不会出现缓存旧版本资源的情况。但由于在测试阶段,发布到测试环境的频次比较高,如果每次发布都改版本号的话需要浪费大量打包的时间,所以最好的方法是测试在发现有缓存时自行清除下终端的缓存。

为了不让测试在一波回测操作之后才发现没有访问到最新代码,我们会在测试环境下打包出时间戳,并在第一次访问项目的时候提示出来测试的使用版本,测试可以依据提示内容来判断是不是访问到了提测的版本。

提示测试版本

提示测试版本

要实现这个提示,主要分以下几步:

1.在打包的时候,配置时间变量,并命名为 buildTime

new webpack.DefinePlugin({
 'process.env': {
   buildTime: JSON.stringify(new Date().toString())
 }
}),

2.单独抽离环境文件 env.js ,用于在不同 webpack 环境下,调用不同的展示

let config = {
buildTime: process.env.buildTime,
isPrd: true // 是否为线上
}
switch (process.env.NODE_ENV) {
case 'development':
    config.isPrd = true;
    break;
case 'upload':
    config.isPrd = false;
    break;
case 'production':
    config.isPrd = true;
    break;
}
export default config;

3.在 App.vue 中弹出提示打开的前端版本

import config from "./config/env";
if (!config.isPrd) {
  this.$toast.text(
      "当前版本:预发 > 版本发布时间 " + config.buildTime,
      {
          duration: 3000
      }
  );
}

高效后悔药--Git 规范化

作为多人快速开发的项目,进行代码提交审核和管理是非常重要的。为了更有效的回溯代码,方便查找定位,以及沟通,我们共同制定了如下的提交规范。

所有的提交建议使用 标识:内容 的形式,让每次提交都有价值

标识 说明
feat 新功能(feature)
fix 修补bug
docs 文档(documentation)
style 格式(不影响代码运行的变动)
refactor 重构(即不是新增功能,也不是修改bug的代码变动)
test 增加测试
chore 构建过程或辅助工具的变动

在践行规范化的过程中,我们发现了一个关于提交 commit 的小窍门。

你可能遇到这样的纠结场景:当在一个分支上正在工作,突然被打断需要紧急修复一个线上的问题,bug 可能在当前分支,也或许在另一个分支,但是手上的代码还没有到能够提交的地步,但又不想将已经写的内容作废,这时你可以使用 git stash xxx 命令。它表示将当前分支上的未提交的代码保存起来作为一个暂存区并命名为 xxx,这个暂存区是可以作用于所有分支,是一块单独的区域,后续可以通过 git apply stash xxx 将暂存的内容恢复到任意分支。

整齐划一--自动格式化代码

作为团队协作开发的项目,通常会出现以下的尴尬的场景:

  1. 在修改了部分代码,并保存后,因为格式化工具的不同,导致整个代码全部格式被修改,很难突出的看到本次提交的内容。
  2. 因为格式不统一导致 git 冲突问题。
  3. 因为代码不规范导致可能的兼容性问题。

为了避免以上情况,我们使用了适合 vue 项目的整套代码规范工具链:vscode+vetur+prettier+eslint

img

整体的安装流程和配置流程如上图所示,其中在配置 vuter 的时候要特别注意,为了让 vuter 在格式化的时候,参考 eslint 的规则,而不是 prettier 的规则。你需要如下配置:

1.在 settings.json 中将 vetur 中 js 的 formatter 设置为 prettier-eslint

{
   ...
   "vetur.format.defaultFormatter.js": "prettier-eslint"
   ...
 }

2.手动安装 prettier-eslint 包

npm i -D prettier-eslint

对于一开始没有使用代码格式化开发的项目,推荐可以在项目的 package.json 中配置全局的 scripts 命令将整个项目的代码按照规范整体格式化。

//"prettier:fix" 是一键格式化项目中src目录下所有js、vue、scss文件;
"prettier:fix": "prettier --write src/**/*.{js,vue,scss}"

更多的命令请参考 prettier-eslint-cli

协作优化

作为一个完整的商城类项目,涉及到方方面面同事的通力合作。如何实现协作效率最大化的同时还能站在全局的角度思考问题?我们主要从这两步进行探索。

img

流程化--开发流程

也许你也遇到过通宵达旦的上线,眼神迷离的看见第二天前来上班的同事,周末早上突然被电话惊醒,支持紧急需求的情况,刚刚下班,却被通知有需求要立刻评审……

作为公共资源部门,大部分时间都会有两个以上的需求,甚至还可能是不同技术栈的不同业务线的需求。所以定义一个以时间线为轴的开发流程,是我们和其他部门负责合作的基础,也是订立协作的基础。

前端开发流程图

体系化的看问题--复盘会

复盘会

每当一个大的需求上线,项目经理都会组织复盘会,按照项目开发的整个流程,从各个协作方的角度复盘这次需求完成的收获和不足,并形成了前端与产研协作机制规范的文档。

就以排期的规范化为例,所有的紧急需求、新插入的需求、线上出现的问题都要再经过产品的梳理、测试排查之后视需求情况而定分为以下情况:

  1. 紧急需求安排较为空闲研发支持;
  2. 不是特别紧急的需求,优化安排下次排期;
  3. 对于线上问题,如果能够很快修复,一般跟随下一个上线需求一起上线;如果不能快速修复的,重新走排期。

同时,协作多方一同解决项目开发中涉及多方的痛点问题:

  1. 真机测试困难;
  2. 内嵌原生 App 的规范;
  3. 测试环境的跨域问题。

经过多次的项目复盘,整体协作的效率以及彼此的互信都有了极大的提升。

结语

回首过往,福礼在两年多的时间里披襟斩棘,快速发展,实现了 30 + 以上的功能模块添加,并实现了月均活跃用户同比增长达到 265 % 的骄人增长。但正如标题所言,持续升级是永无止境的,未来福礼的前端团队成员依然会在保证完成纷至沓来的迭代需求前提下,从优化用户体验、提升开发效率、推进流程优化等方面入手,持续升级福礼项目。
"长风破浪会有时,直挂云帆济沧海"。让我们与福礼一同成长,期待一个更好的福礼出现在未来,也祝愿大客户业务组的业绩蒸蒸日上,加油!!

PS:如何大家对文章中的某个点感兴趣,欢迎咚咚打扰 ~~


京东设计中心JDC
696 声望1k 粉丝

致力为京东零售消费者提供完美的购物体验。以京东零售体验设计为核心,为京东集团各业务条线提供设计支持, 包括线上基础产品体验设计、营销活动体验设计、品牌创意设计、新媒体传播设计、内外部系统产品设计、企...