爲京東PLUS會員保駕護航的日子

前言

流光容易把人拋,紅了櫻桃,綠了芭蕉。悄然間牆上的鐘擺已經指向了 2020 年中旬,時間就像是一隻藏在黑暗中溫柔的手,在你一出神一恍惚之間,斗轉星移。2020 年對人類來講是異常坎坷的一年,不管是澳大利亞的森林火災仍是席捲全球的瘟疫,都給地球蒙上了一層灰色的面紗,然而人類並無坐以待斃,而是奮起反擊!京東做爲國民品牌,更是承擔起社會責任,始終奮鬥在抗爭的第一線。javascript

京東 PLUS 會員,是京東爲向核心客戶提供更優質的購物體驗推出的服務,在庚子年間,歷經半載,茁壯成長:幾大頻道頁輪番改版換新顏,聯名卡也迎來了騰訊 QQ 音樂、芒果 TV 的強勢入駐,計算器頁面改版以及 PLUS 會員自建風控體系的創建,不管是在用戶吸引度仍是在項目可配置化上都下足了功夫,好比:css

頻道首頁增長彈窗信封動畫,加強趣味性,加入挽留彈窗功能,減小佛系用戶來去匆匆,不帶走一片雲彩的狀況;html

img

改版頁面增長沉浸式以及樓層換膚功能,烘托氛圍,加強用戶感知;以及整個頁面實現配置化,減小後期前端維護上線成本。前端

img

此外,2020 年上半年主要從頁面改版、新增權益、優化完善功能、研發優化等方面支持瞭如下需求:vue

img

目前正式用戶已經接近兩千萬大關,一切是那麼的欣欣向榮、朝氣蓬勃。java

然而沉下心來,不管是技術的升級,仍是項目的不斷完善,在《2019年京東PLUS會員前端開發之路》一文中所說起的優化,都至關於萬里長征第一步,咱們要作的事情還不少,尤爲是隨着一次次需求的快速迭代,一些新的問題逐漸暴露出來,咱們逐漸意識到一個優秀的項目,必須可以創建起完善的架構以及周邊系統,才能保證項目的不斷更新迭代和高效的開發。誠然,到目前爲止咱們能夠作的仍然有不少,可是不妨暫停腳步,回首梳理一下這半年來走過的路,取其精華,棄其糟粕。node

接下來本文將從提升開發效率、優化項目架構、完善用戶體驗等方面入手,和你們分享咱們在 2020 年上半年開發項目過程當中的心得體會,旨在拋磚引玉,共同窗習。webpack

img

1、提升開發效率

在項目開發過程當中,每每不起眼的優化,總能帶來意想不到的收穫。持續不斷的發現開發中遇到的問題,如何改進過程,提升開發效率,也是咱們孜孜不倦的追求。git

1.1 自動生成新模板

隨着需求的迭代,之前的頻道頁逐漸沒法知足當前的需求,尤爲是 PLUS 會員自建風控體系頁面、頻道頁改版的需求,都要新增頁面,那麼新增一個頁面須要幾個步驟呢?github

img

如上圖所示,新增一個頁面,須要五個步驟:

一、首先要新增一個 Html 頁面,用於掛載加載靜態資源和骨架屏等內容;

二、新增入口 JS 文件,是新頁面的入口文件,即 Webpack 打包的入口文件;

三、新增 Vue 主文件,用於開發新頁面的邏輯;

四、修改 Webpack 配置文件,增長 entry 入口,以及增長對應的 Html 插件配置,例如:

new HtmlWebpackPlugin({
    template: './src/template/new-expired.html',
    filename: path.resolve(__dirname, 'build/new-expired.html'),
    inject: false
}),
複製代碼

五、修改上傳代碼組件,添加新增長的入口 JS;

所以,每次新增頁面,都要修改上面的五個配置,步驟繁瑣不說,偶爾遺漏一項,致使頁面發生錯誤也不是沒有發生過。那麼如何簡化新增頁面的步驟呢? 因而借鑑團隊的 NutUI 組件庫 新增組件,自動生成相應配置文件的思路,咱們引入 inquirer 庫,一個用戶與命令行交互的工具。執行一個命令,便可自動生成修改以上五個配置文件豈不美哉。

首先引入用戶與命令行交互工具,用來輸入新頁面的名稱:

// 關鍵代碼
inquirer.prompt([
    {
        type: 'input',
        name: 'pageName',
        message: '新建頁面英文名稱:',
        validate(value) {
            const pass = value && value.length <= 20;
            if (pass) {
                return true;
            }
            return '不能爲空,且不能超過20個字符';
        },
    }
])
.then(function (answers) {
    createDir(answers);
});
複製代碼

執行該文件,則在命令行展現以下所示:

img

待咱們輸入新文件的英文名稱,以及中文標題以後,就能夠繼續往下進行了,好比向指定文件夾中生成 Html 新文件:

function createHtml(value) {
   const htmlCode = templateHtml.replace(/\{template\}/g, value.pageName).replace(/{title}/g, value.pageTitle);
   const createHtml = path.resolve(__dirname, `../createTemplate/${value.pageName}.html`);
   fs.writeFileSync(createHtml, htmlCode);
}
複製代碼

此外,還需自動修改 json 格式的配置文件,

function createJson(value) {
    entrys[value.pageName] = `./src/entry/${value.pageName}.js`;
    const createJson = path.resolve(__dirname, './entrys.json');
    fs.writeFileSync(createJson, JSON.stringify(entrys));
}
複製代碼

根據自動生成的 json 文件,在後續啓動本地服務或者打包編譯代碼時,Webpack 就能夠生成對應的 entry 入口和 HtmlWebpackPlugin 插件等配置項。

const entryConfigs = require('./templates/entrys.json');
Reflect.ownKeys(entryConfigs).forEach( key => {      //循環遍歷對象
    webpackConfig.plugins = (webpackConfig.plugins || []).concat([
        new HtmlWebpackPlugin({
            template: `./src/template/${key}.html`,
            filename: path.resolve(__dirname, `build/${key}.html`),
            inject: false
        })
    ])
});
複製代碼

