(下篇)中高階前端大廠面試祕籍,寒冬中為您保駕護航,直通大廠

郭東東發表於2019-04-26

引言

本篇文章會繼續沿著前面兩篇的腳步,繼續梳理前端領域一些比較主流的進階知識點,力求能讓大家在橫向層面有個全面的概念。能在面試時有限的時間裡,能夠快速抓住重點與面試官交流。這些知識點屬於加分項,如果能在面試時從容侃侃而談,想必面試官會記憶深刻,為你折服的~?

另外有許多童鞋提到: 面試造火箭,實踐全不會,對這種應試策略表達一些擔憂。其實我是覺得面試或者這些知識點,也僅僅是個初級的 開始。能幫助在初期的快速成長,但這種策略並沒辦法讓你達到更高的水平,只有後續不斷地真正實踐和深入研究,才能突破自己的瓶頸,繼續成長。面試,不也只是一個開始而已嘛。~?

建議各位小夥從基礎入手,先看

小菜雞部落格求贊 ? blog

進階知識

Hybrid

隨著 Web技術 和 移動裝置 的快速發展,在各家大廠中,Hybrid 技術已經成為一種最主流最不可取代的架構方案之一。一套好的 Hybrid 架構方案能讓 App 既能擁有 極致的體驗和效能,同時也能擁有 Web技術 靈活的開發模式、跨平臺能力以及熱更新機制。因此,相關的 Hybrid 領域人才也是十分的吃香,精通Hybrid 技術和相關的實戰經驗,也是面試中一項大大的加分項。

1. 混合方案簡析

Hybrid App,俗稱 混合應用,即混合了 Native技術 與 Web技術 進行開發的移動應用。現在比較流行的混合方案主要有三種,主要是在UI渲染機制上的不同:

  • Webview UI:

    • 通過 JSBridge 完成 H5 與 Native 的雙向通訊,並 基於 Webview 進行頁面的渲染;
    • 優勢: 簡單易用,架構門檻/成本較低,適用性與靈活性極強;
    • 劣勢: Webview 效能侷限,在複雜頁面中,表現遠不如原生頁面;
  • Native UI:

    • 通過 JSBridge 賦予 H5 原生能力,並進一步將 JS 生成的虛擬節點樹(Virtual DOM)傳遞至 Native 層,並使用 原生系統渲染
    • 優勢: 使用者體驗基本接近原生,且能發揮 Web技術 開發靈活與易更新的特性;
    • 劣勢: 上手/改造門檻較高,最好需要掌握一定程度的客戶端技術。相比於常規 Web開發,需要更高的開發除錯、問題排查成本;
  • 小程式

    • 通過更加定製化的 JSBridge,賦予了 Web 更大的許可權,並使用雙 WebView 雙執行緒的模式隔離了 JS邏輯 與 UI渲染,形成了特殊的開發模式,加強了 H5 與 Native 混合程度,屬於第一種方案的優化版本;
    • 優勢: 使用者體驗好於常規 Webview 方案,且通常依託的平臺也能提供更為友好的開發除錯體驗以及功能;
    • 劣勢: 需要依託於特定的平臺的規範限定

2. Webviev

Webview 是 Native App 中內建的一款基於 Webkit核心 的瀏覽器,主要由兩部分組成:

  • WebCore 排版引擎
  • JSCore 解析引擎

在原生開發 SDK 中 Webview 被封裝成了一個元件,用於作為 Web頁面 的容器。因此,作為宿主的客戶端中擁有更高的許可權,可以對 Webview 中的 Web頁面 進行配置和開發。

Hybrid技術中雙端的互動原理,便是基於 Webview 的一些 API 和特性。

3. 互動原理

Hybrid技術 中最核心的點就是 Native端 與 H5端 之間的 雙向通訊層,其實這裡也可以理解為我們需要一套 跨語言通訊方案,便是我們常聽到的 JSBridge。

  • JavaScript 通知 Native

    • API注入,Native 直接在 JS 上下文中掛載資料或者方法
      • 延遲較低,在安卓4.1以下具有安全性問題,風險較高
    • WebView URL Scheme 跳轉攔截
      • 相容性好,但延遲較高,且有長度限制
    • WebView 中的 prompt/console/alert攔截(通常使用 prompt)
  • Native 通知 Javascript:

    • IOS: stringByEvaluatingJavaScriptFromString
    // Swift
    webview.stringByEvaluatingJavaScriptFromString("alert('NativeCall')")
    複製程式碼
    • Android: loadUrl (4.4-)
    // 呼叫js中的JSBridge.trigger方法
    // 該方法的弊端是無法獲取函式返回值;
    webView.loadUrl("javascript:JSBridge.trigger('NativeCall')")
    複製程式碼
    • Android: evaluateJavascript (4.4+)
    // 4.4+後使用該方法便可呼叫並獲取函式返回值;
    mWebView.evaluateJavascript("javascript:JSBridge.trigger('NativeCall')", 	 new ValueCallback<String>() {
        @Override
        public void onReceiveValue(String value) {
            //此處為 js 返回的結果
        }
    });
    複製程式碼

4. 接入方案

整套方案需要 Web 與 Native 兩部分共同來完成:

  • Native: 負責實現URL攔截與解析、環境資訊的注入、擴充功能的對映、版本更新等功能;
  • JavaScirpt: 負責實現功能協議的拼裝、協議的傳送、引數的傳遞、回撥等一系列基礎功能。

