1. 程式人生 > 實用技巧 >現代前端技術解析讀書筆記

現代前端技術解析讀書筆記

思維導圖連結:http://v3.processon.com/view/link/5f7ec592762131119546c899

取材自《現代前端技術解析》

本文只是個人讀書筆記,更多詳細內容請檢視原書。

前端技術解析

  • Web前端技術基礎
      • 使用者介面包括瀏覽器可見的地址輸入框、瀏覽器前進返回按鈕、開啟書籤、開啟歷史記錄等使用者可操作的功能選項。
      • 瀏覽器引擎可以在使用者介面和渲染引擎之間傳送指令或在客戶端本地快取中讀寫資料等,是瀏覽器中各個部分間相互通訊的核心。
      • 瀏覽器渲染引擎(排版引擎)的功能是解析DOM文件和CSS規則並將內容排版到瀏覽器中顯示有樣式的介面。
      • 網路功能模組是瀏覽器開啟網路執行緒傳送請求或下載資原始檔的模組。
      • UI後端用於繪製基本的瀏覽器視窗內控制元件,如組合選擇框、按鈕等。
      • JS引擎是瀏覽器解釋和執行JS指令碼的部分,如V8引擎。
      • 瀏覽器資料持久化儲存涉及cookie、localStorage等一些客戶端儲存技術,可以通過瀏覽器引擎提供的API進行呼叫。
    • 解析HTML構建DOM樹時渲染引擎會先將HTML元素標籤解析成由多個DOM元素物件節點組成的且具有節點父子關係的DOM樹結構,然後根據DOM樹結構的每個節點順序提取計算使用的CSS規則並重新計算DOM樹結構的樣式資料,生成一個帶樣式描述DOM渲染樹物件。DOM渲染樹生成結束後,進入渲染樹的佈局階段,即根據每個渲染樹節點在頁面中的大小和位置,將節點固定到頁面的對應位置上,這個階段主要是元素的佈局屬性(如position、float、margin)生效,即在瀏覽器中繪製頁面上元素節點的位置。接下來就是繪製階段,將渲染樹節點的背景、顏色、文字等樣式資訊應用到每個節點上,這個階段主要是元素的內部顯示樣式(如color、background、text-shadow)生效,最終完成整個DOM在頁面上的繪製顯示。
    • 瀏覽器資料持久化儲存技術
      • HTTP檔案快取
        • HTTP檔案快取是基於HTTP協議的瀏覽器端檔案級快取機制。在檔案重複請求的情況下,瀏覽器可以根據HTTP響應的協議頭資訊判斷是從伺服器端請求檔案還是從本地讀取檔案。
      • LocalStorage
      • SessionStorage
      • indexDB
        • IndexDB是一個可在客戶端儲存大量結構化資料並且能在這些資料上使用索引進行高效能檢索的一套API。
      • Web SQL
      • Cookie
        • Cookie指網站為了辨別使用者身份或Session跟蹤而儲存在使用者瀏覽器端的資料。Cookie資訊一般會通過HTTP請求傳送到伺服器端。一條Cookie記錄主要由鍵、值、域、過期時間和大小組成,一般用於儲存使用者的網站認證資訊。
        • Cookie設定中有個HTTPOnly引數,前端瀏覽器使用document.cookie是讀取不到HTTPOnly型別Cookie的,被設定為HttpOnly的Cookie記錄只能通過HTTP請求頭髮送到伺服器端進行讀寫操作,這樣就避免了伺服器端的Cookie記錄被前端JS修改,保證了伺服器端驗證Cookie的安全性。
      • CacheStorage
        • CacheStorage是在ServiceWorker規範中定義的,可用於儲存每個ServiceWorker宣告的Cache物件,是未來可能用來代替Application Cache的離線方案。
        • CacheStorage在瀏覽器端未windows下的全域性內建物件caches:caches.has(); // 檢查如果包含Cache物件,則返回一個promise物件caches.open(); // 開啟一個Cache物件,並返回一個promise物件caches.delete(); // 刪除Cache物件,成功則返回一個promise物件,否則返回falsecaches.keys(); // 含有keys中字串的任意一個,則返回一個promise物件caches.match(); // 匹配key中含有該字串的cache物件,返回一個promise物件
      • Application Cache
        • Application Cache是一種允許瀏覽器通過mainfest配置檔案在本地有選擇性地儲存JS、CSS、圖片等靜態資源的檔案級快取機制。當頁面不是首次開啟時,通過一個特定的mainfest檔案配置描述來選擇讀取本地Application Cache裡面的檔案。
        • 優勢
          • 1. 離線瀏覽
          • 2. 快速載入
          • 3. 伺服器負載小
            • 只有在檔案資源更新時,瀏覽器才會從伺服器端下載,這樣就減小了伺服器資源請求的壓力。
        • 問題
          • 1. Application Cache已經開始被標準棄用,漸漸將會由ServiceWorkers來代替,所以現在不建議使用Application Cache來實現離線應用,僅作為一種技術瞭解即可。
          • 2. Application Cache仍不能相容目前全部主流的瀏覽器環境,即使是在移動端。
          • 3. Application Cache為站點離線儲存提供的容量限制是5MB,現在來說顯然不適用。
          • 4. 如果mainfest檔案或內部列表中的某一個檔案不能正常下載,整個更新過程將被視為失敗,瀏覽器將繼續使用舊的快取。
          • 5. 引用mainfest的HTML、快取列表的靜態資源必須與mainfest檔案同源,即保持在同一個域下。
          • 6. 站點中的其他頁面即使沒有設定mainfest屬性,請求的資源也會從快取中訪問。
          • 7. 當mainfest檔案發生改變時,資源請求本身也會觸發更新。
      • Flash快取
  • 前端與協議
    • web安全
      • XSS(Cross Site Script,跨站指令碼攻擊)
        • XSS通常是由帶有頁面可解析內容的資料未經處理直接插入到頁面上解析導致的。
        • 根據攻擊指令碼的引入位置分類
          • 儲存型XSS
            • 儲存型XSS的攻擊指令碼常是由前端提交的資料未經處理直接儲存到資料庫然後從資料庫中讀取出現後又直接插入到頁面中所導致的。
            • <div>{{ content }}</div>
          • 反射型XSS
            • 反射型XSS可能是在網頁URL引數中注入了可解析內容的資料而導致的,如果直接獲取URL中不合法的並插入頁面中則可能出現頁面上XSS攻擊
            • let name = req.query['name'];this.body = `<div>${name}></div>`;
          • DOM XSS
            • MXSS則是在渲染DOM屬性時將攻擊指令碼插入DOM屬性中被解析而導致的。
            • <p class="class-a {{b}}"></p>
        • XSS主要的防範方法是驗證輸入到頁面上所有內容來源是否安全,如果可能含有指令碼標籤等內容則需要進行必要的轉義。
      • SQL注入
      • CSRF(Cross-site Request Forgery,跨站請求偽造)
        • CSRF是指非源站點按照源站點的資料請求格式提交非法資料給源站點伺服器的一種攻擊方法。非源站點在取到使用者登入驗證資訊的情況下,可以直接對源站點的某個資料介面進行提交,如果源站點對該提交請求的資料來源未經驗證,該請求可能被成功執行。通常比較安全的是通過頁面Token提供驗證的方式來驗證請求是否為源站點頁面提交的,來阻止跨站偽請求的發生。
    • 網路劫持
      • 網路劫持一般指網路資源請求在請求過程中因為人為的攻擊導致沒有載入到預期的資源內容。
      • DNS劫持
        • DNS劫持是指攻擊者劫持了DNS伺服器,通過某些手段取得某域名的解析記錄控制權,進而修改此域名的解析結果,導致使用者對該域名地址的訪問由原IP地址轉入到修改後的指定IP地址的現象,其結果就是讓正確的網址不能解析或被解析指向另一網站IP,實現獲取使用者資料或破壞原有網站正常服務的目的。
        • DNS劫持一般通過篡改DNS伺服器上的域名解析記錄,來返回給使用者一個錯誤的DNS查詢結果實現。
      • HTTP劫持
        • HTTP劫持指在使用者瀏覽器與訪問的目的伺服器之間所建立的網路資料傳輸通道中從閘道器或防火牆層上監視特定資料資訊,當滿足一定的條件時,就會在正常的資料包中插入或修改成為攻擊者設計的網路資料包,目的是讓使用者瀏覽器解釋“錯誤”的資料,或以彈出新視窗的形式在使用者瀏覽器介面上展示宣傳性廣告或直接顯示某塊其他的內容。
      • 請求劫持唯一可行的預防方法就是儘量使用HTTPS協議來訪問目標網站。
  • 前端三層結構與應用
    • 桌面瀏覽器器端推薦使用JS直接實現動畫的方式或SVG動畫的實現方式,移動端則可以考慮使用CSS3 transition、CSS3 animation、canvas或requestAnimationFrame。
    • 1rem的計算
      • 1rem=螢幕寬度*螢幕解析度/10=螢幕寬度的10%
      • 1rem=螢幕寬度/設計稿螢幕寬度*10
  • 現代前端互動框架
    • 頁面路由實現思路:讓URL地址內容匹配對應的字串然後進行相應的操作。
    • ```
    • const router = {
    • get(match, fn) {
    • let url = location.href,
    • routeReg = new RegExp(match, 'g');
    • if (routeReg.test(url)) {
    • fn();
    • }
    • return this;
    • }
    • };
    • router.get('#index', function () {
    • _loadIndex(); // 註冊hash含有#index的路由執行對應的操作
    • }).get('#detail', function () {
    • _loadDetail(); // 註冊hash含有#detail的路由執行對應的操作
    • });
    • ```
    • 可以使用html5的pushState來實現路由:history.pushState(state,title,url)可以改變當前頁面的url而不發生跳轉,並將不同的state資料和對應的url對應起來。如果頁面顯示的內容是根據不同的資料狀態來自動完成的,這樣根據state的內容來載入不同的元件就很有用了。history.pushState({page:'A'}, 'page A','a.html'};
    • 主流MVC框架的元件定義
      • ```
      • // 可能有一個公用的Component基類
      • let component = new Component();
      • let A = component.extend({
      • $el: document.getElementById('A'),
      • model: {
      • text: 'ViewA渲染完成',
      • },
      • view(data) {
      • let template = '{{text}}';
      • // 呼叫模板渲染資料獲取HTML片段
      • let html = render(template, data);
      • this.$el.innerHTML = html;
      • },
      • controller() {
      • let self = this;
      • // 呼叫model資料傳入view中渲染內容
      • self.view(self.model);
      • // 使用者操作一般通過Hash來觸發Controller改變Model和View
      • $('window').on('hashchange', function () {
      • self.model.text = location.hash;
      • self.view(self.model);
      • });
      • // 點選事件可以直接觸發Model改變並重新渲染View
      • self.event['change'] = function () {
      • self.model.text = '新的ViewA渲染完成';
      • self.view(self.model);
      • };
      • }
      • });
      • ```
    • Presenter作為中間部分連線Model和View的通訊互動完成所有的邏輯操作,但這樣Presenter層的內容就可能變得很重了。另外使用者在View上的操作會反饋到Presenter中進行Model修改,並更新其他對應部分的View內容。
    • ```
    • // 可能有一個公用的Component基類
    • let component = new Component();
    • let A = component.extend({
    • $el: document.getElementById('A'),
    • model: {
    • text: 'ViewA渲染完成',
    • },
    • view: '{{text}}',
    • presenter() {
    • let self = this;
    • // 呼叫模板渲染資料獲取HTML片段
    • let html = render(self.view, self.model);
    • self.$el.innerHTML = html;
    • // View上的改變將通知Presenter改變Model和其他的View
    • $('#input').on('change', function () {
    • self.model.text = this.value;
    • html = render('{{text}}', self.model);
    • $('#showText').html(html);
    • });
    • // 點選事件可以直接觸發Model改變並重新渲染View
    • self.event['change'] = function () {
    • self.model.text = '新的ViewA渲染完成';
    • html = render('{{text}}', self.model);
    • $('#showText').html(html);
    • };
    • }
    • });
    • ```
    • MVVM設計的一個很大的好處是將MVP中Presenter的工作拆分成多個小的指令步驟,然後繫結到相對應的元素中,根據相對應的資料變化來驅動觸發,自動管理互動操作,同時也免去了檢視Presenter中事件列表的工作,而且一般ViewModel初始化時會自動進行資料繫結,並將頁面中所有的同類操作複用,大大節省了我們自己進行內容渲染和事件繫結的程式碼量。
    • ```
    • let viewModel = new VM({
    • $el: document.getElementById('A'),
    • data: {
    • text: 'ViewA渲染完成'
    • },
    • method: {
    • change() {
    • this.text = '新的ViewA渲染完成';
    • }
    • }
    • });
    • ```
    • 實現資料變更檢測的方法主要有手動觸發繫結、髒資料檢測、物件劫持、Proxy等。
      • 手動觸發繫結主要思路是通過在資料物件上定義get()方法和set()方法(也可以使用其他命名方法),呼叫時手動觸發get()或set()函式來獲取、修改資料,改變資料後會主動觸發get()和set()函式中View層的重新渲染功能。
      • ```
      • 手動觸發繫結
      • let elems = [document.getElementById('el'), document.getElementById('input')];
      • let data = {
      • value: 'hello'
      • };
      • // 定義Directive
      • let directive = {
      • text: function (text) {
      • this.innerHTML = text;
      • },
      • value: function (value) {
      • this.setAttribute('value', value);
      • }
      • };
      • // 資料繫結監聽
      • if (document.addEventListener) {
      • elems[1].addEventListener('keyup', function (e) {
      • ViewModelSet('value', e.target.value);
      • }, false);
      • } else {
      • elems[1].attachEvent('onkeyup', function (e) {
      • ViewModelSet('value', e.target.value);
      • }, false);
      • }
      • // 開始掃描節點
      • scan();
      • // 設定頁面2秒後自動改變資料更新檢視
      • setTimeout(function () {
      • ViewModelSet('value', 'hello ouvenzhang');
      • }, 1000);
      • function scan() {
      • // 掃描帶指令的節點屬性
      • for (let elem of elems) {
      • elem.directive = [];
      • for (let attr of elem.attributes) {
      • if (attr.nodeName.indexOf('q-') >= 0) {
      • // 呼叫屬性指令
      • directive[attr.nodeName.slice(2)].call(elem, data[attr.nodeValue]);
      • elem.directive.push(attr.nodeName.slice(2));
      • }
      • }
      • }
      • }
      • // 設定資料改變後掃描節點
      • function ViewModelSet(key, value) {
      • data[key] = value;
      • scan();
      • }
      • ```
      • 髒資料檢測的基本原理是在ViewModel物件的某個屬性值發生變化時找到與這個屬性值相關的所有元素,然後再比較資料變化,如果變化則進行Directive指令呼叫,對這個元素進行重新掃描渲染。髒資料檢測只針對可能修改的元素進行掃描,提高了ViewModel內容變化後掃描檢視渲染的效率。
      • ```
      • data-binding-drity-check
      • let elems = [document.getElementById('el'), document.getElementById('input')];
      • let data = {
      • value: 'hello'
      • };
      • // 定義Directive
      • let directive = {
      • text: function (text) {
      • this.innerHTML = text;
      • },
      • value: function (value) {
      • this.setAttribute('value', value);
      • }
      • };
      • // 初始化掃描節點
      • scan(elems);
      • $digest('value');
      • // 輸入框資料繫結監聽
      • if (document.addEventListener) {
      • elems[1].addEventListener('keyup', function (e) {
      • data.value = e.target.value;
      • $digest(e.target.getAttribute('q-bind'));
      • }, false);
      • } else {
      • elems[1].attachEvent('onkeyup', function (e) {
      • data.value = e.target.value;
      • $digest(e.target.getAttribute('q-bind'));
      • }, false);
      • }
      • // 設定頁面2秒後自動改變資料更新檢視
      • setTimeout(function () {
      • data.value = 'hello ouvenzhang';
      • // 執行$digest方法來啟動髒檢測
      • $digest('value');
      • }, 1000);
      • function scan(elems) {
      • // 掃描帶指令的節點屬性
      • for (let elem of elems) {
      • elem.directive = [];
      • }
      • }
      • // 可以理解為資料劫持監聽
      • function $digest(value) {
      • let list = document.querySelectorAll('[q-bind=' + value + ']');
      • digest(list);
      • }
      • // 髒資料迴圈檢測
      • function digest(elems) {
      • // 掃描帶指令的節點屬性
      • for (let elem of elems) {
      • for (let attr of elem.attributes) {
      • if (attr.nodeName.indexOf('q-event') >= 0) {
      • // 呼叫屬性指令
      • let dataKey = elem.getAttribute('q-bind') || undefined;
      • // 進行髒資料檢測,如果資料改變,則重新執行指令,否則跳過
      • if (elem.directive[attr.nodeValue] !== data[dataKey]) {
      • directive[attr.nodeValue].call(elem, data[dataKey]);
      • elem.directive[attr.nodeValue] = data[dataKey];
      • }
      • }
      • }
      • }
      • }
      • ```
      • 資料劫持基本思路是使用Object.defineProperty和Object.defineProperties對ViewModel資料物件進行屬性get()和set()的監聽,當有資料讀取和賦值操作時則掃描元素節點,執行指定對應節點的Directive指令,這樣ViewModel使用通用的等號賦值就可以了。
      • ```
      • data-binding-hijacking
      • let elems = [document.getElementById('el'), document.getElementById('input')];
      • let data = {
      • value: 'hello'
      • };
      • // 定義Directive
      • let directive = {
      • text: function (text) {
      • this.innerHTML = text;
      • },
      • value: function (value) {
      • this.setAttribute('value', value);
      • }
      • };
      • let bValue;
      • // 開始掃描節點
      • scan();
      • // 可以理解為資料劫持監聽
      • defineGetAndSet(data, 'value');
      • // 資料繫結監聽
      • if (document.addEventListener) {
      • elems[1].addEventListener('keyup', function (e) {
      • data.value = e.target.value;
      • }, false);
      • } else {
      • elems[1].attachEvent('onkeyup', function (e) {
      • data.value = e.target.value;
      • }, false);
      • }
      • // 設定頁面2秒後自動改變資料更新檢視
      • setTimeout(function () {
      • data.value = 'hello ouvenzhang';
      • }, 1000);
      • function scan() {
      • // 掃描帶指令的節點屬性
      • for (let elem of elems) {
      • elem.directive = [];
      • for (let attr of elem.attributes) {
      • if (attr.nodeName.indexOf('q-') >= 0) {
      • // 呼叫屬性指令
      • directive[attr.nodeName.slice(2)].call(elem, data[attr.nodeValue]);
      • elem.directive.push(attr.nodeName.slice(2));
      • }
      • }
      • }
      • }
      • // 定義物件屬性設定劫持
      • function defineGetAndSet(obj, propName) {
      • Object.defineProperty(obj, propName, {
      • get: function () {
      • return bValue;
      • },
      • set: function (newValue) {
      • bValue = newValue;
      • scan();
      • },
      • enumerable: true,
      • configurable: true
      • });
      • }
      • ```
      • Proxy
      • ```
      • data-binding-proxy
      • let elems = [document.getElementById('el'), document.getElementById('input')];
      • // 定義Directive
      • let directive = {
      • text: function (text) {
      • this.innerHTML = text;
      • },
      • value: function (value) {
      • this.setAttribute('value', value);
      • }
      • };
      • let data = new Proxy({}, {
      • get: function (target, key, receiver) {
      • return target.value;
      • },
      • set: function (target, key, value, receiver) {
      • target.value = value;
      • scan();
      • return target.value;
      • }
      • });
      • data['value'] = 'hello';
      • // 資料繫結監聽
      • if (document.addEventListener) {
      • elems[1].addEventListener('keyup', function (e) {
      • data.value = e.target.value;
      • }, false);
      • } else {
      • elems[1].attachEvent('onkeyup', function (e) {
      • data.value = e.target.value;
      • }, false);
      • }
      • // 設定頁面2秒後自動改變資料更新檢視
      • setTimeout(function () {
      • data.value = 'hello ouvenzhang';
      • }, 1000);
      • function scan() {
      • // 掃描帶指令的節點屬性
      • for (let elem of elems) {
      • elem.directive = [];
      • for (let attr of elem.attributes) {
      • if (attr.nodeName.indexOf('q-') >= 0) {
      • // 呼叫屬性指令
      • directive[attr.nodeName.slice(2)].call(elem, data[attr.nodeValue]);
      • elem.directive.push(attr.nodeName.slice(2));
      • }
      • }
      • }
      • }
      • ```
    • Virtual DOM互動模式
      • Virtual DOM設計理念
        • MVVM的前端互動模式大大提高了程式設計效率,自動雙向資料繫結讓我們可以將頁面邏輯實現的核心轉移到資料層的修改操作上,而不再是在頁面中直接操作DOM。但實際上,儘管MVVM改變了前端開發的邏輯方式,但是最終資料層反應到頁面上View層的渲染和改變仍是通過對應的指令進行DOM操作來完成的,而且通常一次ViewModel的變化可能會觸發頁面上多個指令操作DOM的變化,帶來大量的頁面結構層DOM操作或渲染。
        • Virtual DOM是一個能夠直接描述一段HTML DOM結構的JS物件,瀏覽器可以根據它的結構按照一定規則創建出確定唯一的HTML DOM結構。整體來看,Virtual DOM的互動模式減少了MVVM或其他框架中對DOM的掃描或操作次數,並且在資料發生改變後只在合適的地方根據JS物件來進行最小化的頁面DOM操作,避免大量重新渲染。
      • VIrtual DOM核心實現
        • 使用VM模式來控制頁面DOM結構更新的過程:建立原始頁面或元件的VM結構,使用者操作後需要進行DOM更新時,生成使用者操作後頁面或元件的VM結構並與之前的結構進行對比,找到最小變化VM的差異化描述物件,最後把差異化的VM根據特定的規則渲染到頁面上。
        • 步驟
          • 1. 建立Virtual DOM
            • 建立VIrtual DOM即把一段HTML字串文字解析成一個能夠描述它的JS物件。
            • 通過瀏覽器提供的DOM API掃描這段DOM的節點,遍歷它的屬性,然後新增到JS物件上即可。這樣建立Virtual DOM會直接失去VIrtual DOM的優勢,它是為了避免直接進行DOM操作而設計的。我們不能通過瀏覽器DOM API掃描去生成JS物件,因為掃描過程本身使用到DOM的讀取操作,這個過程很慢。
            • 逐個分析HTML字串中的字元,根據詞法分析內容,將標籤名存為tagName,屬性存入attributes,子標籤內容存入children。這樣,就通過JS直接分析HTML字串文字來生成VIrtual DOM,比DOM API操作要快。
            • 根據HTML字串解析建立VIrtual DOM的過程相當於實現了一個HTML文字解析器,但是沒有生成DOM物件樹,只是生成了一個操作效率更高的JS物件,因此通常不會直接將HTML交給瀏覽器去解析,因為瀏覽器的DOM解析很慢,這也是VIrtual DOM互動模式和普通DOM程式設計最本質的區別。
            • 完成之後再通過VIrtual DOM進行渲染生成一個真實的DOM操作就比較簡單了。
            • ```
            • const render = function (virtualDOM) {
            • let element = document.createElement(virtualDOM.tagName);
            • let attributes = virtualDOM.attributes;
            • // 設定節點的DOM屬性
            • for (let key in attributes) {
            • element.setAttribute(key, attributes[key])
            • }
            • let children = virtualDOM.children || [];
            • for (let child of children) {
            • // 如果是字串則直接插入字串,否則構建子節點
            • let childNode = (typeof children === 'string') ? document.createTextNode(child) : render(child);
            • element.appendChild(childNode)
            • }
            • return element;
            • };
            • ```
          • 2. 對比兩個Virtual DOM生成差異化Virtual DOM
            • Virtual DOM的對比演算法實際上是對於多叉樹結構的遍歷演算法。
            • 可以對VIrtual DOM中的每個節點新增一個唯一的字母id,那麼兩個VIrtual DOM的節點順序分別使用深度優先遍歷演算法表示為ABEFCGHDIJ和AKLMBEFCGHDIJ,這樣我們很容易分析出需要在A和B之間進行插入操作KLM節點,再根據KLM的關係,可以知道只需要插入完整的K節點即可。使用廣度優先演算法遍歷的思路也是類似的,遍歷出兩個VIrtual DOM節點屬性為ABCDEFGHIJ和AKBCDLMEFGHIJ,不過稍微不同的是,這種情況下檢測到有兩處插入,需要進一步判斷來合併操作,優化差異樹的結構。
            • 在VIrtual DOM的對比過程中,除了節點改變的內容,還需要繼續記錄發生差異化改變的型別和位置,例如是針對具體哪一個元素的增加、替換、刪除操作等。
          • 3. 將差異化Virtual DOM渲染到頁面上
      • 與以前互動模式相比,VIrtual DOM最本質的區別在於減少了對DOM物件的操作,通過JS物件來代替DOM物件樹,並且在頁面結構改變時進行最小代價的DOM渲染操作,提高了互動的效能和效率。這就是VM互動模式的優勢,也是提高前端互動效能的根本原因。
      • MNV*框架端的主要任務是解析Model、ViewModel或VIrtual DOM組成JSBridge協議串併發送,而Native端的實現將會比較複雜,需要處理不同的標籤元素解析,還可能需要處理事件的繫結等,即將JS的事件通過Native事件來實現。整體上像是使用移動端原生的方式來解析HTML上需要實現的應用功能。
      • MNV*的基本原理是將JSBridge和DOM程式設計的方式進行結合,讓前端能夠快速構建開發原生介面的應用,從而脫離DOM的互動模式。
  • 前端專案與技術實踐
    • 前端規範
      • 前端頁面開發應做到結構層(HTML)、表現層(CSS)、行為層(JS)分離 ,保證它們之間的最小耦合。移動端開發可以適當地進行CSS樣式、圖片資源、JS內聯,內聯的資源大小標準一般為2KB以內,否則可能會導致HTML檔案過大,頁面首次載入時間過長。
      • head中必須定義title、keyword、description,保證基本的SEO頁面關鍵字和內容描述。移動端頁面head要新增viewpoint控制頁面不縮放,有利於提高頁面渲染效能。
      • CSS樣式書寫順序遵循先佈局後內容的規則。
    • 設計一個高效的元件化規範應該解決的問題
      • 元件之間獨立、鬆耦合。元件之間的HTML、JS、CSS之間相互獨立,儘量不重複,相同部分通過父級或基礎元件來實現,最大限度減少重複程式碼。
      • 元件間巢狀使用。元件可以巢狀使用,但巢狀後仍然是獨立、送耦合的。
      • 元件間通訊。主要指元件之間的函式呼叫或通訊,例如A元件完成某個操作後希望B元件執行某個行為,這種情況就可以使用監聽或觀察者模式在B元件中註冊該行為的事件監聽或加入觀察者,然後選擇合適的時機在A元件中觸發這個事件監聽或通知觀察者來觸發B元件中的行為操作,而不是在A元件中直接拿到B元件的引用並直接進行操作,因為這樣元件之間的行為就會產生耦合。
      • 元件公用部分設計。元件的公用部分應該被抽離出來形成基礎庫,用來增加程式碼的複用性。
      • 元件的構建打包。構建工具能夠自動解析和打包元件內容。
      • 非同步元件的載入模式。在移動端,通常考慮到頁面首屏,非同步的場景應用非常廣泛,所有非同步元件不能和同步元件一起處理。這時可以將非同步元件區別於普通元件的目錄存放,並在打包構建時進行非同步打包處理。
      • 元件繼承與複用性。對於類似的元件要做到基礎元件複用來減少重複編碼。
      • 私有元件的統一管理。為了提高協作效率,可以通過搭建私有源的方式來統一管理元件庫,例如使用包管理工具等。但這點即使在大的團隊裡面也很難實施,因為業務元件的實現常常需要定製化而且經常變更,這樣維護元件庫成本反而更大,目前可以做的是將公用的元件模組使用私有源管理起來。
      • 根據特定場景進行擴充套件或自定義。如果當前的元件框架不能滿足需求,我們應該能夠很便捷地拓展新的框架和樣式,這樣就能適應更多的場景需求。如在通過目錄管理元件的方案下,既可以使用MVVM框架進行開發,也可以使用VIrtual DOM框架進行開發,但要保持基本的規範結構不變。
    • 前端效能測試
      • Performance Timing API
        • Performance Timing API是一個支援IE9以上版本及WebKit核心瀏覽器中用於記錄頁面載入和解析過程中關鍵時間點的機制,它可以詳細記錄每個頁面資源從開始載入到解析完成這一過程中具體操作發生的時間點,這樣根據開始和結束時間戳就可以計算出這個過程所花的時間了。
        • 瀏覽器中載入和解析一個HTML檔案的詳細過程先後經歷unload、redirect、App Cache、DNS、TCP、Request、Response、Processing、onload幾個階段,每個過程開始和結束的關鍵時間戳瀏覽器已經使用performance.timing來記錄了,所以根據這個記錄並結合簡單的計算,我們就可以得到頁面中每個過程所消耗的時間。
        • ```
        • function performanceTest() {
        • let timing = performance.timing,
        • readyStart = timing.fetchStart - timing.navigationStart,
        • redirectTime = timing.redirectEnd - timing.redirectStart,
        • appcacheTime = timing.domainLookupStart - timing.fetchStart,
        • unloadEventTime = timing.unloadEventEnd - timing.unloadEventStart,
        • lookupDomainTime = timing.domainLookupEnd - timing.domainLookupStart,
        • connectTime = timing.connectEnd - timing.connectStart,
        • requestTime = timing.responseEnd - timing.requestStart,
        • initDomTreeTime = timing.domInteractive - timing.responseEnd,
        • domReadyTime = timing.domComplete - timing.domInteractive,
        • loadEventTime = timing.loadEventEnd - timing.loadEventStart,
        • loadTime = timing.loadEventEnd - timing.navigationStart;
        • console.log('準備新頁面時間耗時:' + readyStart);
        • console.log('redirect重定向耗時:' + redirectTime);
        • console.log('Appcache耗時:' + appcacheTime);
        • console.log('unload前文件耗時:' + unloadEventTime);
        • console.log('DNS查詢耗時:' + lookupDomainTime);
        • console.log('TCP連線耗時:' + connectTime);
        • console.log('request請求耗時:' + requestTime);
        • console.log('請求完畢至DOM載入:' + initDomTreeTime);
        • console.log('解析DOM樹耗時:' + domReadyTime);
        • console.log('load事件耗時:' + loadEventTime);
        • console.log('載入時間耗時:' + loadTime);
        • }
        • ```
        • performance.memory // 記憶體佔用的具體資料performance.now() // 返回當前網頁自performance.timing到現在的時間,可以精確到微妙,用於更加精確的計數。performance,getEntries() // 獲取頁面所有載入資源的performance timing情況。瀏覽器獲取網頁時,會對網頁中每一個物件(指令碼檔案、樣式表、圖片檔案等)發出一個HTTP請求。此方法以陣列形式返回所有請求的時間統計資訊。performance.navigation // 提供使用者行為資訊,如網路請求的型別和重定向次數等performance.navigation.redirectCount // 記錄當前網頁重定向跳轉的次數
      • Profile工具
        • Performance Timing API描述了頁面資源從載入到解析各個階段的執行關鍵點時間記錄,但是無法統計JS執行過程中系統資源的佔用情況。Profile是Chrome和Firefox等標準瀏覽器提供的一種用於測試頁面指令碼執行時系統記憶體和CPU資源佔用情況的API。
        • 可實現功能
          • 1. 分析頁面指令碼執行過程中最耗資源的操作
          • 2. 記錄頁面指令碼執行過程中JS物件消耗的記憶體與堆疊的使用情況
          • 3. 檢測頁面指令碼執行過程中CPU佔用情況
        • 使用console.profile()和console.profileEnd()就可以分析中間一段程式碼執行時系統的記憶體或CPU資源的消耗情況,然後配置瀏覽器的Profile檢視比較消耗系統記憶體或CPU資源的操作,這樣就可以有針對性地進行優化了。
      • 頁面埋點計時
        • 使用Profile可以在一定程度上幫助我們分析頁面的效能,但缺點是不夠靈活。實際專案中,我們不會過多關注頁面記憶體或CPU資源的消耗情況,因為JS有自動記憶體回收機制。我們更多關注的是頁面指令碼邏輯執行的時間。除了Performance Timing的關鍵過程耗時計算,我們還希望檢測程式碼的具體解析或執行時間,這就不能寫很多的console.profile()和console.profileEnd()來逐段實現,為了更加簡單地處理這種情況,往往選擇通過指令碼埋點計時的方式來統計每部分程式碼的執行時間。
        • 頁面JS埋點計時的實現:記錄JS程式碼開始執行的時間戳,在需要記錄的地方埋點記錄結束時的時間戳,最後通過差值來計算一段HTML解析或JS解析執行的時間。可以將某個操作開始和結束的時間戳記錄到一個數組中,然後分析陣列之間的間隔就得到每個步驟的執行時間。
        • ```
        • let timeList = [];
        • function addTime(tag) {
        • timeList.push({"tag": tag, "time": +new Date()});
        • }
        • addTime("loading");
        • timeList.push({"tag": "load", "time": +new Date()});
        • // TODO, load載入時的操作
        • timeList.push({"tag": "load", "time": +new Date()});
        • timeList.push({"tag": "process", "time": +new Date()});
        • // TODO,process處理時的操作
        • timeList.push({"tag": "process", "time": +new Date()});
        • // 輸出{load: 時間毫秒數, process: 時間毫秒數}
        • parseTime(timeList);
        • function parseTime(time) {
        • let timeStep = {},
        • endTime;
        • for (let i = 0, len = time.length; i = 0 && endTime.tag) {
        • timeStep[endTime.tag] = endTime.time - time[i].time;
        • }
        • }
        • return timeStep;
        • }
        • ```
      • 資源載入時序圖分析
        • 該方法可以粗粒度地巨集觀分析瀏覽器的所有資原始檔請求耗時和檔案載入順序情況,如保證CSS和資料請求等關鍵資源優先載入,JS檔案和頁面中非關鍵性圖片等內容延後載入。
    • 移動端瀏覽器前端優化策略
      • 網路載入類
        • 1. 首屏資料請求提前,避免JS檔案載入後才請求資料
        • 2. 首屏載入和按需載入,非首屏內容滾屏載入,保證首屏內容最小化
          • 一般推薦移動端頁面首屏資料展示延時最長不超過3秒。
        • 3. 模組化資源並行下載
        • 4. Inline首屏必備的CSS和JS
        • 5. meta dns prefetch設定DNS預解析
          • 設定檔案資源的DNS預解析,讓瀏覽器提前解析獲取靜態資源的主機IP,避免等到請求時才發起DNS解析請求。
          • <!--cdn域名預解析--><meta http-equiv="x-dns-prefetch-control" content="on"><link rel="dns-prefetch" href="//cdn.domain.com">
        • 6. 資源預載入
        • 7. 合理利用MTU策略
          • 通常情況下,TCP網路傳輸的最大傳輸單元MTU為1500B,即一個RTT(網路請求往返時間)內可以傳輸的資料量最大為1500位元組。因此,在前後端分離的開發模式中,儘量保證頁面的HTML內容在1KB以內,這樣整個HTML的內容請求就可以在一個RTT內請求完成,最大限度地提高HTML載入速度。
      • 快取類
        • 1. 合理利用瀏覽器快取
        • 2. 靜態資源離線方案
        • 3. 嘗試使用AMP HTML
      • 圖片類
        • 1. 圖片壓縮處理
        • 2. 使用較小的圖片,合理使用base64內嵌圖片
          • 一般圖片大小超過2KB就不推薦使用base64嵌入顯示了
        • 3. 使用更高壓縮比格式的圖片
        • 4. 圖片懶載入
        • 5. 使用Media Query或srcset根據不同螢幕載入不同大小圖片
        • 6. 使用iconfont代替圖片圖示
          • @font-face { font-family: iconfont; src: url("./iconfont.eot"); src: url("./iconfont.eot?#iefix") format("eot"), url("./iconfont.woff") format("woff"), url("./iconfont.ttf") format("truetype");}
        • 7. 定義圖片大小限制
          • 載入的單張圖片一般建議不超過30KB,推薦在10KB以內。
      • 指令碼類
        • 1. 儘量使用id選擇器
        • 2. 合理快取DOM物件
        • 3. 頁面元素儘量使用事件代理,避免直接事件繫結
          • 使用事件代理可以避免對每個元素都進行繫結,並且可以避免出現記憶體洩露及需要動態新增元素的事件繫結問題,所以儘量不要直接使用事件繫結。
          • $('body').on('click','.btn', function(e){ console.log(this);}
        • 4. 使用touchstart代替click
        • 5. 避免touchmove、scroll連續事件處理
          • 需要對touchmove、scroll這類可能連續觸發回撥的事件設定事件節流,如設定每隔16ms(60幀的幀間隔為16.7ms,因此可以合理地設定為16ms)才進行一次事件處理,避免頻繁的事件呼叫導致移動端頁面卡頓。
        • 6. 避免使用eval、with,使用join代替連線符+,推薦使用ES6的字串模板
        • 7. 儘量使用ES6+的特性來程式設計
      • 渲染類
        • 1. 使用Viewpoint固定螢幕渲染,可以加速頁面渲染內容
          • 在移動端設定Viewpoint可以加速頁面的渲染,也可以避免縮放導致頁面重排重繪。
          • <!--設定viewport不縮放--><meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
        • 2. 避免各種形式重排重繪
        • 3. 使用CSS3動畫,開啟GPU加速
          • 使用CSS3動畫時可以設定transform:translateZ(0)來開啟移動裝置瀏覽器的GPU圖形處理加速,讓動畫過程更加流暢。
        • 4. 合理使用Canvas和requestAnimationFrame來實現動畫
        • 5. SVG代替圖片
        • 6. 不濫用float
          • 推薦使用固定佈局或flex-box彈性佈局的方式來實現頁面元素佈局
        • 7. 不濫用web字型或過多font-size宣告
          • 過多的font-size宣告會增加字型的大小計算
      • 架構協議類
        • 1. 嘗試使用SPDY和HTTP2
          • 在條件允許的情況下可以考慮使用SPDY協議來進行檔案資源傳輸,利用連線複用加快傳輸過程,縮短資源載入時間。
        • 2. 使用後端資料渲染
        • 3. 使用Native View代替DOM的效能劣勢
    • 前端使用者資料分析
      • 使用者訪問統計
        • PV(Page View)
          • PV一般指在一天時間之內頁面被所有使用者訪問的總次數,即每一次頁面重新整理都會增加一次PV
          • PV作為單個頁面的統計量引數,通常用來統計獲取關鍵入口頁面或臨時推廣性頁面的訪問量或推廣效果。
        • UV(Unique Visitor)
          • UV指在一天時間內訪問頁面的不同使用者個數
          • UV可以認為是前端頁面統計中一個最有價值的統計指標,因為其直接反應頁面的訪問使用者數。
          • 目前有較多站點的UV是按照一天之內訪問目標頁面的IP數來計算的,因此我們也可以根據UV來統計站點的周活躍使用者量和月活躍使用者量。
          • 除了根據IP,還需要結合其他的輔助資訊來識別統計不同使用者的UV
            • 根據瀏覽器Cookie和IP統計。問題:Cookie手動被清除,頁面被重新訪問時就只能算第二次。
            • 結合使用者瀏覽器標識userAgent和IP統計。
          • UV一般情況下是無法用於精確統計的,所以通常需要結合PV、UV來一起分析網站被使用者訪問的情況。
          • 我們還可以對站點一天的新訪問數、新訪客比率等進行統計,計算第一次訪問網站的新使用者數和比例,這對判斷網站使用者增長也是很有意義的。
        • VV(Visit View)
          • VV是統計網站被使用者訪問次數的參考資料,通常使用者從進入網站到最終離開該網站的整個過程只算一次VV
        • IP(訪問站點的不同IP數)
      • 使用者行為分析
        • 相對於訪問量的統計,使用者行為分析才是更加直接反映網頁內容是否受使用者喜歡或滿足使用者需求的一個重要標準。
        • 如果我們能知道使用者瀏覽目標頁面時所有的行為操作,一定程度上就可以知道使用者對頁面的哪些內容感興趣,對哪些內容不感興趣,這對產品內容的調整和改進是很有意義的。
        • 引數指標
          • 頁面點選量
            • 頁面點選量用來統計使用者對於頁面某個可點選或可操作區域的點選或操作次數。
          • 使用者點選流
            • 點選流用來統計使用者在頁面中發生點選或操作動作的順序,可以反映使用者在頁面上的操作行為。所以統計上報時需要在瀏覽器上線儲存記錄使用者的操作順序,如在關鍵的按鈕中埋點,點選時向localStorage中記錄點選或操作行為的唯一id,在使用者一次VV結束或在下一次VV開始時進行點選流上報,然後通過後臺歸併統計分析。
          • 使用者訪問路徑
            • 使用者訪問路徑針對每個頁面埋點記錄使用者訪問不同頁面的路徑。通常是在一次VV結束或下一次VV開始時上報使用者的訪問路徑。
          • 使用者點選熱力圖
            • 使用者點選熱力圖是為了統計使用者的點選或操作發生在整個頁面哪些區域位置的一種分析方法,一般是統計使用者操作習慣和頁面某些區域內容是否受使用者關注的一種方式。
            • 獲取上報點的方式主要是捕獲滑鼠事件在螢幕中的座標位置進行上報,然後在服務端進行計算歸類分析並繪圖。
          • 使用者轉化率與導流轉化率
            • 對使用者轉化率的分析一般在一些臨時推廣頁面或拉取新使用者宣傳頁面上較常用。
            • 使用者轉化率=通過該頁面註冊的使用者數/頁面PV
            • 導流是將某個頁面的使用者訪問流量引導到另一個頁面中
            • 導流轉化率=通過源頁面匯入的頁面訪問PV/源頁面PV
          • 使用者訪問時長、訪問內容分析
            • 使用者訪問時長和內容分析是統計分析使用者在某些關鍵內容頁面的停留時間,來判斷使用者對該頁面的內容是否感興趣,從而分析出使用者對網站可能感興趣的內容,方便以後精確地向該使用者推薦他們感興趣的內容。
      • 前端日誌上報
        • 獲取錯誤日誌
          • 瀏覽器提供了try...catch和window.onerror兩種機制來幫助我們獲取使用者頁面的指令碼錯誤資訊。
          • 一般來說,使用try...catch可以捕捉前端JS的執行時錯誤,同時拿到出錯的資訊,如錯誤資訊描述、堆疊、行號、列號、具體的出錯檔案資訊等。我們也可以在這個階段將使用者瀏覽器資訊等靜態內容一起記錄下來,快速地定位問題發生的原因。需要注意的是,try...catch無法捕捉到語法錯誤,只能在單一的作用域內有效捕獲錯誤資訊,如果是非同步函式裡面的內容,就需要把function函式塊內容全部加入到try...catch中執行。
          • window.onerror可以在任何執行上下文中執行,如果給window物件增加一個錯誤處理函式,既能處理捕獲錯誤又能儲存程式碼的優雅性。window.onerror一般用於捕捉指令碼語法錯誤和執行時錯誤,可以獲得出錯的檔案資訊,如出錯資訊、出錯檔案、行號等,當前頁面執行的所有JS指令碼出錯都會被捕捉到。
          • ```
          • window.onerror = function(msg, url, line) {
          • // 可以捕獲非同步函式中的錯誤資訊並進行處理,提示Script error
          • console.log(msg): // 獲取錯誤資訊
          • console.log(url); // 獲取出錯的檔案路徑
          • console.log(line): // 獲取錯誤出錯的行數
          • };
          • setTimeout(function() {
          • console.log(obj): // 可以被捕獲到,並在onerror處理
          • }, 200);
          • ```
          • 使用onerror要注意,在不同的瀏覽器中實現函式處理返回的異常物件是不相同的,而且如果報錯的JS和HTML不在同一個域名下,錯誤時window.onerror中的errorMsg全部為script error而不是具體的錯誤描述資訊,此時需要新增JS指令碼的跨域設定。<script src="//www.domain.com/main.js" crossorigin></script>
          • 我們可以對前端指令碼中常用的非同步方法入口函式或模組引用的入口方法統一使用try...catch進行一層封裝,這樣就可以使用try...catch捕獲每個引用模組作用域下的主要錯誤資訊了。
          • ```
          • // 對setTimeout實現函式進行包裝
          • window.setTimeoutTry = function (fn, time) {
          • let args = arguments;
          • let _fn = function () {
          • try {
          • // 將函式引數用try...catch包裹
          • return fn.apply(this, args);
          • } catch (e) {
          • console.log(e);
          • }
          • };
          • return window['setTimeout'](_fn, time);
          • };
          • try {
          • setTimeoutTry(function () {
          • obj //獲取錯誤資訊 ReferenceError: obj is not defined
          • }, 300);
          • } catch (e) {
          • console.log(e);
          • }
          • ```
        • 將錯誤資訊上傳到伺服器
          • 注意:頁面的訪問量可能很大,如果到達百萬級、千萬級,那麼就需要按照一定的條件上報,如根據一定的概率進行上報,否則大量的錯誤資訊上報請求會佔用日誌收集伺服器的很多資源和流量。
        • 通過高效的方式來找到問題
          • 為了方便檢視收集到的這些資訊,我們通常可以建立一個簡單的內容管理系統(CMS)來管理檢視錯誤日誌,對同一型別的錯誤做歸併統計,也可以建立錯誤量實時統計來檢視錯誤量的即時變化情況。當某個版本釋出後,如果收到的錯誤量明顯增加,就需要格外注意。
        • 檔案載入失敗監控
          • 可以對<img>或<script>的readyChange進行是否載入成功的判斷,但只有部分IE瀏覽器支援<img>或<script>的readyState,因此一般還需要結合其他方式,如onload,針對不同瀏覽器分開處理。
          • ```
          • // 頁面需要載入的三個script指令碼資源
          • let scripts = [script1, script2, script3];
          • // 三個script載入的初始狀態
          • let loaded = {
          • [script1]: false,
          • [script2]: false,
          • [script3]: false
          • };
          • for (let script of scripts) {
          • // IE瀏覽器的情況設定readyState來判斷
          • if (script.readyState) {
          • script.onreadystatechange = function () {
          • let state = this.readyState;
          • if (state === 'loaded' || state === 'complete') {
          • callback();//指令碼載入成功回撥
          • // 表示該指令碼載入成功
          • loaded[script] = true;
          • }
          • }
          • } else {
          • // 其他瀏覽器,如Firefox、Safari、Chrome或Opera,結合onload
          • script.onload = function () {
          • callback();//指令碼載入成功回撥
          • // 表示該指令碼載入成功
          • loaded[script] = true;
          • }
          • }
          • }
          • setTimeout(function () {
          • // 如15秒後執行頁面指令碼載入情況的上報進行統計
          • report(loaded);
          • }, 15000);
          • ```
          • 通過這種方式僅僅是判斷了檔案或指令碼載入成功的情況,我們還需要指定一個檔案載入的列表,在一段時間後將頁面檔案載入的結果物件上報給伺服器端來統計不同檔案的具體載入情況。
    • 前端專案開發流程設計
      • 1. 前端框架選型
      • 2. 模組化方案
      • 3. 程式碼規範化
      • 4. 構建自動化
      • 5. 元件化目錄設計
      • 6. 程式碼優化處理
      • 7. 資料統計
      • 8. 同構專案結構設計