獨孤老人給正在入坑的小白的建議

前言想當初自己也是從一個小白一步一步走到現在(小白中的老人 ),雖然還是一個大廠朋友都沒有,但是始終不放棄學習的機會,有一些踩過的坑呢,想總結一下,不要給別人添麻煩。

技術選型

前端的發展非常快,技術更新可以說是日新月異,以前是JQuery,現在三分天(vue 、react、 angular), 個人覺得(我要開始裝逼啦):如果用一句話形容的話,我覺得用三個漢堡店來形容再不為過了。“前端三大框架,就好比漢堡界的麥當勞、肯德基、漢堡王”。意思是說:三大框架都差不多(都是漢堡),主要是看個人口味。但是呢,如果非要選擇的話 ,我會依據以下三個特點來選:

1.選你或團隊最熟的,保證在遇到棘手的問題時有人能填坑。(百度不出來的東西,說不定問一句就解決了)

2.選市場占有率高的。(換句話意思,就是選好招人的)

3.大廠用什么就選什么(為進大廠而準備,畢竟大廠工資高,福利好,同樣是加班,為啥不去工資高的呢?都是打工人)

(ps:第二點對于小公司來說,特別重要。本來小公司就不好招人,要是還選一個市場占有率不高的框架(例如 Angular),簡歷你都看不到幾個...)

UI 組件庫如果是小公司,推薦用大廠開源的,畢竟開源的項目經過大廠內部孵化,很多坑還是有跡可尋的,如果本身在大廠,就更不用多說了。

統一規范

1.代碼規范

先來看看統一代碼規范的好處:

規范的代碼可以促進團隊合作

規范的代碼可以降低維護成本

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

養成代碼規范的習慣,有助于程序員自身的成長

當團隊的成員都嚴格按照代碼規范來寫代碼時,可以保證每個人的代碼看起來都像是一個人寫的,看別人的代碼就像是在看自己的代碼。更重要的是我們能夠認識到規范的重要性,并堅持規范的開發習慣。

1)如何制訂代碼規范

建議找一份好的代碼規范,在此基礎上結合團隊的需求作個性化修改。

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

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

standard (24.5k star) 中文版

百度前端編碼規范 3.9k

css 代碼規范也有不少,例如:

styleguide 2.3k

spec 3.9k

2)如何檢查代碼規范

使用 eslint 可以檢查代碼符不符合團隊制訂的規范,下面來看一下如何配置 eslint 來檢查代碼。

<1>下載依賴

????//?eslint-config-airbnb-base?使用?airbnb?代碼規范

????npm?i?-D?babel-eslint?eslint?eslint-config-airbnb-base?eslint-plugin-import

<2>在?package.json?的?scripts?加上這行代碼?"lint": "eslint --ext .js test/ src/ bin/"。然后執行?npm run lint?即可開始驗證代碼。

不過這樣檢查代碼效率太低,每次都得手動檢查。并且報錯了還得手動修改代碼。

為了改善以上缺點,我們可以使用 VSCode。使用它并加上適當的配置可以在每次保存代碼的時候,自動驗證代碼并進行格式化,省去了動手的麻煩。

css 檢查代碼規范則使用?stylelint?插件。

具體如何配置可以網上搜索一下,也有很多的配置方案,這里就不做配置配置了。

2.git 規范

git 規范包括兩點:分支管理規范、git commit 規范。

1)分支管理規范

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

當有團隊成員要開發新功能或改 BUG 時,就從 master 分支開一個新的分支。例如項目要從客戶端渲染改成服務端渲染,就開一個分支叫 ssr,開發完了再合并回 master 分支。

如果改一個 BUG,也可以從 master 分支開一個新分支,并用 BUG 號命名(不過我們小團隊嫌麻煩,沒這樣做,除非有特別大的 BUG)。

2)git commit 規范

????<type>(<scope>):?<subject>

????<BLANK?LINE>

????<body>

????<BLANK?LINE>

????<footer>

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

<1>標題行: 必填, 描述主要修改類型和內容

<2>主題內容: 描述為什么修改, 做了什么樣的修改, 以及開發的思路等等

<3>頁腳注釋: 可以寫注釋,BUG 號鏈接

