天天看點

10w字!前端知識體系+大廠面試筆記(工程化篇)

作者首頁:

https://juejin.cn/user/2594503172831208

正文

工程化目的是為了提升團隊的開發效率、提高項目的品質

例如大家所熟悉的建構工具、性能分析與優化、元件庫等知識,都屬于工程化的内容

這篇文章的内容,是我這幾年對工程化的實踐經驗與收獲總結

文中大部分的内容,主要是以 

代碼示例 + 分析總結 + 實踐操作

 來講解的,始終圍繞

實用可操作性

來說明,争取讓小夥伴們更容易了解和運用

看十篇講 webpack 的文章,可能不如手寫一個 mini 版的 webpack 來的透徹

工程化是一個優秀工程師的必修課,也是一個重要的分水嶺
10w字!前端知識體系+大廠面試筆記(工程化篇)

前端工程化導圖

10w字!前端知識體系+大廠面試筆記(工程化篇)

前端工程化.png

建構工具

Webpack

Webpack

是前端最常用的建構工具,重要程度無需多言

之前看過很多關于 Webpack 的文章,總是感覺雲裡霧裡,現在換一種方式,我們一起來解密它,嘗試打開這個

盲盒

手寫一個 mini 版的 Webpack

别擔心

我們不需要去掌握具體的實作細節,而是通過這個案例,了解 webpack 的整體打包流程,明白這個過程中做了哪些事情,最終輸出了什麼結果即可

建立

minipack.js

const fs = require('fs');
const path = require('path');
// babylon解析js文法,生産AST 文法樹
// ast将js代碼轉化為一種JSON資料結構
const babylon = require('babylon');
// babel-traverse是一個對ast進行周遊的工具, 對ast進行替換
const traverse = require('babel-traverse').default;
// 将es6 es7 等進階的文法轉化為es5的文法
const { transformFromAst } = require('babel-core');

// 每一個js檔案,對應一個id
let ID = 0;

// filename參數為檔案路徑, 讀取内容并提取它的依賴關系
function createAsset(filename) {
  const content = fs.readFileSync(filename, 'utf-8');

  // 擷取該檔案對應的ast 抽象文法樹
  const ast = babylon.parse(content, {
    sourceType: 'module'
  });

  // dependencies儲存所依賴的子產品的相對路徑
  const dependencies = [];

  // 通過查找import節點,找到該檔案的依賴關系
  // 因為項目中我們都是通過 import 引入其他檔案的,找到了import節點,就找到這個檔案引用了哪些檔案
  traverse(ast, {
    ImportDeclaration: ({ node }) => {
      // 查找import節點
      dependencies.push(node.source.value);
    }
  });

  // 通過遞增計數器,為此子產品配置設定唯一辨別符, 用于緩存已解析過的檔案
  const id = ID++;
  // 該`presets`選項是一組規則,告訴`babel`如何傳輸我們的代碼.
  // 用`babel-preset-env`将代碼轉換為浏覽器可以運作的東西.
  const { code } = transformFromAst(ast, null, {
    presets: ['env']
  });

  // 傳回此子產品的相關資訊
  return {
    id, // 檔案id(唯一)
    filename, // 檔案路徑
    dependencies, // 檔案的依賴關系
    code // 檔案的代碼
  };
}

// 我們将提取它的每一個依賴檔案的依賴關系,循環下去:找到對應這個項目的`依賴圖`
function createGraph(entry) {
  // 得到入口檔案的依賴關系
  const mainAsset = createAsset(entry);
  const queue = [mainAsset];
  for (const asset of queue) {
    asset.mapping = {};
    // 擷取這個子產品所在的目錄
    const dirname = path.dirname(asset.filename);
    asset.dependencies.forEach((relativePath) => {
      // 通過将相對路徑與父資源目錄的路徑連接配接,将相對路徑轉變為絕對路徑
      // 每個檔案的絕對路徑是固定、唯一的
      const absolutePath = path.join(dirname, relativePath);
      // 遞歸解析其中所引入的其他資源
      const child = createAsset(absolutePath);
      asset.mapping[relativePath] = child.id;
      // 将`child`推入隊列, 通過遞歸實作了這樣它的依賴關系解析
      queue.push(child);
    });
  }

  // queue這就是最終的依賴關系圖譜
  return queue;
}

// 自定義實作了require 方法,找到導出變量的引用邏輯
function bundle(graph) {
  let modules = '';
  graph.forEach((mod) => {
    modules += `${mod.id}: [
      function (require, module, exports) { ${mod.code} },
      ${JSON.stringify(mod.mapping)},
    ],`;
  });
  const result = `
    (function(modules) {
      function require(id) {
        const [fn, mapping] = modules[id];
        function localRequire(name) {
          return require(mapping[name]);
        }
        const module = { exports : {} };
        fn(localRequire, module, module.exports);
        return module.exports;
      }
      require(0);
    })({${modules}})
  `;
  return result;
}

// ❤️ 項目的入口檔案
const graph = createGraph('./example/entry.js');
const result = bundle(graph);

// ⬅️ 建立dist目錄,将打包的内容寫入main.js中
fs.mkdir('dist', (err) => {
  if (!err)
    fs.writeFile('dist/main.js', result, (err1) => {
      if (!err1) console.log('打包成功');
    });
});
           

注:

mini版的Webpack

未涉及 loader 和 plugin 等複雜功能,隻是一個非常簡化的例子

mini 版的 webpack 打包流程

1)從入口檔案開始解析

2)查找入口檔案引入了哪些 js 檔案,找到依賴關系

3)遞歸周遊引入的其他 js,生成最終的依賴關系圖譜

4)同時将 ES6 文法轉化成 ES5

5)最終生成一個可以在浏覽器加載執行的 js 檔案

建立測試目錄 example

在目錄下建立以下 4 個檔案

1)建立入口檔案

entry.js

import message from './message.js';
// 将message的内容顯示到頁面中
let p = document.createElement('p');
p.innerHTML = message;
document.body.appendChild(p);
           

2)建立

message.js

import { name } from './name.js';
export default `hello ${name}!`;
           

3)建立

name.js

export const name = 'Webpack';
           

4)建立

index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Webpack App</title>
  <meta name="viewport" content="width=device-width, initial-scale=1"></head>
  <body>
  <!-- 引入打包後的main.js -->
  <script src="./dist/main.js"></script></body>
</html>
           

5) 執行打包

運作

node minipack.js

,dist 目錄下生成 main.js

6) 浏覽器打開 index.html

頁面上顯示

hello Webpack!

mini-webpack 的 github 源碼位址[1]

分析打包生成的檔案

分析

dist/main.js

檔案内容

1)檔案裡是一個立即執行函數

2)該函數接收的參數是一個對象,該對象有 3 個屬性

 代表

entry.js

;

1

 代表

message.js

;

2

 代表

name.js

dist/main.js 代碼

// 檔案裡是一個立即執行函數
(function(modules) {
  function require(id) {
    const [fn, mapping] = modules[id];
    function localRequire(name) {
      // ⬅️ 第四步 跳轉到這裡 此時mapping[name] = 1,繼續執行require(1)
      // ⬅️ 第六步 又跳轉到這裡 此時mapping[name] = 2,繼續執行require(2)
      return require(mapping[name]);
    }
    // 建立module對象
    const module = { exports: {} };
    // ⬅️ 第二步 執行fn
    fn(localRequire, module, module.exports);

    return module.exports;
  }
  // ⬅️ 第一步 執行require(0)
  require(0);
})({
  // 立即執行函數的參數是一個對象,該對象有3個屬性
  // 0 代表entry.js;
  // 1 代表message.js
  // 2 代表name.js
  0: [
    function(require, module, exports) {
      'use strict';
      // ⬅️ 第三步 跳轉到這裡 繼續執行require('./message.js')
      var _message = require('./message.js');
      // ⬅️ 第九步 跳到這裡 此時_message為 {default: 'hello Webpack!'}
      var _message2 = _interopRequireDefault(_message);

      function _interopRequireDefault(obj) {
        return obj && obj.__esModule ? obj : { default: obj };
      }

      var p = document.createElement('p');
      // ⬅️ 最後一步 将_message2.default: 'hello Webpack!'寫到p标簽中
      p.innerHTML = _message2.default;
      document.body.appendChild(p);
    },
    { './message.js': 1 }
  ],
  1: [
    function(require, module, exports) {
      'use strict';

      Object.defineProperty(exports, '__esModule', {
        value: true
      });
      // ⬅️ 第五步 跳轉到這裡 繼續執行require('./name.js')
      var _name = require('./name.js');
      // ⬅️ 第八步 跳到這裡 此時_name為{name: 'Webpack'}, 在exports對象上設定default屬性,值為'hello Webpack!'
      exports.default = 'hello ' + _name.name + '!';
    },
    { './name.js': 2 }
  ],
  2: [
    function(require, module, exports) {
      'use strict';

      Object.defineProperty(exports, '__esModule', {
        value: true
      });
      // ⬅️ 第七步 跳到這裡 在傳入的exports對象上添加name屬性,值為'Webpack'
      var name = (exports.name = 'Webpack');
    },
    {}
  ]
});
           
⬅️ 分析檔案的執行過程

1)整體大緻分為 10 步,第一步從

require(0)

開始執行,調用内置的自定義 require 函數,跳轉到第二步,執行

fn

函數

2)執行第三步

require('./message.js')

,繼續跳轉到第四步 

require(mapping['./message.js'])

, 最終轉化為

require(1)

3)繼續執行

require(1)

,擷取

