专栏名称: 京东设计中心JDC
UX
目录
相关文章推荐
商业洞察  ·  哪怕雷军出手,绿牌恐怕还是换不掉 ·  3 天前  
21世纪商业评论  ·  深圳青年卖珍珠,年入20亿 ·  2 天前  
哈佛商业评论  ·  挖掘中国市场价值,跨国企业务必学会3种策略 ·  3 天前  
51好读  ›  专栏  ›  京东设计中心JDC

运用 NutUI - 快捷开发京东企业业务之酷兜

京东设计中心JDC  · 掘金  ·  · 2020-04-28 03:11

正文

阅读 462

运用 NutUI - 快捷开发京东企业业务之酷兜

本文由JDC前端开发部杨小璐撰写

窗外日光弹指过,席间花影坐前移。2019 年以飞一般的速度逝去,2020 年在新冠病毒的扰乱下不期而遇。病毒是无情的,它限制了人们的脚步,足不出户,但是这并不能阻止我们思想前进的脚步。2019 年对于负责酷兜项目的前端童鞋来说是异常艰难的一年,需要同时维护 H5 外接版、内嵌微信小程序两套代码。幸运的是我们有相当靠谱的团队,凭借整个团队(产品、后台、测试)的紧密协作,一路披荆斩棘,完成了 10+ 个版本的迭代和几十个日常优化。

2019 年酷兜项目经历了多达 13 次的大小版本迭代。为了在 2020 年不断提升酷兜营销能力,完善产品体验,达到市场预期,新版酷兜被寄托着很大期望。

什么是酷兜

酷兜是京东为优质大型企业客户专门打造的“ 0 预算”员工内购福利平台。酷兜内购商城通过整合包括热销商品、品牌折扣、优选精品、生活服务等优质资源,以内部惠购的方式为企业客户的员工提供福礼特权“酷”体验。 到底“酷”在哪呢?

  • 每日更新上万款基于京东商城精选的优质自营商品
  • 覆盖 7 大消费场景、时尚穿搭、食品酒水、个人美妆、母婴童装等 29 个京东主营类目
  • 支持基于锦礼平台小程序或外部系统对接环境下使用,外接版可打通联合登录账号
  • 完备的物流与售后服务,拥有全球领先的中小件、大件、冷藏冷冻仓配一体化物流设施
  • 支持微信支付,匹配主流在线支付通路

说了这么多,有没有好奇酷兜的庐山真面目呢,请看!

好了,广告宣传部分到此结束,接下来我们将从前端架构、重构性能优化、技术拓展三方面阐述此次重构优化!

前端架构

重构的起因

酷兜在 2020 年要搭建完备的内购价格体系,整合今日上新、折扣榜等营销模块,优化用户体验等一系列产品目标。

但现实情况是,前端框架是 2018 年 10 月创建,使用的技术比较老旧。在这一年多的时间里,需求迭代、功能优化、bug 修改、开发人员的更换造成代码冗余严重,逻辑不清晰、开发联调困难。看似简单的需求修改,往往内含玄机,牵一发而动全身。前端技术日新月异,底层框架版本过低,不单单增加了开发人员的兼容性处理,而且无法实现流畅的交互效果,用户体验不佳。

在几次的版本迭代中,产品都会提出疑问,一个小小的需求,为什么需要这么长的开发时间。说实话,有些需求、bug 在我们看来也是分分钟搞定的事情,可以在酷兜中,就说不准了,一处的改动可能会影响到多处的状态,一个 bug 的解决是以产生多个 bug 为代价的。这简直成为了 bugfest (臭虫集会)。

面对这一系列的问题,也为了更好的满足 2020 年酷兜快速迭代更新的需求以及性能要求,提升用户体验。酷兜团队成员一致认同,前端重构走起!

作为负责酷兜的前端开发人员,允许我在这里小小的 happy 一下,终于可以我的代码我做主了。

前端架构优化

