NutUI 實戰--持續升級企業業務之福禮

前言

自 2018 年 4 月福禮上線以來,經過快速迭代和擴展功能模塊,整個項目快速發展。2019 年,京東福禮的月均活躍用戶同比增加達到 265%,累計爲超過 3500 家企業提供了數字化福利管理服務,有近 600 萬人次經過京東福禮實現了個性化福利發放。回首過往,從只支持一種活動模式到現今支持六種活動模式,從只支持積分支付到支持混合支付、金額支付、單次支付,從只支持掃碼展現 H5 到支持微信小程序內嵌、原生 App 內嵌...福禮正在以飛快的速度完善本身的功能體系,以期給客戶更好的服務。伴隨着項目功能的快速迭代和完善,是研發同窗們在技術上的攻堅克難和不斷突破的過程,也是整個團隊(產品,後臺,測試)一同成長,共同進步的過程。爲此特整理此文,記錄過往的經驗和思考,也指望能給同路人帶來啓發和幫助。

自2019年至今的主要功能迭代進展圖
javascript

什麼是福禮

福禮是京東爲優質企業客戶打造的以年節福禮/季度勞保兌換爲主的員工福利商城。該產品致力於提高員工福利感知度、下降福利發放和領取成本,爲企業客戶提供京東品牌保障的海量正品、搭配企業專屬優惠價格和極速物流配送服務,進而幫助企業合理規劃年度福利方案,完成一站式企業員工福利的管理及採購。css

接下來就帶你們看一下福禮的廬山真面目吧:html

好了,廣告宣傳部分到此結束,接下來咱們將從更好的福禮、開發效率優化、流程優化三個方面來聊聊咱們在福禮項目的持續升級中的收穫和思考!前端

前端架構

福禮項目自 18 年立項以來一直使用 Vue 技術棧,使用了團隊自行開發的 Gaea 構建工具和 NutUI 組件庫,此外引入了 Carefree,SMock,Vuex 等。vue

Gaea構建工具 ,是咱們團隊自主開發的一套 Vue 技術棧構建工具,基於 Node.js、Webpack 模版工程等的 Vue 技術棧的整套解決方案,包含了開發、調試、打包上線完整的工做流程。極大的提升了工做效率,目前團隊全部 Vue 業務都使用該腳手架。java

NutUI組件庫 ,是一套京東風格的輕量級移動端 Vue 組件庫,由咱們團隊歷時數年打磨,目前有 50+ 京東移動端項目使用,github上獲得1.9k+的 star ,176 Used By(不包含私有倉庫)、236 fork,NPM 下載量超過 14.5 K。該 Vue 組件庫提供大量的可複用的 Vue 基礎組件,極大的便利了福禮項目的開發。node

Carefree ,一套不依賴 wifi 熱點的移動 web 真機測試一站式解決方案,是咱們團隊在平常開發中發現真機很依賴電腦發出熱點才能進行調試的痛點,針對這一問題,旨在擺脫 wifi 熱點束縛,讓移動 web 真機測試自由自在而自主研發的一套解決方案。android

SMock,由團隊自主研發,針對項目前期尚無數據的問題,分析須要 mock 的文檔,輸出相應的 mock 數據,並啓動 node 服務,供前端開發時調試使用,提升前端開發效率,支持跨域訪問。webpack

更好的福禮

站在巨人的肩膀--組件庫助力開發

古語道:「君子性非異也,善假於物也「一套值得信賴的組件庫,會使開發事倍功半。

福禮項目自立項開發以來,一直使用 NutUI 組件庫。從 1.x 版本中部分的引用組件到 2.x 版本大範圍使用,NutUI 活躍的社區互動和及時的響應速度,以及具備連續性的升級,逐漸得到了咱們的信賴。ios

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:如何你們對文章中的某個點感興趣,歡迎咚咚打擾 ~~

相關文章
相關標籤/搜索