modules[1]

,也就是執行

message.js

的内容

4)第五步

require('./name.js')

,最終轉化為

require(2)

,執行

name.js

的内容

5)通過遞歸調用,将代碼中導出的屬性,放到

exports

對象中,一層層導出到最外層

6)最終通過

_message2.default

擷取導出的值,頁面顯示

hello Webpack!

Webpack 的打包流程

總結一下 webpack 完整的打包流程

1)webpack 從項目的

entry

入口檔案開始遞歸分析,調用所有配置的 

loader

對子產品進行編譯

因為 webpack 預設隻能識别 js 代碼,是以如 css 檔案、.vue 結尾的檔案,必須要通過對應的 loader 解析成 js 代碼後,webpack 才能識别

2)利用

babel(babylon)

将 js 代碼轉化為

ast抽象文法樹

,然後通過

babel-traverse

對 ast 進行周遊

3)周遊的目的找到檔案的

import引用節點

因為現在我們引入檔案都是通過 import 的方式引入,是以找到了 import 節點,就找到了檔案的依賴關系

4)同時每個子產品生成一個唯一的 id,并将解析過的

子產品緩存

起來,如果其他地方也引入該子產品,就無需重新解析,最後根據依賴關系生成依賴圖譜

5)遞歸周遊所有依賴圖譜的子產品,組裝成一個個包含多個子產品的 

Chunk(塊)

6)最後将生成的檔案輸出到 

output

 的目錄中

熱更新原理

什麼是 webpack 熱更新?

開發過程中,代碼發生變動後,webpack 會重新編譯,編譯後浏覽器替換修改的子產品,局部更新,無需重新整理整個頁面

好處:節省開發時間、提升開發體驗

熱更新原理

主要是通過

websocket

實作,建立本地服務和浏覽器的雙向通信。當代碼變化,重新編譯後,通知浏覽器請求更新的子產品,替換原有的子產品

1) 通過

webpack-dev-server

開啟

server服務

,本地 server 啟動之後,再去啟動 websocket 服務,建立本地服務和浏覽器的雙向通信

2) webpack 每次編譯後,會生成一個

Hash值

,Hash 代表每一次編譯的辨別。本次輸出的 Hash 值會編譯新生成的檔案辨別,被作為下次熱更新的辨別

3)webpack

監聽檔案變化

(主要是通過檔案的生成時間判斷是否有變化),當檔案變化後,重新編譯

4)編譯結束後,通知浏覽器請求變化的資源,同時将新生成的 hash 值傳給浏覽器,用于下次熱更新使用

5)浏覽器拿到更新後的子產品後,用新子產品替換掉舊的子產品,進而實作了局部重新整理

輕松了解 webpack 熱更新原理[2]

深入淺出 Webpack[3]

帶你深度解鎖 Webpack 系列(基礎篇)[4]

Plugin

作用:擴充 webpack 功能

工作原理

webpack 通過内部的事件流機制保證了插件的有序性,底層是利用

釋出訂閱模式

,webpack 在運作過程中會廣播事件,插件隻需要監聽它所關心的事件,在特定的時機對資源做處理

手寫一個 Plugin 插件

// 自定義一個名為MyPlugin插件,該插件在打包完成後,在控制台輸出"打包已完成"
class MyPlugin {
  // 原型上需要定義apply 的方法
  apply(compiler) {
    // 通過compiler擷取webpack内部的鈎子
    compiler.hooks.done.tap("My Plugin", (compilation, cb) => {
      console.log("打包已完成");
      // 分為同步和異步的鈎子,異步鈎子必須執行對應的回調
      cb();
    });
  }
}
module.exports = MyPlugin;
           

在 vue 項目中使用自定義插件

1)在

vue.config.js

引入該插件

const MyPlugin = require('./MyPlugin.js')

2)在

configureWebpack

的 plugins 清單中注冊該插件

module.exports = {
  configureWebpack: {
    plugins: [new MyPlugin()]
  }
};
           

3)執行項目的打包指令

當項目打包成功後,會在控制台輸出:

打包已完成

Plugin 的組成部分

1)Plugin 的本質是一個 

node 子產品

,這個子產品導出一個 JavaScript 類

2)它的原型上需要定義一個

apply

 的方法

3)通過

compiler

擷取 webpack 内部的鈎子,擷取 webpack 打包過程中的各個階段

鈎子分為同步和異步的鈎子,異步鈎子必須執行對應的回調

4)通過

compilation

操作 webpack 内部執行個體特定資料

5)功能完成後,執行 webpack 提供的 cb 回調

compiler 上暴露的一些常用的鈎子簡介

鈎子 類型 調用時機
run AsyncSeriesHook 在編譯器開始讀取記錄前執行
compile SyncHook 在一個新的 compilation 建立之前執行
compilation SyncHook 在一次 compilation 建立後執行插件
make AsyncParallelHook 完成一次編譯之前執行
emit AsyncSeriesHook 在生成檔案到 output 目錄之前執行,回調參數: compilation
afterEmit AsyncSeriesHook 在生成檔案到 output 目錄之後執行
assetEmitted AsyncSeriesHook 生成檔案的時候執行,提供通路産出檔案資訊的入口,回調參數:file,info
done AsyncSeriesHook 一次編譯完成後執行,回調參數:stats

常用的 Plugin 插件

插件名稱 作用
html-webpack-plugin 生成 html 檔案,引入公共的 js 和 css 資源
webpack-bundle-analyzer 對打包後的檔案進行分析,生成資源分析圖
terser-webpack-plugin 代碼壓縮,移除 console.log 列印等
HappyPack Plugin 開啟多線程打包,提升打包速度
Dllplugin 動态連結庫,将項目中依賴的三方子產品抽離出來,單獨打包
DllReferencePlugin 配合 Dllplugin,通過 manifest.json 映射到相關的依賴上去
clean-webpack-plugin 清理上一次項目生成的檔案
vue-skeleton-webpack-plugin vue 項目實作骨架屏

揭秘 webpack-plugin[5]

Loader

Loader 作用

webpack 隻能直接處理 js 格式的資源,任何非 js 檔案都必須被對應的

loader

處理轉換為 js 代碼

手寫一個 loader

一個簡單的

style-loader

// 作用:将css内容,通過style标簽插入到頁面中
// source為要處理的css源檔案
function loader(source) {
  let style = `
    let style = document.createElement('style');
    style.setAttribute("type", "text/css");
    style.innerHTML = ${source};
    document.head.appendChild(style)`;
  return style;
}
module.exports = loader;
           

在 vue 項目中使用自定義 loader

1)在

vue.config.js

引入該 loader

const MyStyleLoader = require('./style-loader')

2)在

configureWebpack

中添加配置

module.exports = {
  configureWebpack: {
    module: {
      rules: [
        {
          // 對main.css檔案使用MyStyleLoader處理
          test: /main.css/,
          loader: MyStyleLoader
        }
      ]
    }
  }
};
           

3)項目重新編譯

main.css

樣式已加載到頁面中

loader 的組成部分

loader 的本質是一個 

node

子產品,該子產品導出一個函數,函數接收

source(源檔案)

,傳回處理後的

source

loader 執行順序

相同優先級的 loader 鍊,執行順序為:

從右到左,從下到上

use: ['loader1', 'loader2', 'loader3']

,執行順序為 

loader3 → loader2 → loader1

常用的 loader

名稱 作用
style-loader 用于将 css 編譯完成的樣式,挂載到頁面 style 标簽上
css-loader 用于識别 .css 檔案, 須配合 style-loader 共同使用
sass-loader/less-loader css 預處理器
postcss-loader 用于補充 css 樣式各種浏覽器核心字首
url-loader 處理圖檔類型資源,可以轉 base64
vue-loader 用于編譯 .vue 檔案
worker-loader 通過内聯 loader 的方式使用 web worker 功能
style-resources-loader 全局引用對應的 css,避免頁面再分别引入

揭秘 webpack-loader[6]

Webpack5 子產品聯邦

webpack5 

子產品聯邦(Module Federation)

 使 JavaScript 應用,得以從另一個 JavaScript 應用中動态的加載代碼,實作共享依賴,用于前端的微服務化

比如

項目A

項目B

,公用

項目C

元件,以往這種情況,可以将 C 元件釋出到 npm 上,然後 A 和 B 再具體引入。當 C 元件發生變化後,需要重新釋出到 npm 上,A 和 B 也需要重新下載下傳安裝

使用子產品聯邦後,可以在遠端子產品的 Webpack 配置中,将 C 元件子產品暴露出去,項目 A 和項目 B 就可以遠端進行依賴引用。當 C 元件發生變化後,A 和 B 無需重新引用

子產品聯邦利用 webpack5 内置的

ModuleFederationPlugin

插件,實作了項目中間互相引用的按需熱插拔

Webpack ModuleFederationPlugin

重要參數說明

1)

name

 目前應用名稱,需要全局唯一

2)

remotes

 可以将其他項目的  

name

 映射到目前項目中

3)

exposes

 表示導出的子產品,隻有在此申明的子產品才可以作為遠端依賴被使用

4)

shared

 是非常重要的參數,制定了這個參數,可以讓遠端加載的子產品對應依賴,改為使用本地項目的依賴,如 React 或 ReactDOM

配置示例

new ModuleFederationPlugin({
     name: "app_1",
     library: { type: "var", name: "app_1" },
     filename: "remoteEntry.js",
     remotes: {
        app_02: 'app_02',
        app_03: 'app_03',
     },
     exposes: {
        antd: './src/antd',
        button: './src/button',
     },
     shared: ['react', 'react-dom'],
}),
           