接入方式:

  • 線上H5: 直接將專案部署於線上伺服器,並由客戶端在 HTML 頭部注入對應的 Bridge。
    • 優勢: 接入/開發成本低,對 App 的侵入小;
    • 劣勢: 重度依賴網路,無法離線使用,首屏載入慢;
  • 內建離線包: 將程式碼直接內建於 App 中,即本地儲存中,可由 H5 或者 客戶端引用 Bridge。
    • 優勢: 首屏載入快,可離線化使用;
    • 劣勢: 開發、除錯成本變高,需要多端合作,且會增加 App 包體積

5. 優化方案簡述

  • Webview 預載入: Webview 的初始化其實挺耗時的。我們測試過,大概在100~200ms之間,因此如果能前置做好初始化於記憶體中,會大大加快渲染速度。
  • 更新機制: 使用離線包的時候,便會涉及到本地離線程式碼的更新問題,因此需要建立一套雲端下發包的機制,由客戶端下載雲端最新程式碼包 (zip包),並解壓替換原生程式碼。
    • 增量更新: 由於下發包是一個下載的過程,因此包的體積越小,下載速度越快,流量損耗越低。只打包改變的檔案,客戶端下載後覆蓋式替換,能大大減小每次更新包的體積。
    • 條件分發: 雲平臺下發更新包時,可以配合客戶端設定一系列的條件與規則,從而實現程式碼的條件更新:
      • 地區 更新: 例如一個只有中國地區才能更新的版本;
      • 語言 更新: 例如只有中文版本會更新;
      • 按 App 版本 更新: 例如只有最新版本的 App 才會更新;
      • 灰度 更新: 只有小比例使用者會更新;
      • AB測試: 只有命中的使用者會更新;
  • 降級機制: 當使用者下載或解壓程式碼包失敗時,需要有套降級方案,通常有兩種做法:
    • 本地內建: 隨著 App 打包時內建一份線上最新完整程式碼包,保證原生程式碼檔案的存在,資源載入均使用本地化路徑;
    • 域名攔截: 資源載入使用線上域名,通過攔截域名對映到本地路徑。當本地不存在時,則請求線上檔案,當存在時,直接載入;
  • 跨平臺部署: Bridge層 可以做一套瀏覽器適配,在一些無法適配的功能,做好降級處理,從而保證程式碼在任何環境的可用性,一套程式碼可同時執行於 App內 與 普通瀏覽器;
  • 環境系統: 與客戶端進行統一配合,搭建出 正式 / 預上線 / 測試 / 開發環境,能大大提高專案穩定性與問題排查;
  • 開發模式:
    • 能連線PC Chrome/safari 進行程式碼除錯;
    • 具有開發除錯入口,可以使用同樣的 Webview 載入開發時的原生程式碼;
    • 具備日誌系統,可以檢視 Log 資訊;

詳細內容由興趣的童鞋可以看文章:

Webpack

1. 原理簡述

Webpack 已經成為了現在前端工程化中最重要的一環,通過WebpackNode的配合,前端領域完成了不可思議的進步。通過預編譯,將軟體程式設計中先進的思想和理念能夠真正運用於生產,讓前端開發領域告別原始的蠻荒階段。深入理解Webpack,可以讓你在程式設計思維及技術領域上產生質的成長,極大擴充技術邊界。這也是在面試中必不可少的一個內容。

  • 核心概念

    • JavaScript 的 模組打包工具 (module bundler)。通過分析模組之間的依賴,最終將所有模組打包成一份或者多份程式碼包 (bundler),供 HTML 直接引用。實質上,Webpack 僅僅提供了 打包功能 和一套 檔案處理機制,然後通過生態中的各種 Loader 和 Plugin 對程式碼進行預編譯和打包。因此 Webpack 具有高度的可擴充性,能更好的發揮社群生態的力量。
      • Entry: 入口檔案,Webpack 會從該檔案開始進行分析與編譯;
      • Output: 出口路徑,打包後建立 bundler 的檔案路徑以及檔名;
      • Module: 模組,在 Webpack 中任何檔案都可以作為一個模組,會根據配置的不同的 Loader 進行載入和打包;
      • Chunk: 程式碼塊,可以根據配置,將所有模組程式碼合併成一個或多個程式碼塊,以便按需載入,提高效能;
      • Loader: 模組載入器,進行各種檔案型別的載入與轉換;
      • Plugin: 擴充外掛,可以通過 Webpack 相應的事件鉤子,介入到打包過程中的任意環節,從而對程式碼按需修改;
  • 工作流程 (載入 - 編譯 - 輸出)

    • 1、讀取配置檔案,按命令 初始化 配置引數,建立 Compiler 物件;
    • 2、呼叫外掛的 apply 方法 掛載外掛 監聽,然後從入口檔案開始執行編譯;
    • 3、按檔案型別,呼叫相應的 Loader 對模組進行 編譯,並在合適的時機點觸發對應的事件,呼叫 Plugin 執行,最後再根據模組 依賴查詢 到所依賴的模組,遞迴執行第三步;
    • 4、將編譯後的所有程式碼包裝成一個個程式碼塊 (Chuck), 並按依賴和配置確定 輸出內容。這個步驟,仍然可以通過 Plugin 進行檔案的修改;
    • 5、最後,根據 Output 把檔案內容一一寫入到指定的資料夾中,完成整個過程;
  • 模組包裝:

(function(modules) {
	// 模擬 require 函式,從記憶體中載入模組;
	function __webpack_require__(moduleId) {
		// 快取模組
		if (installedModules[moduleId]) {
			return installedModules[moduleId].exports;
		}
		
		var module = installedModules[moduleId] = {
			i: moduleId,
			l: false,
			exports: {}
		};
		
		// 執行程式碼;
		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
		
		// Flag: 標記是否載入完成;
		module.l = true;
		
		return module.exports;
	}
	
	// ...
	
	// 開始執行載入入口檔案;
	return __webpack_require__(__webpack_require__.s = "./src/index.js");
 })({
 	"./src/index.js": function (module, __webpack_exports__, __webpack_require__) {
		// 使用 eval 執行編譯後的程式碼;
		// 繼續遞迴引用模組內部依賴;
		// 實際情況並不是使用模板字串,這裡是為了程式碼的可讀性;
		eval(`
			__webpack_require__.r(__webpack_exports__);
			//
			var _test__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("test", ./src/test.js");
		`);
	},
	"./src/test.js": function (module, __webpack_exports__, __webpack_require__) {
		// ...
	},
 })
複製程式碼
  • 總結:
    • 模組機制: webpack 自己實現了一套模擬模組的機制,將其包裹於業務程式碼的外部,從而提供了一套模組機制;
    • 檔案編譯: webpack 規定了一套編譯規則,通過 Loader 和 Plugin,以管道的形式對檔案字串進行處理;

2. Loader

由於 Webpack 是基於 Node,因此 Webpack 其實是隻能識別 js 模組,比如 css / html / 圖片等型別的檔案並無法載入,因此就需要一個對 不同格式檔案轉換器。其實 Loader 做的事,也並不難理解: 對 Webpack 傳入的字串進行按需修改。例如一個最簡單的 Loader:

// html-loader/index.js
module.exports = function(htmlSource) {
	// 返回處理後的程式碼字串
	// 刪除 html 檔案中的所有註釋
	return htmlSource.replace(/<!--[\w\W]*?-->/g, '')
}
複製程式碼

當然,實際的 Loader 不會這麼簡單,通常是需要將程式碼進行分析,構建 AST (抽象語法樹), 遍歷進行定向的修改後,再重新生成新的程式碼字串。如我們常用的 Babel-loader 會執行以下步驟:

  • babylon 將 ES6/ES7 程式碼解析成 AST
  • babel-traverse 對 AST 進行遍歷轉譯,得到新的 AST
  • 新 AST 通過 babel-generator 轉換成 ES5

Loader 特性:

  • 鏈式傳遞,按照配置時相反的順序鏈式執行;
  • 基於 Node 環境,擁有 較高許可權,比如檔案的增刪查改;
  • 可同步也可非同步;

常用 Loader:

  • file-loader: 載入檔案資源,如 字型 / 圖片 等,具有移動/複製/命名等功能;
  • url-loader: 通常用於載入圖片,可以將小圖片直接轉換為 Date Url,減少請求;
  • babel-loader: 載入 js / jsx 檔案, 將 ES6 / ES7 程式碼轉換成 ES5,抹平相容性問題;
  • ts-loader: 載入 ts / tsx 檔案,編譯 TypeScript;
  • style-loader: 將 css 程式碼以<style>標籤的形式插入到 html 中;
  • css-loader: 分析@importurl(),引用 css 檔案與對應的資源;
  • postcss-loader: 用於 css 的相容性處理,具有眾多功能,例如 新增字首,單位轉換 等;
  • less-loader / sass-loader: css前處理器,在 css 中新增了許多語法,提高了開發效率;

編寫原則:

  • 單一原則: 每個 Loader 只做一件事;
  • 鏈式呼叫: Webpack 會按順序鏈式呼叫每個 Loader;
  • 統一原則: 遵循 Webpack 制定的設計規則和結構,輸入與輸出均為字串,各個 Loader 完全獨立,即插即用;

3. Plugin

外掛系統是 Webpack 成功的一個關鍵性因素。在編譯的整個生命週期中,Webpack 會觸發許多事件鉤子,Plugin 可以監聽這些事件,根據需求在相應的時間點對打包內容進行定向的修改。

  • 一個最簡單的 plugin 是這樣的:
class Plugin{
  	// 註冊外掛時,會呼叫 apply 方法
  	// apply 方法接收 compiler 物件
  	// 通過 compiler 上提供的 Api,可以對事件進行監聽,執行相應的操作
  	apply(compiler){
  		// compilation 是監聽每次編譯迴圈
  		// 每次檔案變化,都會生成新的 compilation 物件並觸發該事件
    	compiler.plugin('compilation',function(compilation) {})
  	}
}
複製程式碼
  • 註冊外掛:
// webpack.config.js
module.export = {
	plugins:[
		new Plugin(options),
	]
}
複製程式碼
  • 事件流機制:

Webpack 就像工廠中的一條產品流水線。原材料經過 Loader 與 Plugin 的一道道處理,最後輸出結果。

  • 通過鏈式呼叫,按順序串起一個個 Loader;
  • 通過事件流機制,讓 Plugin 可以插入到整個生產過程中的每個步驟中;

Webpack 事件流程式設計正規化的核心是基礎類 Tapable,是一種 觀察者模式 的實現事件的訂閱與廣播:

const { SyncHook } = require("tapable")

const hook = new SyncHook(['arg'])

// 訂閱
hook.tap('event', (arg) => {
	// 'event-hook'
	console.log(arg)
})

// 廣播
hook.call('event-hook')
複製程式碼

