記錄一次有難度的後臺重構&性能優化

有這麼一個需求,原來的一個後臺須要重構,前端展現爲這樣的: javascript

正如你所看到的,這個有添加有刪除功能,還須要長成這樣。後端給的數據格式通過簡化爲:

{
    "data": {
        "path": "data",
        "type": "dict",
        "showName": "文言文編輯",
        "value": null,
        "isNecessary": true,
        "subDefine": [
            {
                "path": "data/title",
                "type": "string",
                "showName": "標題",
                "value": "周亞夫軍細柳",
                "isNecessary": true,
                "subDefine": null
            },
            {
                "path": "data/book",
                "type": "list",
                "showName": "課本",
                "value": null,
                "isNecessary": true,
                "subDefine": [
                    {
                        "path": "data/book/book_0",
                        "type": "dict",
                        "showName": "1",
                        "value": null,
                        "isNecessary": false,
                        "subDefine": [
                            {
                                "path": "data/book/book_0/version",
                                "type": "string",
                                "showName": "教材",
                                "value": "人教新版",
                                "isNecessary": true,
                                "subDefine": null
                            }
                        ]
                    },
                    {
                        "path": "data/book/book_1",
                        "type": "dict",
                        "showName": "2",
                        "value": null,
                        "isNecessary": false,
                        "subDefine": [
                            {
                                "path": "data/book/book_1/version",
                                "type": "string",
                                "showName": "教材",
                                "value": "部編本",
                                "isNecessary": true,
                                "subDefine": null
                            }
                        ]
                    }
                ]
            }
        ]
    }
}
複製代碼

在看看各個參數的意義:html

  • path: 當前路徑
  • type: 表示類型
  • showName: 展現的字
  • value: 輸入框展現的內容
  • isNecessary: 是不是必須的
  • subDefine: 子元素,若是有就渲染子元素若是沒有就不渲染 後端怎麼把數據傳給個人,我就須要按這樣的格式傳給他,中間用戶可能修改value值,而後須要把這些值進行校驗,而且傳給後端。這個後臺是很古老的東西,具體是用jquery經過字符串拼接的方式,將數據拼接爲想要的html,在往真實的DOM中插入這些字符串,因此形成重複代碼不少,並且字符串相對於html書寫,可讀性更差。爲了加強可維護性,因此準備對其重構

如何繪製結構圖

如何把數據轉換爲上面的結構圖,這種結構圖該如何繪製,下面記錄個人心理路程~~:前端

jsx

拿到這個需求的時候那時候還不太瞭解嵌套組件這種思路,因此首先想到了可否使用jsx,由於當時認爲光靠html是沒法作到這種嵌套結構的。既然這種思路沒法作到,首先想到的能不能經過js遞歸調用的方式由於js更加靈活,最後返回一個html。這樣咱們就用了js替代了html來生成這種嵌套結構的html。那咱們就須要捨棄template轉而使用咱們本身定義的render函數。之前沒有用過jsx,因此先學習了一個下午,準備作點簡單的東西先試水一下,先在這個項目嘗試一些jsx的代碼,加上之後,發現編譯報錯,說我差一個loader,報錯以下:vue

找了一些緣由後,發現是在 vue.config.js中加入了

chainWebpack: (config) => {
    config.module.rules.delete('js'); 
}
複製代碼

這樣就不會對jsx進行編譯,可是和公司本身組件庫的設計有衝突,而後須要組件庫成員來修復這個問題,那麼這個項目可能就不能按時交付了。同時須要考慮到維護成本,vue中不多地方是使用jsx的,那後面的人維護這個是否是須要增長維護成本呢。那麼是否能夠選擇更優秀的方式來解決這個問題呢。java

插件

懶人有懶人的思考,個人第一反應就是找個插件啥的,啥都不用操心了,傳遞數據完事了。自己後臺也用的是element-ui,因此第一想法用一下tree插件,可是tree組件長這個樣 node

不符合產品設計的要求,可是咱們能夠看到的是,須要給 tree傳入的參數和後端傳給個人參數及其的類似,那是否能從 tree的實現中獲取經驗呢

tree組件實現原理

原理可參照element-ui的實現,這裏對其進行簡化jquery

<tree>
    <tree-node v-for="item in data">
    </tree-node>
</tree>
複製代碼

大概就是這個意思,外面是tree組件,裏邊是tree-node,如何實現多層嵌套呢,下面是tree-node組件的實現:算法

<template>
    <div class="tree-node">
        <tree-node v-if="data.children" ></tree-node>
    </div>
</template>
<script> export default { name: 'tree-node' } </script>
複製代碼

能夠看到這樣就實現了嵌套組件的效果,可是須要注意的是,必須須要聲明當前組件的name不然在當前文件中使用當前組件。chrome