精讀《Webpack5 新特性 - 子產品聯邦》[7]

Webpack 5 子產品聯邦引發微前端的革命?[8]

Webpack 5 更新内容(二:子產品聯邦)[9]

Vite

Vite 被譽為

下一代的建構工具

上手了幾個項目後,果然名不虛傳,熱更新速度真的是

快的飛起!

Vite 原理

1)Vite 利用浏覽器支援原生的

es module

子產品,開發時跳過打包的過程,提升編譯效率

2)當通過 import 加載資源時,浏覽器會發出 HTTP 請求對應的檔案,

Vite攔截到該請求

,傳回對應的子產品檔案

es module 簡單示例

<script type="module">import { a } from './a.js'</script>
           

1)當聲明一個 script 标簽類型為 

module

 時,浏覽器将對其内部的 import 引用發起 HTTP 請求,擷取子產品内容

2)浏覽器将發起一個對 

HOST/a.js

 的 HTTP 請求,擷取到内容之後再執行

Vite 的限制

Vite 主要對應的場景是開發模式(生産模式是用 

rollup 打包

Vite 熱更新速度

Vite 熱更新的速度不會随着子產品增多而變慢

1)Webpack 的熱更新原理:一旦某個依賴(比如上面的 a.js)改變,就将這個依賴所處的 整個

module

 更新,并将新的 module 發送給浏覽器重新執行

試想如果依賴越來越多,就算隻修改一個檔案,熱更新的速度會越來越慢

2)Vite 的熱更新原理:如果 a.js 發生了改變,隻會重新編譯這個檔案 a,而其餘檔案都無需重新編譯

是以理論上 Vite 熱更新的速度不會随着檔案增加而變慢

手寫 vite

推薦珠峰的從零手寫 vite 視訊[10]

vite 的實作流程

1)通過

koa

開啟一個服務,擷取請求的靜态檔案内容

2)通過

es-module-lexer

 解析 

ast

 拿到 import 的内容

3)判斷 import 導入子產品是否為

三方子產品

,是的話,傳回

node_module

下的子產品, 如 

import vue

 傳回 

import './@modules/vue'

4)如果是

.vue檔案

,vite 攔截對應的請求,讀取.vue 檔案内容進行編譯,通過

compileTemplate

 編譯模闆,将

template轉化為render函數

5)通過 babel parse 對 js 進行編譯,最終傳回編譯後的 js 檔案

尤雨溪幾年前開發的“玩具 vite”[11]

Vite 原理淺析[12]

Babel

AST 抽象文法樹

這裡先聊一下

AST抽象文法樹

,因為

AST

babel

的核心

什麼是 AST ?

AST

是源代碼的抽象文法結構的樹狀表現形式

在 js 世界中,可以認為

抽象文法樹(AST)是最底層

一個簡單的 AST 示例

let a = 1

,轉化成 AST 的結果

{
  "type": "Program",
  "start": 0,
  "end": 9,
  "body": [
    {
      "type": "VariableDeclaration",
      "start": 0,
      "end": 9,
      "declarations": [
        {
          "type": "VariableDeclarator",
          "start": 4,
          "end": 9,
          "id": {
            "type": "Identifier",
            "start": 4,
            "end": 5,
            "name": "a"
          },
          "init": {
            "type": "Literal",
            "start": 8,
            "end": 9,
            "value": 1,
            "raw": "1"
          }
        }
      ],
      "kind": "let"
    }
  ],
  "sourceType": "module"
}
           

AST 抽象文法樹的結構,可以通過AST 網站[13]線上輸入代碼檢視

AST 抽象文法樹——最基礎的 javascript 重點知識,99%的人根本不了解[14]

Babel 基本原理與作用

Babel

 是一個 

JS 編譯器

,把我們的代碼轉成浏覽器可以運作的代碼

作用

babel 主要用于将新版本的代碼轉換為向後相容的 js 文法(

Polyfill

 方式),以便能夠運作在各版本的浏覽器或其他環境中

基本原理

核心就是 

AST (抽象文法樹)

首先将源碼轉成抽象文法樹,然後對文法樹進行處理生成新的文法樹,最後将新文法樹生成新的 JS 代碼

Babel 的流程

3 個階段: parsing (解析)、transforming (轉換)、generating (生成)

1)通過

babylon

将 js 轉化成 ast (抽象文法樹)

2)通過

babel-traverse

是一個對 ast 進行周遊,使用 babel 插件轉化成新的 ast

3)通過

babel-generator

将 ast 生成新的 js 代碼

配置和使用

1)單個軟體包在 

.babelrc

 中配置

.babelrc {
  // 預設: Babel 官方做了一些預設的插件集,稱之為 Preset,我們隻需要使用對應的 Preset 就可以了
  "presets": [],
   // babel和webpack類似,主要是通過plugin插件進行代碼轉化的,如果不配置插件,babel會将代碼原樣傳回
  "plugins": []
}
           

2)vue 中,在 babel.config.js 中配置

配置 babel-plugin-component 插件,按需引入 elementUI

module.exports = {
   presets: ["@vue/app"],
    // 配置babel-plugin-component插件
   plugins: [
        [
   "component",
   {
     libraryName: "element-ui",
     styleLibraryName: "theme-chalk"
 }
     ]
  ]
};
           

3)配置

browserslist

browserslist

 用來控制要相容浏覽器版本,配置的範圍越具體,就可以更精确控制

Polyfill

轉化後的體積大小

"browserslist": [
   // 全球超過1%人使用的浏覽器
   "> 1%",
   //  所有浏覽器相容到最後兩個版本根據CanIUse.com追蹤的版本
   "last 2 versions",
   // chrome 版本大于70
   "chrome >= 70"
   // 排除部分版本
   "not ie <= 8"
]
           

如何開發一個 babel 插件

Babel 插件的作用

Babel 插件擔負着編譯過程中的核心任務:

轉換 AST

babel 插件的基本格式

1)一個函數,參數是 babel,然後就是傳回一個對象,

key是visitor

,然後裡面的對象是一個箭頭函數

2)函數有兩個參數,

path

表示路徑,

state

表示狀态

3)

CallExpression

就是我們要通路的節點,path 參數表示目前節點的位置,包含的主要是目前

節點(node)

内容以及

父節點(parent)

内容

插件的簡單格式示例

module.exports = function (babel) {
   let t = babel.type
   return {
      visitor: {
        CallExression: (path, state) => {
           do soming
     }}}}
           

一個最簡單的插件: 

将const a 轉化為const b

建立 babelPluginAtoB.js

module.exports = function(babel) {
  let t = babel.types;
  return {
    visitor: {
      VariableDeclarator(path, state) {
        // VariableDeclarator 是要找的節點類型
        if (path.node.id.name == "a") {
        // path.node.id.name = 'b' 是不行的,想改變某個值,就是用對應的ast來替換,是以我們要把id是a的ast換成b的ast
          path.node.id = t.Identifier("b");
        }
      }
    }
  };
};
           

在.babelrc 中引入 babelPluginAtoB 插件

const babelPluginAtoB = require('./babelPluginAtoB.js');
{
    "plugins": [
        [babelPluginAtoB]
    ]
}
           

編寫測試代碼

let a = 1;
console.log(b);
// babel插件生效,沒有報錯,列印 1
           

Babel 入門教程[15]

Babel 中文文檔[16]

不容錯過的 Babel 知識[17]

快速寫一個 babel 插件[18]

Gulp

gulp

 是基于 

node 流

 實作的前端自動化開發的工具

适用場景

在前端開發工作中有很多

“重複工作”

,比如

批量将Scss檔案編譯為CSS檔案

這裡主要聊一下,在開發的元件庫中如何使用 gulp

Gulp 在元件庫中的運用

elementUI

為例,下載下傳elementUI 源碼[19]

打開

packages/theme-chalk/gulpfile.js

該檔案的作用是将 scss 檔案編譯為 css 檔案

'use strict';

// 引入gulp
// series建立任務清單,
// src建立一個流,讀取檔案
// dest 建立一個對象寫入到檔案系統的流
const { series, src, dest } = require('gulp');
// gulp-dart-sass編譯scss檔案
const sass = require('gulp-dart-sass');
// gulp-autoprefixer 給css樣式添加字首
const autoprefixer = require('gulp-autoprefixer');
// gulp-cssmin 壓縮css
const cssmin = require('gulp-cssmin');

// 處理src目錄下的所有scss檔案,轉化為css檔案
function compile() {
  return (
    src('./src/*.scss')
      .pipe(sass.sync().on('error', sass.logError))
      .pipe(
        // 給css樣式添加字首
        autoprefixer({
          overrideBrowserslist: ['ie > 9', 'last 2 versions'],
          cascade: false
        })
      )
      // 壓縮css
      .pipe(cssmin())
      // 将編譯好的css 輸出到lib目錄下
      .pipe(dest('./lib'))
  );
}

// 将src/fonts檔案的字型檔案 copy到 /lib/fonts目錄下
function copyfont() {
  return src('./src/fonts/**')
    .pipe(cssmin())
    .pipe(dest('./lib/fonts'));
}

// series建立任務清單
exports.build = series(compile, copyfont);
           

Gulp 給 elementUI 增加一鍵換膚功能

總體流程

1)使用

css var()

定義顔色變量

2)建立主題

theme.css

檔案,存儲所有的顔色變量

3)使用

gulp

theme.css

合并到

base.css

中,解決按需引入的情況

4)使用

gulp

index.css

base.css