Webpack 中兩個最重要的類 Compiler 與 Compilation 便是繼承於 Tapable,也擁有這樣的事件流機制。

  • Compiler: 可以簡單的理解為 Webpack 例項,它包含了當前 Webpack 中的所有配置資訊,如 options, loaders, plugins 等資訊,全域性唯一,只在啟動時完成初始化建立,隨著生命週期逐一傳遞;

  • Compilation: 可以稱為 編譯例項。當監聽到檔案發生改變時,Webpack 會建立一個新的 Comilation 物件,開始一次新的編譯。它包含了當前的輸入資源,輸出資源,變化的檔案等,同時通過它提供的 api,可以監聽每次編譯過程中觸發的事件鉤子;

  • 區別:

    • Compiler 全域性唯一,且從啟動生存到結束;
    • Compilaation 對應每次編譯,每輪編譯迴圈均會重新建立;
  • 常用 Plugin:

    • UglifyJsPlugin: 壓縮、混淆程式碼;
    • CommonsChunkPlugin: 程式碼分割;
    • ProvidePlugin: 自動載入模組;
    • html-webpack-plugin: 載入 html 檔案,並引入 css / js 檔案;
    • extract-text-webpack-plugin / mini-css-extract-plugin: 抽離樣式,生成 css 檔案;
    • DefinePlugin: 定義全域性變數;
    • optimize-css-assets-webpack-plugin: CSS 程式碼去重;
    • webpack-bundle-analyzer: 程式碼分析;
    • compression-webpack-plugin: 使用 gzip 壓縮 js 和 css;
    • happypack: 使用多程式,加速程式碼構建;
    • EnvironmentPlugin: 定義環境變數;

4. 編譯優化

  • 程式碼優化:

    • 無用程式碼消除,是許多程式語言都具有的優化手段,這個過程稱為 DCE (dead code elimination),即 刪除不可能執行的程式碼

      • 例如我們的 UglifyJs,它就會幫我們在生產環境中刪除不可能被執行的程式碼,例如:
      var fn = function() {
      	return 1;
      	// 下面程式碼便屬於 不可能執行的程式碼;
      	// 通過 UglifyJs (Webpack4+ 已內建) 便會進行 DCE;
      	var a = 1;
      	return a;
      }
      複製程式碼
    • 搖樹優化 (Tree-shaking),這是一種形象比喻。我們把打包後的程式碼比喻成一棵樹,這裡其實表示的就是,通過工具 "搖" 我們打包後的 js 程式碼,將沒有使用到的無用程式碼 "搖" 下來 (刪除)。即 消除那些被 引用了但未被使用 的模組程式碼。

      • 原理: 由於是在編譯時優化,因此最基本的前提就是語法的靜態分析,ES6的模組機制 提供了這種可能性。不需要執行時,便可進行程式碼字面上的靜態分析,確定相應的依賴關係。
      • 問題: 具有 副作用 的函式無法被 tree-shaking。
        • 在引用一些第三方庫,需要去觀察其引入的程式碼量是不是符合預期;
        • 儘量寫純函式,減少函式的副作用;
        • 可使用 webpack-deep-scope-plugin,可以進行作用域分析,減少此類情況的發生,但仍需要注意;
  • code-spliting: 程式碼分割 技術,將程式碼分割成多份進行 懶載入非同步載入,避免打包成一份後導致體積過大,影響頁面的首屏載入;

    • Webpack 中使用 SplitChunksPlugin 進行拆分;
    • 頁面 拆分: 不同頁面打包成不同的檔案;
    • 功能 拆分:
      • 將類似於播放器,計算庫等大模組進行拆分後再懶載入引入;
      • 提取複用的業務程式碼,減少冗餘程式碼;
    • 檔案修改頻率 拆分: 將第三方庫等不常修改的程式碼單獨打包,而且不改變其檔案 hash 值,能最大化運用瀏覽器的快取;
  • scope hoisting: 作用域提升,將分散的模組劃分到同一個作用域中,避免了程式碼的重複引入,有效減少打包後的程式碼體積和執行時的記憶體損耗;

  • 編譯效能優化:

    • 升級至 最新 版本的 webpack,能有效提升編譯效能;
    • 使用 dev-server / 模組熱替換 (HMR) 提升開發體驗;
      • 監聽檔案變動 忽略 node_modules 目錄能有效提高監聽時的編譯效率;
    • 縮小編譯範圍:
      • modules: 指定模組路徑,減少遞迴搜尋;
      • mainFields: 指定入口檔案描述欄位,減少搜尋;
      • noParse: 避免對非模組化檔案的載入;
      • includes/exclude: 指定搜尋範圍/排除不必要的搜尋範圍;
      • alias: 快取目錄,避免重複定址;
    • babel-loader:
      • 忽略node_moudles,避免編譯第三方庫中已經被編譯過的程式碼;
      • 使用cacheDirectory,可以快取編譯結果,避免多次重複編譯;
    • 多程式併發:
      • webpack-parallel-uglify-plugin: 可多程式併發壓縮 js 檔案,提高壓縮速度;
      • HappyPack: 多程式併發檔案的 Loader 解析;
    • 第三方庫模組快取:
      • DLLPlugin 和 DLLReferencePlugin 可以提前進行打包並快取,避免每次都重新編譯;
    • 使用分析:
      • Webpack Analyse / webpack-bundle-analyzer 對打包後的檔案進行分析,尋找可優化的地方;
      • 配置profile:true,對各個編譯階段耗時進行監控,尋找耗時最多的地方;
    • source-map:
      • 開發: cheap-module-eval-source-map
      • 生產: hidden-source-map

專案效能優化

1. 編碼優化