總結

tree的實現中,能夠借鑑這種思路實現當前產品的需求,實現一個子組件,若是children存在,那麼就調用tree-node,若是不存在,就不須要渲染當前組件。首先咱們就實現了這種效果,可是這個需求沒有終結,我又遇到了新的問題--性能問題.element-ui

性能優化

事實上,在咱們寫代碼的時候不多遇到性能問題,可是此次確實發現了當咱們將數據傳給組件的時候,須要大量的時間上面的結構才能渲染出來。因此須要分析是什麼地方形成了這些性能問題。

性能分析

這裏藉助了chrome的性能分析工具performance,進行調用堆棧分析,看最消耗性能的是哪一個部分

使用方法很簡單
點擊這個地方,就能夠對當前頁面進行性能分析,下面是分析結果
具體各類參數可參考這篇文章 chrome-performance頁面性能分析使用教程 能夠看到 scripting佔據了大量的時間,在這個旁邊有一個 call-tree能夠看到是哪一個函數佔據的時候最多
在挨個點下去,看看最消耗時間的是哪一個環節,最後發現以下:
這個方法是 elementtextarea中用於自適應高度的一個函數,下面就是分析這個函數對性能的影響這麼大

組件優化

在需求中我是這麼調用的

<el-input
    autosize
    type="textarea">
</el-input>
複製代碼

autosize拖垮了頁面的性能,下面來看看autosize的實現。autosize最後調用的方式是resizeTextarea,下面來看看具體的實現

resizeTextarea() {
    const { autosize } = this;
    const { minRows, maxRows } = autosize;
    this.textareaCalcStyle = calcTextareaHeight(this.$refs.input, minRows, maxRows);
}
複製代碼

能夠看到的是最後調用的是calcTextareaHeight,具體看看他的實現

function calcTextareaHeight(tragetment, minRows, maxRows) {
    if (!hiddenTextarea) {
        hiddenTextarea = document.createElement('textarea');
        document.body.appendChild(hiddenTextarea)
    }
    const {
        paddingSize,
        borderSize,
        boxSizing,
        contextStyle,
    // 獲取元素的尺寸信息
    } = calculateNodeStyling(targetment)
    // 設置隱藏文本域的樣式
    hiddenTextarea.setAttribute('style', `${contextStyle};${HIDDEN_STYLE}`)
    hiddenTextarea.value = targetElement.value || targetElement.placeholder || '';
    let height = hiddenTextarea.scrollHeight; // 包含pading的高度
    if (boxSizing === 'border-box') {
        height += borderSize;
    } else if (boxSizing === 'content-box') {
        height -= padingSize
    }
    if (hiddenTextarea.parentNode) {
        hiddenTextarea.parentNode.remove(hiddenTextarea)
    }
    hiddenTextarea = null
    return { height: `${height}px`}
}
複製代碼

分析上面的函數,由於組件庫須要考慮的元素衆多,能夠須要加入一些對於咱們自身業務無關的代碼例如上面的代碼就有幾個地方能夠針對業務進行優化:

  1. 這裏經過calculateNodeStyling來獲取元素的一些屬性,這對於業務來講徹底是可控的,paddingborder咱們徹底能夠設置,而不須要用js獲取,在常說的性能優化中,最重要的就是避免對DOM的反覆操做,若是節省了這一步操做是否是效率可以獲得極大的提高呢
  2. 能夠看到這個是如何實現子適應高度的,建立一個咱們看不見的textarea,而且把如今的textarea的樣式賦值給隱藏的textarea從而來計算高度。這對於咱們只須要使用簡單功能的徹底沒有必要的只須要使用height=scrollHeight。而且在代碼咱們又把這個隱藏textarea從文檔流中移除,若是在文檔中有1000textarea中,是否是就須要建立textarea而後將其移除呢,上面提到操做DOM會形成性能的降低 有兩個緣由因此準備作一個簡單的輸入框知足咱們的需求
<textarea ref="textarea" v-model="value">
</textarea>
<script>
export default {
    mounted() {
        this.textarea = this.$refs.textarea;  
    },
    watch: value() {
        textarea.height = 0;
        textarea.height = textarea.scrollHeight;
    }
}
</script>
複製代碼

這樣就可以 簡單實現輸入框的高度隨內容改變而改變,並且去除了一些沒有必要的操做,使得性能大大的提升。

其餘的性能優化

除了更改上面組件的實現方式,這個需求中咱們是否有其餘的地方能夠進行優化

凍結數據

瞭解Vue源碼的都知道,當咱們對data中的值進行set操做,須要對新賦值的數據進行響應式設置,也就是從新定義數據的setget操做。可是在當前業務中,後端的值是一個不會更改的值,咱們對其進行響應式是否有必要嗎,而且這個數據是很是大的,若是對這個數據遞歸進行從新定義getset操做是否是自己就是一種消耗的性能,因此咱們並不須要對其進行以來收集,使用object.freeze就不會讓vue對這些數據進行從新定義settergetter