随着技术的发展,前端承担的业务越来越多,项目也越来越变得像大型工程了,而且越来越复杂了,需要处理好组员之间的协作,也需要做好业务分块、去耦合来降低维护成本,并且还要保持高效率开发。工程目录结构的优化是能达到这个目的的一种方式。一般而言,不论多页面工程还是单页面应用,或二者都有,目录结构大致都是以下三种方式:

  • 类型分组(文件类型/业务类型等进行分组)
  • 模块分块(页面模块/业务模块等进行分块)
  • 类型分组与模块分块的结合

此次重构,采用的是 Vue2 + NutUI + TypeScript + Gaea 技术栈,在目录结构优化方式上,选择第二种:模块分块,先来看一下整体目录结构。

├── .bin                                # Webpack 配置文件
├── build                               # 打包文件
├── node_modules                        # 依赖的模块包(NutUI、postcss-plugin-px2rem)
├── package.json                        # 项目基本信息
├── src                                 # 项目的核心组件
│   ├── asset                           # 资源文件(css、image)
│   ├── component                       # 公共组件
│   ├── config                          # 环境配置文件(evn.ts)
│   ├── icons                           # 存放 svg 格式图标
│   ├── services                        # HTTP 请求配置(HttpClient.ts、GoodsApiService.ts)
│   ├── store                           # 状态管理(vuex)
│   ├── view                            # 根据业务场景开发的组件
│   ├── util                            # 公共方法(util.ts、imgSet.ts、appHelper.ts)
│   ├── app.vue                         # 根组件
│   ├── app.ts                          # 入口文件
│   ├── router.ts                       # 页面路由
│   ├── index.html                      # 主页模板
│   ├── vue-shim-extend.d.ts            # 扩展 Vue 全局类型声明
│   └── vue-shim.d.ts                   # TypeScript 支持 *.vue 文件配置
├── static                              # 静态资源(ico图标、vendor.dll.js)
├── README.md                           # 项目描述信息(一些方法使用的注意事项)
└── tsconfig.json                       # TypeScript 编译设置

复制代码

有没有感觉这样的工程目录结构很清晰(小小的自恋一下)。其实,工程目录结构的设计并没有完成采用模块分块的方式,在 asset 资源文件中,按照文件类型分成了 css、image 文件。虽然对整体目录结构做了调整,但是每个工作空间下的目录结构开发人员可以自由发挥,我们也约定了创建文件的几点规范

  • 在开发业务场景组件中,按照路由划分,该文件与文件夹名称保持一致
  • 在状态管理、请求配置中,按照业务模块划分
  • 就近原则,多处使用的组件放到 component 文件夹中,此路由紧耦合的子组件,放到本文件夹中
  • /src 外的文件不应该被引入

这样的目录结构有什么优势呢?

  • 工程目录中的文件夹都有明确的功能,成员之间能很简单快捷的知道某个页面或某个功能块有哪些文件
  • 成员之间按照分配的模块开发,避免了代码上的冲突、合并
  • 可以根据路由名称,快速定位到文件

好了,有了这样的目录结构,再也不用担心开发时找不到文件了。

前端性能优化

此次重构,前端大大小小的优化点我们大概总结了 14 条:

  • 借鉴 NutUI 组件库中 Icon 图标的开发方式,使用 SVG 图标
  • 使用 postcss-plugin-px2rem 插件完成 px 与 rem 的转换,实现移动端的自适应( dpr 计算)
  • 采用按需加载的方式,提升加载速度
  • 商品图片展示使用 NutUI 组件的 Lazyload (图片懒加载),减轻服务器的压力
  • 采用 RSA 双向加密方式,保证了加密属性的安全性
  • ...

看到这几点有没有调起大家的胃口,来张大图满足一下大家!

NutUI 2.0 组件库

一个聪明的前端工程师,为了能够快速完成版本迭代,除了提高自己的开发效率外,还要懂得会运用工具。选择一款合适的组件库,能大大提升前端的开发效率。

此次重构,我们继续选用 NutUI 组件库,并将 1.x 版本升级到 2.x 。放弃将 NutUI 组件库放到本地的方法,直接使用 NPM 上的最新包,方便实时更新组件。

NutUI 组件库是一套京东风格的轻量级移动端 Vue 组件库。通过 JDRD 前端团队 2 年多的迭代升级,目前有 50+ 京东移动端项目使用,外部使用项目达 30+ 项目。 GitHub 上得到 1.8k 的 star,NPM 下载量超过 12k。

