1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        手把手帶你入門前端工程化——超詳細教程

        共 20796字,需瀏覽 42分鐘

         ·

        2020-11-12 21:39

        授權(quán)自@譚光志 鏈接:https://segmentfault.com/a/1190000037752931,也可點擊閱讀原文

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

        1. 技術(shù)選型
        2. 統(tǒng)一規(guī)范
        3. 測試
        4. 部署
        5. 監(jiān)控
        6. 性能優(yōu)化
        7. 重構(gòu)

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

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

        技術(shù)選型

        對于前端來說,技術(shù)選型挺簡單的。就是做選擇題,三大框架中選一個。個人認為可以依據(jù)以下兩個特點來選:

        1. 選你或團隊最熟的,保證在遇到棘手的問題時有人能填坑。
        2. 選市場占有率高的。換句話說,就是選好招人的。

        第二點對于小公司來說,特別重要。本來小公司就不好招人,要是還選一個市場占有率不高的框架(例如 Angular),簡歷你都看不到幾個...UI 組件庫更簡單,github 上哪個 star 多就用哪個。star 多,說明用的人就多,很多坑別人都替你踩過了,省事。

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

        代碼規(guī)范

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

        • 規(guī)范的代碼可以促進團隊合作
        • 規(guī)范的代碼可以降低維護成本
        • 規(guī)范的代碼有助于 code review(代碼審查)
        • 養(yǎng)成代碼規(guī)范的習(xí)慣,有助于程序員自身的成長

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

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

        建議找一份好的代碼規(guī)范,在此基礎(chǔ)上結(jié)合團隊的需求作個性化修改。下面列舉一些 star 較多的 js 代碼規(guī)范:

        • airbnb (101k star 英文版),airbnb-中文版
        • standard (24.5k star) 中文版
        • 百度前端編碼規(guī)范 3.9k

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

        • styleguide 2.3k
        • spec 3.9k

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

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

        1. 下載依賴
        //?eslint-config-airbnb-base?使用?airbnb?代碼規(guī)范
        npm?i?-D?babel-eslint?eslint?eslint-config-airbnb-base?eslint-plugin-import
        1. package.jsonscripts加上這行代碼"lint": "eslint --ext .js test/ src/ bin/"。然后執(zhí)行npm run lint即可開始驗證代碼。

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

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

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

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

        git 規(guī)范

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

        分支管理規(guī)范

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

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

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

        git commit 規(guī)范

        <type>():?




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

        1. 標(biāo)題行: 必填, 描述主要修改類型和內(nèi)容
        2. 主題內(nèi)容: 描述為什么修改, 做了什么樣的修改, 以及開發(fā)的思路等等
        3. 頁腳注釋: 可以寫注釋,BUG 號鏈接

        type: commit 的類型

        • feat: 新功能、新特性
        • fix: 修改 bug
        • perf: 更改代碼,以提高性能
        • refactor: 代碼重構(gòu)(重構(gòu),在不影響代碼內(nèi)部行為、功能下的代碼修改)
        • docs: 文檔修改
        • style: 代碼格式修改, 注意不是 css 修改(例如分號修改)
        • test: 測試用例新增、修改
        • build: 影響項目構(gòu)建或依賴項修改
        • 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ù)的這個BUG只影響當(dāng)前修改的文件,可不加范圍。如果影響的范圍比較大,要加上范圍描述。

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

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

        這是一個示例,假設(shè)對點檢任務(wù)靜態(tài)頁面進行了一些描述。
        ?
        這里是備注,可以是放BUG鏈接或者一些重要性的東西。
        chore(其他修改)

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

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

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

        驗證 git commit 規(guī)范

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

        下載輔助工具

        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 提交規(guī)范:https://github.com/woai3c/Front-end-articles/blob/master/git%20commit%20style.md
        ????`)

        ????process.exit(1)
        }

        現(xiàn)在來解釋下各個鉤子的含義:

        1. "pre-commit": "npm run lint",在git commit前執(zhí)行npm run lint檢查代碼格式。
        2. "commit-msg": "node script/verify-commit.js",在git commit時執(zhí)行腳本verify-commit.js驗證 commit 消息。如果不符合腳本中定義的格式,將會報錯。
        3. "pre-push": "npm test",在你執(zhí)行git push將代碼推送到遠程倉庫前,執(zhí)行npm test進行測試。如果測試失敗,將不會執(zhí)行這次推送。

        項目規(guī)范

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

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

        ├─public
        ├─src
        ├─test

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

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

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

        UI 規(guī)范

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

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

        • 統(tǒng)一頁面 UI 標(biāo)準(zhǔn),節(jié)省 UI 設(shè)計時間
        • 提高前端開發(fā)效率

        測試

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

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

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

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

        單元測試

        單元測試就是對一個函數(shù)、一個組件、一個類做的測試,它針對的粒度比較小。它應(yīng)該怎么寫呢?

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

        對一個函數(shù)做測試

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

        對一個類做測試

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

        class?Math?{
        ????abs()?{

        ????}

        ????sqrt()?{

        ????}

        ????pow()?{

        ????}
        ????...
        }

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

        對一個組件做測試

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

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

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

        1. 點擊,選擇圖片上傳。
        2. 觸發(fā)inputchange事件,獲取file對象。
        3. FileReader將圖片轉(zhuǎn)換成 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)
        }

        上面的代碼只是模擬,真實情況下應(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 的事件對象event、FileReader。也就是說,只要我們能夠提供這兩個對象,就可以在任何環(huán)境下運行它。所以我們可以在測試環(huán)境下加上這兩個對象:

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

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

        然后測試可以這樣寫:

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

        //?提供一個假的?file?對象給?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í)行測試
        test()

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

        TDD 測試驅(qū)動開發(fā)

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

        TDD 的初衷是好的,但如果你的需求經(jīng)常變(你懂的),那就不是一件好事了。很有可能你天天都在改測試代碼,業(yè)務(wù)代碼反而沒怎么動。

        所以到現(xiàn)在為止,三年多的程序員生涯,我還沒嘗試過 TDD 開發(fā)。

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

        測試框架推薦

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

        部署

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

        1. 執(zhí)行測試npm run test。
        2. 推送代碼git push
        3. 構(gòu)建項目npm run build。
        4. 將打包好的文件放到靜態(tài)服務(wù)器。

        一次兩次還行,如果天天都這樣,就會把很多時間浪費在重復(fù)的操作上。所以我們要學(xué)會自動部署,徹底解放雙手。自動部署(又叫持續(xù)部署 Continuous Deployment,英文縮寫 CD)一般有兩種觸發(fā)方式:

        1. 輪詢。
        2. 監(jiān)聽webhook事件。

        輪詢

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

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

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

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

        監(jiān)聽webhook事件

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

        例如Github Actions,就有這個功能。對于新人來說,僅看我這一段講解是不可能學(xué)會自動部署的。為此我特地寫了一篇自動化部署教程,不需要你提前學(xué)習(xí)自動化部署的知識,只要照著指引做,就能實現(xiàn)前端項目自動化部署。

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

        監(jiān)控

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

        性能監(jiān)控

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

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

        這個 API 的屬性timing,包含了頁面加載各個階段的起始及結(jié)束時間。
        為了方便大家理解timing各個屬性的意義,我在知乎找到一位網(wǎng)友對于timing寫的簡介(忘了姓名,后來找不到了,見諒),在此轉(zhuǎn)載一下。

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

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

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

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

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

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

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

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

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

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

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

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

        ????//?返回瀏覽器從服務(wù)器收到(或從本地緩存讀?。┑谝粋€字節(jié)時的時間戳。
        ????????//如果傳輸層在開始請求之后失敗并且連接被重開,該屬性將會被數(shù)制成新的請求的相對應(yīng)的發(fā)起時間。
        ????responseStart:?1543806782516,

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

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

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

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

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

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

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

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

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

        //?重定向耗時
        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,
        //?獲取性能信息時當(dāng)前時間
        time:?new?Date().getTime(),

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

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


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

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

        它一般包括以下幾個類型

        • sciprt
        • link
        • img
        • css
        • fetch
        • other
        • xmlhttprequest

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

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

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

        //?收集性能信息
        const?getPerformance?=?()?=>?{
        ????if?(!window.performance)?return
        ????const?timing?=?window.performance.timing
        ????const?performance?=?{
        ????????//?重定向耗時
        ????????redirect:?timing.redirectEnd?-?timing.redirectStart,
        ????????//?白屏?xí)r間
        ????????whiteScreen:?whiteScreen,
        ????????//?DOM?渲染耗時
        ????????dom:?timing.domComplete?-?timing.domLoading,
        ????????//?頁面加載耗時
        ????????load:?timing.loadEventEnd?-?timing.navigationStart,
        ????????//?頁面卸載耗時
        ????????unload:?timing.unloadEventEnd?-?timing.unloadEventStart,
        ????????//?請求耗時
        ????????request:?timing.responseEnd?-?timing.requestStart,
        ????????//?獲取性能信息時當(dāng)前時間
        ????????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:?[],
        ????????//?獲取資源信息時當(dāng)前時間
        ????????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,
        ????????????//?資源所用協(xié)議
        ????????????protocol:?item.nextHopProtocol,
        ????????})
        ????})

        ????return?resource
        }

        小結(jié)

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

        1. 資源過多
        2. 網(wǎng)速過慢
        3. DOM元素過多

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

        錯誤監(jiān)控

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

        1. 資源加載錯誤,通過addEventListener('error', callback, true)在捕獲階段捕捉資源加載失敗錯誤。
        2. js 執(zhí)行錯誤,通過window.onerror捕捉 js 錯誤。
        3. promise 錯誤,通過addEventListener('unhandledrejection', callback)捕捉 promise 錯誤,但是沒有發(fā)生錯誤的行數(shù),列數(shù)等信息,只能手動拋出相關(guān)錯誤信息。

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

        //?捕獲資源加載失敗錯誤?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',
        ????????????//?錯誤發(fā)生的時間
        ????????????time:?new?Date().getTime(),
        ????????})
        ????}
        },?true)

        //?監(jiān)聽?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,
        ????????//?錯誤發(fā)生的時間
        ????????time:?new?Date().getTime(),
        ????})
        }

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

        小結(jié)

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

        數(shù)據(jù)上報

        性能數(shù)據(jù)上報

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

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

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

        錯誤數(shù)據(jù)上報

        我在DEMO里提供的代碼,是用一個errors數(shù)組收集所有的錯誤,再在某一階段統(tǒng)一上報(延時上報)。

        其實,也可以改成在錯誤發(fā)生時上報(即時上報)。這樣可以避免在收集完錯誤延時上報還沒觸發(fā),用戶卻已經(jīng)關(guān)掉網(wǎng)頁導(dǎo)致錯誤數(shù)據(jù)丟失的問題。

        //?監(jiān)聽?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,
        ????????//?錯誤發(fā)生的時間
        ????????time:?new?Date().getTime(),
        ????}
        ????
        ????//?即時上報
        ????axios.post({?url:?'xxx',?data,?})
        }

        SPA

        window.performanceAPI 是有缺點的,在 SPA 切換路由時,window.performance.timing的數(shù)據(jù)不會更新。

        所以我們需要另想辦法來統(tǒng)計切換路由到加載完成的時間。

        拿 Vue 舉例,一個可行的辦法就是切換路由時,在路由的全局前置守衛(wèi)beforeEach里獲取開始時間,在組件的mounted鉤子里執(zhí)行vm.$nextTick函數(shù)來獲取組件的渲染完畢時間。

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

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

        用戶信息收集

        navigator

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

        UV(Unique visitor)

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

        PV(Page View)

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

        頁面停留時間

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

        瀏覽深度

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

        頁面跳轉(zhuǎn)來源

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

        小結(jié)

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

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

        前面說的都是監(jiān)控原理,但要實現(xiàn)還是得自己動手寫代碼。為了避免麻煩,我們可以用現(xiàn)有的工具 sentry 去做這件事。sentry 是一個用 python 寫的性能和錯誤監(jiān)控工具,你可以使用 sentry 提供的服務(wù)(免費功能少),也可以自己部署服務(wù)?,F(xiàn)在來看一下如何使用 sentry 提供的服務(wù)實現(xiàn)監(jiān)控。

        注冊賬號

        打開https://sentry.io/signup/網(wǎng)站,進行注冊。選擇項目,我選的 Vue。

        安裝 sentry 依賴

        選完項目,下面會有具體的 sentry 依賴安裝指南。根據(jù)提示,在你的 Vue 項目執(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?地址,注冊完就有
        ??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)。

        創(chuàng)建第一個錯誤

        在你的 Vue 項目執(zhí)行一個打印語句console.log(b)。這時點開 sentry 主頁的 issues 一項,可以發(fā)現(xiàn)有一個報錯信息b is not defined這個報錯信息包含了錯誤的具體信息,還有你的 IP、瀏覽器信息等等。但奇怪的是,我們的瀏覽器控制臺并沒有輸出報錯信息。這是因為被 sentry 屏蔽了,所以我們需要加上一個選項logErrors: true。然后再查看頁面,發(fā)現(xiàn)控制臺也有報錯信息了:

        上傳 sourcemap

        一般打包后的代碼都是經(jīng)過壓縮的,如果沒有 sourcemap,即使有報錯信息,你也很難根據(jù)提示找到對應(yīng)的源碼在哪。下面來看一下如何上傳 sourcemap。首先創(chuàng)建 auth token。這個生成的 token 一會要用到。安裝sentry-cli@sentry/webpack-plugin

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

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

        [auth]
        token=xxx

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

        把 xxx 替換成剛才生成的 token。org是你的組織名稱。project是你的項目名稱,根據(jù)下面的提示可以找到。在項目下新建vue.config.js文件,把下面的內(nèi)容填進去:

        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é)果了。我們再來看一下沒上傳 sourcemap 和上傳之后的報錯信息對比。未上傳 sourcemap已上傳 sourcemap可以看到,上傳 sourcemap 后的報錯信息更加準(zhǔn)確。

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

        選完刷新即可。

        性能監(jiān)控

        打開 performance 選項,就能看到你每個項目的運行情況。具體的參數(shù)解釋請看文檔Performance Monitoring。

        性能優(yōu)化

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

        1. 加載時優(yōu)化
        2. 運行時優(yōu)化

        例如壓縮文件、使用 CDN 就屬于加載時優(yōu)化;減少 DOM 操作,使用事件委托屬于運行時優(yōu)化。在解決問題之前,必須先找出問題,否則無從下手。所以在做性能優(yōu)化之前,最好先調(diào)查一下網(wǎng)站的加載性能和運行性能。

        手動檢查

        檢查加載性能

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

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

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


        首屏?xí)r間比較復(fù)雜,得考慮有圖片和沒有圖片的情況。如果沒有圖片,則在window.onload事件里執(zhí)行new Date() - performance.timing.navigationStart即可獲取首屏?xí)r間。如果有圖片,則要在最后一個在首屏渲染的圖片的onload事件里執(zhí)行new Date() - performance.timing.navigationStart獲取首屏?xí)r間,實施起來比較復(fù)雜,在這里限于篇幅就不說了。

        檢查運行性能

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

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

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

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

        利用工具檢查

        監(jiān)控工具

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

        chrome 工具 Lighthouse

        如果你安裝了 Chrome 52+ 版本,請按 F12 打開開發(fā)者工具。
        它不僅會對你網(wǎng)站的性能打分,還會對 SEO 打分。使用 Lighthouse 審查網(wǎng)絡(luò)應(yīng)用

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

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

        重構(gòu)

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

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

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

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

        重構(gòu)可以一邊寫代碼一邊重構(gòu),也可以在程序?qū)懲旰?,拿出一段時間專門去做重構(gòu)。沒有說哪個方式更好,視個人情況而定。

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

        重構(gòu)的原則

        1. 事不過三,三則重構(gòu)。即不能重復(fù)寫同樣的代碼,在這種情況下要去重構(gòu)。
        2. 如果一段代碼讓人很難看懂,那就該考慮重構(gòu)了。
        3. 如果已經(jīng)理解了代碼,但是非常繁瑣或者不夠好,也可以重構(gòu)。
        4. 過長的函數(shù),需要重構(gòu)。
        5. 一個函數(shù)最好對應(yīng)一個功能,如果一個函數(shù)被塞入多個功能,那就要對它進行重構(gòu)了。

        重構(gòu)手法

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

        1. 提取重復(fù)代碼,封裝成函數(shù)
        2. 拆分太長或功能太多的函數(shù)

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

        假設(shè)有一個查詢數(shù)據(jù)的接口/getUserData?age=17&city=beijing?,F(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

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

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

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

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

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

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

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

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

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

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

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

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

        如果你對重構(gòu)有興趣,強烈推薦你閱讀《重構(gòu)2》這本書。參考資料:

        • 《重構(gòu)2》

        總結(jié)

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

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

        求職啟事

        本人具有三年+前端工作經(jīng)驗,32歲,高中學(xué)歷,現(xiàn)尋求天津、北京地區(qū)的前端工作機會。下面是我掌握的一些技能:

        1. 熟練掌握 HTML、CSS、JavaScript。
        2. 熟練掌握 Vue 全家桶并研究過 Vue1.0 源碼及 Vue3.0 部分源碼。
        3. 使用 nodejs 寫過腳本和個人博客,沒有開發(fā)過企業(yè)應(yīng)用。
        4. 學(xué)習(xí)計算機原理并實現(xiàn)一個簡單的 cpu 和內(nèi)存模塊運行在模擬器上(github 項目地址)。
        5. 學(xué)習(xí)操作系統(tǒng)并做實驗實現(xiàn)了一個簡單的內(nèi)核(github 項目地址)。
        6. 學(xué)習(xí)編譯原理寫過一個簡單編譯器(github 項目地址)。
        7. 對計算機網(wǎng)絡(luò)應(yīng)用層和傳輸層的知識比較了解。
        8. 數(shù)據(jù)結(jié)構(gòu)與算法有學(xué)習(xí)過,還刷了 300+ 道 leetcode,但效果不是很好。

        社交網(wǎng)站

        • Github
        • 知乎

        如果您覺得我的條件還可以,可以私信我或在評論區(qū)留言,謝謝。

        瀏覽 28
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            91国产在线精品 | 操中国女人逼视频 | 久久久精品一二三区 | 忘穿内裤被同桌摸到爽 | 精品久久久久久久久久久久 | 色噜噜在线 | 人人摸人人舔 | 免费精品国产va自在自线 | 97超碰在线免费观看 | 午夜免费福利视频 |