合并,解決全局引入的情況

步驟一:建立基礎顔色變量

theme.css

檔案

10w字!前端知識體系+大廠面試筆記(工程化篇)

theme.jpg

步驟二:修改

packages/theme-chalk/src/common/var.scss

檔案

将該檔案的中定義的 scss 變量,替換成 var()變量

10w字!前端知識體系+大廠面試筆記(工程化篇)

var.jpg

步驟三:修改後的

packages/theme-chalk/gulpfile.js

'use strict';

const {series, src, dest} = require('gulp');
const sass = require('gulp-dart-sass');
const autoprefixer = require('gulp-autoprefixer');
const cssmin = require('gulp-cssmin');
const concat = require('gulp-concat');

function compile() {
  return src('./src/*.scss')
    .pipe(sass.sync().on('error', sass.logError))
    .pipe(autoprefixer({
      overrideBrowserslist: ['ie > 9', 'last 2 versions'],
      cascade: false
    }))
    .pipe(cssmin())
    .pipe(dest('./lib'));
}

// 将 theme.css 和 lib/base.css合并成 最終的 base.css
function compile1() {
  return src(['./src/theme.css', './lib/base.css'])
    .pipe(concat('base.css'))
    .pipe(dest('./lib'));
}

// 将 base.css、 index.css 合并成 最終的 index.css
function compile2() {
  return src(['./lib/base.css', './lib/index.css'])
    .pipe(concat('index.css'))
    .pipe(dest('./lib'));
}

function copyfont() {
  return src('./src/fonts/**')
    .pipe(cssmin())
    .pipe(dest('./lib/fonts'));
}

exports.build = series(compile, compile1, compile2, copyfont);
           

elementUI 多套主題下 按需引入和全局引入 的換膚方案[20]

腳手架

腳手架是開發中經常會使用的工具,比如

vue-cli

create-react-app

等,這些腳手架可以通過簡單的指令,快速去搭建項目,讓我們更專注于項目的開發

随着項目的增多、人員的擴充,大家開發的基礎元件和公共方法也越來越多,希望把這些積累添加到腳手架中,當成項目模闆留存下來

這樣再建立項目時,就不用每次去其他項目中來回 copy

手寫一個 mini 版的腳手架

下面我們一起,手寫一個 mini 版的腳手架

通過這個案例來了解腳手架的工作流程,以及使用了哪些常用工具

建立檔案夾

my-build-cli

,執行

npm init -y

10w字!前端知識體系+大廠面試筆記(工程化篇)

init-y.png

配置腳手架入口檔案

1)建立

bin

目錄,該目錄下建立

www.js

bin/www.js

 内容

#! /usr/bin/env node

console.log('link 成功');
           

注:

/usr/bin/env node

 這行的意思是使用 node 來執行此檔案

2)

package.json

中配置入口檔案的路徑

{
  "name": "my-build-cli",
  "version": "1.0.0",
  "description": "",
  "bin": "./bin/www.js", // 手動添加入口檔案為 ./bin/www.js
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}
           

3)項目目錄結構

my-build-cli
├─ bin
│  └─ www.js
└─ package.json
           

npm link 到全局

在控制台輸入

npm link

10w字!前端知識體系+大廠面試筆記(工程化篇)

link.jpg

測試是否連接配接成功

在控制台輸入

my-build-cli

10w字!前端知識體系+大廠面試筆記(工程化篇)

linksuccess.jpg

在控制台輸出

link 成功

, 項目配置成功

安裝腳手架所需的工具

一次性安裝所需的工具

npm install commander inquirer download-git-repo util ora fs-extra axios

工具名稱 作用
commander 自定義指令行工具
inquirer 指令行互動工具
download-git-repo 從 git 上下載下傳項目模闆工具
util download-git-repo 不支援異步調用,需要使用 util 插件的

util.promisify

進行轉換
ora 指令行 loading 動效
fs-extra 提供檔案操作方法
axios 發送接口,請求 git 上的模闆清單

commander 自定義指令行工具

commander.js

 是自定義指令行工具

這裡用來建立

create

 指令,使用者可以通過輸入 

my-cli creat appName

 來建立項目

修改

www.js

#! /usr/bin/env node

const program = require('commander');

program
  // 建立create 指令,使用者可以通過 my-cli creat appName 來建立項目
  .command('create <app-name>')
  // 命名的描述
  .description('create a new project')
  // create指令的選項
  .option('-f, --force', 'overwrite target if it exist')
  .action((name, options) => {
    // 執行'./create.js',傳入項目名稱和 使用者選項
    require('./create')(name, options);
  });

program.parse();
           

inquirer 指令行互動工具

inquirer.js

 指令行互動工具,用來詢問使用者的操作,讓使用者輸入指定的資訊,或給出對應的選項讓使用者選擇

此處 inquirer 的運用場景有 2 個

1)場景 1:當使用者要建立的項目目錄已存在時,提示使用者是否要覆寫 or 取消

2)場景 2:讓使用者輸入項目的

author

作者和項目

description

描述

建立

create.js

bin/create.js

const path = require('path');
const fs = require('fs-extra');
const inquirer = require('inquirer');
const Generator = require('./generator');

module.exports = async function (name, options) {
  // process.cwd擷取目前的工作目錄
  const cwd = process.cwd();
  // path.join拼接 要建立項目的目錄
  const targetAir = path.join(cwd, name);

  // 如果該目錄已存在
  if (fs.existsSync(targetAir)) {
    // 強制删除
    if (options.force) {
      await fs.remove(targetAir);
    } else {
      // 通過inquirer:詢問使用者是否确定要覆寫 or 取消
      let { action } = await inquirer.prompt([
        {
          name: 'action',
          type: 'list',
          message: 'Target already exists',
          choices: [
            {
              name: 'overwrite',
              value: 'overwrite'
            },
            {
              name: 'cancel',
              value: false
            }
          ]
        }
      ]);
      if (!action) {
        return;
      } else {
        // 删除檔案夾
        await fs.remove(targetAir);
      }
    }
  }

  const args = require('./ask');

  // 通過inquirer,讓使用者輸入的項目内容:作者和描述
  const ask = await inquirer.prompt(args);
  // 建立項目
  const generator = new Generator(name, targetAir, ask);
  generator.create();
};
           

建立

ask.js

配置 

ask

 選項,讓使用者輸入作者和項目描述

bin/create.js

// 配置ask 選項
module.exports = [
  {
    type: 'input',
    name: 'author',
    message: 'author?'
  },
  {
    type: 'input',
    name: 'description',
    message: 'description?'
  }
];
           

建立

generator.js

generator.js

的工作流程

1)通過接口擷取

git

上的模闆目錄

2)通過

inquirer

讓使用者選擇需要下載下傳的項目

3)使用

download-git-repo

下載下傳使用者選擇的項目模闆

4)将使用者建立時,将

項目名稱、作者名字、描述

寫入到項目模闆的

package.json

檔案中

bin/generator.js

const path = require('path');
const fs = require('fs-extra');
// 引入ora工具:指令行loading 動效
const ora = require('ora');
const inquirer = require('inquirer');
// 引入download-git-repo工具
const downloadGitRepo = require('download-git-repo');
// download-git-repo 預設不支援異步調用,需要使用util插件的util.promisify 進行轉換
const util = require('util');
// 擷取git項目清單
const { getRepolist } = require('./http');

async function wrapLoading(fn, message, ...args) {
  const spinner = ora(message);
  // 下載下傳開始
  spinner.start();

  try {
    const result = await fn(...args);
    // 下載下傳成功
    spinner.succeed();
    return result;
  } catch (e) {
    // 下載下傳失敗
    spinner.fail('Request failed ……');
  }
}

// 建立項目類
class Generator {
  // name 項目名稱
  // target 建立項目的路徑
  // 使用者輸入的 作者和項目描述 資訊
  constructor(name, target, ask) {
    this.name = name;
    this.target = target;
    this.ask = ask;
    // download-git-repo 預設不支援異步調用,需要使用util插件的util.promisify 進行轉換
    this.downloadGitRepo = util.promisify(downloadGitRepo);
  }
  async getRepo() {
    // 擷取git倉庫的項目清單
    const repolist = await wrapLoading(getRepolist, 'waiting fetch template');
    if (!repolist) return;

    const repos = repolist.map((item) => item.name);

    // 通過inquirer 讓使用者選擇要下載下傳的項目模闆
    const { repo } = await inquirer.prompt({
      name: 'repo',
      type: 'list',
      choices: repos,
      message: 'Please choose a template'
    });

    return repo;
  }

  // 下載下傳使用者選擇的項目模闆
  async download(repo, tag) {
    const requestUrl = `yuan-cli/${repo}`;
    await wrapLoading(this.downloadGitRepo, 'waiting download template', requestUrl, path.resolve(process.cwd(), this.target));
  }

  // 檔案入口,在create.js中 執行generator.create();
  async create() {
    const repo = await this.getRepo();
    console.log('使用者選擇了', repo);

    // 下載下傳使用者選擇的項目模闆
    await this.download(repo);

    // 下載下傳完成後,擷取項目裡的package.json
    // 将使用者建立項目的填寫的資訊(項目名稱、作者名字、描述),寫入到package.json中
    let targetPath = path.resolve(process.cwd(), this.target);

    let jsonPath = path.join(targetPath, 'package.json');

    if (fs.existsSync(jsonPath)) {
      // 讀取已下載下傳模闆中package.json的内容
      const data = fs.readFileSync(jsonPath).toString();
      let json = JSON.parse(data);
      json.name = this.name;
      // 讓使用者輸入的内容 替換到 package.json中對應的字段
      Object.keys(this.ask).forEach((item) => {
        json[item] = this.ask[item];
      });

      //修改項目檔案夾中 package.json 檔案
      fs.writeFileSync(jsonPath, JSON.stringify(json, null, '\t'), 'utf-8');
    }
  }
}