NutUI 2.x 采用全新架构

与 1.x 版本相比,NutUI 2.x 紧跟时代潮流,基于全新的架构开发:

  • 基于 Webpack4.0 开发,拥有更快的构建速度,输出更小的 bundle 文件
  • 一次性构建出多种类型的 bundle,兼容各种主流模块化场景和非模块化引用场景
  • 基于 Babel7 实现了 Polyfill 的智能加载,无须额外引入 Babel-polyfill 文件也可兼容低版本浏览器
  • 集成 Carefree 方案,大幅提升开发环境的真机调试效率
  • 示例页面 PWA 加持,支持离线缓存和创建主屏图标
  • 接入持续化集成和自动化测试,提升代码可靠性
  • 支持自动生成新组件模板
  • ...

高效率

现在开源的 UI 组件库琳琅满目,到底选哪个更合适呢?我们先来看一下 MintUI、Vant 、NutUI 版本最后版本迭代时间:

团队 GitHub 最后更新时间 简介
MintUI 饿了么团队开发、维护 2018 年 1 月 16 号 --
Vant 有赞团队开发,维护 2020 年 4 月5 号 Vant 是一个轻量、可靠的移动端 Vue 组件库
NutUI 京东团队开发、维护 2020 年 4 月 3 号 一套京东风格的轻量级移动端 Vue 组件库

注:GitHub 最后更新时间是在 2020 年 4 月 3 号查看的。

从最后版本迭代时间来说,NutUI 更近一些。开发组件时,研发人员都是尽可能让组件适用于任何的业务场景,较近的迭代时间可以说明该组件在日常维护中,不用担心在开发过程中,bug 的反馈无人理会,影响开发进度。

在重构过程中,购物车商品左滑删除功能,商品列表使用的是 scroller 组件,左滑删除使用的是 leftslip 组件

在苹果手机上使用的非常流畅,但是在安卓手机上,左滑商品无法出现删除按钮。原因是 NutUI 库中的 scroller 组件与 leftslip 组件有兼容问题。我们将问题及时反馈后,组件的开发人员快速响应,及时作出修改。几天之后 NutUI 也发布了新的版本,丝毫没有影响项目的开发进度。

高复用率

选择一个适合的组件库还有更重要的一点就是组件库的使用率,如果只是为了组件库中的一个组件,而引入整个库,就有点儿太耗性能了。在重构开始前,我们梳理了可能用到的组件。

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

支持 TypeScript

最后选择 NutUI 组件库最重要的一点是,NutUI 2.x 开始支持 TypeScript ,能有效减少重构时间。

🔔 想要了解更多关于 NutUI 2.0 的特性,可以戳这里哟! nutui.jd.com

Gaea 脚手架自动化升级

Gaea 脚手架,是我们团队自主开发的一套 Vue 技术栈构建工具,基于 Node.js、Webpack 模版工程等的 Vue 技术栈的整套解决方案,包含了开发、调试、打包上线完整的工作流程。极大的提高了工作效率。是不是觉得这些话有些空,到底做了哪些升级呢?那就来点干货吧!

  • 新增 HappyPack。HappyPack 与 thread-loader 结合,实现多线程编译,加快编译速度,但是需要注意 thread-loader 不可以和 mini-css-extract-plugin 结合使用
  • 新增 progress-bar-webpack-plugin 编译进度条。
  • 新增 cache-loader。在开发环境编译时,使用模块编译缓存,加快编译速度
  • 新增 webpack-bundle-analyzer。能让开发者清晰的看到项目各模块的大小
  • 新增 webpack-build-notifier 。webpack 构建完成,能够像咚咚那样,弹出构建结果
  • 去掉 uglifyjs-webpack-plugin 。Webpack 版本由 3.x 升级到了 4.x , JS 压缩,webpack4 中内置,不需要单独引入
  • ...

更多的特性、优化升级,我们在这里就不赘述了。为了让 Gaea 能更有效的提升开发效率,我们对 Gaea 进行了小小的改动。