編碼優化,指的就是 在程式碼編寫時的,通過一些 最佳實踐,提升程式碼的執行效能。通常這並不會帶來非常大的收益,但這屬於 程式猿的自我修養,而且這也是面試中經常被問到的一個方面,考察自我管理與細節的處理。

  • 資料讀取:

    • 通過作用域鏈 / 原型鏈 讀取變數或方法時,需要更多的耗時,且越長越慢;
    • 物件巢狀越深,讀取值也越慢;
    • 最佳實踐:
      • 儘量在區域性作用域中進行 變數快取
      • 避免巢狀過深的資料結構,資料扁平化 有利於資料的讀取和維護;
  • 迴圈: 迴圈通常是編碼效能的關鍵點;

    • 程式碼的效能問題會再迴圈中被指數倍放大;
    • 最佳實踐:
      • 儘可能 減少迴圈次數
        • 減少遍歷的資料量;
        • 完成目的後馬上結束迴圈;
      • 避免在迴圈中執行大量的運算,避免重複計算,相同的執行結果應該使用快取;
      • js 中使用 倒序迴圈 會略微提升效能;
      • 儘量避免使用 for-in 迴圈,因為它會列舉原型物件,耗時大於普通迴圈;
  • 條件流程效能: Map / Object > switch > if-else

// 使用 if-else
if(type === 1) {

} else if (type === 2) {

} else if (type === 3) {

}

// 使用 switch
switch (type) {
	case 1:
		break;4
	case 2:
		break;
	case 3:
		break;
    default:
        break;
}

// 使用 Map
const map = new Map([
	[1, () => {}],
	[2, () => {}],
	[3, () => {}],
])
map.get(type)()

// 使用 Objext
const obj = {
	1: () => {},
	2: () => {},
	3: () => {},
}
obj[type]()
複製程式碼
  • 減少 cookie 體積: 能有效減少每次請求的體積和響應時間;

    • 去除不必要的 cookie;
    • 壓縮 cookie 大小;
    • 設定 domain 與 過期時間;
  • dom 優化:

    • 減少訪問 dom 的次數,如需多次,將 dom 快取於變數中;
    • 減少重繪與迴流:
      • 多次操作合併為一次;
      • 減少對計算屬性的訪問;
        • 例如 offsetTop, getComputedStyle 等
        • 因為瀏覽器需要獲取最新準確的值,因此必須立即進行重排,這樣會破壞了瀏覽器的佇列整合,儘量將值進行快取使用;
      • 大量操作時,可將 dom 脫離文件流或者隱藏,待操作完成後再重新恢復;
      • 使用DocumentFragment / cloneNode / replaceChild進行操作;
    • 使用事件委託,避免大量的事件繫結;
  • css 優化:

    • 層級扁平,避免過於多層級的選擇器巢狀;
    • 特定的選擇器 好過一層一層查詢: .xxx-child-text{} 優於 .xxx .child .text{}
    • 減少使用萬用字元與屬性選擇器
    • 減少不必要的多餘屬性
    • 使用 動畫屬性 實現動畫,動畫時脫離文件流,開啟硬體加速,優先使用 css 動畫;
    • 使用 <link> 替代原生 @import;
  • html 優化:

    • 減少 dom 數量,避免不必要的節點或巢狀;
    • 避免<img src="" />空標籤,能減少伺服器壓力,因為 src 為空時,瀏覽器仍然會發起請求
      • IE 向頁面所在的目錄傳送請求;
      • Safari、Chrome、Firefox 向頁面本身傳送請求;
      • Opera 不執行任何操作。
    • 圖片提前 指定寬高 或者 脫離文件流,能有效減少因圖片載入導致的頁面迴流;
    • 語義化標籤 有利於 SEO 與瀏覽器的解析時間;
    • 減少使用 table 進行佈局,避免使用<br /><hr />

2. 頁面基礎優化

  • 引入位置: css 檔案<head>中引入, js 檔案<body>底部引入;
    • 影響首屏的,優先順序很高的 js 也可以頭部引入,甚至內聯;
  • 減少請求 (http 1.0 - 1.1),合併請求,正確設定 http 快取;
  • 減少檔案體積:
    • 刪除多餘程式碼:
      • tree-shaking
      • UglifyJs
      • code-spliting
    • 混淆 / 壓縮程式碼,開啟 gzip 壓縮;
    • 多份編譯檔案按條件引入:
      • 針對現代瀏覽器直接給 ES6 檔案,只針對低端瀏覽器引用編譯後的 ES5 檔案;
      • 可以利用<script type="module"> / <script type="module">進行條件引入用
    • 動態 polyfill,只針對不支援的瀏覽器引入 polyfill;
  • 圖片優化:
    • 根據業務場景,與UI探討選擇 合適質量,合適尺寸
    • 根據需求和平臺,選擇 合適格式,例如非透明時可用 jpg;非蘋果端,使用 webp;
    • 小圖片合成 雪碧圖,低於 5K 的圖片可以轉換成 base64 內嵌;
    • 合適場景下,使用 iconfont 或者 svg
  • 使用快取:
    • 瀏覽器快取: 通過設定請求的過期時間,合理運用瀏覽器快取;
    • CDN快取: 靜態檔案合理使用 CDN 快取技術;
      • HTML 放於自己的伺服器上;
      • 打包後的圖片 / js / css 等資源上傳到 CDN 上,檔案帶上 hash 值;
      • 由於瀏覽器對單個域名請求的限制,可以將資源放在多個不同域的 CDN 上,可以繞開該限制;
    • 伺服器快取: 將不變的資料、頁面快取到 記憶體 或 遠端儲存(redis等) 上;
    • 資料快取: 通過各種儲存將不常變的資料進行快取,縮短資料的獲取時間;