module.exports = Generator;
           

建立

http.js

用來發送接口,擷取 git 上的模闆清單

bin/http.js

// 引入axios
const axios = require('axios');

axios.interceptors.response.use((res) => {
  return res.data;
});

// 擷取git上的項目清單
async function getRepolist() {
  return axios.get('https://api.github.com/orgs/yuan-cli/repos');
}

module.exports = {
  getRepolist
};
           

最終的目錄結構

10w字!前端知識體系+大廠面試筆記(工程化篇)

list.jpg

腳手架效果示範

10w字!前端知識體系+大廠面試筆記(工程化篇)

text8.gif

腳手架釋出到 npm 庫

完善 package.json

1)配置

main

屬性,指定包的入口 

"main": "./bin/www.js"

2)增加

files

屬性,files 用來描述當把 npm 包,作為依賴包安裝的檔案清單。當 npm 包釋出時,files 指定的檔案會被推送到 npm 伺服器中

3)增加

description

keywords

等描述字段

{
  "name": "my-2022-cli",
  "version": "1.1.0",
  "description": "一個mini版的腳手架",
  "main": "./bin/www.js",
  "bin": "./bin/www.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "files": [
    "bin"
  ],
  "keywords": [
    "my-yuan-cli",
    "自定義腳手架"
  ],
  "author": "海闊天空",
  "license": "ISC",
  "dependencies": {
    "axios": "^0.24.0",
    "commander": "^8.3.0",
    "download-git-repo": "^3.0.2",
    "fs-extra": "^10.0.0",
    "inquirer": "^8.2.0",
    "ora": "^5.4.1",
    "util": "^0.12.4"
  }
}
           

增加 README.md 說明文檔

## my-2022-cli

一個 mini 版的自定義腳手架

### 安裝

npm install my-2022-cli -g

### 使用說明

1)通過 my-2022-cli create appName 建立項目

2)author? 輸入項目作者

3)description? 輸入項目描述

4)選擇項目子產品 appDemo or pcDemo

5)安裝選擇的模闆

### 示範示例

![Image text](https://wx1.sinaimg.cn/mw2000/927e36bfgy1h69k6ee9z1g20rs0jfwit.gif)
           

釋出成功後,在 npm 網站搜尋

my-2022-cli

10w字!前端知識體系+大廠面試筆記(工程化篇)

my-2022-cli.jpg

自定義腳手架 github 源碼位址[21]

my-2022-cli[22]

性能分析與優化

一百使用者與一百萬使用者的網站有着本質差別

随着使用者的增長,任何細節的優化都變得更為重要,網站的性能差異直接影響着使用者的體驗

試想,如果我們在網上購物,商城頁面好幾秒才打開,或圖檔加載不出來,購物的欲望瞬間消減,擡手就去其他競品平台了

我曾經負責過幾個大型項目的整體性能優化,盡量從實戰的角度聊一聊自己所了解的性能問題

性能分析工具

好比去醫院看病一樣,得了什麼病,通過檢測化驗後才知道。網站也是一樣,需要借助性能分析工具來檢測

Lighthouse 工具

Lighthouse

是 Chrome 自帶的性能分析工具,它能夠生成一個有關頁面性能的報告

通過報告我們可以知道需要采取哪些措施,來改進應用的性能和體驗

并且 Lighthouse 可以對頁面多方面的效果名額進行評測,并給出最佳實踐的建議,以幫助開發者改進網站的品質

Lighthouse 拿到頁面的“病情”報告

通過 Lighthouse 拿到網站的整體分析報告,通過報告來診斷“病情”

這裡以https://juejin.cn[23]網站為例, 打開 Chrome 浏覽器控制台,選擇

Lighthouse

選項,點選

Generate report

10w字!前端知識體系+大廠面試筆記(工程化篇)

Lighthouse.jpg

Lighthouse 能夠生成一份該網站的報告,比如下圖:

10w字!前端知識體系+大廠面試筆記(工程化篇)

performance.jpg

這裡重點關注

Performance性能評分

性能評分的分值區間是 0 到 100,如果出現 0 分,通常是在運作 Lighthouse 時發生了錯誤,滿分 100 分代表了網站已經達到了 98 分位值的資料,而 50 分則對應 75 分位值的資料

小夥伴看看自己開發的項目得分是多少,處于什麼樣的水準

Lighthouse 給出 Opportunities 優化建議

Lighthouse 會針對目前網站,給出一些

Opportunities

優化建議

Opportunities 指的是優化機會,它提供了詳細的建議和文檔,來解釋低分的原因,幫助我們具體進行實作和改進

10w字!前端知識體系+大廠面試筆記(工程化篇)

opportunity.jpg

舉一個我曾開發過的一個項目,以下是

Opportunities 給出優化建議清單

問題 建議
Remove unused JavaScript 去掉無用 js 代碼
Preload key requests 首頁資源 preload 預加載
Remove unused CSS 去掉無用 css 代碼
Serve images in next-gen formats 使用新的圖檔格式,比如 webp 相對 png jpg 格式體積更小
Efficiently encode images 比如壓縮圖檔大小
Preconnect to required origins 使用 preconnect or dns-prefetch DNS 預解析

Lighthouse 給出 Diagnostics 診斷問題清單

Diagnostics

 指的是現在存在的問題,為進一步改善性能的驗證和調整給出了指導

10w字!前端知識體系+大廠面試筆記(工程化篇)

DiagNo.jpg

Diagnostics 診斷問題清單

問題 影響
A long cache lifetime can speed up repeat visits to your page 這些資源需要提供長的緩存期,現發現圖檔都是用的協商緩存,顯然不合理
Image elements do not have explicit width and height 給圖檔設定具體的寬高,減少 cls 的值
Avoid enormous network payloads 資源太大增加網絡負載
Minimize main-thread work 最小化主線程 這裡會執行解析 Html、樣式計算、布局、繪制、合成等動作
Reduce JavaScript execution time 減少非必要 js 資源的加載,減少必要 js 資源的大小
Avoid large layout shifts 避免大的布局變化,從中可以看到影響布局變化最大的元素

這些Opportunities建議和Diagnostics診斷問題是非常具體且有效的(親測),開發者可以根據這些建議,一條條去修改或優化

Lighthouse 列出 Performance 各名額得分

Performance 列出了

FCP、SP、LCP、TTI、TBI、CLS

 六個名額的用時和得分情況

下文會聊一聊這些名額的用法與作用

10w字!前端知識體系+大廠面試筆記(工程化篇)

performance1.jpg

性能測評工具 lighthouse 的使用[24]

Web-vitals 官方标準

web-vitals[25]是 Google 給出的定義是 一個良好網站的基本名額

過去要衡量一個網站的好壞,需要使用的名額太多了,現在我們可以将重點聚焦于 Web Vitals 名額的表現即可

官方名額标準

名額 作用 标準
FCP(First Contentful Paint) 首次内容繪制時間 标準 ≤1s
LCP(Largest Contentful Paint) 最大内容繪制時間 标準 ≤2 秒
FID(first input delay) 首次輸入延遲,标準是使用者觸發後,到浏覽器響應時間 标準 ≤100ms
CLS(Cumulative Layout Shift) 累積布局偏移 标準 ≤0.1
TTFB(Time to First Byte) 頁面送出請求,到接收第一個位元組所花費的毫秒數(首位元組時間) 标準<= 100 毫秒

我們将 Lighthouse 中 Performance 列出的名額表現,與官方名額标準做對比,可以發現頁面哪些名額超出了範圍

Performance 工具

通過 Lighthouse 我們知道了頁面整體的性能得分,但是頁面打開慢或者卡頓的瓶頸在哪裡?

具體是

加載資源慢

dom渲染慢

、還是

js執行慢

呢?

chrome 浏覽器提供的

performance

是常用來檢視網頁性能的工具,通過該工具,我們可以知道頁面在浏覽器運作時的性能表現

Performance 尋找性能瓶頸

打開 Chrome 浏覽器控制台,選擇

Performance

選項,點選左側

reload圖示

10w字!前端知識體系+大廠面試筆記(工程化篇)

perfromance1.gif

Performance 面闆可以記錄和分析頁面在運作時的所有活動,大緻分為以下 4 個區域

10w字!前端知識體系+大廠面試筆記(工程化篇)

performance2.png

Performance 各區域功能介紹

1)FPS

FPS(Frames Per Second),表示每秒傳輸幀數,是用來分析

頁面是否卡頓

的一個主要性能名額

如下圖所示,

綠色的長條越高,說明FPS越高,使用者體驗越好

如果發現了

一個紅色的長條,那麼就說明這些幀存在嚴重問題

,可能會造成頁面卡頓

10w字!前端知識體系+大廠面試筆記(工程化篇)

FPS.png

2)NET

NET 記錄資源的等待、下載下傳、執行時間,每條彩色橫杠表示一種資源

橫杠越長,檢索資源所需的時間越長。每個橫杠的淺色部分表示等待時間(從請求資源到第一個位元組下載下傳完成的時間)

Network 的顔色說明:白色表示等待的顔色、淺黃色表示請求的時間、深黃色表示下載下傳的時間

在這裡,我們可以看到所有資源的加載過程,有兩個地方重點關注:

1)資源等待的時間是否過長(标準 ≤100ms)

2)資源檔案體積是否過大,造成加載很慢(就要考慮如何拆分該資源)

10w字!前端知識體系+大廠面試筆記(工程化篇)

net.png

3)火焰圖

火焰圖(Flame Chart)用來可視化 CPU 堆棧資訊記錄

10w字!前端知識體系+大廠面試筆記(工程化篇)

1)Network: 表示加載了哪些資源

2)Frames:表示每幅幀的運作情況

3)Timings: 記錄頁面中關鍵名額的時間

4)

Main:表示主線程(重點,下文會詳細介紹)

5)GPU:表示 GPU 占用情況

4)統計彙總

Summary: 表示各名額時間占用統計報表

1)Loading: 加載時間
2)Scripting: js計算時間
3)Rendering: 渲染時間
4)Painting: 繪制時間
5)Other: 其他時間
6)Idle: 浏覽器閑置時間
           
10w字!前端知識體系+大廠面試筆記(工程化篇)

sum.jpg

Performance Main 性能瓶頸的突破口

Main 表示主線程,主要負責

1)Javascript 的計算與執行

2)CSS 樣式計算

3)Layout 布局計算

4)将頁面元素繪制成位圖(paint),也就是光栅化(Raster)

展開 Main,可以發現很多

紅色三角(long task)

,這些執行時間超過 

50ms

就屬于

長任務

,會造成頁面卡頓,嚴重時會造成頁面卡死

10w字!前端知識體系+大廠面試筆記(工程化篇)

main.jpg

展開其中一個紅色三角,Devtools 在

Summary

面闆裡展示了更多關于這個事件的資訊

10w字!前端知識體系+大廠面試筆記(工程化篇)

app.jpg

在 summary 面闆裡點選

app.js

連結,Devtools 可以跳轉到需要優化的代碼處

10w字!前端知識體系+大廠面試筆記(工程化篇)

source.jpg

下面我們需要結合自己的代碼邏輯,去判斷這塊代碼為什麼執行時間超長?

如何去解決或優化這些

long task

,進而解決去頁面的性能瓶頸

全新 Chrome Devtool Performance 使用指南[26]

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

Chrome Devtool — Performance[28]

性能監控

項目釋出生産後,使用者使用時的性能如何,頁面整體的打開速度是多少、白屏時間多少,FP、FCP、LCP、FID、CLS 等名額,要設定多大的閥值呢,才能滿足

TP50、TP90、TP99

的要求呢?

TP 名額: 總次數 * 名額數 = 對應 TP 名額的值。

設定每個名額的閥值,比如 FP 名額,設定閥值為 1s,要求 Tp95,即 95%的 FP 名額,要在 1s 以下,剩餘 5%的名額超過 1s

TP50 相對較低,TP90 則比較高,TP99,TP999 則對性能要求很高

這裡就需要性能監控,采集到使用者的頁面資料

性能名額的計算

常用的兩種方式:

方式一:通過 web-vitals 官方庫進行計算

import {onLCP, onFID, onCLS} from 'web-vitals';

onCLS(console.log);
onFID(console.log);
onLCP(console.log);
           

方式二:通過

performance api

進行計算

下面聊一下 performance api 來計算各種名額

打開任意網頁,在控制台中輸入 performance 回車,可以看到一系列的參數,

performance.timing

重點看下

performance.timing

,記錄了頁面的各個關鍵時間點

時間 作用

navigationStart

(可以了解為該頁面的起始時間)同一個浏覽器上下文的上一個文檔解除安裝結束時的時間戳,如果沒有上一個文檔,這個值會和 fetchStart 相同
unloadEventStart unload 事件抛出時的時間戳,如果沒有上一個文檔,這個值會是 0
unloadEventEnd unload 事件處理完成的時間戳,如果沒有上一個文檔,這個值會是 0
redirectStart 第一個 HTTP 重定向開始時的時間戳,沒有重定向或者重定向中的不同源,這個值會是 0
redirectEnd 最後一個 HTTP 重定向開始時的時間戳,沒有重定向或者重定向中的不同源,這個值會是 0
fetchStart 浏覽器準備好使用 HTTP 請求來擷取文檔的時間戳。發送在檢查緩存之前
domainLookupStart 域名查詢開始的時間戳,如果使用了持續連接配接或者緩存,則與 fetchStart 一緻
domainLookupEnd 域名查詢結束的時間戳,如果使用了持續連接配接或者緩存,則與 fetchStart 一緻
connectStart HTTP 請求開始向伺服器發送時的時間戳,如果使用了持續連接配接,則與 fetchStart 一緻
connectEnd 浏覽器與伺服器之間連接配接建立(所有握手和認證過程全部結束)的時間戳,如果使用了持續連接配接,則與 fetchStart 一緻
secureConnectionStart 浏覽器與伺服器開始安全連接配接握手時的時間戳,如果目前網頁不需要安全連接配接,這個值會是 0

requestStart

浏覽器向伺服器發出 HTTP 請求的時間戳

responseStart

浏覽器從伺服器收到(或從本地緩存讀取)第一個位元組時的時間戳
responseEnd 浏覽器從伺服器收到(或從本地緩存讀取)最後一個位元組時(如果在此之前 HTTP 連接配接已經關閉,則傳回關閉時)的時間戳

domLoading

目前網頁 DOM 結構開始解析時的時間戳
domInteractive 目前網頁 DOM 結構解析完成,開始加載内嵌資源時的時間戳
domContentLoadedEventStart 需要被執行的腳本已經被解析的時間戳
domContentLoadedEventEnd 需要立即執行的腳本已經被執行的時間戳

domComplete

目前文檔解析完成的時間戳
loadEventStart load 事件被發送時的時間戳,如果這個事件還未被發送,它的值将會是 0

loadEventEnd

load 事件結束時的時間戳,如果這個事件還未被發送,它的值将會是 0

白屏時間 FP

白屏時間 FP(First Paint)指的是從使用者輸入 url 的時刻開始計算,一直到頁面有内容展示出來的時間節點,

标準≤2s

這個過程包括 dns 查詢、建立 tcp 連接配接、發送 http 請求、傳回 html 文檔、html 文檔解析

const entryHandler = (list) => {
    for (const entry of list.getEntries()) {
        if (entry.name === 'first-paint') {
            observer.disconnect()}
       // 其中startTime 就是白屏時間
       let FP = entry.startTime)
    }
}
const observer = new PerformanceObserver(entryHandler)
// buffered 屬性表示是否觀察緩存資料,也就是說觀察代碼添加時機比事件觸發時機晚也沒關系。
observer.observe({ type: 'paint', buffered: true })
           

首次内容繪制時間 FCP

FCP(First Contentful Paint) 表示頁面任一部分渲染完成的時間,

标準≤1s

// 計算方式:
const entryHandler = (list) => {
    for (const entry of list.getEntries()) {
        if (entry.name === 'first-contentful-paint') {
            observer.disconnect()
        }
        // 計算首次内容繪制時間
       let FCP = entry.startTime
    }
}
const observer = new PerformanceObserver(entryHandler)
observer.observe({ type: 'paint', buffered: true })
           

最大内容繪制時間 LCP

LCP(Largest Contentful Paint)表示最大内容繪制時間,

标準≤2 秒

// 計算方式:
const entryHandler = (list) => {
    if (observer) {
        observer.disconnect()
    }
    for (const entry of list.getEntries()) {
        // 最大内容繪制時間
       let LCP = entry.startTime
    }
}
const observer = new PerformanceObserver(entryHandler)
observer.observe({ type: 'largest-contentful-paint', buffered: true })
           

累積布局偏移值 CLS

CLS(Cumulative Layout Shift) 表示累積布局偏移,

标準≤0.1

// cls為累積布局偏移值
let cls = 0;
new PerformanceObserver((entryList) => {
  for (const entry of entryList.getEntries()) {
    if (!entry.hadRecentInput) {
      cls += entry.value;
    }
  }
}).observe({type: 'layout-shift', buffered: true});
           

首位元組時間 TTFB

平常所說的

TTFB

,預設指導航請求的

TTFB

導航請求:在浏覽器切換頁面時建立,從導航開始到該請求傳回 HTML

window.onload = function () {
  // 首位元組時間
  let TTFB = responseStart - navigationStart;
};
           

首次輸入延遲 FID

FID(first input delay)首次輸入延遲,标準是使用者觸發後,浏覽器的響應時間, 

标準≤100ms

new PerformanceObserver((entryList) => {
  for (const entry of entryList.getEntries()) {
    // 計算首次輸入延遲時間
    const FID = entry.processingStart - entry.startTime;
  }
}).observe({ type: 'first-input', buffered: true });
           

FID 推薦使用 web-vitals 庫,因為官方相容了很多場景

首頁加載時間

window.onload = function () {
  // 首頁加載時間
  // domComplete 是document的readyState = complete(完成)的狀态
  let firstScreenTime = performance.timing.domComplete - performance.timing.navigationStart;
};
           

首屏加載時間

首屏加載時間和首頁加載時間不一樣,首屏指的是使用者看到螢幕内頁面渲染完成的時間

比如首頁很長需要好幾屏展示,這種情況下螢幕以外的元素不考慮在内

計算首屏加載時間流程

1)利用

MutationObserver

監聽

document

對象,每當 dom 變化時觸發該事件

2)判斷監聽的 dom 是否在首屏内,如果在首屏内,将該 dom 放到指定的數組中,記錄下目前 dom 變化的時間點