开发、测试、上线往往需要不同的环境,我们不能使用线上环境进行修改、操作的。现阶段,在开发、测试阶段,前端通过判断请求地址上是否有 debug 参数,来进行环境的切换。看似小小的举动,费不了多大的事,但是这对于开发、测试来说,已经相当繁琐了。不光 “京东锦礼” 小程序为了配合预发环境,在请求路径上添加 debug 参数,就连前端开发时要时刻注意 debug 参数,后台研发在生成免密登录串时,也需要注意 debug 的存在。真真的牵一发而动全身。就在我们面对这个问题,没有更好的解决办法时, Gaea 脚手架的更新为我们提供了思路。

新版本的 Gaea ,Webpack 版本有 3.x 升级到了 4.x ,修改了 Webpack 配置文件,采用多命令 dev、build、upload 自动化配置请求 API。那我们为何不根据执行自动化配置的命令,来决定使用环境呢?有了新的思路,那就马上行动起来吧!

// config / evn.ts
switch (process.env.NODE_ENV) {
  case 'development':
  case 'upload':
    config.baseUrl = 'https://xxxx-fy.jd.com' // 开发环境 => npm run dev 或 npm run upload
    break
  case 'production':
    config.baseUrl = 'https://xxxx.jd.com' // 线上环境 => npm run build
    break
}
复制代码

通过上面的配置,在结合 API Service 整合,统一配置 HTTP 请求环境,完美的解决了这一问题,开发、测试再也不用担心 debug 参数困扰,或者是搞错线上环境。

封装 AppHelper Hybrid 多端交互类,定制 JS API 接入文档

酷兜当前项目已经支持跨平台多端(微信小程序、第三方 APP 内嵌、H5),作为一个可被外接的 H5 ,原生APP API 是必然少不了的,为了良好的用户体验 ,我们需要第三方接入者来严格按照我们的 API 标准开发。 首先我们先思考一下,为什么要有 API ,举一个常见的场景:打开一个新页面,不同端是如何处理

  • 微信小程序 内部开发可控
wx.miniProgram.navigateTo({
    url: `/pages/xxx?url=` + encodeURIComponent(url)
})
复制代码
  • H5 内部开发可控
window.open(url)
复制代码
  • 原生 APP ios 或者 android | 第三方 APP 内嵌 开发不可控

这个地方就要详细讲解一下,首先我们的 H5 页面是要被第三方 APP 进行内嵌 WebView 打开, 首先作为前端的我们不知道每个第三方 APP 具体语法调用,那么就需要定制一套 JS API 来约定好调用规则,由前端发起,主动调用 JS 发送至 原生 APP 端,大致思路就是,

  1. 先确认当前 webview 是否支持 kudou API , 通过查看 navigator.userAgent 来确认,navigator.userAgent 的值原生 APP 可以进行自定义设置
  2. 区分不同端 android 、ios 分别调用 callApp.postMessage webkit.messageHandlers.callApp.postMessage
  3. postMessage 统一发送约定值 Json 字符串 { name : ' 唯一key,对应不同功能 ' , data : ' 任意参数,根据 key 自定义调整' }
  4. 客户端各自收到 json 进行 name 键值匹配,做对应逻辑处理

知道了基本调用,那么我们在想一想,多平台肯定有对应不同的调用方式,那么此处可以简单运用一个工厂模式来处理此逻辑,废话不多说上代码,大家细品一下

  1. 创建抽象 APP 类,定制具体功能方法
  2. 创建实现类(小程序、原生 APP、H5 )
  3. 创建代工厂类(对外暴露具体方法),初始化时,根据当前场景实例化对应类