3. 首屏渲染優化

  • css / js 分割,使首屏依賴的檔案體積最小,內聯首屏關鍵 css / js;
  • 非關鍵性的檔案儘可能的 非同步載入和懶載入,避免阻塞首頁渲染;
  • 使用dns-prefetch / preconnect / prefetch / preload等瀏覽器提供的資源提示,加快檔案傳輸;
  • 謹慎控制好 Web字型,一個大字型包足夠讓你功虧一簣;
    • 控制字型包的載入時機;
    • 如果使用的字型有限,那儘可能只將使用的文字單獨打包,能有效減少體積;
  • 合理利用 Localstorage / server-worker 等儲存方式進行 資料與資源快取
  • 分清輕重緩急:
    • 重要的元素優先渲染;
    • 視窗內的元素優先渲染;
  • 服務端渲染(SSR):
    • 減少首屏需要的資料量,剔除冗餘資料和請求;
    • 控制好快取,對資料/頁面進行合理的快取;
    • 頁面的請求使用流的形式進行傳遞;
  • 優化使用者感知:
    • 利用一些動畫 過渡效果,能有效減少使用者對卡頓的感知;
    • 儘可能利用 骨架屏(Placeholder) / Loading 等減少使用者對白屏的感知;
    • 動畫幀數儘量保證在 30幀 以上,低幀數、卡頓的動畫寧願不要;
    • js 執行時間避免超過 100ms,超過的話就需要做:
      • 尋找可 快取 的點;
      • 任務的 分割非同步 或 web worker 執行;

全棧基礎

其實我覺得並不能講前端的天花板低,只是說前端是項更多元化的工作,它需要涉及的知識面很廣。你能發現,從最開始的簡單頁面到現在,其實整個領域是在不斷地往外拓張。在許多的大廠的面試中,具備一定程度的 服務端知識、運維知識,甚至數學、圖形學、設計 等等,都可能是你佔得先機的法寶。

Nginx

輕量級、高效能的 Web 伺服器,在現今的大型應用、網站基本都離不開 Nginx,已經成為了一項必選的技術;其實可以把它理解成 入口閘道器,這裡我舉個例子可能更好理解:

當你去銀行辦理業務時,剛走進銀行,需要到入門處的機器排隊取號,然後按指令到對應的櫃檯辦理業務,或者也有可能告訴你,今天不能排號了,回家吧!

這樣一個場景中,取號機器就是 Nginx(入口閘道器)。一個個櫃檯就是我們的業務伺服器(辦理業務);銀行中的保險箱就是我們的資料庫(存取資料);?