這樣,原來每次新增頁面都要修改或者新建五個文件的歷史一去不復返了,一行命令就能夠完成新頁面的構建,即提升了效率,又減小了人爲操做帶來遺漏的風險。

1.2 代碼上線前自動提示

俗話說大禮不辭小讓,細節決定成敗。人生如此,程序亦如此。因爲需求迭代快且平均每週要並行開發五六個需求,咱們採用多個分支並行開發,每次上線更新版本號來避免用戶靜態資源的緩存,然而設想一下若是辛辛苦苦的開發完需求,滿懷期待的上線後,忽然發現沒有合併以前分支代碼!或者沒有更改版本號!想必猶如五雷轟頂,腦海中確定一萬頭羊駝飛奔而過。。。 不要問我爲什麼會想到這個問題,那確定是痛苦的回憶!永遠不要靠人爲的記憶來保證上線前的必要操做,不然你將和我感同身受。那麼咱們如何來避免這類問題的發生呢?能不能在上線前有人告訴我一下,檢查一下必要的操做呢?

梳理一下要實現這個功能須要知足如下要求:

  1. 開發者提交代碼到 master 分支上,才觸發該提示功能;

  2. 攔截提交代碼進程,提示開發者注意事項,若是開發者選擇了 true,則繼續提交代碼進程,不然退出提交代碼。

因而我把目光轉向了 git-hook 技術,

項目要使用 git 進行代碼提交時,使用 pre-commit 的 git 鉤子,在調用 git commit 命令時自動執行某些腳本檢測代碼,若檢測出錯,則阻止 commit 代碼,也就沒法 push,保證了出錯代碼只在咱們本地,不會把問題提交到遠程倉庫。

git-hooks 存在在 git 倉庫的 .git/hooks 文件夾下,包含了不少的 hooks,這些文件都是在建立 git 倉庫的時候自動生成的,打開 hooks 文件夾能夠看到:

img

後綴爲 .sample 表示默認是不生效的,因此咱們要作的就是生成 pre-commit 文件,該文件會在提交代碼時執行該文件的代碼,所以咱們就能夠把提示功能的代碼放在該文件中開發。

以下所示,再提交代碼時,首先判斷是否爲 master 分支,若是是 master 分支,表示要上線了,因而執行 pre-commit 文件中的代碼,以下圖所示:

img

只有全部回答爲 y,纔會繼續提交代碼進程。

那麼還有個問題,如何讓全團隊的人都用到該功能呢?難道要讓每一個成員都在本地添加這個文件嗎?咱們在運行一個項目時,通常都會運行 npm run dev,也就是本地啓動服務,因此咱們能夠利用這一個必要步驟,把修改 pre-commit 文件的代碼放在這一步驟中,這樣每一個成員在啓動本地服務的時候,就會向 hooks 中加入該文件,因而就在團隊成員神不知鬼不覺的狀況下注入了 pre-commit 功能,待小夥伴上線前發現這個彩蛋吧~

1.3 提交代碼前自動檢查

雖然 1.2 中的方法只是在上線前進行了提示攔截,在開發過程當中是否還有可操做空間呢?下面先來看幾種常見場景:

一、新需求在開發的過程當中,主幹分支可能已經更新過不少次了,因此咱們要及時合併主幹分支,來保持當前的開發分支是最新的;

二、不一樣開發者使用一個分支的時候,常常會忘記其餘人合併代碼就編譯上傳,致使別人的代碼被覆蓋;

三、本身合併完主幹分支,偶爾會忘記上傳;

實現目標

這些問題,咱們能夠靠腳本去運行,用腳本去逐條檢查全部的項目是否完成。下面咱們先來看下實現方法。 在更新以前首先要定義咱們要實現的目的:自動檢查當前分支和本地分支的狀態。

一、須要把全部的文件fetch到本地

由於只有本地和線上保持同步才能進行正確的判斷,這個操做只會把服務器的下載到本地不會合並。

git fetch
複製代碼

二、檢查主幹分支是否有未提交或者待更新內容

在 master 分支下,咱們能夠經過命令去查看當前的狀態,下面可能有兩種狀態,ahead or behind 表明了跟遠程分支的一種關係,待更新 or 待提交,下圖就是一種待提交的狀態

git status
複製代碼

img

三、判斷當前分支與主幹分支的狀態

若是當前所在分支不是主幹分支,須要進行這項判斷,若是發現有待合併,則提示用戶須要合併代碼。能夠經過如下命令去查看當前分支已經合併過的分支來判斷是否合併主幹分支。

git branch --merged
複製代碼

四、判斷當前分支是否有未提交或者待更新內容

若是當前分支不爲主幹分支的話則進行這項內容,判斷邏輯同2。

流程圖以下:

img

具體實現

用執行腳本的方法來代替人工操做,前端的腳手架環境爲 node,第一步就是在 npm 庫選擇一個可操做 git 的庫,我選擇的是 simple-git

一、第一步先判斷是否有 remote,若是沒有 remote 地址那麼就沒法 fetch,後面的比較也就無正確性可言。

const git = require('simple-git');
git().getRemotes(true, (err, res) => {
    //do something
})

複製代碼

二、判斷主幹是否須要提交, 經過 isBehind 和 isAhead 的狀態去判斷主幹分支的狀態:

const mainBranch = master;
const behindReg = /behind(?= \d+\])/gi;
const aheadReg = /ahead(?= \d+\])/gi;
git().branch(['-vv'], (err, res) => { 
     const mainBranchInfo = res.branches[mainBranch]
    const isBehind = mainBranchInfo.label.search(behindReg);
    const isAhead = mainBranchInfo.label.search(aheadReg);
});
     
複製代碼

三、判斷當前分支是否合併主幹,經過以下方法去檢查是否合併,若是沒有合併能夠提示用戶:

const mainBranch = master;

git().branch(['--merged'], (err, res) => {
    const isMerged = res.all.includes(mainBranch);
});
複製代碼

四、判斷當前分支的是否須要提交,方法以下,經過判斷 變量 Behind 是否須要更新:

getCurrentBehind() {
    return new Promise((resolve, reject) => {
        git().status((err, res) => {
            if (err) reject();
            else resolve(res.behind);
        });
    });
}
const Behind = await gitFn.getCurrentBehind();
複製代碼

最後造成以下提示:

img

1.4 自動生成說明文檔

PLUS 會員 M 端項目在不斷壯大,程序的維護和調試難度也在上升,有時候咱們封裝的工具函數或者業務邏輯因爲缺乏註釋,其餘小夥伴在使用或者二次開發的時候理解起來較爲麻煩。爲了增長程序的可讀性和程序的健壯性,咱們在項目中加入了 API 文檔,方便團隊成員可以快速查詢和入手項目開發,另外爲了節約 API 文檔的維護成本,咱們使用了 jsDoc 自動化生成文檔。

JsDoc能夠根據規範化的註釋、自動生成接口文檔,舉個例子:

/** * @description 判斷是否在小程序環境中 * @returns {Boolean} result 結果 */
export function isMiniprogram() {
    const plusFrom = getCookie('plusFrom');
    return !!~['xcx', 'xcxplus'].indexOf(plusFrom);
}
複製代碼

這樣一個函數就會被jsDoc收集起來,放到開發文檔裏了,而後咱們能夠本身創建一個 npm script 工做流,方便在命令行中啓動:

"docs": "rimraf docs && jsdoc -c ./jsdoc-conf.js && live-server docs"
複製代碼

jsdoc-conf.jsjsdoc的配置文件,包括了一些文檔的配置項,而後咱們就能夠開始進行文檔的自動構建了!

執行 npm run docs,或者能夠把該命令合併到 npm run dev 中執行。

接下來瀏覽器會在本地自動打開文檔頁面:

enter image description here

頁面左邊是 api 目錄,Globals 下的 api 就是咱們在 JS 文件裏邊寫的工具函數了,而 Modules下的就是 Vue 組件模塊,下面咱們來看如何給 Vue 組件模塊添加註釋:

/** * @module drag * @description 拖拽組件,用於頁面中須要拖拽的元素 * @vue-prop {Boolean} [isSide=true] - 拖拽元素是否須要吸邊 * @vue-prop {String} [direction='h'] - 拖拽元素的拖拽方向 * @vue-prop {Number | String} [zIndex=11] - 拖拽元素的堆疊順序 * @vue-prop {Number | String} [opacity=1] - 拖拽元素的不透明級別 * @vue-prop {Object} [boundary={top: 0,left: 0,right: 0,bottom: 0}] - 拖拽元素的拖拽邊界 * @vue-data {Object} position 鼠標點擊的位置,包含距離x軸和y軸的距離 */
 //業務代碼。。。
複製代碼

因爲 Vue 組件不能被原生的 jsDoc 支持,這裏咱們藉助了 jsDoc-vue ,因此組件的註釋寫法也有所不一樣,具體的規範你們能夠查詢官方文檔。而後咱們就能夠在 api 文檔中看到這個組件相關的註釋了。

enter image description here

vue組件的註釋規範能夠查詢jsdoc-vue的官方文檔。當咱們寫完註釋以後,須要執行一下npm run docs來從新生成文檔。

1.5 優化版本號邏輯

爲了保證每次上線後,用戶都可以獲取到最新的代碼,而不是用緩存資源,因此要修改每次上線的版本號,好比修改下面連接中的 4.1.2:

https://static.360buyimg.com/exploit/mplus/4.1.2/v4/js/index.js

因爲咱們接入了公司的頭尾系統,因此只須要改動頭尾系統中的版本號便可。

什麼是頭尾系統呢?能夠簡單的理解爲:服務器從這個系統中引入 A 配置文件,前端在 A 文件中輸入代碼,一鍵推送到指定的服務器上,來更新該服務器上引入的前端資源。

可是咱們發現,每次上線的時候,都要在頭尾系統中推送幾十臺服務器,每每總有服務器推送失敗,或者上線後在預發中發現有新的問題,要緊急回滾版本號,因而又要從新推送頭尾文件,每每要花費很長時間。

因而咱們在想是否是有更好的方法來解決?抑或是緩解此類問題呢。這時版本號比較邏輯從新進入咱們眼簾。之因此用「從新」一詞,是由於這個邏輯以前就有,不過當時沒有解決動態生成靜態資源腳本,致使沒法保證執行順序,從而頁面白屏的問題(該問題詳見文章:《2019年京東PLUS會員前端開發之路》),因此當時就把該功能去掉了。如今是時候從新審視這個功能了:

一、服務器端的 Html 模板中維護一個版本號 V1;

二、前端在頭尾系統中維護一個版本號 V2;

前端在 Html 中開發好版本號比較的邏輯,使用兩個版本號中較大值來動態生成對應的靜態資源。

/* V1 就是放在 Html 中的版本號 V2 就是前端在頭尾文件中放置的版本號 最終使用的是較大的版本號 */
if (typeof V2 != 'undefined' 
&& Number(V2.replace(/\./g, '')) > Number(V1.replace(/\./g, ''))) {
    V1 = V2;
}
複製代碼

對應的有兩種狀況:

一、只須要前端上線的需求,前端在頭尾系統中推送更新的版本號,這個和以前同樣;

二、若是是前、後端都要上線的項目,則後端在 Html 中修改版本號 V1,根據 Html 中版本號比較邏輯,會使用兩個版本號中較大的那個,則生成的靜態資源使用了後端在 Html 中設置的版本號,因而前端省去了推進頭尾文件的步驟,只須要後端上線便可

不過按下葫蘆瓢又起,雖然緩解了前端推送頭尾文件的狀況,可是今後要求保證版本號有大小順序關係。因而咱們按照需求的上線順序,規定好每一個需求的版本號,運行了一段時間,緊急需求過來了、線上 bug 緊急需求過來了,都要在原來已經排序好的版本號之間插入版本號,相似於一個有序數組,若是向數組的前面插入元素,則後續元素都要跟着變化,通過商量以後咱們改成以下策略:

一、只有三個數字表示版本號,則使用前兩位做爲主版本號,好比:4.1.0、4.2.0...4.99.0;