this.data = Object.freeze(data);
複製代碼

遞歸組件

Vue自己的原理決定了父子建立時生命週期子的前後順序爲:

父beforeCreated => 父created =>  父beforeMount => 子beforeCreated
=> 子created => 子beforeMount => 子mounted => 父mounted
複製代碼

當數據更新的時候父子週期的前後順序爲:

&emsp;父beforeUpdate->子beforeUpdate->子updated->父updated
複製代碼

爲何渲染這麼慢呢,就是由於整個組件須要等內部的子元素都渲染完成之後,才把整個父組件掛載到真實DOM,可是對於整個部分沒有太好的解決辦法

數據處理

由於在數據處理的時候,咱們對後端給的數據每條數據都進行了遍歷,在上面代碼中爲了給某條數據加一個required屬性,對數組進行了深度遍歷,這樣是爲了讓template中的表達式更加簡單。後端返回給咱們的數據可能及其龐大,進行遞歸可能就會影響性能,原則是能不算就不算。因此轉而在template中使用表達式來書寫判斷條件,可能這個表達式寫的很長,可是節約了性能。

需求具體實現

在需求中咱們可能須要對一個元素進行子類擴展或者刪除,那麼該如何實現呢。

刪除

刪除簡單,點擊刪除實際把該元素的父親的subDefine刪除最後一個元素,也就是把父元素的trees刪除最後一個元素,代碼以下:

trees.pop()
複製代碼

這樣就能刪除最後一個元素了

添加

後端在傳給前端的時候,除了一個data,還有一個minData, 這個minData的數據格式和data相同,不一樣的是每一項的value都是空的,若是該項能夠擴展,意思是說可以往subDefine中添加子元素,這個subDefine是不爲空的,可是隻有一個元素。這個數據在添加子元素的時候極爲的有用,好比說如今當前的元素的subDefine是個空的,當咱們向其中添加元素的時候,那這時候這個新元素的數據結構應該是怎麼樣的。這時就須要經過找到minData中哪個元素的path和當前的path是相同的。先前想過循環遍歷找到相同的,可是瞬間被本身否認了,雖然我們對算法沒什麼研究,可是也不能使用這麼low的想法吧。因此首先對mindData進行處理,在先前提到每一個元素的path都是不一樣的,那是否是能夠從新建立一個對象,其中的key就是每條數據的pathvalue就是該條數據。這時候但咱們須要添加一個新元素的時候,只須要知道對應的path,而後從minData中取出key等於path的那條數據,而後取出那條數據的subDefine的第一條數據就好了。
下面是minData的數據處理函數:

constructPathObj(subDefine, res = {}) {
  subDefine.forEach((value) => {
    res[value.path] = value;
    if (value.subDefine) {
      this.constructPathObj(value.subDefine, res);
    }
  });
  return res;
}
minData = constructPathObj(data)
複製代碼

這樣就獲得了一個已pathkey,數據爲value的一個對象。這裏還須要注意一點就是由於前面提到path是惟一的,因此在添加新元素的時候不可以讓path重複。例如如今subDefine中有一個元素的pathdata/book_1,後端要求新添加的元素pathdata/book_2,因此有了如下代碼

const { subDefine } = item;
let index;
if (subDefine.length === 0) {
  // 根據path找到子元素
  index = 0;
} else {
  index = subDefine.length;
}
const temp = this.dealData(minData[information.path].subDefine[0], index);
subDefine.push(temp);

function dealData(data, index) {
  const temp = {};
  if (data.subDefine) {
    temp.subDefine = [];
    data.subDefine.forEach((val) => {
      // 先對傳給後面的數據path進行處理
      val.path = val.path.replace(/(.*)_[0-9]/, `$1_${data.showName}`);
      temp.subDefine.push(this.dealData(val));
    });
  }
  if (data.type === 'dict') {
    temp.showName = index + 1;
    temp.path = data.path.replace(/(.*)_[0-9]/, `$1_${index}`);
  }
  return {
    ...data,
    ...temp,
  };
}
複製代碼

這樣就會對生成的每一個元素的path進行規範,也就到達了添加一個新元素的一個效果。

總結

由於是項目重構,因此須要現有的一個接口,那麼接口的內容就不能變更。那就須要在原有的數據結構上進行修改,把這些數據處理爲咱們想要的數據格式。在實現項目的過程當中遇到了一些性能問題,並對其分析產生這些的緣由並進行解決,加深了對需求的理解,也提升對性能優化這塊的重視。

相關文章
相關標籤/搜索