type: commit 的類型

feat: 新功能、新特性

fix: 修改 bug

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

refactor: 代碼重構(重構,在不影響代碼內部行為、功能下的代碼修改)

docs: 文檔修改

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

test: 測試用例新增、修改

build: 影響項目構建或依賴項修改

revert: 恢復上一次提交

ci: 持續集成相關文件修改

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

release: 發布新版本

workflow: 工作流相關文件修改

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

b)subject: commit 的概述

c)body: commit 具體修改內容, 可以分為多行.

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

示例

fix(修復BUG)

如果修復的這個BUG只影響當前修改的文件,可不加范圍。如果影響的范圍比較大,要加上范圍描述。

例如這次 BUG 修復影響到全局,可以加個 global。如果影響的是某個目錄或某個功能,可以加上該目錄的路徑,或者對應的功能名稱。

????//?示例1

????fix(global):修復checkbox不能復選的問題

????//?示例2?下面圓括號里的?common?為通用管理的名稱

????fix(common):?修復字體過小的BUG,將通用管理下所有頁面的默認字體大小修改為?14px

????//?示例3

????fix:?value.length?->?values.length

feat(添加新功能或新頁面)

????feat:?添加網站主頁靜態頁面


????這是一個示例,假設對點檢任務靜態頁面進行了一些描述。


????這里是備注,可以是放BUG鏈接或者一些重要性的東西。

chore(其他修改)

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

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

其他類型的 commit 和上面三個示例差不多,就不說了。

驗證 git commit 規范

驗證 git commit 規范,主要通過 git 的?pre-commit?鉤子函數來進行。當然,你還需要下載一個輔助工具來幫助你進行驗證。

下載輔助工具

????npm?i?-D?husky

在?package.json?加上下面的代碼

????"husky":?{

??????"hooks":?{

????????"pre-commit":?"npm?run?lint",

????????"commit-msg":?"node?script/verify-commit.js",

????????"pre-push":?"npm?test"

??????}

????}

然后在你項目根目錄下新建一個文件夾?script,并在下面新建一個文件?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?消息格式。

????????????請查看?git?commit?提交規范:https://github.com/woai3c/Front-end-articles/blob/master/git%20commit%20style.md