二、使用第三個版本號數字表示緊急需求的版本號,好比目前版本號是 4.2.0,若是插入緊急需求則爲 4.2.1;

這樣擴展了主版本號的個數:前兩位數字能夠擴展到很大;並且插入的緊急需求版本號不會影響以前已經排序好的版本號,還可以減小上線步驟,可謂一箭三雕!

1.6 自動圖片壓縮

圖片壓縮一直是前端優化中很重要的一部分,也能夠說是開發流程中必不可少的一個環節。以前 PLUS 項目的圖片壓縮,一直處於自發的、手動的處理狀態,這就很是考驗你們的細心程度和自覺性了。

爲了規範這一流程,咱們引入了 Gaea 腳手架中自動壓縮圖片及轉換 webp 的功能。話很少說,上代碼

const imagemin = require('imagemin');
const imageminWebp = require('imagemin-webp');
const path =  require('path');
const fs = require('fs');
const tinify = require("tinify");
const config = require("./package.json");
tinify.key = config.tinypngkey;
const filePath = './src/asset/img';
const files = fs.readdirSync(filePath);
const reg = /\.(jpg|png)$/;
async function compress(){
	for(let file of files){
		let filePathAll = path.join(filePath,file);
		if(reg.test(file)){
			await new Promise((resolve,reject)=>{
				fs.readFile(filePathAll,(err,sourceData)=>{
                    tinify.fromBuffer(sourceData).toBuffer((err,resultData)=>{
                        //將壓縮後的文件保存覆蓋
                        fs.writeFile(filePathAll,resultData,err=>{
                            resolve();
                        })
                    })
				})
			})
		}
	}
    imagemin(['./src/asset/img/*.{jpg,png}'],'src/asset/img/webp',{
        use:[
            imageminWebp()
        ]
    }).then(()=>{
        console.log(chalk.green(`webp轉換已完成~`));
    })
}
compress();
複製代碼

在 css 中使用方式:

@mixin webpbg($url, $name) {
  background-image: url($url + $name);
  background-repeat: no-repeat;
  @at-root .webp & {
    background-image: url($url + "webp/" + (
        str-slice($name, 0, str-index($name, ".") - 1)
      ) + ".webp");
  }
}
複製代碼

str-slice(string, start, end) 從 string 中截取子字符串,經過 start 和 end 設置始末位置,未指定結束索引值則默認截取到字符串末尾。 str-index(string, substring) 返回 substring 子字符串第一次在 string 中出現的位置。若是沒有匹配到子字符串,則返回 null。

因爲這個 webpbg 方法定義在公共的 common-mixin.scss 裏,而調用是分佈在各個組件中的,所以組件中的調用會報錯找不到這個 webpbg 函數。若是要在全局使用這個 webpbg 方法,就須要在 webpack.config.js 中全局導入,修改方式以下

@include webpbg("../../asset/img/index-formal/", "formal-title.png");
複製代碼

結果,出師不利,居然報錯了!

image

咱們通常是把 Mixin 放在當前 Sass 文件中,若是要在全局使用,須要在 webpack.config.js 中全局導入。

image

好了,圖片壓縮,自動轉 webp,樣式支持 webp,一切順利的進行着。

然而,隨着咱們圖片的增多,壓縮次數的增長,問題又來了: 因爲壓縮圖片用到的是 tinypng 工具,咱們在使用時須要用郵箱註冊獲得個 key。對於免費用戶,同一個 key 在同一個月中只能壓縮 500 張圖片。

image

所以,咱們須要破除這個限制。除了多申請幾個 key,能不能從優化策略上進行改善呢?

實際上,通常圖片切好後,不會常常去改動,尤爲是已上線的部分,改動的可能性更小。所以,咱們能夠將圖片的全量壓縮,改成增量壓縮,只壓縮修改過的或者是新增的。基於這種策略,壓縮圖片的數量不會很大,限制就這樣破除了~ 下面來看看具體實現步驟吧:

img

下面是生成 hash 值的代碼片斷:

let rs = fs.createReadStream(filedir); //打開一個可讀的文件流而且返回一個fs.ReadStream對象
let hash = crypto.createHash("md5"); //建立並返回一個 Hash 對象,該對象可用於生成哈希摘要
let hex;

return await new Promise((resolve, reject) => {
  //在內部不斷觸發rs.emit('data',數據);
  rs.on("data", hash.update.bind(hash)); // hash.update使用給定的 data 更新哈希的內容

  //end事件表示這個流已經到末尾了 ,沒有數據能夠讀取了
  rs.on("end", function () {
    hex = hash.digest("hex"); //計算傳入要被哈希的全部數據的摘要,返回字符串
    result[filedir.replace(/\/|\\/g, "/")] = hex; // 統一mac及windows下的文件路徑,將其做爲key值,生成的hash值爲value,存入result中
    resolve();
  });
  //error事件表示出錯了
  rs.on("error", function (msg) {
    console.log("error", filedir);
    reject();
  });
});
複製代碼

2、優化項目架構

爲何要持續進行項目架構的優化?項目就像一座建好的大廈,若是時不時的要砸掉承重牆進行裝修,不及時維護的話,最終會千瘡百孔,朝不保夕。相似地,京東 PLUS 會員項目做爲一個長期維護的項目,隨着需求的快速迭代,緊急需求的插入實現,最初沒有考慮到的問題,或者阻礙項目開發進度的問題慢慢浮出水面,爲了項目可以長久運行,避免代碼更加臃腫,咱們主要作了如下工做:

2.1 提取基礎組件

一千我的眼中有一千個哈姆雷特。如何劃分組件,想必每位開發者心中的認識都全部不一樣。那麼 PLUS 會員項目的組件是如何劃分的呢?

好比一個下面這個彈窗:

img

咱們首先使用了 NutUI 組件庫中的基礎組件——Dialog彈窗組件,而後以該組件爲基礎,開發了業務計算器彈窗組件,爲了更好的提升組件複用性,以及減小業務邏輯改動對組件的影響,應該是由如下形式構成:

img