(下篇)中高階前端大廠面試祕籍,寒冬中為您保駕護航,直通大廠

  • 特點:

    • 輕量級,配置方便靈活,無侵入性;
    • 佔用記憶體少,啟動快,效能好;
    • 高併發,事件驅動,非同步;
    • 熱部署,修改配置熱生效;
  • 架構模型:

    • 基於 socket 與 Linux epoll (I/O 事件通知機制),實現了 高併發
      • 使用模組化、事件通知、回撥函式、計時器、輪詢實現非阻塞的非同步模式;
      • 磁碟不足的情況,可能會導致阻塞;
    • Master-worker 程式模式:
      • Nginx 啟動時會在記憶體中常駐一個 Master 主程式,功能:
        • 讀取配置檔案;
        • 建立、繫結、關閉 socket;
        • 啟動、維護、配置 worker 程式;
        • 編譯指令碼、開啟日誌;
      • master 程式會開啟配置數量的 worker 程式,比如根據 CPU 核數等:
        • 利用 socket 監聽連線,不會新開程式或執行緒,節約了建立與銷燬程式的成本;
        • 檢查網路、儲存,把新連線加入到輪詢佇列中,非同步處理;
        • 能有效利用 cpu 多核,並避免了執行緒切換和鎖等待;
    • 熱部署模式:
      • 當我們修改配置熱重啟後,master 程式會以新的配置新建立 worker 程式,新連線會全部交給新程式處理;
      • 老的 worker 程式會在處理完之前的連線後被 kill 掉,逐步全替換成新配置的 worker 程式;
  • 配置:

    • 官網下載;

    • 配置檔案路徑: /usr/local/etc/nginx/nginx.conf

    • 啟動: 終端輸入 nginx,訪問 localhost:8080 就能看到 Welcome...

    • nginx -s stop: 停止服務;

    • nginx -s reload: 熱重啟服務;

    • 配置代理: proxy_pass

      • 在配置檔案中配置即可完成;
      server {
          listen 80;
          location / {
              proxy_pass http://xxx.xxx.xx.xx:3000;
          }
      }
      複製程式碼
  • 常用場景:

    • 代理:
      • 其實 Nginx 可以算一層 代理伺服器,將客戶端的請求處理一層後,再轉發到業務伺服器,這裡可以分成兩種型別,其實實質就是 請求的轉發,使用 Nginx 非常合適、高效;
    • 正向代理:
      • 即使用者通過訪問這層正向代理伺服器,再由代理伺服器去到原始伺服器請求內容後,再返回給使用者;
      • 例如我們常使用的 VPN 就是一種常見的正向代理模式。通常我們無法直接訪問谷歌伺服器,但是通過訪問一臺國外的伺服器,再由這臺伺服器去請求谷歌返回給使用者,使用者即可訪問谷歌;
      • 特點:
        • 代理伺服器屬於 客戶端層,稱之為正向代理;
        • 代理伺服器是 為使用者服務,對於使用者是透明的,使用者知道自己訪問代理伺服器;
        • 對內容伺服器來說是 隱藏 的,內容伺服器並無法分清訪問是來自使用者或者代理;

    (下篇)中高階前端大廠面試祕籍,寒冬中為您保駕護航,直通大廠

    • 反向代理:
      • 使用者訪問頭條的反向代理閘道器,通過閘道器的一層處理和排程後,再由閘道器將訪問轉發到內部的伺服器上,返回內容給使用者;
      • 特點:
        • 代理伺服器屬於 服務端層,因此稱為反向代理。通常代理伺服器與內部內容伺服器會隸屬於同一內網或者叢集;
        • 代理伺服器是 為內容伺服器服務 的,對使用者是隱藏的,使用者不清楚自己訪問的具體是哪臺內部伺服器;
        • 能有效保證內部伺服器的 穩定與安全

    (下篇)中高階前端大廠面試祕籍,寒冬中為您保駕護航,直通大廠

    • 反向代理的好處:

      • 安全與許可權:
        • 使用者訪問必須通過反向代理伺服器,也就是便可以在做這層做統一的請求校驗,過濾攔截不合法、危險的請求,從而就能更好的保證伺服器的安全與穩定;
      • 負載均衡: 能有效分配流量,最大化叢集的穩定性,保證使用者的訪問質量;
    • 負載均衡:

      • 負載均衡是基於反向代理下實現的一種 流量分配 功能,目的是為了達到伺服器資源的充分利用,以及更快的訪問響應;
      • 其實很好理解,還是以上面銀行的例子來看: 通過門口的取號器,系統就可以根據每個櫃檯的業務排隊情況進行使用者的分,使每個櫃檯都保持在一個比較高效的運作狀態,避免出現分配不均的情況
      • 由於使用者並不知道內部伺服器中的佇列情況,而反向代理伺服器是清楚的,因此通過 Nginx,便能很簡單地實現流量的均衡分配;
      • Nginx 實現: Upstream模組, 這樣當使用者訪問 http://xxx 時,流量便會被按照一定的規則分配到upstream中的3臺伺服器上;
      http {
          upstream xxx {
              server 1.1.1.1:3001;
              server 2.2.2.2:3001;
              server 3.3.3.3:3001;
          }
          server {
              listen 8080;
              location / {
                  proxy_pass http://xxx;
              }
          }
      }
      複製程式碼
      • 分配策略:
        • 伺服器權重(weight):

          • 可以為每臺伺服器配置訪問權重,傳入引數weight,例如:
           upstream xxx {
              server 1.1.1.1:3001 weight=1;
              server 2.2.2.2:3001 weight=1;
              server 3.3.3.3:3001 weight=8;
          }
          複製程式碼
        • 時間順序(預設): 按使用者的訪問的順序逐一的分配到正常執行的伺服器上;

        • 連線數優先(least_conn): 優先將訪問分配到列表中連線數佇列最短的伺服器上;

        • 響應時間優先(fair): 優先將訪問分配到列表中訪問響應時間最短的伺服器上;

        • ip_hash: 通過 ip_hash 指定,使每個 ip 使用者都訪問固定的伺服器上,有利於使用者特異性資料的快取,例如本地 session 服務等;

        • url_hash: 通過 url_hash 指定,使每個 url 都分配到固定的伺服器上,有利於快取;

    • Nginx 對於前端的作用:

      • 1. 快速配置靜態伺服器,當訪問 localhost:80 時,就會預設訪問到 /Users/files/index.html
      server {
      	listen 80;                                                         
      	server_name localhost;                                               
      		
      	location / {
      		root   /Users/files;
      		index  index.html;
      	}
      }
      複製程式碼
      • 2. 訪問限制: 可以制定一系列的規則進行訪問的控制,例如直接通過 ip 限制:
      # 遮蔽 192.168.1.1 的訪問;
      # 允許 192.168.1.2 ~ 10 的訪問;
      location / {
          deny  192.168.1.1;
          allow 192.168.1.2/10;
          deny  all;
      }
      複製程式碼
      • 3. 解決跨域: 其實跨域是 瀏覽器的安全策略,這意味著只要不是通過瀏覽器,就可以繞開跨域的問題。所以只要通過在同域下啟動一個 Nginx 服務,轉發請求即可;
      location ^~/api/ {
      	 # 重寫請求並代理到對應域名下
          rewrite ^/api/(.*)$ /$1 break;
          proxy_pass https://www.cross-target.com/;
      }  
      複製程式碼
      • 4. 圖片處理: 通過 ngx_http_image_filter_module 這個模組,可以作為一層圖片伺服器的代理,在訪問的時候 對圖片進行特定的操作,例如裁剪,旋轉,壓縮等

      • 5. 本地代理,繞過白名單限制: 例如我們在接入一些第三方服務時經常會有一些域名白名單的限制,如果我們在本地通過localhost進行開發,便無法完成功能。這裡我們可以做一層本地代理,便可以直接通過指定域名訪問本地開發環境;

      server {
      listen 80;
      server_name www.toutiao.com;
      
      location / {
          proxy_pass http://localhost:3000;
      }
      }
      複製程式碼

Docker