????????`)


????????process.exit(1)

????}

現在來解釋下各個鉤子的含義:

"pre-commit": "npm run lint",在?git commit?前執行?npm run lint?檢查代碼格式。

"commit-msg": "node script/verify-commit.js",在?git commit?時執行腳本?verify-commit.js?驗證 commit 消息。如果不符合腳本中定義的格式,將會報錯。

"pre-push": "npm test",在你執行?git push?將代碼推送到遠程倉庫前,執行?npm test?進行測試。如果測試失敗,將不會執行這次推送。

項目規范

主要是項目文件的組織方式和命名方式。

用我們的 Vue 項目舉個例子。

????├─public

????├─src

????├─test

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

????├─api?(接口)

????├─assets?(靜態資源)

????├─components?(公共組件)

????├─styles?(公共樣式)

????├─router?(路由)

????├─store?(vuex?全局數據)

????├─utils?(工具函數)

????└─views?(頁面)

文件名稱如果過長則用 - 隔開。

3.UI 規范

UI 規范需要前端、UI、產品溝通,互相商量,最后制定下來,建議使用統一的 UI 組件庫。(大廠都會有一定的UI規范的)

制定 UI 規范的好處:

統一頁面 UI 標準,節省 UI 設計時間

提高前端開發效率

測試

測試是前端工程化建設必不可少的一部分,它的作用就是找出 bug,越早發現 bug,所需要付出的成本就越低。并且,它更重要的作用是在將來,而不是當下。==》設想一下半年后,你的項目要加一個新功能。在加完新功能后,你不確定有沒有影響到原有的功能,需要測試一下。由于時間過去太久,你對項目的代碼已經不了解了。在這種情況下,如果沒有寫測試,你就得手動一遍一遍的去試。而如果寫了測試,你只需要跑一遍測試代碼就 OK 了,省時省力。

寫測試還可以讓你修改代碼時沒有心理負擔,不用一直想著改這里有沒有問題?會不會引起 BUG?而寫了測試就沒有這種擔心了。

在前端用得最多的就是單元測試了。

1.單元測試

單元測試就是對一個函數、一個組件、一個類做的測試,它針對的粒度比較小。

它應該怎么寫呢?

1)根據正確性寫測試,即正確的輸入應該有正常的結果。

2)根據異常寫測試,即錯誤的輸入應該是錯誤的結果。

a)對一個函數做測試

例如一個取絕對值的函數?abs(),輸入?1,2,結果應該與輸入相同;輸入?-1,-2,結果應該與輸入相反。如果輸入非數字,例如?"abc",應該拋出一個類型錯誤。

b)對一個類做測試

假設有這樣一個類:

????class?Math?{

????????abs()?{


????????}


????????sqrt()?{


????????}


????????pow()?{


????????}

????????...

????}

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

c)對一個組件做測試

組件測試比較難,因為很多組件都涉及了 DOM 操作。

例如一個上傳圖片組件,它有一個將圖片轉成 base64 碼的方法,那要怎么測試呢?一般測試都是跑在 node 環境下的,而 node 環境沒有 DOM 對象。

我們先來回顧一下上傳圖片的過程:

<1>點擊?<input type="file" />,選擇圖片上傳。

<2>觸發?input?的?change?事件,獲取?file?對象。

<3>用?FileReader?將圖片轉換成 base64 碼。

這個過程和下面的代碼是一樣的:

????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)

????}

上面的代碼只是模擬,真實情況下應該是這樣使用

????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)

????????})

????}

可以看到,上面代碼出現了 window 的事件對象?event、FileReader。也就是說,只要我們能夠提供這兩個對象,就可以在任何環境下運行它。所以我們可以在測試環境下加上這兩個對象:

????//?重寫?File

????window.File?=?function?()?{}


????//?重寫?FileReader

????window.FileReader?=?function?()?{

????????this.readAsDataURL?=?function?()?{

????????????this.onload

????????????????&&?this.onload({

????????????????????target:?{

????????????????????????result:?fileData,

????????????????????},

????????????????})

????????}

????}

然后測試可以這樣寫:

????//?提前寫好文件內容

????const?fileData?=?'data:image/test'


????//?提供一個假的?file?對象給?tobase64()?函數

????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()

????????????})

????????})

????}


????//?執行測試

????test()

通過這種 hack 的方式,我們就實現了對涉及 DOM 操作的組件的測試。

2.TDD 測試驅動開發

TDD 就是根據需求提前把測試代碼寫好,然后根據測試代碼實現功能。

TDD 的初衷是好的,但如果你的需求經常變(你懂的),那就不是一件好事了。很有可能你天天都在改測試代碼,業務代碼反而沒怎么動。 所以到現在為止,兩年多的程序員生涯,我還沒嘗試過 TDD 開發。

雖然環境如此艱難,但有條件的情況下還是應該試一下 TDD 的。例如在你自己負責一個項目又不忙的時候,可以采用此方法編寫測試用例。

3.測試框架推薦

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

部署

在沒有學會自動部署前,我是這樣部署項目的:

1.執行測試?npm run test。

2.推送代碼?git push。

3.構建項目?npm run build。

4.將打包好的文件放到靜態服務器。

一次兩次還行,如果天天都這樣,就會把很多時間浪費在重復的操作上。所以我們要學會自動部署,徹底解放雙手。

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

5.輪詢。

6.監聽?webhook?事件。

輪詢

輪詢,就是構建軟件每隔一段時間自動執行打包、部署操作。

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

另外還有一個副作用,假如我一天都沒更改代碼,構建軟件還是會不停的執行打包、部署操作,白白的浪費資源。

所以現在的構建軟件基本采用監聽?webhook?事件的方式來進行部署。

監聽?webhook?事件

webhook 鉤子函數,就是在你的構建軟件上進行設置,監聽某一個事件(一般是監聽?push?事件),當事件觸發時,自動執行定義好的腳本。

例如?Github Actions,就有這個功能。

監控

監控,又分性能監控和錯誤監控,它的作用是預警和追蹤定位問題。

1.性能監控

性能監控一般利用?window.performance?來進行數據采集。

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

這個 API 的屬性?timing,包含了頁面加載各個階段的起始及結束時間。

在這里插入圖片描述

為了方便大家理解?timing?各個屬性的意義,我在知乎找到一位網友對于?timing?寫的簡介(忘了姓名,后來找不到了,見諒),在此轉載一下。

????timing:?{

????????????//?同一個瀏覽器上一個頁面卸載(unload)結束時的時間戳。如果沒有上一個頁面,這個值會和fetchStart相同。

?????navigationStart:?1543806782096,


?????//?上一個頁面unload事件拋出時的時間戳。如果沒有上一個頁面,這個值會返回0。

?????unloadEventStart:?1543806782523,


?????//?和?unloadEventStart?相對應,unload事件處理完成時的時間戳。如果沒有上一個頁面,這個值會返回0。

?????unloadEventEnd:?1543806782523,


?????//?第一個HTTP重定向開始時的時間戳。如果沒有重定向,或者重定向中的一個不同源,這個值會返回0。

?????redirectStart:?0,


?????//?最后一個HTTP重定向完成時(也就是說是HTTP響應的最后一個比特直接被收到的時間)的時間戳。

?????//?如果沒有重定向,或者重定向中的一個不同源,這個值會返回0.?

?????redirectEnd:?0,


?????//?瀏覽器準備好使用HTTP請求來獲取(fetch)文檔的時間戳。這個時間點會在檢查任何應用緩存之前。

?????fetchStart:?1543806782096,


?????//?DNS?域名查詢開始的UNIX時間戳。

????????????//如果使用了持續連接(persistent?connection),或者這個信息存儲到了緩存或者本地資源上,這個值將和fetchStart一致。

?????domainLookupStart:?1543806782096,


?????//?DNS?域名查詢完成的時間.

?????//如果使用了本地緩存(即無?DNS?查詢)或持久連接,則與?fetchStart?值相等

?????domainLookupEnd:?1543806782096,


?????//?HTTP(TCP)?域名查詢結束的時間戳。

????????????//如果使用了持續連接(persistent?connection),或者這個信息存儲到了緩存或者本地資源上,這個值將和?fetchStart一致。

?????connectStart:?1543806782099,


?????//?HTTP(TCP)?返回瀏覽器與服務器之間的連接建立時的時間戳。

????????????//?如果建立的是持久連接,則返回值等同于fetchStart屬性的值。連接建立指的是所有握手和認證過程全部結束。

?????connectEnd:?1543806782227,


?????//?HTTPS?返回瀏覽器與服務器開始安全鏈接的握手時的時間戳。如果當前網頁不要求安全連接,則返回0。

?????secureConnectionStart:?1543806782162,


?????//?返回瀏覽器向服務器發出HTTP請求時(或開始讀取本地緩存時)的時間戳。

?????requestStart:?1543806782241,


?????//?返回瀏覽器從服務器收到(或從本地緩存讀取)第一個字節時的時間戳。

????????????//如果傳輸層在開始請求之后失敗并且連接被重開,該屬性將會被數制成新的請求的相對應的發起時間。

?????responseStart:?1543806782516,


?????//?返回瀏覽器從服務器收到(或從本地緩存讀取,或從本地資源讀取)最后一個字節時

????????????//(如果在此之前HTTP連接已經關閉,則返回關閉時)的時間戳。

?????responseEnd:?1543806782537,


?????//?當前網頁DOM結構開始解析時(即Document.readyState屬性變為“loading”、相應的?readystatechange事件觸發時)的時間戳。

?????domLoading:?1543806782573,


?????//?當前網頁DOM結構結束解析、開始加載內嵌資源時(即Document.readyState屬性變為“interactive”、相應的readystatechange事件觸發時)的時間戳。

?????domInteractive:?1543806783203,


?????//?當解析器發送DOMContentLoaded?事件,即所有需要被執行的腳本已經被解析時的時間戳。

?????domContentLoadedEventStart:?1543806783203,


?????//?當所有需要立即執行的腳本已經被執行(不論執行順序)時的時間戳。

?????domContentLoadedEventEnd:?1543806783216,


?????//?當前文檔解析完成,即Document.readyState?變為?'complete'且相對應的readystatechange?被觸發時的時間戳

?????domComplete:?1543806783796,


?????//?load事件被發送時的時間戳。如果這個事件還未被發送,它的值將會是0。

?????loadEventStart:?1543806783796,


?????//?當load事件結束,即加載事件完成時的時間戳。如果這個事件還未被發送,或者尚未完成,它的值將會是0.

?????loadEventEnd:?1543806783802

????}

通過以上數據,我們可以得到幾個有用的時間

????//?重定向耗時

????redirect:?timing.redirectEnd?-?timing.redirectStart,

????//?DOM?渲染耗時

????dom:?timing.domComplete?-?timing.domLoading,

????//?頁面加載耗時

????load:?timing.loadEventEnd?-?timing.navigationStart,

????//?頁面卸載耗時

????unload:?timing.unloadEventEnd?-?timing.unloadEventStart,

????//?請求耗時

????request:?timing.responseEnd?-?timing.requestStart,

????//?獲取性能信息時當前時間

????time:?new?Date().getTime(),

還有一個比較重要的時間就是白屏時間,它指從輸入網址,到頁面開始顯示內容的時間。

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

????<script>

????????whiteScreen?=?new?Date()?-?performance.timing.navigationStart

????</script>

通過這幾個時間,就可以得知頁面首屏加載性能如何了。

另外,通過?window.performance.getEntriesByType('resource')?這個方法,我們還可以獲取相關資源(js、css、img...)的加載時間,它會返回頁面當前所加載的所有資源。

它一般包括以下幾個類型

sciprt

link

img

css

fetch

other

xmlhttprequest

我們只需用到以下幾個信息

????//?資源的名稱

????name:?item.name,

????//?資源加載耗時

????duration:?item.duration.toFixed(2),

????//?資源大小

????size:?item.transferSize,

????//?資源所用協議

????protocol:?item.nextHopProtocol,

現在,寫幾行代碼來收集這些數據。

????//?收集性能信息

????const?getPerformance?=?()?=>?{

????????if?(!window.performance)?return

????????const?timing?=?window.performance.timing

????????const?performance?=?{

????????????//?重定向耗時

????????????redirect:?timing.redirectEnd?-?timing.redirectStart,

????????????//?白屏時間

????????????whiteScreen:?whiteScreen,

????????????//?DOM?渲染耗時

????????????dom:?timing.domComplete?-?timing.domLoading,

????????????//?頁面加載耗時

????????????load:?timing.loadEventEnd?-?timing.navigationStart,

????????????//?頁面卸載耗時

????????????unload:?timing.unloadEventEnd?-?timing.unloadEventStart,

????????????//?請求耗時

????????????request:?timing.responseEnd?-?timing.requestStart,

????????????//?獲取性能信息時當前時間

????????????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:?[],

????????????//?獲取資源信息時當前時間

????????????time:?new?Date().getTime(),

????????}


????????data.forEach(item?=>?{

????????????const?arry?=?resource[item.initiatorType]

????????????arry?&&?arry.push({

????????????????//?資源的名稱

????????????????name:?item.name,

????????????????//?資源加載耗時

????????????????duration:?item.duration.toFixed(2),

????????????????//?資源大小

????????????????size:?item.transferSize,

????????????????//?資源所用協議

????????????????protocol:?item.nextHopProtocol,

????????????})

????????})


????????return?resource

????}

小結:

通過對性能及資源信息的解讀,我們可以判斷出頁面加載慢有以下幾個原因:

1)資源過多

2)網速過慢

3)DOM元素過多

除了用戶網速過慢,我們沒辦法之外,其他兩個原因都是有辦法解決的,性能優化將在下一節《性能優化》中會講到。

2.錯誤監控

現在能捕捉的錯誤有三種。

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

2)js 執行錯誤,通過?window.onerror?捕捉 js 錯誤。

3)promise 錯誤,通過?addEventListener('unhandledrejection', callback)捕捉 promise 錯誤,但是沒有發生錯誤的行數,列數等信息,只能手動拋出相關錯誤信息。

我們可以建一個錯誤數組變量?errors?在錯誤發生時,將錯誤的相關信息添加到數組,然后在某個階段統一上報,具體如何操作請看代碼

????//?捕獲資源加載失敗錯誤?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',

????????????????//?錯誤發生的時間

????????????????time:?new?Date().getTime(),

????????????})

????????}

????},?true)


????//?監聽?js?錯誤

????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,

????????????//?錯誤發生的時間

????????????time:?new?Date().getTime(),

????????})

????}


????//?監聽?promise?錯誤?缺點是獲取不到行數數據

????addEventListener('unhandledrejection',?e?=>?{

????????monitor.errors.push({

????????????type:?'promise',

????????????msg:?(e.reason?&&?e.reason.msg)?||?e.reason?||?'',

????????????//?錯誤發生的時間

????????????time:?new?Date().getTime(),

????????})

????})

3.數據上報

1)性能數據上報

性能數據可以在頁面加載完之后上報,盡量不要對頁面性能造成影響。

????window.onload?=?()?=>?{

????????//?在瀏覽器空閑時間獲取性能及資源信息

????????//?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)

????????}

????}

當然,你也可以設一個定時器,循環上報。不過每次上報最好做一下對比去重再上報,避免同樣的數據重復上報。

2)錯誤數據上報

我在DEMO里提供的代碼,是用一個?errors?數組收集所有的錯誤,再在某一階段統一上報(延時上報)。 其實,也可以改成在錯誤發生時上報(即時上報)。這樣可以避免在收集完錯誤延時上報還沒觸發,用戶卻已經關掉網頁導致錯誤數據丟失的問題。

????//?監聽?js?錯誤

????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,

????????????//?錯誤發生的時間

????????????time:?new?Date().getTime(),

????????}


????????//?即時上報

????????axios.post({?url:?'xxx',?data,?})

????}

4.SPA

window.performance?API 是有缺點的,在 SPA 切換路由時,window.performance.timing?的數據不會更新。 所以我們需要另想辦法來統計切換路由到加載完成的時間。 拿 Vue 舉例,一個可行的辦法就是切換路由時,在路由的全局前置守衛?beforeEach?里獲取開始時間,在組件的?mounted?鉤子里執行?vm.$nextTick?函數來獲取組件的渲染完畢時間。

????router.beforeEach((to,?from,?next)?=>?{

?????store.commit('setPageLoadedStartTime',?new?Date())

????})

????mounted()?{

?????this.$nextTick(()?=>?{

??????this.$store.commit('setPageLoadedTime',?new?Date()?-?this.$store.state.pageLoadedStartTime)

?????})

????}

除了性能和錯誤監控,其實我們還可以做得更多。

5.用戶信息收集

1)navigator

使用?window.navigator?可以收集到用戶的設備信息,操作系統,瀏覽器信息...

2)UV(Unique visitor)

是指通過互聯網訪問、瀏覽這個網頁的自然人。訪問您網站的一臺電腦客戶端為一個訪客。00:00-24:00內相同的客戶端只被計算一次。一天內同個訪客多次訪問僅計算一個UV。 在用戶訪問網站時,可以生成一個隨機字符串+時間日期,保存在本地。在網頁發生請求時(如果超過當天24小時,則重新生成),把這些參數傳到后端,后端利用這些信息生成 UV 統計報告。

3)PV(Page View)

即頁面瀏覽量或點擊量,用戶每1次對網站中的每個網頁訪問均被記錄1個PV。用戶對同一頁面的多次訪問,訪問量累計,用以衡量網站用戶訪問的網頁數量。

4)頁面停留時間

傳統網站?用戶在進入 A 頁面時,通過后臺請求把用戶進入頁面的時間捎上。過了 10 分鐘,用戶進入 B 頁面,這時后臺可以通過接口捎帶的參數可以判斷出用戶在 A 頁面停留了 10 分鐘。?SPA?可以利用 router 來獲取用戶停留時間,拿 Vue 舉例,通過?router.beforeEach?destroyed?這兩個鉤子函數來獲取用戶停留該路由組件的時間。

5)瀏覽深度

通過?document.documentElement.scrollTop?屬性以及屏幕高度,可以判斷用戶是否瀏覽完網站內容。

5)頁面跳轉來源

通過?document.referrer?屬性,可以知道用戶是從哪個網站跳轉而來。

小結:

通過分析用戶數據,我們可以了解到用戶的瀏覽習慣、愛好等等信息,想想真是恐怖,毫無隱私可言。

前端監控部署教程

前面說的都是監控原理,但要實現還是得自己動手寫代碼。為了避免麻煩,我們可以用現有的工具 sentry 去做這件事。

sentry 是一個用 python 寫的性能和錯誤監控工具,你可以使用 sentry 提供的服務(免費功能少),也可以自己部署服務。現在來看一下如何使用 sentry 提供的服務實現監控。

1.注冊賬號

1)打開?https://sentry.io/signup/?網站,進行注冊。

2)選擇項目,我選的 Vue。

3)安裝 sentry 依賴

選完項目,下面會有具體的 sentry 依賴安裝指南。

根據提示,在你的 Vue 項目執行這段代碼?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?地址,注冊完就有

??????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,

????});

然后點擊第一步中的?skip this onboarding,進入控制臺頁面。

如果忘了自己的 DSN,請點擊左邊的菜單欄選擇?Settings?->?Projects?-> 點擊自己的項目 ->?Client Keys(DSN)。

創建第一個錯誤

在你的 Vue 項目執行一個打印語句?console.log(b)。

這時點開 sentry 主頁的 issues 一項,可以發現有一個報錯信息?b is not defined:

這個報錯信息包含了錯誤的具體信息,還有你的 IP、瀏覽器信息等等。

但奇怪的是,我們的瀏覽器控制臺并沒有輸出報錯信息。

這是因為被 sentry 屏蔽了,所以我們需要加上一個選項?logErrors: true。

然后再查看頁面,發現控制臺也有報錯信息了:

2.上傳 sourcemap

一般打包后的代碼都是經過壓縮的,如果沒有 sourcemap,即使有報錯信息,你也很難根據提示找到對應的源碼在哪。

下面來看一下如何上傳 sourcemap。

首先創建 auth token。

這個生成的 token 一會要用到。

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

????npm?install?sentry-cli-binary?-g

????npm?install?--save-dev?@sentry/webpack-plugin

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

????[auth]

????token=xxx


????[defaults]

????url=https://sentry.io/

????org=woai3c

????project=woai3c

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

org?是你的組織名稱。

project?是你的項目名稱,根據下面的提示可以找到。

在項目下新建?vue.config.js?文件,把下面的內容填進去:

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


????const?config?=?{

????????configureWebpack:?{

????????????plugins:?[

????????????????new?SentryWebpackPlugin({

????????????????????include:?'./dist',?//?打包后的目錄

????????????????????ignore:?['node_modules',?'vue.config.js',?'babel.config.js'],

????????????????}),

????????????],

????????},

????}


????//?只在生產環境下上傳?sourcemap

????module.exports?=?process.env.NODE_ENV?==?'production'??config?:?{}

填完以后,執行?npm run build,就可以看到?sourcemap?的上傳結果了。

上傳 sourcemap 后的報錯信息更加準確了。

性能優化

性能優化主要分為兩類:

1.加載時優化

2.運行時優化

例如壓縮文件、使用 CDN 就屬于加載時優化;減少 DOM 操作,使用事件委托屬于運行時優化。

在解決問題之前,必須先找出問題,否則無從下手。所以在做性能優化之前,最好先調查一下網站的加載性能和運行性能。

手動檢查

1.檢查加載性能

一個網站加載性能如何主要看白屏時間和首屏時間。

白屏時間:指從輸入網址,到頁面開始顯示內容的時間。

首屏時間:指從輸入網址,到頁面完全渲染的時間。

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

????<script>

?????new?Date()?-?performance.timing.navigationStart

????</script>

首屏時間比較復雜,得考慮有圖片和沒有圖片的情況。

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

如果有圖片,則要在最后一個在首屏渲染的圖片的?onload?事件里執行?new Date() - performance.timing.navigationStart?獲取首屏時間,實施起來比較復雜,在這里限于篇幅就不說了。

2.檢查運行性能

配合 chrome 的開發者工具,我們可以查看網站在運行時的性能。

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

另外,在 performance 標簽下,按 ESC 會彈出來一個小框。點擊小框左邊的三個點,把 rendering 勾出來。

這兩個選項,第一個是高亮重繪區域,另一個是顯示幀渲染信息。把這兩個選項勾上,然后瀏覽網頁,可以實時的看到你網頁渲染變化。

利用工具檢查

1.監控工具

可以部署一個前端監控系統來監控網站性能,上一節中講到的 sentry 就屬于這一類。

2.chrome 工具 Lighthouse

如果你安裝了 Chrome 52+ 版本,請按 F12 打開開發者工具。

它不僅會對你網站的性能打分,還會對 SEO 打分。

使用 Lighthouse 審查網絡應用(要科學上網)

如何做性能優化

網上關于性能優化的文章和書籍多不勝數(或者看我另外寫的文章性能優化),這里不做介紹了。

重構

《重構2》一書中對重構進行了定義:

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

重構和性能優化有相同點,也有不同點。

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

重構可以一邊寫代碼一邊重構,也可以在程序寫完后,拿出一段時間專門去做重構。沒有說哪個方式更好,視個人情況而定。

如果你專門拿一段時間來做重構,建議你在重構一段代碼后,立即進行測試。這樣可以避免修改代碼太多,在出錯時找不到錯誤點。

重構的原則

1.事不過三,三則重構。即不能重復寫同樣的代碼,在這種情況下要去重構。

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

3.如果已經理解了代碼,但是非常繁瑣或者不夠好,也可以重構。

4.過長的函數,需要重構。

5.一個函數最好對應一個功能,如果一個函數被塞入多個功能,那就要對它進行重構了。

重構手法

在《重構2》這本書中,介紹了多達上百個重構手法。但我覺得有兩個是比較常用的:

6.提取重復代碼,封裝成函數

7.拆分太長或功能太多的函數

提取重復代碼,封裝成函數

假設有一個查詢數據的接口?/getUserData?age=17&city=beijing。現在需要做的是把用戶數據:{ age: 17, city: 'beijing' }?轉成 URL 參數的形式:

????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

如果只有這一個接口需要轉換,不封裝成函數是沒問題的。但如果有多個接口都有這種需求,那就得把它封裝成函數了:

????function?JSON2Params(data)?{

????????let?result?=?''

????????const?keys?=?Object.keys(data)

????????keys.forEach(key?=>?{

????????????result?+=?'&'?+?key?+?'='?+?data[key]

????????})


????????return?result.substr(1)

????}

拆分太長或功能太多的函數

假設現在有一個注冊功能,用偽代碼表示:

????function?register(data)?{

????????//?1.?驗證用戶數據是否合法

????????/**

?????????*?驗證賬號

?????????*?驗證密碼

?????????*?驗證短信驗證碼

?????????*?驗證身份證

?????????*?驗證郵箱

?????????*/


????????//?2.?如果用戶上傳了頭像,則將用戶頭像轉成?base64?碼保存

????????/**

?????????*?新建?FileReader?對象

?????????*?將圖片轉換成?base64?碼

?????????*/


????????//?3.?調用注冊接口

????????//?...

????}

這個函數包含了三個功能,驗證、轉換、注冊。其中驗證和轉換功能是可以提取出來單獨封裝成函數的:

????function?register(data)?{

????????//?1.?驗證用戶數據是否合法

????????//?verify()


????????//?2.?如果用戶上傳了頭像,則將用戶頭像轉成?base64?碼保存

????????//?tobase64()


????????//?3.?調用注冊接口

????????//?...

????}

如果你對重構有興趣,強烈推薦你閱讀《重構2》這本書。

參考資料:

《重構2》

總結

寫這篇文章主要是為了對我這兩年多工作經驗作總結,因為我基本上都在研究前端工程化以及如何提升團隊的開發效率。希望這篇文章能幫助一些對前端工程化沒有經驗的新手,通過這篇文章入門前端工程化。

如果這篇文章對你有幫助,請點一下贊,感激不盡。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。