當前咱們項目中的組件也在朝着這個方向努力,由於發現 PLUS 項目中引入了一些 NutUI 基礎組件,以後又作一些開發來匹配業務需求。在經歷了很長時間的穩定運行後,這些組件的改動不多,所以爲了給項目瘦身,咱們將這些基礎組件抽取出來發佈到 npm 中,最終將其打包到 node_modules 文件夾中,這樣項目中就會大量減小這些基礎組件的代碼,而且不須要每次都打包編譯這些組件代碼。

值得一提的是,在本地開發時全部組件都完美運行,可是打包部署後卻失敗了。通過排查原來是 npm 包只是存放的組件源碼,並未對其編譯,因此在項目中直接使用就會報錯,問題暴露出來了。那麼要爲了這幾個組件,去單獨搭個腳手架處理嗎? 咱們機智的小夥伴想到了一個借雞生蛋的方法——使用團隊的 NutUI 組件庫腳手架做爲載體,在組件庫中中建立 PLUS 的基礎組件,而後把這些須要打包的組件都經過組件庫編譯後導出。這樣把編譯後的代碼部署到 npm上,就可以在項目中直接安裝依賴包使用了,效果以下,能夠看到,打包後項目中的引入代碼也精簡很多:

image

2.2 縮減現有分支

PLUS 項目平均每週都須要並行5、六個需求進行開發,如何才能保證並行開發的需求不會互相干擾,咱們採用的是多分支的方法,每位研發從主幹分支 v二、v三、v4的基礎上新建分支,分支的名字使用當時需求的拼音縮寫,好比正式改版需求就是:zsgb;開發完畢後,每次上線時再合併到 master 分支上準備上線,以下所示:

img

不知你們有沒有發現什麼端倪?這樣命名分支會不會有所隱患? 在運行了半年以後,咱們發現代碼庫中的分支愈來愈多,不少新建的分支在開發完以後,就不在使用了,可是每次都要人爲的去刪除,而須要人爲自發的去操做的都是不可靠的。通過思索以後,咱們決定使用每一個開發成員的姓名縮寫做爲分支名,好比名字叫「張大胖」的同窗,新建的分支就是「zdp」,若是多人開發同一個需求,則在某我的的分支上開發便可,這樣的好處以下:

  1. 每一個主分支下的子分支個數是固定的,每一個研發有一個對應的子分支進行開發;

  2. 避免了人爲的刪除代碼庫中冗餘分支步驟,且減小誤刪分支的狀況;

  3. 避免了子分支的不斷增多的問題;

通過上述操做,原來代碼庫中幾十個子分支縮減到幾個分支,大大減小了代碼量,下載代碼速度也快了起來。

2.3 PC端腳手架優化

因爲歷史緣由,PC 端的 PLUS 會員項目用的是 React 技術棧進行的開發,且隨着科技的進步,移動端所佔比例愈來愈大,相應的 PC 端所佔比例逐年縮小,不少功能也是採起的引流到 M 端,這就致使一個問題,咱們對 PC 端的改動也隨之變少,可是最初的 PC 端腳手架已經比較老舊,在編譯的過程當中會時常出現問題,好比:

一、打包代碼速度巨慢,打包期間能夠喝茶、嗑瓜子、打豆豆;

二、生成 hash 值命名的文件,每次聯調和版本回滾都要挨個替換每一個hash值,不利於心情舒暢,容易讓人暴躁;

三、不支持熱更新,致使每次更改都要人爲的去刷新頁面,反作用是能夠矯正處女座強迫症;

四、不支持按需打包文件,每次打包都會把全部的文件都打包,而上線只是上其中的幾個文件;

忍無可忍,則無需再忍,基於此,咱們主要作了如下優化,升級了 Webpack 從 2 到 4 版本,全部的配置文件從新開發,並作了如下的優化:

img

打包效率較低,開發聯調比較麻煩,因此腳手架有待挑戰優化。而且隨着技術的發展,不少新的技術能夠用於咱們項目中,來提高開發效率。因爲此處代碼量巨大,因此只是寫明瞭方向,有疑問的小夥伴能夠在評論區留言討論~

2.4 代碼提交規範

代碼提交規範化的目的是爲了更好的追溯代碼、篩選和快速的定位提交代碼所涉及的範圍和實現功能。對於 PLUS 這樣一個不斷開發迭代的項目,增長代碼提交規範化是頗有必要的。正所謂,無規矩不成方圓。所以咱們在項目中引入了 vue-cli-plugin-commitlint 來約束和規範代碼提交。它既能夠加強團隊成員對 commit 規範的概念,同時也能夠統一咱們代碼的提交風格,更重要的是,它能夠自動生成自動 ChangeLog,方便咱們查找提交版本,對後期遇到問題,快速定位提供便利。

vue-cli-plugin-commitlint 是開箱即用的 git commit 規範,它結合了 commitizen、commitlint、conventional-changelog-cli 和 husky。

下面咱們看一下如何在項目中使用。

安裝依賴

npm i vue-cli-plugin-commitlint commitizen commitlint conventional-changelog-cli husky -D 
複製代碼

在 package.json 中添加

{
    ...
    "scripts": {
        "log": "conventional-changelog --config ./node_modules/vue-cli-plugin-commitlint/lib/log -i CHANGELOG.md -s -r 0",
        "cz": "npm run log && git add . && git status && git cz"
    },
    "husky": {
        "hooks": {
            "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
        }
    },
    "config": {
        "commitizen": {
            "path": "./node_modules/vue-cli-plugin-commitlint/lib/cz"
        }
    }
}
複製代碼

增長 commitlint.config.js 文件

module.exports = {
    extends: ['./node_modules/vue-cli-plugin-commitlint/lib/lint']
};
複製代碼

而後執行 npm run cz, 這時,就會提示你選擇,而後根據提示依次填寫,生成符合格式的 Commit message。

image

提交完成以後,會在項目根目錄生成 CHANGELOG.md 日誌文件,點擊文件中的 commitId, 就可跳轉查看對應提交內容了。

img

2.5 提升腳手架打包速度