3)在 MutationObserver 的 callback 函數中,通過防抖函數,監聽

document.readyState

狀态的變化

4)當

document.readyState === 'complete'

,停止定時器和 取消對 document 的監聽

5)周遊存放 dom 的數組,找出最後變化節點的時間,用該時間點減去

performance.timing.navigationStart

 得出首屏的加載時間

定義 performance.js

// firstScreenPaint為首屏加載時間的變量
let firstScreenPaint = 0;
// 頁面是否渲染完成
let isOnLoaded = false;
let timer;
let observer;

// 定時器循環監聽dom的變化,當document.readyState === 'complete'時,停止監聽
function checkDOMChange(callback) {
  cancelAnimationFrame(timer);
  timer = requestAnimationFrame(() => {
    if (document.readyState === 'complete') {
      isOnLoaded = true;
    }
    if (isOnLoaded) {
      // 取消監聽
      observer && observer.disconnect();

      // document.readyState === 'complete'時,計算首屏渲染時間
      firstScreenPaint = getRenderTime();
      entries = null;

      // 執行使用者傳入的callback函數
      callback && callback(firstScreenPaint);
    } else {
      checkDOMChange();
    }
  });
}
function getRenderTime() {
  let startTime = 0;
  entries.forEach((entry) => {
    if (entry.startTime > startTime) {
      startTime = entry.startTime;
    }
  });
  // performance.timing.navigationStart 頁面的起始時間
  return startTime - performance.timing.navigationStart;
}
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
// dom 對象是否在螢幕内
function isInScreen(dom) {
  const rectInfo = dom.getBoundingClientRect();
  if (rectInfo.left < viewportWidth && rectInfo.top < viewportHeight) {
    return true;
  }
  return false;
}
let entries = [];

// 外部通過callback 拿到首屏加載時間
export default function observeFirstScreenPaint(callback) {
  const ignoreDOMList = ['STYLE', 'SCRIPT', 'LINK'];
  observer = new window.MutationObserver((mutationList) => {
    checkDOMChange(callback);
    const entry = { children: [] };
    for (const mutation of mutationList) {
      if (mutation.addedNodes.length && isInScreen(mutation.target)) {
        for (const node of mutation.addedNodes) {
          // 忽略掉以上标簽的變化
          if (node.nodeType === 1 && !ignoreDOMList.includes(node.tagName) && isInScreen(node)) {
            entry.children.push(node);
          }
        }
      }
    }

    if (entry.children.length) {
      entries.push(entry);
      entry.startTime = new Date().getTime();
    }
  });
  observer.observe(document, {
    childList: true, // 監聽添加或删除子節點
    subtree: true, // 監聽整個子樹
    characterData: true, // 監聽元素的文本是否變化
    attributes: true // 監聽元素的屬性是否變化
  });
}
           

外部引入使用

import observeFirstScreenPaint from './performance';

// 通過回調函數,拿到首屏加載時間
observeFirstScreenPaint((data) => {
  console.log(data, '首屏加載時間');
});
           

DOM 渲染時間和 window.onload 時間

DOM 的渲染的時間和 window.onload 執行的時間不是一回事

DOM 渲染的時間

DOM渲染的時間 =  performance.timing.domComplete - performance.timing.domLoading
           

window.onload 要晚于 DOM 的渲染,window.onload 是頁面中所有的資源都加載後才執行(包括圖檔的加載)

window.onload 的時間

window.onload的時間 =  performance.timing.loadEventEnd
           

計算資源的緩存命中率

緩存命中率:從緩存中得到資料的請求數與所有請求數的比率

理想狀态是緩存命中率越高越好,緩存命中率越高說明網站的緩存政策越有效,使用者打開頁面的速度也會相應提高

如何判斷該資源是否命中緩存?

1)通過

performance.getEntries()

找到所有資源的資訊

2)在這些資源對象中有一個

transferSize

 字段,它表示擷取資源的大小,包括響應頭字段和響應資料的大小

3)如果這個值為 0,說明是從緩存中直接讀取的(強制緩存)

4)如果這個值不為 0,但是

encodedBodySize

 字段為 0,說明它走的是協商緩存(

encodedBodySize 表示請求響應資料 body 的大小

function isCache(entry) {
  // 直接從緩存讀取或 304
  return entry.transferSize === 0 || (entry.transferSize !== 0 && entry.encodedBodySize === 0);
}
           

所有命中緩存的資料 / 總資料

 就能得出

緩存命中率

性能資料上報

上報方式

一般使用

圖檔打點

的方式,通過動态建立 img 标簽的方式,new 出像素為

1x1 px

gif Image

(gif 體積最小)對象就能發起請求,可以跨域、不需要等待伺服器傳回資料

上報時機

可以利用

requestIdleCallback

,浏覽器空閑的時候上報,好處是:不阻塞其他流程的進行

如果浏覽器不支援該

requestIdleCallback

,就使用

setTimeout

上報

// 優先使用requestIdleCallback
if (window.requestIdleCallback) {
  window.requestIdleCallback(
    () => {
      // 擷取浏覽器的剩餘空閑時間
      console.log(deadline.timeRemaining());
      report(data); // 上報資料
    },
    // timeout設定為1000,如果在1000ms内沒有執行該後調,在下次空閑時間時,callback會強制執行
    { timeout: 1000 }
  );
} else {
  setTimeout(() => {
    report(data); // 上報資料
  });
}
           

記憶體分析與優化

性能分析很火,但記憶體分析相比就低調很多了

舉一個我之前遇到的情況,客戶電腦組態低,打開公司開發的頁面,經常出現頁面崩潰

調查原因,就是因為頁面記憶體占用太大,客戶打開幾個頁面後,記憶體直接拉滿,也是經過這件事,我開始重視記憶體分析與優化

下面,聊一聊記憶體這塊有哪些知識

Memory 工具

Memory 工具,通過

記憶體快照

的方式,分析目前頁面的記憶體使用情況

Memory 工具使用流程

1)打開 chrome 浏覽器控制台,選擇

Memory

工具

2)點選左側

start按鈕

,重新整理頁面,開始錄制的

JS堆動态配置設定時間線

,會生成頁面加載過程記憶體變化的柱狀統計圖(藍色表示未回收,灰色表示已回收)

10w字!前端知識體系+大廠面試筆記(工程化篇)

memory.jpg

Memory 工具中的關鍵項

關鍵項

Constructor:對象的類名;

Distance:對象到根的引用層級;

Objects Count:對象的數量;

Shallow Size: 對象本身占用的記憶體,不包括引用的對象所占記憶體;

Retained Size: 對象所占總記憶體,包含引用的其他對象所占記憶體;

Retainers:對象的引用層級關系

通過一段測試代碼來了解 Memory 工具各關鍵性的關系

// 測試代碼
class Jane {}
class Tom {
  constructor () { this.jane = new Jane();}
}
Array(1000000).fill('').map(() => new Tom())
           
10w字!前端知識體系+大廠面試筆記(工程化篇)

cpudemo.jpg

shallow size 和 retained size 的差別,以用紅框裡的 

Tom

 和 

Jane

 更直覺的展示

Tom 的 shallow 占了 32M,retained 占用了 56M,這是因為 retained 包括了引用的指針對應的記憶體大小,即 

tom.jane

 所占用的記憶體

是以 Tom 的 retained 總和比 shallow 多出來的 24M,正好跟 Jane 占用的 24M 相同

retained size 可以了解為當回收掉該對象時可以釋放的記憶體大小,在記憶體調優中具有重要參考意義

記憶體分析的關鍵點

找到記憶體最高的節點,分析這些時刻執行了哪些代碼,發生了什麼操作,盡可能去優化它們

1)從柱狀圖中找到最高的點,重點分析該時間内造成記憶體變大的原因

2)按照

Retainers size

(總記憶體大小)排序,點選展開記憶體最高的哪一項,點選展開構造函數,可以看到所有構造函數相關的對象執行個體

3)選中構造函數,底部會顯示對應源碼檔案,點選源碼檔案,可以跳轉到具體的代碼,這樣我們就知道是哪裡的代碼造成記憶體過大

4)結合具體的代碼邏輯,來判斷這塊記憶體變大的原因,重點是如何去優化它們,降低記憶體的使用大小

10w字!前端知識體系+大廠面試筆記(工程化篇)

retainedSize.jpg

點選

keyghost.js

可以跳轉到具體的源碼

10w字!前端知識體系+大廠面試筆記(工程化篇)

localkey.png

記憶體洩露的情況

1)意外的全局變量, 挂載到 window 上全局變量

2)遺忘的定時器,定時器沒有清除

3)閉包不當的使用

記憶體分析總結

1)利用 Memory 工具,了解頁面整體的記憶體使用情況

2)通過 JS 堆動态配置設定時間線,找到記憶體最高的時刻

3)按照 Retainers size(總記憶體大小)排序,點選展開記憶體最高的前幾項,分析由于哪個函數操作導緻了記憶體過大,甚至是記憶體洩露

4)結合具體的代碼,去解決或優化記憶體變大的情況

chrome 記憶體洩露(一)、記憶體洩漏分析工具[29]

chrome 記憶體洩露(二)、記憶體洩漏執行個體[30]

JavaScript 進階-常見記憶體洩露及如何避免[31]

項目優化總結

優化的本質:響應更快,展示更快

更詳細的說,是指在使用者輸入 url,到頁面完整展示出來的過程中,通過各種優化政策和方法,讓頁面加載更快;在使用者使用過程中,讓使用者的操作響應更及時,有更好的使用者體驗

經典:雅虎軍規