Docker,是一款現在最流行的 軟體容器平臺,提供了軟體執行時所依賴的環境。

  • 物理機:

    • 硬體環境,真實的 計算機實體,包含了例如實體記憶體,硬碟等等硬體;
  • 虛擬機器:

    • 在物理機上 模擬出一套硬體環境和作業系統,應用軟體可以執行於其中,並且毫無感知,是一套隔離的完整環境。本質上,它只是物理機上的一份 執行檔案
  • 為什麼需要虛擬機器?

    • 環境配置與遷移:
      • 在軟體開發和執行中,環境依賴一直是一個很頭疼的難題,比如你想執行 node 應用,那至少環境得安裝 node 吧,而且不同版本,不同系統都會影響執行。解決的辦法,就是我們的包裝包中直接包含執行環境的安裝,讓同一份環境可以快速複製到任意一臺物理機上。
    • 資源利用率與隔離:
      • 通過硬體模擬,幷包含一套完整的作業系統,應用可以獨立執行在虛擬機器中,與外界隔離。並且可以在同一臺物理機上,開啟多個不同的虛擬機器啟動服務,即一臺伺服器,提供多套服務,且資源完全相互隔離,互不影響。不僅能更好提高資源利用率率,降低成本,而且也有利於服務的穩定性。
  • 傳統虛擬機器的缺點:

    • 資源佔用大:
      • 由於虛擬機器是模擬出一套 完整系統,包含眾多系統級別的檔案和庫,執行也需要佔用一部分資源,單單啟動一個空的虛擬機器,可能就要佔用 100+MB 的記憶體了。
    • 啟動緩慢:
      • 同樣是由於完整系統,在啟動過程中就需要執行各種系統應用和步驟,也就是跟我們平時啟動電腦一樣的耗時。
    • 冗餘步驟多:
      • 系統有許多內建的系統操作,例如使用者登入,系統檢查等等,有些場景其實我們要的只是一個隔離的環境,其實也就是說,虛擬機器對部分需求痛點來說,其實是有點過重的。
  • Linux 容器:

    • Linux 中的一項虛擬化技術,稱為 Linux 容器技術(LXC)。
    • 它在 程式層面 模擬出一套隔離的環境配置,但並沒有模擬硬體和完整的作業系統。因此它完全規避了傳統虛擬機器的缺點,在啟動速度,資源利用上遠遠優於虛擬機器;
  • Docker:

    • Docker 就是基於 Linux 容器的一種上層封裝,提供了更為簡單易用的 API 用於操作 Docker,屬於一種 容器解決方案
    • 基本概念: 在 Docker 中,有三個核心的概念:
      • 映象 (Image):
        • 從原理上說,映象屬於一種 root 檔案系統,包含了一些系統檔案和環境配置等,可以將其理解成一套 最小作業系統。為了讓映象輕量化和可移植,Docker 採用了 Union FS 的分層儲存模式。將檔案系統分成一層一層的結構,逐步從底層往上層構建,每層檔案都可以進行繼承和定製。這裡從前端的角度來理解: 映象就類似於程式碼中的 class,可以通過繼承與上層封裝進行復用
        • 從外層系統看來,一個映象就是一個 Image 二進位制檔案,可以任意遷移,刪除,新增;
    • 容器 (Container):
      • 映象是一份靜態檔案系統,無法進行執行時操作,就如class,如果我們不進行例項化時,便無法進行操作和使用。因此 容器可以理解成映象的例項,即 new 映象(),這樣我們便可以建立、修改、操作容器;一旦建立後,就可以簡單理解成一個輕量級的作業系統,可以在內部進行各種操作,例如執行 node 應用,拉取 git 等;
      • 基於映象的分層結構,容器是 以映象為基礎底層,在上面封裝了一層 容器的儲存層
        • 儲存空間的生命週期與容器一致;
        • 該層儲存層會隨著容器的銷燬而銷燬;
        • 儘量避免往容器層寫入資料;
      • 容器中的資料的持久化管理主要由兩種方式:
        • 資料卷 (Volume): 一種可以在多個容器間共享的特殊目錄,其處於容器外層,並不會隨著容器銷燬而刪除;
        • 掛載主機目錄: 直接將一個主機目錄掛載到容器中進行寫入;
    • 倉庫 (Repository):
      • 為了便於映象的使用,Docker 提供了類似於 git 的倉庫機制,在倉庫中包含著各種各樣版本的映象。官方服務是 Docker Hub;
      • 可以快速地從倉庫中拉取各種型別的映象,也可以基於某些映象進行自定義,甚至釋出到倉庫供社群使用;

結語

不知不覺,一個月又過去了,也終於完成了整個系列。其實下篇涉及的許多知識點都是有比較深的擴充空間,博主自己也水平有限,無法面面俱到,也許甚至會有些爭議或者錯誤的見解,還望小夥伴們共同指出和糾正。希望這個面試系列能幫助到大家,好好地將這些知識點進行消化和理解,閉關修煉雖然辛苦,但現在已經是時候出山征戰江湖,收割 Offer 啦~

整個系列其實仍然是屬於淺嘗輒止的階段,後續如果大家想要繼續提升,可以往自己感興趣的方向進行深挖,例如:

  • 全棧: 那可能得更多的去了解 Node / Nginx / 反向代理 / 負載均衡 / PM2 / Docker 等服務端或者運維知識;
  • 跨平臺: 可以學習 Hybrid / Flutter / React Native / Swift 等;
  • 視覺遊戲: WebGL / 動畫 / Three.js / Canvas / 遊戲引擎 / VR / AR 等;
  • 底層框架: 瀏覽器引擎 / 框架底層 / 機器學習 / 演算法等;

總之,學無止境吶。造火箭無止境吶。?。感謝各位小夥伴的觀看,共同進步,一起成長!

Tips:

博主真的寫得很辛苦,再不 star 下,真的要哭了。~ github。?

聯絡我請發郵件: 159042708@qq.com ?

(下篇)中高階前端大廠面試祕籍,寒冬中為您保駕護航,直通大廠

相關文章