隨着項目的不斷迭代,文件數量不斷增多,項目變得龐大,從而致使 Webpack 構建變得愈來愈慢。每次構建都須要必定時間,提高構建速度,變得格外有必要。

衆所周知,Webpack 在 Node.js 上運行都是採用單線程模型的,處理任務也是依次去執行,不能併發處理多個任務,須要排隊,那是否有什麼方法可讓 Webpack 同時並行處理多個任務呢?

爲了解決上述疑問,咱們接入了 HappyPack,它能讓 Webpack 作到這點,能夠把任務分解給多個子進程去併發的執行,子進程處理完後再把結果發送給主進程。HappyPack 的核心原理就是把這部分任務分解到多個進程去並行處理,從而減小了總的構建時間。

下面,咱們來看一下如何在項目中接入。

安裝依賴

npm install happypack -D
複製代碼

對 webpack.config.js 進行配置

在整個 Webpack 構建流程中,最耗時的就是 Loader 對文件的轉換操做,由於要轉換的文件數據特別多,並且轉換操做須要依次排隊處理。

配置 Loader,上代碼。

module.exports = (env, argv) => {
  const webpackConfig = {
    //...
    module: {
      rules: [{
		test: /\.js$/,
		loader: 'happypack/loader?id=happyBabel',
        // 排除 node_modules 目錄下的文件
		exclude: [
		  path.resolve(__dirname, 'node_modules'),
		  path.resolve(__dirname, 'jssdk.min.js')
		]
	  },
      //...
      ]
    },
  //...
  }
}
複製代碼

咱們把對 .js 的文件處理交給 happypack/loader,而後經過 id 標識肯定 happypack/loader 選擇哪一個 HappyPack 實例處理文件。

增長對應的 HappyPack 實例。

const HappyPack = require('happypack');
const os = require('os');
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });
//...
module.exports = (env, argv) => {
const webpackConfig = {
//...
plugins: [
  new HappyPack({
  // 惟一的id標識
  id: 'happyBabel',
  // 如何處理 .js 文件,用法和 Loader 的配置同樣
  loaders: [{
    loader: 'babel-loader?cacheDirectory=true',
  }],
  // 共享進程池
  threadPool: happyThreadPool,
  // 容許 HappyPack 輸出日誌,默認爲 true,可不寫
  verbose: true
  }),
],
//...
}
}
複製代碼

咱們先建立共享進程池,藉助 Node.js os,讓進程池中包含 os.cpus().length 個子進程,而後增長對應的 HappyPack 實例,傳入以前定義好的 id 標識, 告訴 happypack/loader 去處理 .js 文件,loaders 屬性和上面 Loader 配置中同樣, threadPool 屬性 傳入 預先定義好的 happyThreadPool 參數, 告訴 HappyPack 實例都使用同一個共享進程池中的子進程去處理任務。

而後執行打包編譯構建,構建完成以後咱們能夠看到 HappyPack 的構建日誌。

image

咱們能夠看到,HappyPack 啓動了8個子進程去並行處理任務。激動地鼓鼓掌!

最後,再讓咱們看一下,加載速度的對比圖,省了1488ms,提高了將近20%。

image

2.6 構建結果輸出分析

可視化的資源分析工具備不少,咱們選用了 webpack-bundle-analyze ,它以圖形的方式展現,相比其餘工具更簡單、直觀,輸出分析結果,可讓咱們快速分析到問題所在。下面咱們看一下如何在項目中接入。

安裝依賴

npm install webpack-bundle-analyzer  -D
複製代碼

對webpack.config.js進行配置

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
...
module.exports = (env, argv) => {
	const webpackConfig = {
        ...
        plugins: [
            ...
	    new BundleAnalyzerPlugin()
        ],
        ...
    }
}
複製代碼

配置方法也很簡單,通常默認的選項就足夠使用, 無需修改。

接着,咱們在package.json 的 scripts 中增長

"analyz": "NODE_ENV=production npm_config_report=true npm run build"
複製代碼

而後執行 npm run analyz 後,瀏覽器會自動打開 http://127.0.0.1:8888/ ,顯示分析視圖界面。

image

若是不想每次都自動彈出,能夠把參數 openAnalyzer 的值改成 false,而後按需手動打開。

經過視圖,能夠看到項目各模塊的大小,看到文件打包壓縮後真正的內容,而後分析出拿些文件佔用比較大,有了分析思路,就有了優化的目標。

經過最後的分析圖先發現公用的 toast.js 文件佔比是比較大的,能夠會對 toast.js 文件進行優化。另外佔比比較大的第三方庫 Swiper.js 和 lazyload.js,也能夠考慮進行 DLL 抽離,進一步的提高空間。

2.7 從新劃分組件

在 PLUS 項目中咱們有一個 component 文件夾,這裏邊放的是一些公用的組件,或者一些頁面的子組件。因爲歷史遺留問題,該文件夾通過項目的迭代和頁面的增長,這裏有許多冗餘的組件,整個文件夾的目錄顯得很臃腫。因此關於從新劃分組件勢在必行,下面讓咱們來看看如何對其劃分。

首先讓我來看看它裏邊都有什麼?

component文件夾

抱歉一屏截不下!

根據上圖咱們大體將 component 文件夾中的內容歸了下類。

image

一、歷史遺留類:這類文件能夠追溯到初始建立的元老級文件,當時目錄規劃仍是單頁面形式的,這類文件大可能是針對首頁的子組件文件,也有一些根據用戶狀態作的頁面子組件文件。

二、頁面組件類:隨着項目迭代,項目裏新增了一些新的頁面,而這些頁面中複雜的子組件也散落在了 component 文件夾中。

三、功能類組件:功能類的組件如彈窗組件、返回頂部組件、計算器組件等公共組件也放到這個文件夾下邊管理。

要如何劃分呢?

首先,對於歷史遺留類的頁面文件,在改版時有意識的將其歸類到其對應的頁面文件夾中,方便維護。對於零散的頁面子組件,也統一歸類到其對應的頁面文件夾中。

而對於有些公共的頁面組件,咱們建立一個公共的業務組件文件夾 plus-components將其統一放到裏邊管理。