很多前端優化準則都是圍繞着這個展開

10w字!前端知識體系+大廠面試筆記(工程化篇)

雅虎35條軍規.jpg

優化建議

結合我曾經負責優化的項目實踐,在下面總結了一些經驗與方法,提供給大家參考

1、分析打包後的檔案

可以使用webpack-bundle-analyzer[32]插件(vue 項目可以使用--report)生成資源分析圖

我們要清楚的知道項目中使用了哪些三方依賴,以及依賴的作用。特别對于體積大的依賴,分析是否能優化

比如:元件庫如

elementUI

的按需引入、

Swiper輪播圖

元件打包後的體積約 200k,看是否能替換成體積更小的插件、

momentjs

去掉無用的語言包等

10w字!前端知識體系+大廠面試筆記(工程化篇)

vendors.png

2、合理處理公共資源

如果項目支援 CDN,可以配置

externals

,将

Vue、Vue-router、Vuex、echarts

等公共資源,通過 CDN 的方式引入,不打到項目裡邊

如果項目不支援 CDN,可以使用

DllPlugin

動态連結庫,将業務代碼和公共資源代碼相分離,公共資源單獨打包,給這些公共資源設定強緩存(公共資源基本不會變),這樣以後可以隻打包業務代碼,提升打包速度

3、首屏必要資源 preload 預加載 和 DNS 預解析

preload 預加載

<link rel="preload" href="/path/style.css" target="_blank" rel="external nofollow" as="style">

<link rel="preload" href="/path/home.js" target="_blank" rel="external nofollow" as="script">

preload 預加載是告訴浏覽器頁面必定需要的資源,浏覽器會優先加載這些資源;使用 link 标簽建立(vue 項目打包後,會将首頁所用到的資源都加上 preload)

注意:preload 隻是預加載資源,但不會執行,還需要引入具體的檔案後才會執行 

<script src='/path/home.js'>

DNS 預解析

DNS Prefetch

 是一種 DNS 預解析技術,當你浏覽網頁時,浏覽器會在加載網頁時,對網頁中的域名進行解析緩存

這樣在你單擊目前網頁中的連接配接時就無需進行

DNS

 的解析,減少使用者等待時間,提高使用者體驗

使用

dns-prefetch

,如

<link rel="dns-prefetch" href="//img1.taobao.com" target="_blank" rel="external nofollow" >

很多大型的網站,都會用

N

 個

CDN

 域名來做圖檔、靜态檔案等資源通路。解析單個域名同樣的地點加上高并發難免有點堵塞,通過多個 CDN 域名來分擔高并發下的堵塞

4、首屏不必要資源延遲加載

方式一: defer 或 async

使用 script 标簽的

defer或async

屬性,這兩種方式都是異步加載 js,不會阻塞 DOM 的渲染

async 是無順序的加載,而 defer 是有順序的加載

1)使用 defer 可以用來控制 js 檔案的加載順序

比如 jq 和 Bootstrap,因為 Bootstrap 中的 js 插件依賴于 jqery,是以必須先引入 jQuery,再引入 Bootstrap js 檔案

2)如果你的腳本并不關心頁面中的 DOM 元素(文檔是否解析完畢),并且也不會産生其他腳本需要的資料,可以使用 async,如添加統計、埋點等資源

方式二:依賴動态引入

項目依賴的資源,推薦在各自的頁面中動态引入,不要全部都放到 index.html 中

比如

echart.js

,隻有 A 頁面使用,可以在 A 頁面的鈎子函數中動态加載,在

onload事件

中進行 echart 初始化

資源動态加載的代碼示例

// url 要加載的資源
// isMustLoad 是否強制加載
cont asyncLoadJs = (url, isMustLoad = false) => {
  return new Promise((resolve, reject) => {
    if (!isMustLoad) {
      let srcArr = document.getElementsByTagName("script");
      let hasLoaded = false;
      let aTemp = [];
      for (let i = 0; i < srcArr.length; i++) {
        // 判斷目前js是否加載上
        if (srcArr[i].src) {
          aTemp.push(srcArr[i].src);
        }
      }
      hasLoaded = aTemp.indexOf(url) > -1;
      if (hasLoaded) {
        resolve();
        return;
      }
    }

    let script = document.createElement("script");
    script.type = "text/javascript";
    script.src = url;
    document.body.appendChild(script);
    // 資源加載成功的回調
    script.onload = () => {
      resolve();
    };
    script.onerror = () => {
      // reject();
    };
  });
}
           

方式三:import()

使用

import() 動态加載路由群組件

,對資源進行拆分,隻有使用的時候才進行動态加載

// 路由懶加載
const Home = () => import(/* webpackChunkName: "home" */ '../views/home/index.vue')
const routes = [ { path: '/', name: 'home', component: Home} ]

// 元件懶加載
// 在visible屬性為true時,動态去加載demoComponent元件
<demoComponent v-if="visible == true" />

components: {
    demoComponent: () => import(/* webpackChunkName: "demoComponent" */ './demoComponent.vue')
  },
           

5、合理利用緩存

html 資源設定協商緩存,其他 js、css、圖檔等資源設定強緩存

當使用者再次打開頁面時,html 先和伺服器校驗,如果該資源未變化,伺服器傳回 304,直接使用緩存的檔案;若傳回 200,則傳回最新的 html 資源

6、網絡方面的優化

1)開啟伺服器 Gzip 壓縮,減少請求内容的體積,對文本類能壓縮 60%以上

2)使用 HTTP2,接口解析速度快、多路複用、首部壓縮等

3)減少 HTTP 請求,使用 url-loader,limit 限制圖檔大小,小圖檔轉 base64

7、代碼層面的優化

1)前端長清單渲染優化,分頁 + 虛拟清單,長清單渲染的性能效率與使用者體驗成正比

2)圖檔的懶加載、

圖檔的動态裁剪

特點是手機端項目,圖檔幾乎不需要原圖,使用七牛或阿裡雲的動态裁剪功能,可以将原本

幾M

的大小裁剪成

幾k

3)動畫的優化,動畫可以使用絕對定位,讓其脫離文檔流,修改動畫不造成主界面的影響

使用 GPU 硬體加速包括:

transform 不為none、opacity、filter、will-change

4)函數的節流和防抖,減少接口的請求次數

5)使用骨架屏優化使用者等待體驗,可以根據不同路由配置不同的骨架

vue 項目推薦使用

vue-skeleton-webpack-plugin

,骨架屏原理将

<div id="app"></div> 

中的内容替換掉

6)大資料的渲染,如果資料不會變化,vue 項目可以使用

Object.freeze()

Object.freeze()方法可以當機一個對象,Vue 正常情況下,會将 data 中定義的是對象變成響應式,但如果判斷對象的自身屬性不可修改,就直接傳回改對象,省去了遞歸周遊對象的時間與記憶體消耗

7)定時器和綁定的事件,在頁面銷毀時解除安裝

8、webpack 優化

提升建構速度或優化代碼體積,推薦以下兩篇文章

Webpack 優化——将你的建構效率提速翻倍[33]

帶你深度解鎖 Webpack 系列(優化篇)[34]

性能分析總結

1)先用 Lighthouse 得到目前頁面的性能得分,了解頁面的整體情況,重點關注 Opportunities 優化建議和 Diagnostics 診斷問題清單

2)通過 Performance 工具了解頁面加載的整個過程,分析到底是資源加載慢、dom 渲染慢、還是 js 執行慢,找到具體的性能瓶頸在哪裡,重點關注長任務(long task)

3)利用 Memory 工具,了解頁面整體的記憶體使用情況,通過 JS 堆動态配置設定時間線,找到記憶體最高的時刻。結合具體的代碼,去解決或優化記憶體變大的問題

前端監控

沒有監控的項目,就是在

“裸奔”

需要通過監控才能真正了解項目的整體情況,各項名額要通過大量的資料采集、資料分析,變得更有意義

監控的好處:事前預警和事後分析

事前預警:設定一個門檻值,當監控的資料達到門檻值時,通過各種管道通知管理者和開發,提前避免可能會造成的當機或崩潰

事後分析:通過監控日志檔案,分析故障原因和故障發生點。進而做出修改,防止這種情況再次發生

我們可以使用市面上現有的工具,也可以自建 sentry 監控,關鍵在于通過這些資料,能看到這些

冰冷資料背後的故事

元件庫

元件庫是開發項目時必備的工具,因為現在各個大廠提供的元件庫都太好用了,反而讓大家輕視了元件庫的重要性

現在開發一個新項目,如果要求不能用現成的元件庫,我估計要瞬間懵逼,無從下手了,不知不覺中,我已經患上嚴重的

元件庫依賴症

如果讓我隻能說出一種,快速提升程式設計技能的方法,我一定會推薦去看看元件庫源碼

因為元件庫源碼中,都是最經典的案例,是集大成的傑作

相比于前端架構源碼的晦澀,元件庫源碼更容易上手,裡邊很多經典場景的寫法,我們都可以借鑒,然後運用到實際的項目中

比如最常用的彈框元件,裡邊的

流程控制、層級控制、元件間事件派發與廣播、遞歸查找元件

等設計,相當驚豔,真沒想到一個小小的彈框元件,也是内有乾坤

推薦一下我正在寫的ElementUI 源碼-打造自己的元件庫[35]文章,經典的東西永遠是經典

總結

工程化,是一個非常容易

出彩

的方向,是展現一個俠者内功是否深厚的視窗

文中大部分知識點,隻是

入門級教程

,也是我以後需要持續努力的方向

繼續閱讀