引言
本篇文章會繼續沿着前面兩篇的腳步,繼續梳理前端領域一些比較主流的進階知識點,力求能讓大家在橫向層面有個全面的概念。能在面試時有限的時間裡,能夠快速抓住重點與面試官交流。這些知識點屬于加分項,如果能在面試時從容侃侃而談,想必面試官會記憶深刻,為你折服的~🤤
另外有許多童鞋提到: 面試造火箭,實踐全不會,對這種應試政策表達一些擔憂。其實我是覺得面試或者這些知識點,也僅僅是個初級的 開始。能幫助在初期的快速成長,但這種政策并沒辦法讓你達到更高的水準,隻有後續不斷地真正實踐和深入研究,才能突破自己的瓶頸,繼續成長。面試,不也隻是一個開始而已嘛。~😋
建議各位小夥從基礎入手,先看
- (上篇)中進階前端大廠面試秘籍,寒冬中為您保駕護航,直通大廠
- (中篇)中進階前端大廠面試秘籍,寒冬中為您保駕護航,直通大廠
- (下篇)中進階前端大廠面試秘籍,寒冬中為您保駕護航,直通大廠
進階知識
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() {
@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 資訊;
詳細内容由興趣的童鞋可以看文章:
- Hybrid App技術解析 – 原理篇
- Hybrid App技術解析 – 實戰篇
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 代碼以
标簽的形式插入到 html 中;<style>
- css-loader: 分析
和@import
,引用 css 檔案與對應的資源;url()
- 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 會建立一個新的 Compilation 對象,開始一次新的編譯。它包含了目前的輸入資源,輸出資源,變化的檔案等,同時通過它提供的 api,可以監聽每次編譯過程中觸發的事件鈎子;
- 差別:
- Compiler 全局唯一,且從啟動生存到結束;
- Compilation 對應每次編譯,每輪編譯循環均會重新建立;
- 常用 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,可以進行作用域分析,減少此類情況的發生,但仍需要注意;
- 無用代碼消除,是許多程式設計語言都具有的優化手段,這個過程稱為 DCE (dead code elimination),即 删除不可能執行的代碼;
- 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: () => {},
}
objtype
複制代碼
- 減少 cookie 體積: 能有效減少每次請求的體積和響應時間;
- 去除不必要的 cookie;
- 壓縮 cookie 大小;
- 設定 domain 與 過期時間;
- dom 優化:
- 減少通路 dom 的次數,如需多次,将 dom 緩存于變量中;
- 減少重繪與回流:
- 多次操作合并為一次;
- 減少對計算屬性的通路;
- 例如 offsetTop, getComputedStyle 等
- 因為浏覽器需要擷取最新準确的值,是以必須立即進行重排,這樣會破壞了浏覽器的隊列整合,盡量将值進行緩存使用;
- 大量操作時,可将 dom 脫離文檔流或者隐藏,待操作完成後再重新恢複;
- 使用
進行操作;DocumentFragment / cloneNode / replaceChild
- 使用事件委托,避免大量的事件綁定;
- css 優化:
- 層級扁平,避免過于多層級的選擇器嵌套;
- 特定的選擇器 好過一層一層查找: .xxx-child-text{} 優于 .xxx .child .text{}
- 減少使用通配符與屬性選擇器;
- 減少不必要的多餘屬性;
- 使用 動畫屬性 實作動畫,動畫時脫離文檔流,開啟硬體加速,優先使用 css 動畫;
- 使用
替代原生 @import;<link>
- html 優化:
- 減少 dom 數量,避免不必要的節點或嵌套;
- 避免``空标簽,能減少伺服器壓力,因為 src 為空時,浏覽器仍然會發起請求
- IE 向頁面所在的目錄發送請求;
- Safari、Chrome、Firefox 向頁面本身發送請求;
- Opera 不執行任何操作。
- 圖檔提前 指定寬高 或者 脫離文檔流,能有效減少因圖檔加載導緻的頁面回流;
- 語義化标簽 有利于 SEO 與浏覽器的解析時間;
- 減少使用 table 進行布局,避免使用
與<br />
;<hr />
2. 頁面基礎優化
- 引入位置: css 檔案
中引入, js 檔案<head>
底部引入;<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
,這樣我們便可以建立、修改、操作容器;一旦建立後,就可以簡單了解成一個輕量級的作業系統,可以在内部進行各種操作,例如運作 node 應用,拉取 git 等;new 鏡像()
- 基于鏡像的分層結構,容器是 以鏡像為基礎底層,在上面封裝了一層 容器的存儲層;
- 存儲空間的生命周期與容器一緻;
- 該層存儲層會随着容器的銷毀而銷毀;
- 盡量避免往容器層寫入資料;
- 容器中的資料的持久化管理主要由兩種方式:
- 資料卷 (Volume): 一種可以在多個容器間共享的特殊目錄,其處于容器外層,并不會随着容器銷毀而删除;
- 挂載主機目錄: 直接将一個主機目錄挂載到容器中進行寫入;
- 鏡像是一份靜态檔案系統,無法進行運作時操作,就如
- 倉庫 (Repository):
- 為了便于鏡像的使用,Docker 提供了類似于 git 的倉庫機制,在倉庫中包含着各種各樣版本的鏡像。官方服務是 Docker Hub;
- 可以快速地從倉庫中拉取各種類型的鏡像,也可以基于某些鏡像進行自定義,甚至釋出到倉庫供社群使用;
結語
不知不覺,一個月又過去了,也終于完成了整個系列。其實下篇涉及的許多知識點都是有比較深的拓展空間,部落客自己也水準有限,無法面面俱到,也許甚至會有些争議或者錯誤的見解,還望小夥伴們共同指出和糾正。希望這個面試系列能幫助到大家,好好地将這些知識點進行消化和了解,閉關修煉雖然辛苦,但現在已經是時候出山征戰江湖,收割 Offer 啦~
整個系列其實仍然是屬于淺嘗辄止的階段,後續如果大家想要繼續提升,可以往自己感興趣的方向進行深挖,例如:
- 全棧: 那可能得更多的去了解 Node / Nginx / 反向代理 / 負載均衡 / PM2 / Docker 等服務端或者運維知識;
- 跨平台: 可以學習 Hybrid / Flutter / React Native / Swift 等;
- 視覺遊戲: WebGL / 動畫 / Three.js / Canvas / 遊戲引擎 / VR / AR 等;
- 底層架構: 浏覽器引擎 / 架構底層 / 機器學習 / 算法等;
總之,學無止境呐。造火箭無止境呐。😂。感謝各位小夥伴的觀看,共同進步,一起成長!
- (上篇)中進階前端大廠面試秘籍,寒冬中為您保駕護航,直通大廠
- (中篇)中進階前端大廠面試秘籍,寒冬中為您保駕護航,直通大廠
- (下篇)中進階前端大廠面試秘籍,寒冬中為您保駕護航,直通大廠