像上邊提到的功能類的組件如 dialogcountdown 等咱們用npm的形式去引用,再也不佔用本地文件夾,縮減了 component 目錄。優化後以下:

image

從上圖能夠看出優化後的 component 文件夾根目錄下建立了 plus-components 文件夾存放公用業務組件,同級存放着各個頁面的頁面文件夾,而且在跟目錄下建立了一個 other-components文件來管理其餘功能組件。這樣一來 component 的目錄結構看起來是否是清晰不少,更方便管理。

2.8 Vuex 優化

Vuex 是 Vue 項目中的一種狀態管理模式,通俗點說就是集中管理項目中全部組件公享狀態的一個機制。Vuex 通常運用在中大型的單頁面應用中,若是是簡單的單頁面項目就不建議使用它,由於 Vuex 對於簡單的應用多是繁瑣冗餘的。

而對於 PLUS 項目來講,Vuex 的存在是很是必要的,由於 PLUS 項目有太多須要共享的狀態了。

咱們先來看下項目原有的 Vuex 代碼:

import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

export default new Vuex.Store({
    state: {...},
    getters: {...},
    mutations: {...},
    actions: {...}
});
複製代碼

原有 Vuex 代碼把全部的公共狀態寫在同一個 index 文件中,state、getters、mutations、actions 所有是統一管理。這樣寫沒有錯,大多數中小型項目也都是這麼作的。但對於一個大型項目來講,全部頁面的共享狀態都放在一塊兒,會使整個 Vuex 文件看起來很臃腫,不利於維護。而且有些公共方法通過需求的迭代及改動,會產生不少冗餘代碼,而一些公共方法針對不一樣頁面的引用會產生耦合使用問題。

因此對於優化 Vuex ,咱們嘗試引入modules來模塊化管理 Vuex。

咱們在store的根目錄建立一個 modules文件夾,在文件夾中建立 refund文件。

image

modules 文件夾中能夠聽任意你想區分管理的模塊,這裏僅以 refund 爲例。在 modules 文件夾中建立好 js 或 ts 模塊後,在 index 根文件中引入。

import Vue from 'vue';
import Vuex,  { StoreOptions }  from 'vuex';
import state from './states';
import mutations from './mutations';
import actions from './actions';
import getters from './getters';
import refundModule from './modules/refund';

Vue.use(Vuex);

export default new Vuex.Store({
    state,
    mutations,
    actions,
    getters,
    modules:{
        refundModule
    }

});
複製代碼

上述代碼展現了在 store 根目錄下的 index.ts 中引入 refund,並將其命名爲 refundModule 以便調用。而對於 refund 文件,咱們能夠向以往寫 Vuex 同樣將須要的state 、action、 mutation 寫在裏邊。

const state = {
    orderId: null,
};
const actions = {
    getOrderId: ({ commit }, data) => {
        commit("setOrderid", data);
    }
};
const mutations = {
    setOrderId: (state, data) => {
        state.orderId = data;
    },
};
export default {
    namespaced: true, // module 命名空間,
    state,
    actions,
    mutations
};
複製代碼

如代碼所示,該文件中記錄了與 refund 頁面有關的 state、action、mutations。另外能夠看到代碼中多了一行 namespaced:true。折行代碼意思是開啓命名空間,既當模塊被註冊後,它的全部 getter、action 及 mutation 都會自動根據模塊註冊的路徑調整命名。簡單來講,它是用來區分你調用的是哪一個模塊中的 state、getter、action、mutations的。

在作完以上工做後,要如何調用呢?

若是你是用 JS 寫的 Vuex,能夠這麼引用:

import { mapState,  mapActions } from "vuex";

