引言
本篇文章會繼續沿著前面兩篇的腳步,繼續梳理前端領域一些比較主流的進階知識點,力求能讓大家在橫向層面有個全面的概念。能在面試時有限的時間裡,能夠快速抓住重點與面試官交流。這些知識點屬於加分項,如果能在面試時從容侃侃而談,想必面試官會記憶深刻,為你折服的~?
另外有許多童鞋提到: 面試造火箭,實踐全不會,對這種應試策略表達一些擔憂。其實我是覺得面試或者這些知識點,也僅僅是個初級的 開始。能幫助在初期的快速成長,但這種策略並沒辦法讓你達到更高的水平,只有後續不斷地真正實踐和深入研究,才能突破自己的瓶頸,繼續成長。面試,不也只是一個開始而已嘛。~?
建議各位小夥從基礎入手,先看
小菜雞部落格求贊 ? 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)
- API注入,Native 直接在 JS 上下文中掛載資料或者方法
-
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 返回的結果 } }); 複製程式碼
- IOS:
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 已經成為了現在前端工程化中最重要的一環,通過Webpack
與Node
的配合,前端領域完成了不可思議的進步。通過預編譯,將軟體程式設計中先進的思想和理念能夠真正運用於生產,讓前端開發領域告別原始的蠻荒階段。深入理解Webpack
,可以讓你在程式設計思維及技術領域上產生質的成長,極大擴充技術邊界。這也是在面試中必不可少的一個內容。
-
核心概念
- JavaScript 的 模組打包工具 (module bundler)。通過分析模組之間的依賴,最終將所有模組打包成一份或者多份程式碼包 (bundler),供 HTML 直接引用。實質上,Webpack 僅僅提供了 打包功能 和一套 檔案處理機制,然後通過生態中的各種 Loader 和 Plugin 對程式碼進行預編譯和打包。因此 Webpack 具有高度的可擴充性,能更好的發揮社群生態的力量。
- Entry: 入口檔案,Webpack 會從該檔案開始進行分析與編譯;
- Output: 出口路徑,打包後建立 bundler 的檔案路徑以及檔名;
- Module: 模組,在 Webpack 中任何檔案都可以作為一個模組,會根據配置的不同的 Loader 進行載入和打包;
- Chunk: 程式碼塊,可以根據配置,將所有模組程式碼合併成一個或多個程式碼塊,以便按需載入,提高效能;
- Loader: 模組載入器,進行各種檔案型別的載入與轉換;
- Plugin: 擴充外掛,可以通過 Webpack 相應的事件鉤子,介入到打包過程中的任意環節,從而對程式碼按需修改;
- JavaScript 的 模組打包工具 (module bundler)。通過分析模組之間的依賴,最終將所有模組打包成一份或者多份程式碼包 (bundler),供 HTML 直接引用。實質上,Webpack 僅僅提供了 打包功能 和一套 檔案處理機制,然後通過生態中的各種 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: 分析
@import
和url()
,引用 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 多核,並避免了執行緒切換和鎖等待;
- Nginx 啟動時會在記憶體中常駐一個 Master 主程式,功能:
- 熱部署模式:
- 當我們修改配置熱重啟後,master 程式會以新的配置新建立 worker 程式,新連線會全部交給新程式處理;
- 老的 worker 程式會在處理完之前的連線後被 kill 掉,逐步全替換成新配置的 worker 程式;
- 基於 socket 與 Linux epoll (I/O 事件通知機制),實現了 高併發;
-
配置:
-
官網下載;
-
配置檔案路徑:
/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; } } 複製程式碼
- 1. 快速配置靜態伺服器,當訪問
- 代理:
Docker
Docker,是一款現在最流行的 軟體容器平臺,提供了軟體執行時所依賴的環境。
-
物理機:
- 硬體環境,真實的 計算機實體,包含了例如實體記憶體,硬碟等等硬體;
-
虛擬機器:
- 在物理機上 模擬出一套硬體環境和作業系統,應用軟體可以執行於其中,並且毫無感知,是一套隔離的完整環境。本質上,它只是物理機上的一份 執行檔案。
-
為什麼需要虛擬機器?
- 環境配置與遷移:
- 在軟體開發和執行中,環境依賴一直是一個很頭疼的難題,比如你想執行 node 應用,那至少環境得安裝 node 吧,而且不同版本,不同系統都會影響執行。解決的辦法,就是我們的包裝包中直接包含執行環境的安裝,讓同一份環境可以快速複製到任意一臺物理機上。
- 資源利用率與隔離:
- 通過硬體模擬,幷包含一套完整的作業系統,應用可以獨立執行在虛擬機器中,與外界隔離。並且可以在同一臺物理機上,開啟多個不同的虛擬機器啟動服務,即一臺伺服器,提供多套服務,且資源完全相互隔離,互不影響。不僅能更好提高資源利用率率,降低成本,而且也有利於服務的穩定性。
- 環境配置與遷移:
-
傳統虛擬機器的缺點:
- 資源佔用大:
- 由於虛擬機器是模擬出一套 完整系統,包含眾多系統級別的檔案和庫,執行也需要佔用一部分資源,單單啟動一個空的虛擬機器,可能就要佔用 100+MB 的記憶體了。
- 啟動緩慢:
- 同樣是由於完整系統,在啟動過程中就需要執行各種系統應用和步驟,也就是跟我們平時啟動電腦一樣的耗時。
- 冗餘步驟多:
- 系統有許多內建的系統操作,例如使用者登入,系統檢查等等,有些場景其實我們要的只是一個隔離的環境,其實也就是說,虛擬機器對部分需求痛點來說,其實是有點過重的。
- 資源佔用大:
-
Linux 容器:
- Linux 中的一項虛擬化技術,稱為 Linux 容器技術(LXC)。
- 它在 程式層面 模擬出一套隔離的環境配置,但並沒有模擬硬體和完整的作業系統。因此它完全規避了傳統虛擬機器的缺點,在啟動速度,資源利用上遠遠優於虛擬機器;
-
Docker:
- Docker 就是基於 Linux 容器的一種上層封裝,提供了更為簡單易用的 API 用於操作 Docker,屬於一種 容器解決方案。
- 基本概念: 在 Docker 中,有三個核心的概念:
- 映象 (Image):
- 從原理上說,映象屬於一種 root 檔案系統,包含了一些系統檔案和環境配置等,可以將其理解成一套 最小作業系統。為了讓映象輕量化和可移植,Docker 採用了 Union FS 的分層儲存模式。將檔案系統分成一層一層的結構,逐步從底層往上層構建,每層檔案都可以進行繼承和定製。這裡從前端的角度來理解: 映象就類似於程式碼中的 class,可以通過繼承與上層封裝進行復用。
- 從外層系統看來,一個映象就是一個 Image 二進位制檔案,可以任意遷移,刪除,新增;
- 映象 (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 ?