<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          手把手帶你入門前端工程化——超詳細(xì)教程(高級(jí)前端必備)

          共 21807字,需瀏覽 44分鐘

           ·

          2021-03-13 10:33

          本文將分成以下 7 個(gè)小節(jié):

          1. 技術(shù)選型

          2. 統(tǒng)一規(guī)范

          3. 測(cè)試

          4. 部署

          5. 監(jiān)控

          6. 性能優(yōu)化

          7. 重構(gòu)

          部分小節(jié)提供了非常詳細(xì)的實(shí)戰(zhàn)教程,讓大家動(dòng)手實(shí)踐。

          另外我還寫了一個(gè)前端工程化 demo 放在 github 上。這個(gè) demo 包含了 js、css、git 驗(yàn)證,其中 js、css 驗(yàn)證需要安裝 VSCode,具體教程在下文中會(huì)有提及。

          技術(shù)選型

          對(duì)于前端來(lái)說(shuō),技術(shù)選型挺簡(jiǎn)單的。就是做選擇題,三大框架中選一個(gè)。個(gè)人認(rèn)為可以依據(jù)以下兩個(gè)特點(diǎn)來(lái)選:

          1. 選你或團(tuán)隊(duì)最熟的,保證在遇到棘手的問(wèn)題時(shí)有人能填坑。

          2. 選市場(chǎng)占有率高的。換句話說(shuō),就是選好招人的。

          第二點(diǎn)對(duì)于小公司來(lái)說(shuō),特別重要。本來(lái)小公司就不好招人,要是還選一個(gè)市場(chǎng)占有率不高的框架(例如 Angular),簡(jiǎn)歷你都看不到幾個(gè)...

          UI 組件庫(kù)更簡(jiǎn)單,github 上哪個(gè) star 多就用哪個(gè)。star 多,說(shuō)明用的人就多,很多坑別人都替你踩過(guò)了,省事。

          統(tǒng)一規(guī)范

          代碼規(guī)范

          先來(lái)看看統(tǒng)一代碼規(guī)范的好處:

          • 規(guī)范的代碼可以促進(jìn)團(tuán)隊(duì)合作

          • 規(guī)范的代碼可以降低維護(hù)成本

          • 規(guī)范的代碼有助于 code review(代碼審查)

          • 養(yǎng)成代碼規(guī)范的習(xí)慣,有助于程序員自身的成長(zhǎng)

          當(dāng)團(tuán)隊(duì)的成員都嚴(yán)格按照代碼規(guī)范來(lái)寫代碼時(shí),可以保證每個(gè)人的代碼看起來(lái)都像是一個(gè)人寫的,看別人的代碼就像是在看自己的代碼。更重要的是我們能夠認(rèn)識(shí)到規(guī)范的重要性,并堅(jiān)持規(guī)范的開(kāi)發(fā)習(xí)慣。

          如何制訂代碼規(guī)范

          建議找一份好的代碼規(guī)范,在此基礎(chǔ)上結(jié)合團(tuán)隊(duì)的需求作個(gè)性化修改。

          下面列舉一些 star 較多的 js 代碼規(guī)范:

          • airbnb (101k star 英文版),airbnb-中文版

          • standard (24.5k star) 中文版

          • 百度前端編碼規(guī)范 3.9k

          css 代碼規(guī)范也有不少,例如:

          • styleguide 2.3k

          • spec 3.9k

          如何檢查代碼規(guī)范

          使用 eslint 可以檢查代碼符不符合團(tuán)隊(duì)制訂的規(guī)范,下面來(lái)看一下如何配置 eslint 來(lái)檢查代碼。

          1. 下載依賴

          // eslint-config-airbnb-base 使用 airbnb 代碼規(guī)范
          npm i -D babel-eslint eslint eslint-config-airbnb-base eslint-plugin-import
          1. 在 package.json 的 scripts 加上這行代碼 "lint": "eslint --ext .js test/ src/ bin/"。然后執(zhí)行 npm run lint 即可開(kāi)始驗(yàn)證代碼。

          不過(guò)這樣檢查代碼效率太低,每次都得手動(dòng)檢查。并且報(bào)錯(cuò)了還得手動(dòng)修改代碼。

          為了改善以上缺點(diǎn),我們可以使用 VSCode。使用它并加上適當(dāng)?shù)呐渲每梢栽诿看伪4娲a的時(shí)候,自動(dòng)驗(yàn)證代碼并進(jìn)行格式化,省去了動(dòng)手的麻煩。

          css 檢查代碼規(guī)范則使用 stylelint 插件。

          由于篇幅有限,具體如何配置請(qǐng)看我的另一篇文章ESlint + stylelint + VSCode自動(dòng)格式化代碼(2020)。

          git 規(guī)范

          git 規(guī)范包括兩點(diǎn):分支管理規(guī)范、git commit 規(guī)范。

          分支管理規(guī)范

          一般項(xiàng)目分主分支(master)和其他分支。

          當(dāng)有團(tuán)隊(duì)成員要開(kāi)發(fā)新功能或改 BUG 時(shí),就從 master 分支開(kāi)一個(gè)新的分支。例如項(xiàng)目要從客戶端渲染改成服務(wù)端渲染,就開(kāi)一個(gè)分支叫 ssr,開(kāi)發(fā)完了再合并回 master 分支。

          如果改一個(gè) BUG,也可以從 master 分支開(kāi)一個(gè)新分支,并用 BUG 號(hào)命名(不過(guò)我們小團(tuán)隊(duì)嫌麻煩,沒(méi)這樣做,除非有特別大的 BUG)。

          git commit 規(guī)范

          <type>(<scope>): <subject>
          <BLANK LINE>
          <body>
          <BLANK LINE>
          <footer>

          大致分為三個(gè)部分(使用空行分割):

          1. 標(biāo)題行: 必填, 描述主要修改類型和內(nèi)容

          2. 主題內(nèi)容: 描述為什么修改, 做了什么樣的修改, 以及開(kāi)發(fā)的思路等等

          3. 頁(yè)腳注釋: 可以寫注釋,BUG 號(hào)鏈接

          type: commit 的類型

          • feat: 新功能、新特性

          • fix: 修改 bug

          • perf: 更改代碼,以提高性能

          • refactor: 代碼重構(gòu)(重構(gòu),在不影響代碼內(nèi)部行為、功能下的代碼修改)

          • docs: 文檔修改

          • style: 代碼格式修改, 注意不是 css 修改(例如分號(hào)修改)

          • test: 測(cè)試用例新增、修改

          • build: 影響項(xiàng)目構(gòu)建或依賴項(xiàng)修改

          • revert: 恢復(fù)上一次提交

          • ci: 持續(xù)集成相關(guān)文件修改

          • chore: 其他修改(不在上述類型中的修改)

          • release: 發(fā)布新版本

          • workflow: 工作流相關(guān)文件修改

          1. scope: commit 影響的范圍, 比如: route, component, utils, build...

          2. subject: commit 的概述

          3. body: commit 具體修改內(nèi)容, 可以分為多行.

          4. footer: 一些備注, 通常是 BREAKING CHANGE 或修復(fù)的 bug 的鏈接.

          示例

          fix(修復(fù)BUG)

          如果修復(fù)的這個(gè)BUG只影響當(dāng)前修改的文件,可不加范圍。如果影響的范圍比較大,要加上范圍描述。

          例如這次 BUG 修復(fù)影響到全局,可以加個(gè) global。如果影響的是某個(gè)目錄或某個(gè)功能,可以加上該目錄的路徑,或者對(duì)應(yīng)的功能名稱。

          // 示例1
          fix(global):修復(fù)checkbox不能復(fù)選的問(wèn)題
          // 示例2 下面圓括號(hào)里的 common 為通用管理的名稱
          fix(common): 修復(fù)字體過(guò)小的BUG,將通用管理下所有頁(yè)面的默認(rèn)字體大小修改為 14px
          // 示例3
          fix: value.length -> values.length
          feat(添加新功能或新頁(yè)面)
          feat: 添加網(wǎng)站主頁(yè)靜態(tài)頁(yè)面

          這是一個(gè)示例,假設(shè)對(duì)點(diǎn)檢任務(wù)靜態(tài)頁(yè)面進(jìn)行了一些描述。

          這里是備注,可以是放BUG鏈接或者一些重要性的東西。
          chore(其他修改)

          chore 的中文翻譯為日常事務(wù)、例行工作,顧名思義,即不在其他 commit 類型中的修改,都可以用 chore 表示。

          chore: 將表格中的查看詳情改為詳情

          其他類型的 commit 和上面三個(gè)示例差不多,就不說(shuō)了。

          驗(yàn)證 git commit 規(guī)范

          驗(yàn)證 git commit 規(guī)范,主要通過(guò) git 的 pre-commit 鉤子函數(shù)來(lái)進(jìn)行。當(dāng)然,你還需要下載一個(gè)輔助工具來(lái)幫助你進(jìn)行驗(yàn)證。

          下載輔助工具

          npm i -D husky

          在 package.json 加上下面的代碼

          "husky": {
          "hooks": {
          "pre-commit": "npm run lint",
          "commit-msg": "node script/verify-commit.js",
          "pre-push": "npm test"
          }
          }

          然后在你項(xiàng)目根目錄下新建一個(gè)文件夾 script,并在下面新建一個(gè)文件 verify-commit.js,輸入以下代碼:

          const msgPath = process.env.HUSKY_GIT_PARAMS
          const msg = require('fs')
          .readFileSync(msgPath, 'utf-8')
          .trim()

          const commitRE = /^(feat|fix|docs|style|refactor|perf|test|workflow|build|ci|chore|release|workflow)(\(.+\))?: .{1,50}/

          if (!commitRE.test(msg)) {
          console.log()
          console.error(`
          不合法的 commit 消息格式。
          請(qǐng)查看 git commit 提交規(guī)范:https://github.com/woai3c/Front-end-articles/blob/master/git%20commit%20style.md
          `
          )

          process.exit(1)
          }

          現(xiàn)在來(lái)解釋下各個(gè)鉤子的含義:

          1. "pre-commit": "npm run lint",在 git commit 前執(zhí)行 npm run lint 檢查代碼格式。

          2. "commit-msg": "node script/verify-commit.js",在 git commit 時(shí)執(zhí)行腳本 verify-commit.js 驗(yàn)證 commit 消息。如果不符合腳本中定義的格式,將會(huì)報(bào)錯(cuò)。

          3. "pre-push": "npm test",在你執(zhí)行 git push 將代碼推送到遠(yuǎn)程倉(cāng)庫(kù)前,執(zhí)行 npm test 進(jìn)行測(cè)試。如果測(cè)試失敗,將不會(huì)執(zhí)行這次推送。

          項(xiàng)目規(guī)范

          主要是項(xiàng)目文件的組織方式和命名方式。

          用我們的 Vue 項(xiàng)目舉個(gè)例子。

          ├─public
          ├─src
          ├─test

          一個(gè)項(xiàng)目包含 public(公共資源,不會(huì)被 webpack 處理)、src(源碼)、test(測(cè)試代碼),其中 src 目錄,又可以細(xì)分。

          ├─api (接口)
          ├─assets (靜態(tài)資源)
          ├─components (公共組件)
          ├─styles (公共樣式)
          ├─router (路由)
          ├─store (vuex 全局?jǐn)?shù)據(jù))
          ├─utils (工具函數(shù))
          └─views (頁(yè)面)

          文件名稱如果過(guò)長(zhǎng)則用 - 隔開(kāi)。

          UI 規(guī)范

          UI 規(guī)范需要前端、UI、產(chǎn)品溝通,互相商量,最后制定下來(lái),建議使用統(tǒng)一的 UI 組件庫(kù)。

          制定 UI 規(guī)范的好處:

          • 統(tǒng)一頁(yè)面 UI 標(biāo)準(zhǔn),節(jié)省 UI 設(shè)計(jì)時(shí)間

          • 提高前端開(kāi)發(fā)效率

          測(cè)試

          測(cè)試是前端工程化建設(shè)必不可少的一部分,它的作用就是找出 bug,越早發(fā)現(xiàn) bug,所需要付出的成本就越低。并且,它更重要的作用是在將來(lái),而不是當(dāng)下。

          設(shè)想一下半年后,你的項(xiàng)目要加一個(gè)新功能。在加完新功能后,你不確定有沒(méi)有影響到原有的功能,需要測(cè)試一下。由于時(shí)間過(guò)去太久,你對(duì)項(xiàng)目的代碼已經(jīng)不了解了。在這種情況下,如果沒(méi)有寫測(cè)試,你就得手動(dòng)一遍一遍的去試。而如果寫了測(cè)試,你只需要跑一遍測(cè)試代碼就 OK 了,省時(shí)省力。

          寫測(cè)試還可以讓你修改代碼時(shí)沒(méi)有心理負(fù)擔(dān),不用一直想著改這里有沒(méi)有問(wèn)題?會(huì)不會(huì)引起 BUG?而寫了測(cè)試就沒(méi)有這種擔(dān)心了。

          在前端用得最多的就是單元測(cè)試(主要是端到端測(cè)試我用得很少,不熟),這里著重講解一下。

          單元測(cè)試

          單元測(cè)試就是對(duì)一個(gè)函數(shù)、一個(gè)組件、一個(gè)類做的測(cè)試,它針對(duì)的粒度比較小。

          它應(yīng)該怎么寫呢?

          1. 根據(jù)正確性寫測(cè)試,即正確的輸入應(yīng)該有正常的結(jié)果。

          2. 根據(jù)異常寫測(cè)試,即錯(cuò)誤的輸入應(yīng)該是錯(cuò)誤的結(jié)果。

          對(duì)一個(gè)函數(shù)做測(cè)試

          例如一個(gè)取絕對(duì)值的函數(shù) abs(),輸入 1,2,結(jié)果應(yīng)該與輸入相同;輸入 -1,-2,結(jié)果應(yīng)該與輸入相反。如果輸入非數(shù)字,例如 "abc",應(yīng)該拋出一個(gè)類型錯(cuò)誤。

          對(duì)一個(gè)類做測(cè)試

          假設(shè)有這樣一個(gè)類:

          class Math {
          abs() {

          }

          sqrt() {

          }

          pow() {

          }
          ...
          }

          單元測(cè)試,必須把這個(gè)類的所有方法都測(cè)一遍。

          對(duì)一個(gè)組件做測(cè)試

          組件測(cè)試比較難,因?yàn)楹芏嘟M件都涉及了 DOM 操作。

          例如一個(gè)上傳圖片組件,它有一個(gè)將圖片轉(zhuǎn)成 base64 碼的方法,那要怎么測(cè)試呢?一般測(cè)試都是跑在 node 環(huán)境下的,而 node 環(huán)境沒(méi)有 DOM 對(duì)象。

          我們先來(lái)回顧一下上傳圖片的過(guò)程:

          1. 點(diǎn)擊 <input type="file" />,選擇圖片上傳。

          2. 觸發(fā) input 的 change 事件,獲取 file 對(duì)象。

          3. 用 FileReader 將圖片轉(zhuǎn)換成 base64 碼。

          這個(gè)過(guò)程和下面的代碼是一樣的:

          document.querySelector('input').onchange = function fileChangeHandler(e) {
          const file = e.target.files[0]
          const reader = new FileReader()
          reader.onload = (res) => {
          const fileResult = res.target.result
          console.log(fileResult) // 輸出 base64 碼
          }

          reader.readAsDataURL(file)
          }

          上面的代碼只是模擬,真實(shí)情況下應(yīng)該是這樣使用

          document.querySelector('input').onchange = function fileChangeHandler(e) {
          const file = e.target.files[0]
          tobase64(file)
          }

          function tobase64(file) {
          return new Promise((resolve, reject) => {
          const reader = new FileReader()
          reader.onload = (res) => {
          const fileResult = res.target.result
          resolve(fileResult) // 輸出 base64 碼
          }

          reader.readAsDataURL(file)
          })
          }

          可以看到,上面代碼出現(xiàn)了 window 的事件對(duì)象 eventFileReader。也就是說(shuō),只要我們能夠提供這兩個(gè)對(duì)象,就可以在任何環(huán)境下運(yùn)行它。所以我們可以在測(cè)試環(huán)境下加上這兩個(gè)對(duì)象:

          // 重寫 File
          window.File = function () {}

          // 重寫 FileReader
          window.FileReader = function () {
          this.readAsDataURL = function () {
          this.onload
          && this.onload({
          target: {
          result: fileData,
          },
          })
          }
          }

          然后測(cè)試可以這樣寫:

          // 提前寫好文件內(nèi)容
          const fileData = 'data:image/test'

          // 提供一個(gè)假的 file 對(duì)象給 tobase64() 函數(shù)
          function test() {
          const file = new File()
          const event = { target: { files: [file] } }
          file.type = 'image/png'
          file.name = 'test.png'
          file.size = 1024

          it('file content', (done) => {
          tobase64(file).then(base64 => {
          expect(base64).toEqual(fileData) // 'data:image/test'
          done()
          })
          })
          }

          // 執(zhí)行測(cè)試
          test()

          通過(guò)這種 hack 的方式,我們就實(shí)現(xiàn)了對(duì)涉及 DOM 操作的組件的測(cè)試。我的 vue-upload-imgs 庫(kù)就是通過(guò)這種方式寫的單元測(cè)試,有興趣可以了解一下。

          TDD 測(cè)試驅(qū)動(dòng)開(kāi)發(fā)

          TDD 就是根據(jù)需求提前把測(cè)試代碼寫好,然后根據(jù)測(cè)試代碼實(shí)現(xiàn)功能。

          TDD 的初衷是好的,但如果你的需求經(jīng)常變(你懂的),那就不是一件好事了。很有可能你天天都在改測(cè)試代碼,業(yè)務(wù)代碼反而沒(méi)怎么動(dòng)。
          所以到現(xiàn)在為止,三年多的程序員生涯,我還沒(méi)嘗試過(guò) TDD 開(kāi)發(fā)。

          雖然環(huán)境如此艱難,但有條件的情況下還是應(yīng)該試一下 TDD 的。例如在你自己負(fù)責(zé)一個(gè)項(xiàng)目又不忙的時(shí)候,可以采用此方法編寫測(cè)試用例。

          測(cè)試框架推薦

          我常用的測(cè)試框架是 jest,好處是有中文文檔,API 清晰明了,一看就知道是干什么用的。

          部署

          在沒(méi)有學(xué)會(huì)自動(dòng)部署前,我是這樣部署項(xiàng)目的:

          1. 執(zhí)行測(cè)試 npm run test

          2. 推送代碼 git push

          3. 構(gòu)建項(xiàng)目 npm run build

          4. 將打包好的文件放到靜態(tài)服務(wù)器。

          一次兩次還行,如果天天都這樣,就會(huì)把很多時(shí)間浪費(fèi)在重復(fù)的操作上。所以我們要學(xué)會(huì)自動(dòng)部署,徹底解放雙手。

          自動(dòng)部署(又叫持續(xù)部署 Continuous Deployment,英文縮寫 CD)一般有兩種觸發(fā)方式:

          1. 輪詢。

          2. 監(jiān)聽(tīng) webhook 事件。

          輪詢

          輪詢,就是構(gòu)建軟件每隔一段時(shí)間自動(dòng)執(zhí)行打包、部署操作。

          這種方式不太好,很有可能軟件剛部署完我就改代碼了。為了看到新的頁(yè)面效果,不得不等到下一次構(gòu)建開(kāi)始。

          另外還有一個(gè)副作用,假如我一天都沒(méi)更改代碼,構(gòu)建軟件還是會(huì)不停的執(zhí)行打包、部署操作,白白的浪費(fèi)資源。

          所以現(xiàn)在的構(gòu)建軟件基本采用監(jiān)聽(tīng) webhook 事件的方式來(lái)進(jìn)行部署。

          監(jiān)聽(tīng) webhook 事件

          webhook 鉤子函數(shù),就是在你的構(gòu)建軟件上進(jìn)行設(shè)置,監(jiān)聽(tīng)某一個(gè)事件(一般是監(jiān)聽(tīng) push 事件),當(dāng)事件觸發(fā)時(shí),自動(dòng)執(zhí)行定義好的腳本。

          例如 Github Actions,就有這個(gè)功能。

          對(duì)于新人來(lái)說(shuō),僅看我這一段講解是不可能學(xué)會(huì)自動(dòng)部署的。為此我特地寫了一篇自動(dòng)化部署教程,不需要你提前學(xué)習(xí)自動(dòng)化部署的知識(shí),只要照著指引做,就能實(shí)現(xiàn)前端項(xiàng)目自動(dòng)化部署。

          前端項(xiàng)目自動(dòng)化部署——超詳細(xì)教程(Jenkins、Github Actions),教程已經(jīng)奉上,各位大佬看完后要是覺(jué)得有用,不要忘了點(diǎn)贊,感激不盡。

          監(jiān)控

          監(jiān)控,又分性能監(jiān)控和錯(cuò)誤監(jiān)控,它的作用是預(yù)警和追蹤定位問(wèn)題。

          性能監(jiān)控

          性能監(jiān)控一般利用 window.performance 來(lái)進(jìn)行數(shù)據(jù)采集。

          Performance 接口可以獲取到當(dāng)前頁(yè)面中與性能相關(guān)的信息,它是 High Resolution Time API 的一部分,同時(shí)也融合了 Performance Timeline API、Navigation Timing API、 User Timing API 和 Resource Timing API。

          這個(gè) API 的屬性 timing,包含了頁(yè)面加載各個(gè)階段的起始及結(jié)束時(shí)間。


          為了方便大家理解 timing 各個(gè)屬性的意義,我在知乎找到一位網(wǎng)友對(duì)于 timing 寫的簡(jiǎn)介(忘了姓名,后來(lái)找不到了,見(jiàn)諒),在此轉(zhuǎn)載一下。

          timing: {
          // 同一個(gè)瀏覽器上一個(gè)頁(yè)面卸載(unload)結(jié)束時(shí)的時(shí)間戳。如果沒(méi)有上一個(gè)頁(yè)面,這個(gè)值會(huì)和fetchStart相同。
          navigationStart: 1543806782096,

          // 上一個(gè)頁(yè)面unload事件拋出時(shí)的時(shí)間戳。如果沒(méi)有上一個(gè)頁(yè)面,這個(gè)值會(huì)返回0。
          unloadEventStart: 1543806782523,

          // 和 unloadEventStart 相對(duì)應(yīng),unload事件處理完成時(shí)的時(shí)間戳。如果沒(méi)有上一個(gè)頁(yè)面,這個(gè)值會(huì)返回0。
          unloadEventEnd: 1543806782523,

          // 第一個(gè)HTTP重定向開(kāi)始時(shí)的時(shí)間戳。如果沒(méi)有重定向,或者重定向中的一個(gè)不同源,這個(gè)值會(huì)返回0。
          redirectStart: 0,

          // 最后一個(gè)HTTP重定向完成時(shí)(也就是說(shuō)是HTTP響應(yīng)的最后一個(gè)比特直接被收到的時(shí)間)的時(shí)間戳。
          // 如果沒(méi)有重定向,或者重定向中的一個(gè)不同源,這個(gè)值會(huì)返回0.
          redirectEnd: 0,

          // 瀏覽器準(zhǔn)備好使用HTTP請(qǐng)求來(lái)獲取(fetch)文檔的時(shí)間戳。這個(gè)時(shí)間點(diǎn)會(huì)在檢查任何應(yīng)用緩存之前。
          fetchStart: 1543806782096,

          // DNS 域名查詢開(kāi)始的UNIX時(shí)間戳。
          //如果使用了持續(xù)連接(persistent connection),或者這個(gè)信息存儲(chǔ)到了緩存或者本地資源上,這個(gè)值將和fetchStart一致。
          domainLookupStart: 1543806782096,

          // DNS 域名查詢完成的時(shí)間.
          //如果使用了本地緩存(即無(wú) DNS 查詢)或持久連接,則與 fetchStart 值相等
          domainLookupEnd: 1543806782096,

          // HTTP(TCP) 域名查詢結(jié)束的時(shí)間戳。
          //如果使用了持續(xù)連接(persistent connection),或者這個(gè)信息存儲(chǔ)到了緩存或者本地資源上,這個(gè)值將和 fetchStart一致。
          connectStart: 1543806782099,

          // HTTP(TCP) 返回瀏覽器與服務(wù)器之間的連接建立時(shí)的時(shí)間戳。
          // 如果建立的是持久連接,則返回值等同于fetchStart屬性的值。連接建立指的是所有握手和認(rèn)證過(guò)程全部結(jié)束。
          connectEnd: 1543806782227,

          // HTTPS 返回瀏覽器與服務(wù)器開(kāi)始安全鏈接的握手時(shí)的時(shí)間戳。如果當(dāng)前網(wǎng)頁(yè)不要求安全連接,則返回0。
          secureConnectionStart: 1543806782162,

          // 返回瀏覽器向服務(wù)器發(fā)出HTTP請(qǐng)求時(shí)(或開(kāi)始讀取本地緩存時(shí))的時(shí)間戳。
          requestStart: 1543806782241,

          // 返回瀏覽器從服務(wù)器收到(或從本地緩存讀取)第一個(gè)字節(jié)時(shí)的時(shí)間戳。
          //如果傳輸層在開(kāi)始請(qǐng)求之后失敗并且連接被重開(kāi),該屬性將會(huì)被數(shù)制成新的請(qǐng)求的相對(duì)應(yīng)的發(fā)起時(shí)間。
          responseStart: 1543806782516,

          // 返回瀏覽器從服務(wù)器收到(或從本地緩存讀取,或從本地資源讀取)最后一個(gè)字節(jié)時(shí)
          //(如果在此之前HTTP連接已經(jīng)關(guān)閉,則返回關(guān)閉時(shí))的時(shí)間戳。
          responseEnd: 1543806782537,

          // 當(dāng)前網(wǎng)頁(yè)DOM結(jié)構(gòu)開(kāi)始解析時(shí)(即Document.readyState屬性變?yōu)椤發(fā)oading”、相應(yīng)的 readystatechange事件觸發(fā)時(shí))的時(shí)間戳。
          domLoading: 1543806782573,

          // 當(dāng)前網(wǎng)頁(yè)DOM結(jié)構(gòu)結(jié)束解析、開(kāi)始加載內(nèi)嵌資源時(shí)(即Document.readyState屬性變?yōu)椤癷nteractive”、相應(yīng)的readystatechange事件觸發(fā)時(shí))的時(shí)間戳。
          domInteractive: 1543806783203,

          // 當(dāng)解析器發(fā)送DOMContentLoaded 事件,即所有需要被執(zhí)行的腳本已經(jīng)被解析時(shí)的時(shí)間戳。
          domContentLoadedEventStart: 1543806783203,

          // 當(dāng)所有需要立即執(zhí)行的腳本已經(jīng)被執(zhí)行(不論執(zhí)行順序)時(shí)的時(shí)間戳。
          domContentLoadedEventEnd: 1543806783216,

          // 當(dāng)前文檔解析完成,即Document.readyState 變?yōu)?'complete'且相對(duì)應(yīng)的readystatechange 被觸發(fā)時(shí)的時(shí)間戳
          domComplete: 1543806783796,

          // load事件被發(fā)送時(shí)的時(shí)間戳。如果這個(gè)事件還未被發(fā)送,它的值將會(huì)是0。
          loadEventStart: 1543806783796,

          // 當(dāng)load事件結(jié)束,即加載事件完成時(shí)的時(shí)間戳。如果這個(gè)事件還未被發(fā)送,或者尚未完成,它的值將會(huì)是0.
          loadEventEnd: 1543806783802
          }

          通過(guò)以上數(shù)據(jù),我們可以得到幾個(gè)有用的時(shí)間

          // 重定向耗時(shí)
          redirect: timing.redirectEnd - timing.redirectStart,
          // DOM 渲染耗時(shí)
          dom: timing.domComplete - timing.domLoading,
          // 頁(yè)面加載耗時(shí)
          load: timing.loadEventEnd - timing.navigationStart,
          // 頁(yè)面卸載耗時(shí)
          unload: timing.unloadEventEnd - timing.unloadEventStart,
          // 請(qǐng)求耗時(shí)
          request: timing.responseEnd - timing.requestStart,
          // 獲取性能信息時(shí)當(dāng)前時(shí)間
          time: new Date().getTime(),

          還有一個(gè)比較重要的時(shí)間就是白屏?xí)r間,它指從輸入網(wǎng)址,到頁(yè)面開(kāi)始顯示內(nèi)容的時(shí)間。

          將以下腳本放在 </head> 前面就能獲取白屏?xí)r間。

          <script>
          whiteScreen = new Date() - performance.timing.navigationStart
          </script>

          通過(guò)這幾個(gè)時(shí)間,就可以得知頁(yè)面首屏加載性能如何了。

          另外,通過(guò) window.performance.getEntriesByType('resource') 這個(gè)方法,我們還可以獲取相關(guān)資源(js、css、img...)的加載時(shí)間,它會(huì)返回頁(yè)面當(dāng)前所加載的所有資源。

          它一般包括以下幾個(gè)類型

          • sciprt

          • link

          • img

          • css

          • fetch

          • other

          • xmlhttprequest

          我們只需用到以下幾個(gè)信息

          // 資源的名稱
          name: item.name,
          // 資源加載耗時(shí)
          duration: item.duration.toFixed(2),
          // 資源大小
          size: item.transferSize,
          // 資源所用協(xié)議
          protocol: item.nextHopProtocol,

          現(xiàn)在,寫幾行代碼來(lái)收集這些數(shù)據(jù)。

          // 收集性能信息
          const getPerformance = () => {
          if (!window.performance) return
          const timing = window.performance.timing
          const performance = {
          // 重定向耗時(shí)
          redirect: timing.redirectEnd - timing.redirectStart,
          // 白屏?xí)r間
          whiteScreen: whiteScreen,
          // DOM 渲染耗時(shí)
          dom: timing.domComplete - timing.domLoading,
          // 頁(yè)面加載耗時(shí)
          load: timing.loadEventEnd - timing.navigationStart,
          // 頁(yè)面卸載耗時(shí)
          unload: timing.unloadEventEnd - timing.unloadEventStart,
          // 請(qǐng)求耗時(shí)
          request: timing.responseEnd - timing.requestStart,
          // 獲取性能信息時(shí)當(dāng)前時(shí)間
          time: new Date().getTime(),
          }

          return performance
          }

          // 獲取資源信息
          const getResources = () => {
          if (!window.performance) return
          const data = window.performance.getEntriesByType('resource')
          const resource = {
          xmlhttprequest: [],
          css: [],
          other: [],
          script: [],
          img: [],
          link: [],
          fetch: [],
          // 獲取資源信息時(shí)當(dāng)前時(shí)間
          time: new Date().getTime(),
          }

          data.forEach(item => {
          const arry = resource[item.initiatorType]
          arry && arry.push({
          // 資源的名稱
          name: item.name,
          // 資源加載耗時(shí)
          duration: item.duration.toFixed(2),
          // 資源大小
          size: item.transferSize,
          // 資源所用協(xié)議
          protocol: item.nextHopProtocol,
          })
          })

          return resource
          }

          小結(jié)

          通過(guò)對(duì)性能及資源信息的解讀,我們可以判斷出頁(yè)面加載慢有以下幾個(gè)原因:

          1. 資源過(guò)多

          2. 網(wǎng)速過(guò)慢

          3. DOM元素過(guò)多

          除了用戶網(wǎng)速過(guò)慢,我們沒(méi)辦法之外,其他兩個(gè)原因都是有辦法解決的,性能優(yōu)化將在下一節(jié)《性能優(yōu)化》中會(huì)講到。

          錯(cuò)誤監(jiān)控

          現(xiàn)在能捕捉的錯(cuò)誤有三種。

          1. 資源加載錯(cuò)誤,通過(guò) addEventListener('error', callback, true) 在捕獲階段捕捉資源加載失敗錯(cuò)誤。

          2. js 執(zhí)行錯(cuò)誤,通過(guò) window.onerror 捕捉 js 錯(cuò)誤。

          3. promise 錯(cuò)誤,通過(guò) addEventListener('unhandledrejection', callback)捕捉 promise 錯(cuò)誤,但是沒(méi)有發(fā)生錯(cuò)誤的行數(shù),列數(shù)等信息,只能手動(dòng)拋出相關(guān)錯(cuò)誤信息。

          我們可以建一個(gè)錯(cuò)誤數(shù)組變量 errors 在錯(cuò)誤發(fā)生時(shí),將錯(cuò)誤的相關(guān)信息添加到數(shù)組,然后在某個(gè)階段統(tǒng)一上報(bào),具體如何操作請(qǐng)看代碼

          // 捕獲資源加載失敗錯(cuò)誤 js css img...
          addEventListener('error', e => {
          const target = e.target
          if (target != window) {
          monitor.errors.push({
          type: target.localName,
          url: target.src || target.href,
          msg: (target.src || target.href) + ' is load error',
          // 錯(cuò)誤發(fā)生的時(shí)間
          time: new Date().getTime(),
          })
          }
          }, true)

          // 監(jiān)聽(tīng) js 錯(cuò)誤
          window.onerror = function(msg, url, row, col, error) {
          monitor.errors.push({
          type: 'javascript',
          row: row,
          col: col,
          msg: error && error.stack? error.stack : msg,
          url: url,
          // 錯(cuò)誤發(fā)生的時(shí)間
          time: new Date().getTime(),
          })
          }

          // 監(jiān)聽(tīng) promise 錯(cuò)誤 缺點(diǎn)是獲取不到行數(shù)數(shù)據(jù)
          addEventListener('unhandledrejection', e => {
          monitor.errors.push({
          type: 'promise',
          msg: (e.reason && e.reason.msg) || e.reason || '',
          // 錯(cuò)誤發(fā)生的時(shí)間
          time: new Date().getTime(),
          })
          })

          小結(jié)

          通過(guò)錯(cuò)誤收集,可以了解到網(wǎng)站錯(cuò)誤發(fā)生的類型及數(shù)量,從而可以做相應(yīng)的調(diào)整,以減少錯(cuò)誤發(fā)生。
          完整代碼和 DEMO 請(qǐng)看我另一篇文章前端性能和錯(cuò)誤監(jiān)控的末尾,大家可以復(fù)制代碼(HTML文件)在本地測(cè)試一下。

          數(shù)據(jù)上報(bào)

          性能數(shù)據(jù)上報(bào)

          性能數(shù)據(jù)可以在頁(yè)面加載完之后上報(bào),盡量不要對(duì)頁(yè)面性能造成影響。

          window.onload = () => {
          // 在瀏覽器空閑時(shí)間獲取性能及資源信息
          // https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestIdleCallback
          if (window.requestIdleCallback) {
          window.requestIdleCallback(() => {
          monitor.performance = getPerformance()
          monitor.resources = getResources()
          })
          } else {
          setTimeout(() => {
          monitor.performance = getPerformance()
          monitor.resources = getResources()
          }, 0)
          }
          }

          當(dāng)然,你也可以設(shè)一個(gè)定時(shí)器,循環(huán)上報(bào)。不過(guò)每次上報(bào)最好做一下對(duì)比去重再上報(bào),避免同樣的數(shù)據(jù)重復(fù)上報(bào)。

          錯(cuò)誤數(shù)據(jù)上報(bào)

          我在DEMO里提供的代碼,是用一個(gè) errors 數(shù)組收集所有的錯(cuò)誤,再在某一階段統(tǒng)一上報(bào)(延時(shí)上報(bào))。
          其實(shí),也可以改成在錯(cuò)誤發(fā)生時(shí)上報(bào)(即時(shí)上報(bào))。這樣可以避免在收集完錯(cuò)誤延時(shí)上報(bào)還沒(méi)觸發(fā),用戶卻已經(jīng)關(guān)掉網(wǎng)頁(yè)導(dǎo)致錯(cuò)誤數(shù)據(jù)丟失的問(wèn)題。

          // 監(jiān)聽(tīng) js 錯(cuò)誤
          window.onerror = function(msg, url, row, col, error) {
          const data = {
          type: 'javascript',
          row: row,
          col: col,
          msg: error && error.stack? error.stack : msg,
          url: url,
          // 錯(cuò)誤發(fā)生的時(shí)間
          time: new Date().getTime(),
          }

          // 即時(shí)上報(bào)
          axios.post({ url: 'xxx', data, })
          }

          SPA

          window.performance API 是有缺點(diǎn)的,在 SPA 切換路由時(shí),window.performance.timing 的數(shù)據(jù)不會(huì)更新。
          所以我們需要另想辦法來(lái)統(tǒng)計(jì)切換路由到加載完成的時(shí)間。
          拿 Vue 舉例,一個(gè)可行的辦法就是切換路由時(shí),在路由的全局前置守衛(wèi) beforeEach 里獲取開(kāi)始時(shí)間,在組件的 mounted 鉤子里執(zhí)行 vm.$nextTick 函數(shù)來(lái)獲取組件的渲染完畢時(shí)間。

          router.beforeEach((to, from, next) => {
          store.commit('setPageLoadedStartTime', new Date())
          })
          mounted() {
          this.$nextTick(() => {
          this.$store.commit('setPageLoadedTime', new Date() - this.$store.state.pageLoadedStartTime)
          })
          }

          除了性能和錯(cuò)誤監(jiān)控,其實(shí)我們還可以做得更多。

          用戶信息收集

          navigator

          使用 window.navigator 可以收集到用戶的設(shè)備信息,操作系統(tǒng),瀏覽器信息...

          UV(Unique visitor)

          是指通過(guò)互聯(lián)網(wǎng)訪問(wèn)、瀏覽這個(gè)網(wǎng)頁(yè)的自然人。訪問(wèn)您網(wǎng)站的一臺(tái)電腦客戶端為一個(gè)訪客。00:00-24:00內(nèi)相同的客戶端只被計(jì)算一次。一天內(nèi)同個(gè)訪客多次訪問(wèn)僅計(jì)算一個(gè)UV。
          在用戶訪問(wèn)網(wǎng)站時(shí),可以生成一個(gè)隨機(jī)字符串+時(shí)間日期,保存在本地。在網(wǎng)頁(yè)發(fā)生請(qǐng)求時(shí)(如果超過(guò)當(dāng)天24小時(shí),則重新生成),把這些參數(shù)傳到后端,后端利用這些信息生成 UV 統(tǒng)計(jì)報(bào)告。

          PV(Page View)

          即頁(yè)面瀏覽量或點(diǎn)擊量,用戶每1次對(duì)網(wǎng)站中的每個(gè)網(wǎng)頁(yè)訪問(wèn)均被記錄1個(gè)PV。用戶對(duì)同一頁(yè)面的多次訪問(wèn),訪問(wèn)量累計(jì),用以衡量網(wǎng)站用戶訪問(wèn)的網(wǎng)頁(yè)數(shù)量。

          頁(yè)面停留時(shí)間

          傳統(tǒng)網(wǎng)站
          用戶在進(jìn)入 A 頁(yè)面時(shí),通過(guò)后臺(tái)請(qǐng)求把用戶進(jìn)入頁(yè)面的時(shí)間捎上。過(guò)了 10 分鐘,用戶進(jìn)入 B 頁(yè)面,這時(shí)后臺(tái)可以通過(guò)接口捎帶的參數(shù)可以判斷出用戶在 A 頁(yè)面停留了 10 分鐘。
          SPA
          可以利用 router 來(lái)獲取用戶停留時(shí)間,拿 Vue 舉例,通過(guò) router.beforeEach destroyed 這兩個(gè)鉤子函數(shù)來(lái)獲取用戶停留該路由組件的時(shí)間。

          瀏覽深度

          通過(guò) document.documentElement.scrollTop 屬性以及屏幕高度,可以判斷用戶是否瀏覽完網(wǎng)站內(nèi)容。

          頁(yè)面跳轉(zhuǎn)來(lái)源

          通過(guò) document.referrer 屬性,可以知道用戶是從哪個(gè)網(wǎng)站跳轉(zhuǎn)而來(lái)。

          小結(jié)

          通過(guò)分析用戶數(shù)據(jù),我們可以了解到用戶的瀏覽習(xí)慣、愛(ài)好等等信息,想想真是恐怖,毫無(wú)隱私可言。

          前端監(jiān)控部署教程

          前面說(shuō)的都是監(jiān)控原理,但要實(shí)現(xiàn)還是得自己動(dòng)手寫代碼。為了避免麻煩,我們可以用現(xiàn)有的工具 sentry 去做這件事。

          sentry 是一個(gè)用 python 寫的性能和錯(cuò)誤監(jiān)控工具,你可以使用 sentry 提供的服務(wù)(免費(fèi)功能少),也可以自己部署服務(wù)。現(xiàn)在來(lái)看一下如何使用 sentry 提供的服務(wù)實(shí)現(xiàn)監(jiān)控。

          注冊(cè)賬號(hào)

          打開(kāi) https://sentry.io/signup/ 網(wǎng)站,進(jìn)行注冊(cè)。

          選擇項(xiàng)目,我選的 Vue。

          安裝 sentry 依賴

          選完項(xiàng)目,下面會(huì)有具體的 sentry 依賴安裝指南。

          根據(jù)提示,在你的 Vue 項(xiàng)目執(zhí)行這段代碼 npm install --save @sentry/browser @sentry/integrations @sentry/tracing,安裝 sentry 所需的依賴。

          再將下面的代碼拷到你的 main.js,放在 new Vue() 之前。

          import * as Sentry from "@sentry/browser";
          import { Vue as VueIntegration } from "@sentry/integrations";
          import { Integrations } from "@sentry/tracing";

          Sentry.init({
          dsn: "xxxxx", // 這里是你的 dsn 地址,注冊(cè)完就有
          integrations: [
          new VueIntegration({
          Vue,
          tracing: true,
          }),
          new Integrations.BrowserTracing(),
          ],

          // We recommend adjusting this value in production, or using tracesSampler
          // for finer control
          tracesSampleRate: 1.0,
          });

          然后點(diǎn)擊第一步中的 skip this onboarding,進(jìn)入控制臺(tái)頁(yè)面。

          如果忘了自己的 DSN,請(qǐng)點(diǎn)擊左邊的菜單欄選擇 Settings -> Projects -> 點(diǎn)擊自己的項(xiàng)目 -> Client Keys(DSN)

          創(chuàng)建第一個(gè)錯(cuò)誤

          在你的 Vue 項(xiàng)目執(zhí)行一個(gè)打印語(yǔ)句 console.log(b)

          這時(shí)點(diǎn)開(kāi) sentry 主頁(yè)的 issues 一項(xiàng),可以發(fā)現(xiàn)有一個(gè)報(bào)錯(cuò)信息 b is not defined

          這個(gè)報(bào)錯(cuò)信息包含了錯(cuò)誤的具體信息,還有你的 IP、瀏覽器信息等等。

          但奇怪的是,我們的瀏覽器控制臺(tái)并沒(méi)有輸出報(bào)錯(cuò)信息。

          這是因?yàn)楸?sentry 屏蔽了,所以我們需要加上一個(gè)選項(xiàng) logErrors: true

          然后再查看頁(yè)面,發(fā)現(xiàn)控制臺(tái)也有報(bào)錯(cuò)信息了:

          上傳 sourcemap

          一般打包后的代碼都是經(jīng)過(guò)壓縮的,如果沒(méi)有 sourcemap,即使有報(bào)錯(cuò)信息,你也很難根據(jù)提示找到對(duì)應(yīng)的源碼在哪。

          下面來(lái)看一下如何上傳 sourcemap。

          首先創(chuàng)建 auth token。

          這個(gè)生成的 token 一會(huì)要用到。

          安裝 sentry-cli 和 @sentry/webpack-plugin

          npm install sentry-cli-binary -g
          npm install --save-dev @sentry/webpack-plugin

          安裝完上面兩個(gè)插件后,在項(xiàng)目根目錄創(chuàng)建一個(gè) .sentryclirc 文件(不要忘了在 .gitignore 把這個(gè)文件添加上,以免暴露 token),內(nèi)容如下:

          [auth]
          token=xxx

          [defaults]
          url=https://sentry.io/
          org=woai3c
          project=woai3c

          把 xxx 替換成剛才生成的 token。

          org 是你的組織名稱。

          project 是你的項(xiàng)目名稱,根據(jù)下面的提示可以找到。

          在項(xiàng)目下新建 vue.config.js 文件,把下面的內(nèi)容填進(jìn)去:

          const SentryWebpackPlugin = require('@sentry/webpack-plugin')

          const config = {
          configureWebpack: {
          plugins: [
          new SentryWebpackPlugin({
          include: './dist', // 打包后的目錄
          ignore: ['node_modules', 'vue.config.js', 'babel.config.js'],
          }),
          ],
          },
          }

          // 只在生產(chǎn)環(huán)境下上傳 sourcemap
          module.exports = process.env.NODE_ENV == 'production'? config : {}

          填完以后,執(zhí)行 npm run build,就可以看到 sourcemap 的上傳結(jié)果了。

          我們?cè)賮?lái)看一下沒(méi)上傳 sourcemap 和上傳之后的報(bào)錯(cuò)信息對(duì)比。

          未上傳 sourcemap

          已上傳 sourcemap

          可以看到,上傳 sourcemap 后的報(bào)錯(cuò)信息更加準(zhǔn)確。

          切換中文環(huán)境和時(shí)區(qū)

          選完刷新即可。

          性能監(jiān)控

          打開(kāi) performance 選項(xiàng),就能看到你每個(gè)項(xiàng)目的運(yùn)行情況。具體的參數(shù)解釋請(qǐng)看文檔 Performance Monitoring。

          性能優(yōu)化

          性能優(yōu)化主要分為兩類:

          1. 加載時(shí)優(yōu)化

          2. 運(yùn)行時(shí)優(yōu)化

          例如壓縮文件、使用 CDN 就屬于加載時(shí)優(yōu)化;減少 DOM 操作,使用事件委托屬于運(yùn)行時(shí)優(yōu)化。

          在解決問(wèn)題之前,必須先找出問(wèn)題,否則無(wú)從下手。所以在做性能優(yōu)化之前,最好先調(diào)查一下網(wǎng)站的加載性能和運(yùn)行性能。

          手動(dòng)檢查

          檢查加載性能

          一個(gè)網(wǎng)站加載性能如何主要看白屏?xí)r間和首屏?xí)r間。

          • 白屏?xí)r間:指從輸入網(wǎng)址,到頁(yè)面開(kāi)始顯示內(nèi)容的時(shí)間。

          • 首屏?xí)r間:指從輸入網(wǎng)址,到頁(yè)面完全渲染的時(shí)間。

          將以下腳本放在 </head> 前面就能獲取白屏?xí)r間。

          <script>
          new Date() - performance.timing.navigationStart
          </script>

          首屏?xí)r間比較復(fù)雜,得考慮有圖片和沒(méi)有圖片的情況。

          如果沒(méi)有圖片,則在 window.onload 事件里執(zhí)行 new Date() - performance.timing.navigationStart 即可獲取首屏?xí)r間。

          如果有圖片,則要在最后一個(gè)在首屏渲染的圖片的 onload 事件里執(zhí)行 new Date() - performance.timing.navigationStart 獲取首屏?xí)r間,實(shí)施起來(lái)比較復(fù)雜,在這里限于篇幅就不說(shuō)了。

          檢查運(yùn)行性能

          配合 chrome 的開(kāi)發(fā)者工具,我們可以查看網(wǎng)站在運(yùn)行時(shí)的性能。

          打開(kāi)網(wǎng)站,按 F12 選擇 performance,點(diǎn)擊左上角的灰色圓點(diǎn),變成紅色就代表開(kāi)始記錄了。這時(shí)可以模仿用戶使用網(wǎng)站,在使用完畢后,點(diǎn)擊 stop,然后你就能看到網(wǎng)站運(yùn)行期間的性能報(bào)告。如果有紅色的塊,代表有掉幀的情況;如果是綠色,則代表 FPS 很好。

          另外,在 performance 標(biāo)簽下,按 ESC 會(huì)彈出來(lái)一個(gè)小框。點(diǎn)擊小框左邊的三個(gè)點(diǎn),把 rendering 勾出來(lái)。

          這兩個(gè)選項(xiàng),第一個(gè)是高亮重繪區(qū)域,另一個(gè)是顯示幀渲染信息。把這兩個(gè)選項(xiàng)勾上,然后瀏覽網(wǎng)頁(yè),可以實(shí)時(shí)的看到你網(wǎng)頁(yè)渲染變化。

          利用工具檢查

          監(jiān)控工具

          可以部署一個(gè)前端監(jiān)控系統(tǒng)來(lái)監(jiān)控網(wǎng)站性能,上一節(jié)中講到的 sentry 就屬于這一類。

          chrome 工具 Lighthouse

          如果你安裝了 Chrome 52+ 版本,請(qǐng)按 F12 打開(kāi)開(kāi)發(fā)者工具。

          它不僅會(huì)對(duì)你網(wǎng)站的性能打分,還會(huì)對(duì) SEO 打分。

          使用 Lighthouse 審查網(wǎng)絡(luò)應(yīng)用

          如何做性能優(yōu)化

          網(wǎng)上關(guān)于性能優(yōu)化的文章和書籍多不勝數(shù),但有很多優(yōu)化規(guī)則已經(jīng)過(guò)時(shí)了。所以我寫了一篇性能優(yōu)化文章前端性能優(yōu)化 24 條建議(2020),分析總結(jié)出了 24 條性能優(yōu)化建議,強(qiáng)烈推薦。

          重構(gòu)

          《重構(gòu)2》一書中對(duì)重構(gòu)進(jìn)行了定義:

          所謂重構(gòu)(refactoring)是這樣一個(gè)過(guò)程:在不改變代碼外在行為的前提下,對(duì)代碼做出修改,以改進(jìn)程序的內(nèi)部結(jié)構(gòu)。重構(gòu)是一種經(jīng)千錘百煉形成的有條不紊的程序整理方法,可以最大限度地減小整理過(guò)程中引入錯(cuò)誤的概率。本質(zhì)上說(shuō),重構(gòu)就是在代碼寫好之后改進(jìn)它的設(shè)計(jì)。

          重構(gòu)和性能優(yōu)化有相同點(diǎn),也有不同點(diǎn)。

          相同的地方是它們都在不改變程序功能的情況下修改代碼;不同的地方是重構(gòu)為了讓代碼變得更加易讀、理解,性能優(yōu)化則是為了讓程序運(yùn)行得更快。

          重構(gòu)可以一邊寫代碼一邊重構(gòu),也可以在程序?qū)懲旰螅贸鲆欢螘r(shí)間專門去做重構(gòu)。沒(méi)有說(shuō)哪個(gè)方式更好,視個(gè)人情況而定。

          如果你專門拿一段時(shí)間來(lái)做重構(gòu),建議你在重構(gòu)一段代碼后,立即進(jìn)行測(cè)試。這樣可以避免修改代碼太多,在出錯(cuò)時(shí)找不到錯(cuò)誤點(diǎn)。

          重構(gòu)的原則

          1. 事不過(guò)三,三則重構(gòu)。即不能重復(fù)寫同樣的代碼,在這種情況下要去重構(gòu)。

          2. 如果一段代碼讓人很難看懂,那就該考慮重構(gòu)了。

          3. 如果已經(jīng)理解了代碼,但是非常繁瑣或者不夠好,也可以重構(gòu)。

          4. 過(guò)長(zhǎng)的函數(shù),需要重構(gòu)。

          5. 一個(gè)函數(shù)最好對(duì)應(yīng)一個(gè)功能,如果一個(gè)函數(shù)被塞入多個(gè)功能,那就要對(duì)它進(jìn)行重構(gòu)了。

          重構(gòu)手法

          在《重構(gòu)2》這本書中,介紹了多達(dá)上百個(gè)重構(gòu)手法。但我覺(jué)得有兩個(gè)是比較常用的:

          1. 提取重復(fù)代碼,封裝成函數(shù)

          2. 拆分太長(zhǎng)或功能太多的函數(shù)

          提取重復(fù)代碼,封裝成函數(shù)

          假設(shè)有一個(gè)查詢數(shù)據(jù)的接口 /getUserData?age=17&city=beijing。現(xiàn)在需要做的是把用戶數(shù)據(jù):{ age: 17, city: 'beijing' } 轉(zhuǎn)成 URL 參數(shù)的形式:

          let result = ''
          const keys = Object.keys(data) // { age: 17, city: 'beijing' }
          keys.forEach(key => {
          result += '&' + key + '=' + data[key]
          })

          result.substr(1) // age=17&city=beijing

          如果只有這一個(gè)接口需要轉(zhuǎn)換,不封裝成函數(shù)是沒(méi)問(wèn)題的。但如果有多個(gè)接口都有這種需求,那就得把它封裝成函數(shù)了:

          function JSON2Params(data) {
          let result = ''
          const keys = Object.keys(data)
          keys.forEach(key => {
          result += '&' + key + '=' + data[key]
          })

          return result.substr(1)
          }

          拆分太長(zhǎng)或功能太多的函數(shù)

          假設(shè)現(xiàn)在有一個(gè)注冊(cè)功能,用偽代碼表示:

          function register(data) {
          // 1. 驗(yàn)證用戶數(shù)據(jù)是否合法
          /**
          * 驗(yàn)證賬號(hào)
          * 驗(yàn)證密碼
          * 驗(yàn)證短信驗(yàn)證碼
          * 驗(yàn)證身份證
          * 驗(yàn)證郵箱
          */


          // 2. 如果用戶上傳了頭像,則將用戶頭像轉(zhuǎn)成 base64 碼保存
          /**
          * 新建 FileReader 對(duì)象
          * 將圖片轉(zhuǎn)換成 base64 碼
          */


          // 3. 調(diào)用注冊(cè)接口
          // ...
          }

          這個(gè)函數(shù)包含了三個(gè)功能,驗(yàn)證、轉(zhuǎn)換、注冊(cè)。其中驗(yàn)證和轉(zhuǎn)換功能是可以提取出來(lái)單獨(dú)封裝成函數(shù)的:

          function register(data) {
          // 1. 驗(yàn)證用戶數(shù)據(jù)是否合法
          // verify()

          // 2. 如果用戶上傳了頭像,則將用戶頭像轉(zhuǎn)成 base64 碼保存
          // tobase64()

          // 3. 調(diào)用注冊(cè)接口
          // ...
          }

          如果你對(duì)重構(gòu)有興趣,強(qiáng)烈推薦你閱讀《重構(gòu)2》這本書。

          參考資料:

          • 《重構(gòu)2》

          總結(jié)

          寫這篇文章主要是為了對(duì)我這一年多工作經(jīng)驗(yàn)作總結(jié),因?yàn)槲一旧隙荚谘芯壳岸斯こ袒约叭绾翁嵘龍F(tuán)隊(duì)的開(kāi)發(fā)效率。希望這篇文章能幫助一些對(duì)前端工程化沒(méi)有經(jīng)驗(yàn)的新手,通過(guò)這篇文章入門前端工程化。

          如果這篇文章對(duì)你有幫助,請(qǐng)點(diǎn)一下贊,感激不盡。


          可以加超級(jí)貓的 wx:CB834301747 ,一起閑聊前端。

          微信搜 “前端GitHub”,回復(fù) “電子書” 即可以獲得 160 本前端精華書籍哦。

          往期精文

          瀏覽 82
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产亚洲精品久久久久久 | 午夜成人无码 | 大香蕉国产免费影院 | 欧美高潮性爱中文字幕在线播放 | 卡一卡二在线视频 |