export default {
computed:{
    ...mapStated('模塊名(好比:menuNav/getNavMenus)',{ 
        a:state=>state.a,
        b:state=>state.b
    })
},
methods:{
    ...mapActions('模塊名(好比:menuNav/getNavMenus)',[  
        'foo',
        'bar'
    ])
}
複製代碼

在 Vuex 官網有不少相似的例子,感興趣的同窗能夠查閱官網關於 modules 的飲用方法。

若是你是用 TS 寫的 Vuex,在頁面中調用 modules 能夠這麼寫:

import {Component, Prop, Vue, Watch} from 'vue-property-decorator';
import {State, Action, Getter,namespace} from 'vuex-class';

const refundMdl = namespace('refundModule')

@Component
export default class Refund extends Vue {
    @refundMdl.State(state => state.orderId) orderId;
    @refundMdl.Action('getOrderId') getOrderId;
}
複製代碼

在調用時必定要引入 namespace ,而且經過 namespace 去調用 modules 的公用狀態和方法。

通過上邊對 Vuex 模塊化的改造,咱們能夠有針對性的對須要被區別開的模塊狀態進行管理,爲 Vuex 文件減重。

3、優化用戶體驗

在 2020 年伊始,PLUS 會員幾個頻道首頁輪番改版換新顏,產品團隊也是嘔心瀝血的給用戶呈現更加友好的界面和功能,做爲前端團隊,除了完成需求以外,也有本身的小九九,咱們做爲其中的一份子,也想給用戶體驗做出應有的優化改善:

3.1 減小用戶等待時間

在 PLUS 會員多個頻道頁大刀闊斧的進行改版以後,每一個人展現的樓層以及樓層順序都有多是不同的,因此前端須要依賴後端接口的數據來進行樓層的展現,這就致使了頁面須要等待配置接口返回數據後才能展現出來,一旦接口返回緩慢,頁面好久才能渲染出來,這就對於用戶體驗至關很差,可是咱們沒法控制後端返回接口的速度,那麼從前端入手,咱們作了如下工做:

一、增長骨架屏,在渲染頁面前就顯示出頁面的總體樣式,減小白屏時間;

二、HTML 頁面直接放置用戶信息的數據,前端不用再請求接口,直接能夠渲染我的卡片一些區域;

三、和產品確認首屏會出現的樓層,好比我的卡片樓層、會員尊享權益樓層,這些樓層都作了前端佔位區域,在接口返回前就顯示出這幾個樓層的初步樣式,避免待配置接口返回後,致使的樓層順序的抖動;

四、這幾個樓層,按照後端返回的數據格式,前端設置初步數據,來渲染初步的樓層頁面,待接口返回數據後,再替換頁面中的關鍵字段;

img

如上圖所示,在尚未拿到數據接口的時候,頁面已經能夠顯示出基本架構了,縮減了頁面白屏時間,減小了用戶等待時長。

3.2 多重保障樓層顯示

除了上面介紹到減小用戶等待時間,咱們還作了多重保護頁面的展現。因爲整個頁面的樓層都是根據接口返回的數據作的配置化顯示。通常來講,一旦沒有接口返回數據,則頁面再也不展現,甩給用戶一個骨架屏。可是爲了追求更好的體驗,咱們是否是能夠採起多重保險來最大化的解決某個接口掛掉的帶來的問題呢? 首先,咱們要求後端直接把樓層配置信息放在 Html 頁面中,前端根據返回的信息渲染首屏的樓層,這樣能夠直接根據接口信息判斷頁面要顯示哪些樓層,若是這一層失敗了,則再去調用對應的樓層配置接口,退一步說,這個接口也掛了,爲了不用戶掀桌子的心情,咱們會直接調取首屏確定會請求的幾個樓層的接口,這樣通過三層接口保障,減小了由於某個接口有問題,致使頁面白屏的風險。

img

多重保證顯示頁面樓層,首先獲取到首屏直出數據,不然獲取樓層數據接口,再不行直接請求首屏樓層內部數據,避免因一層數據有誤致使頁面白屏,減小用戶投訴;

3.3 完善優化 PWA

自從去年在風控用戶狀態頁面增長了 PWA 緩存技術以後,本來要趁熱打鐵推動全量,卻發現同一個域名下全部的請求都會被 serverWork 的攔截,因而戛然而止,從長計議。

那麼咱們怎麼控制 PWA 在指定的用戶狀態下生效呢?

方案一

我作了不少嘗試,在 PLUS 項目中不一樣用戶狀態是在不一樣的 Html 中,咱們是否是能夠在 Html 中向 serivceWorker 中發送消息,只在某些場景在起做用 在 html 中

navigator.serviceWorker.controller.postMessage()

複製代碼

在 serviceWorker 中

self.addEventListener('message', function(e) {
  
})
複製代碼

可是這個方案是行不通的,由於 serviceWorker 一旦註冊,下次 PWA 啓動是在 Html 讀取成功以前,因此這個方案存在某些問題

方案二

PWA 能夠設置指定的做用域

navigator.serviceWorker.register('service-worker.js', {scope: './xxx'})
複製代碼

可是針對咱們的不一樣狀態的域名所有爲 plus.m.jd.com/index 因此這種方案也不太適合咱們。

方案三

在 service-worker 的 fetch 作攔截,經過判斷某些標誌,去控制頁面的讀取。好比經過 getUserInfo 接口返回的用戶狀態去判斷當前是否須要開啓 fetch 攔截,經過黑名單的方式去禁止掉在某些狀態下啓動 PWA,代碼以下所示:

self.addEventListener('fetch', function (event) {
    event.respondWith(
      caches.match(event.request).then(
        (response) =>
          response ||
          fetch(event.request.clone()).then(function (httpRes) {
            if (/getUserInfo/gi.test(event.request.url)) {
              httpRes.json().then((res) => {
                //do something 
              });
            }
            return httpRes;
          })
      )
    );
  });
複製代碼

通過上述處理,原來只要訪問過風控首頁,再訪問同域名下的全部狀態,都會通過 serviceWorker 攔截,以下圖所示:

img

通過修改以後,在其餘用戶狀態頁面上能夠禁止掉 PWA 的攔截,以下圖所示:

img

3.4 圖片處理

現現在網頁中圖片使用了大量的圖片,可以給用戶帶來更爲直接的視覺衝擊,做爲 PLUS 會員的入口,更是展現了大量的商品圖,如何在圖片處理上下功夫,咱們也用了些心思。 PLUS 會員頁面中的圖片使用的都是京東圖片系統,其中讓咱們眼前一亮的是,能夠在 url 上配置參數來處理圖片,譬如說:

http://img30.360buyimg.com/test/s720x540_jfs/t2362/199/2707005502/100242/616257ce/56e66b21N7b8c2be8.jpg

s720x540_jfs ,向業務名和文件地址間添加的參數,表示把圖片縮放到寬 720、高540; 直接向url後面添加 webp 後綴,則轉成訪問 webp 格式的圖片,這樣訪問服務器端的圖片,就能夠像如下操做了:

function imgCut(item, str) {
 if (/(((img){1}\d{2})|m{1}).360buyimg.com/.test(item)) {
  if (str) {
   item = item.replace('jfs', 's' + str + '_jfs');
  }
  if (check_support_webp()) {
   return item + '.webp'; //須要判斷支持webp的狀況下寫上webp後綴
  } else {
   return item;
  }
 } else {
  return item;
 }
}
複製代碼

按照上述方式,請求服務端的圖片,既能夠進行圖片的裁剪,保證頁面中圖片的尺寸一致,而且還能夠無縫轉換 webp 圖片,真是研發一大利器!注意的是,該功能是處理的向服務端請求的圖片,而不是前端本地提供的圖片。

總結

回首望去,PLUS 會員項目從最初的懵懂,轉眼間已經聚集了數十個複雜邏輯的頁面,迭代了多個版本,創建了數個分支。最近從項目中脫離出來,才發現缺乏從一個大局上把握項目的走向,保證一個項目可以歷經迭代需求,PLUS 會員項目仍有不少待以完善的地方,在此期間也收到了一些團隊的大力支持和很好的建議,以後咱們會繼續打磨下去,創建完善的機制,提升代碼質量,完善用戶體驗,爲 PLUS 會員保駕護航。

最後,用我最喜歡的一句話結尾 「我雖隻身前行,彷彿率領百萬雄兵。身在井隅,心向星光,眼裏有詩,自在遠方!」,對生活,對將來充滿但願,與君共勉之~

相關文章
相關標籤/搜索