// 枚举值功能key值
enum KudouAppSdkType {
    NewWebView = "newWebView", /** 打开新webview页面 */   
    CloseWebView = "closeWebView", /** 关闭当前webview */   
    OpenLogin = "openLogin", /** 调起登录页 */
    SetTitle = "setTitle" /** 设置webview标题 */
}
class NewWebViewParams {
    constructor(url: string = "", title: string = "") {
        this.url = url;
    }
    url: string = "";
    title: string = "";
}
/** 抽象类 APP 提供具体功能 API */
abstract class App {
    abstract newWebView(data: NewWebViewParams): void; /** 打开新页面 */   
    abstract closeWebView(): void; /** 关闭当前 webview 页面 */   
    abstract openLogin(): void; /** 打开登录页 */   
    abstract setTitle(title: string): void; /** 设置 webview 标题 */
}
/** 方法实现类-小程序 */
class Miniprogram extends App {
    setTitle(title: string): void { }// 微信小程序 自动读取当前 document title
    newWebView(data: NewWebViewParams): void {
        wx.miniProgram.navigateTo({
            url: `/pages/xxx?url=` + encodeURIComponent(data.url)
        })
    }
    closeWebView(): void {
        wx.miniProgram.reLaunch({
            url: `/pages/xxx`
        });
    }
    openLogin(): void {
        wx.miniProgram.redirectTo({
            url: `/pages/xxx?clear=${true}`
        });
    }
    ...
}
/** 方法实现类-原生APP */
class NativeApp extends App {
    executed(name: KudouAppSdkType, data: any = {}) {
        let params = { name, data }
        let str = JSON.stringify(params) // 调用 app 参数输出
        const _window: any = window
        let _userAgent = navigator.userAgent //app userAgent 输出
        if (_userAgent.indexOf('xxx/android') !== -1) { // 调用 android
            try { _window.callApp.postMessage(str) } catch (error) { alert('android error :' + JSON.stringify(error) + 'post android str :' + str) }
        } else if (_userAgent.indexOf('xxx/ios') !== -1) { // 调用 ios
            try { _window.webkit.messageHandlers.callApp.postMessage(str) } catch (error) { alert('ios error :' + JSON.stringify(error) + 'post ios str :' + str) }
        }
    }
    setTitle(title: string): void {
        this.executed(KudouAppSdkType.SetTitle, { title })
    }
    newWebView(data: NewWebViewParams): void {
        this.executed(KudouAppSdkType.NewWebView, data)
    }
    closeWebView(): void {
        this.executed(KudouAppSdkType.CloseWebView)
    }
    openLogin(): void {
        this.executed(KudouAppSdkType.OpenLogin)
    }
    ...
}
/** 方法实现类 - H5 */
class H5 extends App {
    setTitle(title: string): void {
        window.document.title = title;
    }
    newWebView(data: NewWebViewParams): void {
        window.open(data.url)
    }
    closeWebView(): void {
        window.close();
        window.history.back();
    }
    openLogin(): void {
        // code ...
    }
}
/** 代工厂 AppHelper 类 根据不同场景实现对应类 */
export class AppHelper {
    koudApp: App;
    constructor() {
        if (this.isNativeApp) {
            this.koudApp = new NativeApp(); // 原生 APP 场景
        } else if (this.isWeChatMiniprogram) { // 小程序场景
            this.koudApp = new Miniprogram();
        } else {
            this.koudApp = new H5(); // H5 场景
        }
    }
    // 检查是否为原生 APP
    get isNativeApp() {
        return window.navigator.userAgent.indexOf('xxx') !== -1;
    }
    // 检查是否为微信小程序
    get isWeChatMiniprogram() {
        const ua = navigator.userAgent.toLowerCase().match(/MicroMessenger/gi)
        return window.__wxjs_environment === 'miniprogram' || ua && ua[0] === 'micromessenger';
    }
    newWebViewPage(params: NewWebViewParams) {
        if (params.url) {
            this.koudApp.newWebView(params);
        }
    }
    // 设置标题
    setTitle(title: string) {
        this.koudApp.setTitle(title);
    }
    // 令牌失效,调用登录
    loginout() {
        this.koudApp.openLogin()
    }
}
export default {
    install: function (vm) {
        vm.prototype.$appHelper = new AppHelper()
    },
    AppHelper: new AppHelper()
}
复制代码

请求接口 API Service 模块化

项目中,后端是通过判断请求头中携带的 cookie 值是否正确,来返回请求信息的。那也就是说,每一次数据请求,都需要在请求头上添加 cookie 字段,项目中的接口请求有 50 多个,如果每个都添加,或者是未来的某一天,后端要前端配合修改请求头。天啊!这简直是场噩梦,没有技术含量不说,还容易出错。







请到「今天看啥」查看全文