免费视频淫片aa毛片_日韩高清在线亚洲专区vr_日韩大片免费观看视频播放_亚洲欧美国产精品完整版

打開APP
userphoto
未登錄

開通VIP,暢享免費電子書等14項超值服

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

2022.10.21 江蘇

關注

作者主頁:

https://juejin.cn/user/2594503172831208

正文

工程化目的是為了提升團隊的開發(fā)效率、提高項目的質量

例如大家所熟悉的構建工具、性能分析與優(yōu)化、組件庫等知識,都屬于工程化的內容

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

文中大部分的內容,主要是以 代碼示例 + 分析總結 + 實踐操作 來講解的,始終圍繞實用可操作性來說明,爭取讓小伙伴們更容易理解和運用

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

工程化是一個優(yōu)秀工程師的必修課,也是一個重要的分水嶺

前端工程化導圖

前端工程化.png

構建工具

Webpack

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

之前看過很多關于 Webpack 的文章,總是感覺云里霧里,現(xiàn)在換一種方式,我們一起來解密它,嘗試打開這個盲盒

手寫一個 mini 版的 Webpack

別擔心

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

創(chuàng)建minipack.js
const fs = require('fs');
const path = require('path');
// babylon解析js語法,生產AST 語法樹
// ast將js代碼轉化為一種JSON數(shù)據(jù)結構
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參數(shù)為文件路徑, 讀取內容并提取它的依賴關系
function createAsset(filename) {
  const content = fs.readFileSync(filename, 'utf-8');

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

  // dependencies保存所依賴的模塊的相對路徑
  const dependencies = [];

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

  // 通過遞增計數(shù)器,為此模塊分配唯一標識符, 用于緩存已解析過的文件
  const id = ID++;
  // 該`presets`選項是一組規(guī)則,告訴`babel`如何傳輸我們的代碼.
  // 用`babel-preset-env`將代碼轉換為瀏覽器可以運行的東西.
  const { code } = transformFromAst(ast, null, {
    presets: ['env']
  });

  // 返回此模塊的相關信息
  return {
    id, // 文件id(唯一)
    filename, // 文件路徑
    dependencies, // 文件的依賴關系
    code // 文件的代碼
  };
}

// 我們將提取它的每一個依賴文件的依賴關系,循環(huán)下去:找到對應這個項目的`依賴圖`
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) => {
      // 通過將相對路徑與父資源目錄的路徑連接,將相對路徑轉變?yōu)榻^對路徑
      // 每個文件的絕對路徑是固定、唯一的
      const absolutePath = path.join(dirname, relativePath);
      // 遞歸解析其中所引入的其他資源
      const child = createAsset(absolutePath);
      asset.mapping[relativePath] = child.id;
      // 將`child`推入隊列, 通過遞歸實現(xiàn)了這樣它的依賴關系解析
      queue.push(child);
    });
  }

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

// 自定義實現(xiàn)了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);

// ?? 創(chuàng)建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)最終生成一個可以在瀏覽器加載執(zhí)行的 js 文件

創(chuàng)建測試目錄 example

在目錄下創(chuàng)建以下 4 個文件

1)創(chuàng)建入口文件entry.js

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

2)創(chuàng)建message.js

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

3)創(chuàng)建name.js

export const name = 'Webpack';

4)創(chuàng)建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) 執(zhí)行打包

運行node minipack.js,dist 目錄下生成 main.js

6) 瀏覽器打開 index.html

頁面上顯示hello Webpack!

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

分析打包生成的文件

分析dist/main.js

文件內容

1)文件里是一個立即執(zhí)行函數(shù)

2)該函數(shù)接收的參數(shù)是一個對象,該對象有 3 個屬性
0 代表entry.js;
1 代表message.js;
2 代表name.js

dist/main.js 代碼

// 文件里是一個立即執(zhí)行函數(shù)
(function(modules) {
  function require(id) {
    const [fn, mapping] = modules[id];
    function localRequire(name) {
      // ?? 第四步 跳轉到這里 此時mapping[name] = 1,繼續(xù)執(zhí)行require(1)
      // ?? 第六步 又跳轉到這里 此時mapping[name] = 2,繼續(xù)執(zhí)行require(2)
      return require(mapping[name]);
    }
    // 創(chuàng)建module對象
    const module = { exports: {} };
    // ?? 第二步 執(zhí)行fn
    fn(localRequire, module, module.exports);

    return module.exports;
  }
  // ?? 第一步 執(zhí)行require(0)
  require(0);
})({
  // 立即執(zhí)行函數(shù)的參數(shù)是一個對象,該對象有3個屬性
  // 0 代表entry.js;
  // 1 代表message.js
  // 2 代表name.js
  0: [
    function(require, module, exports) {
      'use strict';
      // ?? 第三步 跳轉到這里 繼續(xù)執(zhí)行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
      });
      // ?? 第五步 跳轉到這里 繼續(xù)執(zhí)行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');
    },
    {}
  ]
});

?? 分析文件的執(zhí)行過程

1)整體大致分為 10 步,第一步從require(0)開始執(zhí)行,調用內置的自定義 require 函數(shù),跳轉到第二步,執(zhí)行fn函數(shù)

2)執(zhí)行第三步require('./message.js'),繼續(xù)跳轉到第四步 require(mapping['./message.js']), 最終轉化為require(1)

3)繼續(xù)執(zhí)行require(1),獲取modules[1],也就是執(zhí)行message.js的內容

4)第五步require('./name.js'),最終轉化為require(2),執(zhí)行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引用節(jié)點

因為現(xiàn)在我們引入文件都是通過 import 的方式引入,所以找到了 import 節(jié)點,就找到了文件的依賴關系

4)同時每個模塊生成一個唯一的 id,并將解析過的模塊緩存起來,如果其他地方也引入該模塊,就無需重新解析,最后根據(jù)依賴關系生成依賴圖譜

5)遞歸遍歷所有依賴圖譜的模塊,組裝成一個個包含多個模塊的 Chunk(塊)

6)最后將生成的文件輸出到 output 的目錄中

熱更新原理

什么是 webpack 熱更新?

開發(fā)過程中,代碼發(fā)生變動后,webpack 會重新編譯,編譯后瀏覽器替換修改的模塊,局部更新,無需刷新整個頁面

好處:節(jié)省開發(fā)時間、提升開發(fā)體驗

熱更新原理

主要是通過websocket實現(xiàn),建立本地服務和瀏覽器的雙向通信。當代碼變化,重新編譯后,通知瀏覽器請求更新的模塊,替換原有的模塊

1) 通過webpack-dev-server開啟server服務,本地 server 啟動之后,再去啟動 websocket 服務,建立本地服務和瀏覽器的雙向通信

2) webpack 每次編譯后,會生成一個Hash值,Hash 代表每一次編譯的標識。本次輸出的 Hash 值會編譯新生成的文件標識,被作為下次熱更新的標識

3)webpack監(jiān)聽文件變化(主要是通過文件的生成時間判斷是否有變化),當文件變化后,重新編譯

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

5)瀏覽器拿到更新后的模塊后,用新模塊替換掉舊的模塊,從而實現(xiàn)了局部刷新

輕松理解 webpack 熱更新原理[2]
深入淺出 Webpack[3]
帶你深度解鎖 Webpack 系列(基礎篇)[4]

Plugin

作用:擴展 webpack 功能

工作原理

webpack 通過內部的事件流機制保證了插件的有序性,底層是利用發(fā)布訂閱模式,webpack 在運行過程中會廣播事件,插件只需要監(jiān)聽它所關心的事件,在特定的時機對資源做處理

手寫一個 Plugin 插件

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

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

1)在vue.config.js引入該插件

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

2)在configureWebpack的 plugins 列表中注冊該插件

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

3)執(zhí)行項目的打包命令
當項目打包成功后,會在控制臺輸出:打包已完成

Plugin 的組成部分

1)Plugin 的本質是一個 node 模塊,這個模塊導出一個 JavaScript 類

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

3)通過compiler獲取 webpack 內部的鉤子,獲取 webpack 打包過程中的各個階段

鉤子分為同步和異步的鉤子,異步鉤子必須執(zhí)行對應的回調

4)通過compilation操作 webpack 內部實例特定數(shù)據(jù)

5)功能完成后,執(zhí)行 webpack 提供的 cb 回調

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

鉤子類型調用時機
runAsyncSeriesHook在編譯器開始讀取記錄前執(zhí)行
compileSyncHook在一個新的 compilation 創(chuàng)建之前執(zhí)行
compilationSyncHook在一次 compilation 創(chuàng)建后執(zhí)行插件
makeAsyncParallelHook完成一次編譯之前執(zhí)行
emitAsyncSeriesHook在生成文件到 output 目錄之前執(zhí)行,回調參數(shù): compilation
afterEmitAsyncSeriesHook在生成文件到 output 目錄之后執(zhí)行
assetEmittedAsyncSeriesHook生成文件的時候執(zhí)行,提供訪問產出文件信息的入口,回調參數(shù):file,info
doneAsyncSeriesHook一次編譯完成后執(zhí)行,回調參數(shù):stats

常用的 Plugin 插件

插件名稱作用
html-webpack-plugin生成 html 文件,引入公共的 js 和 css 資源
webpack-bundle-analyzer對打包后的文件進行分析,生成資源分析圖
terser-webpack-plugin代碼壓縮,移除 console.log 打印等
HappyPack Plugin開啟多線程打包,提升打包速度
Dllplugin動態(tài)鏈接庫,將項目中依賴的三方模塊抽離出來,單獨打包
DllReferencePlugin配合 Dllplugin,通過 manifest.json 映射到相關的依賴上去
clean-webpack-plugin清理上一次項目生成的文件
vue-skeleton-webpack-pluginvue 項目實現(xiàn)骨架屏

揭秘 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模塊,該模塊導出一個函數(shù),函數(shù)接收source(源文件),返回處理后的source

loader 執(zhí)行順序

相同優(yōu)先級的 loader 鏈,執(zhí)行順序為:從右到左,從下到上

use: ['loader1', 'loader2', 'loader3'],執(zhí)行順序為 loader3 → loader2 → loader1

常用的 loader

名稱作用
style-loader用于將 css 編譯完成的樣式,掛載到頁面 style 標簽上
css-loader用于識別 .css 文件, 須配合 style-loader 共同使用
sass-loader/less-loadercss 預處理器
postcss-loader用于補充 css 樣式各種瀏覽器內核前綴
url-loader處理圖片類型資源,可以轉 base64
vue-loader用于編譯 .vue 文件
worker-loader通過內聯(lián) loader 的方式使用 web worker 功能
style-resources-loader全局引用對應的 css,避免頁面再分別引入

揭秘 webpack-loader[6]

Webpack5 模塊聯(lián)邦

webpack5 模塊聯(lián)邦(Module Federation) 使 JavaScript 應用,得以從另一個 JavaScript 應用中動態(tài)的加載代碼,實現(xiàn)共享依賴,用于前端的微服務化

比如項目A項目B,公用項目C組件,以往這種情況,可以將 C 組件發(fā)布到 npm 上,然后 A 和 B 再具體引入。當 C 組件發(fā)生變化后,需要重新發(fā)布到 npm 上,A 和 B 也需要重新下載安裝

使用模塊聯(lián)邦后,可以在遠程模塊的 Webpack 配置中,將 C 組件模塊暴露出去,項目 A 和項目 B 就可以遠程進行依賴引用。當 C 組件發(fā)生變化后,A 和 B 無需重新引用

模塊聯(lián)邦利用 webpack5 內置的ModuleFederationPlugin插件,實現(xiàn)了項目中間相互引用的按需熱插拔

Webpack ModuleFederationPlugin

重要參數(shù)說明

1)name 當前應用名稱,需要全局唯一

2)remotes 可以將其他項目的  name 映射到當前項目中

3)exposes 表示導出的模塊,只有在此申明的模塊才可以作為遠程依賴被使用

4)shared 是非常重要的參數(shù),制定了這個參數(shù),可以讓遠程加載的模塊對應依賴,改為使用本地項目的依賴,如 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 新特性 - 模塊聯(lián)邦》[7]
Webpack 5 模塊聯(lián)邦引發(fā)微前端的革命?[8]
Webpack 5 升級內容(二:模塊聯(lián)邦)[9]

Vite

Vite 被譽為下一代的構建工具

上手了幾個項目后,果然名不虛傳,熱更新速度真的是快的飛起!

Vite 原理

1)Vite 利用瀏覽器支持原生的es module模塊,開發(fā)時跳過打包的過程,提升編譯效率

2)當通過 import 加載資源時,瀏覽器會發(fā)出 HTTP 請求對應的文件,Vite攔截到該請求,返回對應的模塊文件

es module 簡單示例

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

1)當聲明一個 script 標簽類型為 module 時,瀏覽器將對其內部的 import 引用發(fā)起 HTTP 請求,獲取模塊內容

2)瀏覽器將發(fā)起一個對 HOST/a.js 的 HTTP 請求,獲取到內容之后再執(zhí)行

Vite 的限制

Vite 主要對應的場景是開發(fā)模式(生產模式是用 rollup 打包

Vite 熱更新速度

Vite 熱更新的速度不會隨著模塊增多而變慢

1)Webpack 的熱更新原理:一旦某個依賴(比如上面的 a.js)改變,就將這個依賴所處的 整個module 更新,并將新的 module 發(fā)送給瀏覽器重新執(zhí)行

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

2)Vite 的熱更新原理:如果 a.js 發(fā)生了改變,只會重新編譯這個文件 a,而其余文件都無需重新編譯

所以理論上 Vite 熱更新的速度不會隨著文件增加而變慢

手寫 vite

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

vite 的實現(xiàn)流程

1)通過koa開啟一個服務,獲取請求的靜態(tài)文件內容

2)通過es-module-lexer 解析 ast 拿到 import 的內容

3)判斷 import 導入模塊是否為三方模塊,是的話,返回node_module下的模塊, 如 import vue 返回 import './@modules/vue'

4)如果是.vue文件,vite 攔截對應的請求,讀取.vue 文件內容進行編譯,通過compileTemplate 編譯模板,將template轉化為render函數(shù)

5)通過 babel parse 對 js 進行編譯,最終返回編譯后的 js 文件

尤雨溪幾年前開發(fā)的“玩具 vite”[11]
Vite 原理淺析[12]

Babel

AST 抽象語法樹

這里先聊一下AST抽象語法樹,因為ASTbabel的核心

什么是 AST ?

AST是源代碼的抽象語法結構的樹狀表現(xiàn)形式

在 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 網(wǎng)站[13]在線輸入代碼查看

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

Babel 基本原理與作用

Babel 是一個 JS 編譯器,把我們的代碼轉成瀏覽器可以運行的代碼

作用

babel 主要用于將新版本的代碼轉換為向后兼容的 js 語法(Polyfill 方式),以便能夠運行在各版本的瀏覽器或其他環(huán)境中

基本原理

核心就是 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%',
   //  所有瀏覽器兼容到最后兩個版本根據(jù)CanIUse.com追蹤的版本
   'last 2 versions',
   // chrome 版本大于70
   'chrome >= 70'
   // 排除部分版本
   'not ie <= 8'
]

如何開發(fā)一個 babel 插件

Babel 插件的作用

Babel 插件擔負著編譯過程中的核心任務:轉換 AST

babel 插件的基本格式

1)一個函數(shù),參數(shù)是 babel,然后就是返回一個對象,key是visitor,然后里面的對象是一個箭頭函數(shù)

2)函數(shù)有兩個參數(shù),path表示路徑,state表示狀態(tài)

3)CallExpression就是我們要訪問的節(jié)點,path 參數(shù)表示當前節(jié)點的位置,包含的主要是當前節(jié)點(node)內容以及父節(jié)點(parent)內容

插件的簡單格式示例

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

一個最簡單的插件: 將const a 轉化為const b

創(chuàng)建 babelPluginAtoB.js

module.exports = function(babel) {
  let t = babel.types;
  return {
    visitor: {
      VariableDeclarator(path, state) {
        // VariableDeclarator 是要找的節(jié)點類型
        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 流 實現(xiàn)的前端自動化開發(fā)的工具

適用場景

在前端開發(fā)工作中有很多“重復工作”,比如批量將Scss文件編譯為CSS文件

這里主要聊一下,在開發(fā)的組件庫中如何使用 gulp

Gulp 在組件庫中的運用

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

打開packages/theme-chalk/gulpfile.js

該文件的作用是將 scss 文件編譯為 css 文件

'use strict';

// 引入gulp
// series創(chuàng)建任務列表,
// src創(chuàng)建一個流,讀取文件
// dest 創(chuàng)建一個對象寫入到文件系統(tǒng)的流
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創(chuàng)建任務列表
exports.build = series(compile, copyfont);

Gulp 給 elementUI 增加一鍵換膚功能

總體流程

1)使用css var()定義顏色變量

2)創(chuàng)建主題theme.css文件,存儲所有的顏色變量

3)使用gulptheme.css合并到base.css中,解決按需引入的情況

4)使用gulpindex.cssbase.css合并,解決全局引入的情況

步驟一:創(chuàng)建基礎顏色變量theme.css文件

theme.jpg

步驟二:修改packages/theme-chalk/src/common/var.scss文件

將該文件的中定義的 scss 變量,替換成 var()變量

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]

腳手架

腳手架是開發(fā)中經常會使用的工具,比如vue-clicreate-react-app等,這些腳手架可以通過簡單的命令,快速去搭建項目,讓我們更專注于項目的開發(fā)

隨著項目的增多、人員的擴展,大家開發(fā)的基礎組件和公共方法也越來越多,希望把這些積累添加到腳手架中,當成項目模板留存下來

這樣再創(chuàng)建項目時,就不用每次去其他項目中來回 copy

手寫一個 mini 版的腳手架

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

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

新建文件夾my-build-cli,執(zhí)行npm init -y

init-y.png

配置腳手架入口文件

1)創(chuàng)建bin目錄,該目錄下創(chuàng)建www.js

bin/www.js 內容

#! /usr/bin/env node

console.log('link 成功');

注:/usr/bin/env node 這行的意思是使用 node 來執(zhí)行此文件

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

link.jpg

測試是否連接成功

在控制臺輸入my-build-cli

linksuccess.jpg

在控制臺輸出link 成功, 項目配置成功

安裝腳手架所需的工具

一次性安裝所需的工具

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

工具名稱作用
commander自定義命令行工具
inquirer命令行交互工具
download-git-repo從 git 上下載項目模板工具
utildownload-git-repo 不支持異步調用,需要使用 util 插件的util.promisify進行轉換
ora命令行 loading 動效
fs-extra提供文件操作方法
axios發(fā)送接口,請求 git 上的模板列表

commander 自定義命令行工具

commander.js 是自定義命令行工具

這里用來創(chuàng)建create 命令,用戶可以通過輸入 my-cli creat appName 來創(chuàng)建項目

修改www.js

#! /usr/bin/env node

const program = require('commander');

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

program.parse();

inquirer 命令行交互工具

inquirer.js 命令行交互工具,用來詢問用戶的操作,讓用戶輸入指定的信息,或給出對應的選項讓用戶選擇

此處 inquirer 的運用場景有 2 個

1)場景 1:當用戶要創(chuàng)建的項目目錄已存在時,提示用戶是否要覆蓋 or 取消

2)場景 2:讓用戶輸入項目的author作者和項目description描述

創(chuàng)建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拼接 要創(chuàng)建項目的目錄
  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);
  // 創(chuàng)建項目
  const generator = new Generator(name, targetAir, ask);
  generator.create();
};

創(chuàng)建ask.js

配置 ask 選項,讓用戶輸入作者和項目描述

bin/create.js

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

創(chuàng)建generator.js

generator.js的工作流程

1)通過接口獲取git上的模板目錄

2)通過inquirer讓用戶選擇需要下載的項目

3)使用download-git-repo下載用戶選擇的項目模板

4)將用戶創(chuàng)建時,將項目名稱、作者名字、描述寫入到項目模板的package.json文件中

bin/generator.js

const path = require('path');
const fs = require('fs-extra');
// 引入ora工具:命令行l(wèi)oading 動效
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 ……');
  }
}

// 創(chuàng)建項目類
class Generator {
  // name 項目名稱
  // target 創(chuàng)建項目的路徑
  // 用戶輸入的 作者和項目描述 信息
  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中 執(zhí)行generator.create();
  async create() {
    const repo = await this.getRepo();
    console.log('用戶選擇了', repo);

    // 下載用戶選擇的項目模板
    await this.download(repo);

    // 下載完成后,獲取項目里的package.json
    // 將用戶創(chuàng)建項目的填寫的信息(項目名稱、作者名字、描述),寫入到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;

創(chuàng)建http.js

用來發(fā)送接口,獲取 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
};

最終的目錄結構

list.jpg

腳手架效果演示

text8.gif

腳手架發(fā)布到 npm 庫

完善 package.json

1)配置main屬性,指定包的入口 'main': './bin/www.js'

2)增加files屬性,files 用來描述當把 npm 包,作為依賴包安裝的文件列表。當 npm 包發(fā)布時,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 創(chuàng)建項目

2)author? 輸入項目作者

3)description? 輸入項目描述

4)選擇項目模塊 appDemo or pcDemo

5)安裝選擇的模板

### 演示示例

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

發(fā)布成功后,在 npm 網(wǎng)站搜索my-2022-cli

my-2022-cli.jpg

自定義腳手架 github 源碼地址[21]
my-2022-cli[22]

性能分析與優(yōu)化

一百用戶與一百萬用戶的網(wǎng)站有著本質區(qū)別

隨著用戶的增長,任何細節(jié)的優(yōu)化都變得更為重要,網(wǎng)站的性能差異直接影響著用戶的體驗

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

我曾經負責過幾個大型項目的整體性能優(yōu)化,盡量從實戰(zhàn)的角度聊一聊自己所理解的性能問題

性能分析工具

好比去醫(yī)院看病一樣,得了什么病,通過檢測化驗后才知道。網(wǎng)站也是一樣,需要借助性能分析工具來檢測

Lighthouse 工具

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

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

并且 Lighthouse 可以對頁面多方面的效果指標進行評測,并給出最佳實踐的建議,以幫助開發(fā)者改進網(wǎng)站的質量

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

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

這里以https://juejin.cn[23]網(wǎng)站為例, 打開 Chrome 瀏覽器控制臺,選擇Lighthouse選項,點擊Generate report

Lighthouse.jpg

Lighthouse 能夠生成一份該網(wǎng)站的報告,比如下圖:

performance.jpg

這里重點關注Performance性能評分

性能評分的分值區(qū)間是 0 到 100,如果出現(xiàn) 0 分,通常是在運行 Lighthouse 時發(fā)生了錯誤,滿分 100 分代表了網(wǎng)站已經達到了 98 分位值的數(shù)據(jù),而 50 分則對應 75 分位值的數(shù)據(jù)

小伙伴看看自己開發(fā)的項目得分是多少,處于什么樣的水平

Lighthouse 給出 Opportunities 優(yōu)化建議

Lighthouse 會針對當前網(wǎng)站,給出一些Opportunities優(yōu)化建議

Opportunities 指的是優(yōu)化機會,它提供了詳細的建議和文檔,來解釋低分的原因,幫助我們具體進行實現(xiàn)和改進

opportunity.jpg

舉一個我曾開發(fā)過的一個項目,以下是

Opportunities 給出優(yōu)化建議列表

問題建議
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 指的是現(xiàn)在存在的問題,為進一步改善性能的驗證和調整給出了指導

DiagNo.jpg

Diagnostics 診斷問題列表

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

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

Lighthouse 列出 Performance 各指標得分

Performance 列出了FCP、SP、LCP、TTI、TBI、CLS 六個指標的用時和得分情況

下文會聊一聊這些指標的用法與作用

performance1.jpg

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

Web-vitals 官方標準

web-vitals[25]是 Google 給出的定義是 一個良好網(wǎng)站的基本指標

過去要衡量一個網(wǎng)站的好壞,需要使用的指標太多了,現(xiàn)在我們可以將重點聚焦于 Web Vitals 指標的表現(xiàn)即可

官方指標標準

指標作用標準
FCP(First Contentful Paint)首次內容繪制時間標準 ≤1s
LCP(Largest Contentful Paint)最大內容繪制時間標準 ≤2 秒
FID(first input delay)首次輸入延遲,標準是用戶觸發(fā)后,到瀏覽器響應時間標準 ≤100ms
CLS(Cumulative Layout Shift)累積布局偏移標準 ≤0.1
TTFB(Time to First Byte)頁面發(fā)出請求,到接收第一個字節(jié)所花費的毫秒數(shù)(首字節(jié)時間)標準<= 100 毫秒

我們將 Lighthouse 中 Performance 列出的指標表現(xiàn),與官方指標標準做對比,可以發(fā)現(xiàn)頁面哪些指標超出了范圍

Performance 工具

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

具體是加載資源慢、dom渲染慢、還是js執(zhí)行慢呢?

chrome 瀏覽器提供的performance是常用來查看網(wǎng)頁性能的工具,通過該工具,我們可以知道頁面在瀏覽器運行時的性能表現(xiàn)

Performance 尋找性能瓶頸

打開 Chrome 瀏覽器控制臺,選擇Performance選項,點擊左側reload圖標

perfromance1.gif

Performance 面板可以記錄和分析頁面在運行時的所有活動,大致分為以下 4 個區(qū)域

performance2.png

Performance 各區(qū)域功能介紹

1)FPS

FPS(Frames Per Second),表示每秒傳輸幀數(shù),是用來分析頁面是否卡頓的一個主要性能指標

如下圖所示,綠色的長條越高,說明FPS越高,用戶體驗越好

如果發(fā)現(xiàn)了一個紅色的長條,那么就說明這些幀存在嚴重問題,可能會造成頁面卡頓

FPS.png

2)NET

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

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

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

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

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

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

net.png

3)火焰圖

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

1)Network: 表示加載了哪些資源
2)Frames:表示每幅幀的運行情況
3)Timings: 記錄頁面中關鍵指標的時間
4)Main:表示主線程(重點,下文會詳細介紹)
5)GPU:表示 GPU 占用情況

4)統(tǒng)計匯總

Summary: 表示各指標時間占用統(tǒng)計報表

1)Loading: 加載時間
2)Scripting: js計算時間
3)Rendering: 渲染時間
4)Painting: 繪制時間
5)Other: 其他時間
6)Idle: 瀏覽器閑置時間
sum.jpg

Performance Main 性能瓶頸的突破口

Main 表示主線程,主要負責

1)Javascript 的計算與執(zhí)行
2)CSS 樣式計算
3)Layout 布局計算
4)將頁面元素繪制成位圖(paint),也就是光柵化(Raster)

展開 Main,可以發(fā)現(xiàn)很多紅色三角(long task),這些執(zhí)行時間超過 50ms就屬于長任務,會造成頁面卡頓,嚴重時會造成頁面卡死

main.jpg

展開其中一個紅色三角,Devtools 在Summary面板里展示了更多關于這個事件的信息

app.jpg

在 summary 面板里點擊app.js鏈接,Devtools 可以跳轉到需要優(yōu)化的代碼處

source.jpg

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

如何去解決或優(yōu)化這些long task,從而解決去頁面的性能瓶頸

全新 Chrome Devtool Performance 使用指南[26]
手把手帶你入門前端工程化——超詳細教程[27]
Chrome Devtool — Performance[28]

性能監(jiān)控

項目發(fā)布生產后,用戶使用時的性能如何,頁面整體的打開速度是多少、白屏時間多少,F(xiàn)P、FCP、LCP、FID、CLS 等指標,要設置多大的閥值呢,才能滿足TP50、TP90、TP99的要求呢?

TP 指標: 總次數(shù) * 指標數(shù) = 對應 TP 指標的值。

設置每個指標的閥值,比如 FP 指標,設置閥值為 1s,要求 Tp95,即 95%的 FP 指標,要在 1s 以下,剩余 5%的指標超過 1s

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

這里就需要性能監(jiān)控,采集到用戶的頁面數(shù)據(jù)

性能指標的計算

常用的兩種方式:

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

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

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

方式二:通過performance api進行計算

下面聊一下 performance api 來計算各種指標

打開任意網(wǎng)頁,在控制臺中輸入 performance 回車,可以看到一系列的參數(shù),

performance.timing

重點看下performance.timing,記錄了頁面的各個關鍵時間點

時間作用
navigationStart(可以理解為該頁面的起始時間)同一個瀏覽器上下文的上一個文檔卸載結束時的時間戳,如果沒有上一個文檔,這個值會和 fetchStart 相同
unloadEventStartunload 事件拋出時的時間戳,如果沒有上一個文檔,這個值會是 0
unloadEventEndunload 事件處理完成的時間戳,如果沒有上一個文檔,這個值會是 0
redirectStart第一個 HTTP 重定向開始時的時間戳,沒有重定向或者重定向中的不同源,這個值會是 0
redirectEnd最后一個 HTTP 重定向開始時的時間戳,沒有重定向或者重定向中的不同源,這個值會是 0
fetchStart瀏覽器準備好使用 HTTP 請求來獲取文檔的時間戳。發(fā)送在檢查緩存之前
domainLookupStart域名查詢開始的時間戳,如果使用了持續(xù)連接或者緩存,則與 fetchStart 一致
domainLookupEnd域名查詢結束的時間戳,如果使用了持續(xù)連接或者緩存,則與 fetchStart 一致
connectStartHTTP 請求開始向服務器發(fā)送時的時間戳,如果使用了持續(xù)連接,則與 fetchStart 一致
connectEnd瀏覽器與服務器之間連接建立(所有握手和認證過程全部結束)的時間戳,如果使用了持續(xù)連接,則與 fetchStart 一致
secureConnectionStart瀏覽器與服務器開始安全連接握手時的時間戳,如果當前網(wǎng)頁不需要安全連接,這個值會是 0
requestStart瀏覽器向服務器發(fā)出 HTTP 請求的時間戳
responseStart瀏覽器從服務器收到(或從本地緩存讀?。┑谝粋€字節(jié)時的時間戳
responseEnd瀏覽器從服務器收到(或從本地緩存讀?。┳詈笠粋€字節(jié)時(如果在此之前 HTTP 連接已經關閉,則返回關閉時)的時間戳
domLoading當前網(wǎng)頁 DOM 結構開始解析時的時間戳
domInteractive當前網(wǎng)頁 DOM 結構解析完成,開始加載內嵌資源時的時間戳
domContentLoadedEventStart需要被執(zhí)行的腳本已經被解析的時間戳
domContentLoadedEventEnd需要立即執(zhí)行的腳本已經被執(zhí)行的時間戳
domComplete當前文檔解析完成的時間戳
loadEventStartload 事件被發(fā)送時的時間戳,如果這個事件還未被發(fā)送,它的值將會是 0
loadEventEndload 事件結束時的時間戳,如果這個事件還未被發(fā)送,它的值將會是 0

白屏時間 FP

白屏時間 FP(First Paint)指的是從用戶輸入 url 的時刻開始計算,一直到頁面有內容展示出來的時間節(jié)點,標準≤2s

這個過程包括 dns 查詢、建立 tcp 連接、發(fā)送 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 屬性表示是否觀察緩存數(shù)據(jù),也就是說觀察代碼添加時機比事件觸發(fā)時機晚也沒關系。
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});

首字節(jié)時間 TTFB

平常所說的TTFB,默認指導航請求的TTFB

導航請求:在瀏覽器切換頁面時創(chuàng)建,從導航開始到該請求返回 HTML

window.onload = function () {
  // 首字節(jié)時間
  let TTFB = responseStart - navigationStart;
};

首次輸入延遲 FID

FID(first input delay)首次輸入延遲,標準是用戶觸發(fā)后,瀏覽器的響應時間, 標準≤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(完成)的狀態(tài)
  let firstScreenTime = performance.timing.domComplete - performance.timing.navigationStart;
};

首屏加載時間

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

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

計算首屏加載時間流程

1)利用MutationObserver監(jiān)聽document對象,每當 dom 變化時觸發(fā)該事件

2)判斷監(jiān)聽的 dom 是否在首屏內,如果在首屏內,將該 dom 放到指定的數(shù)組中,記錄下當前 dom 變化的時間點

3)在 MutationObserver 的 callback 函數(shù)中,通過防抖函數(shù),監(jiān)聽document.readyState狀態(tài)的變化

4)當document.readyState === 'complete',停止定時器和 取消對 document 的監(jiān)聽

5)遍歷存放 dom 的數(shù)組,找出最后變化節(jié)點的時間,用該時間點減去performance.timing.navigationStart 得出首屏的加載時間

定義 performance.js

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

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

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

      // 執(zhí)行用戶傳入的callback函數(shù)
      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, // 監(jiān)聽添加或刪除子節(jié)點
    subtree: true, // 監(jiān)聽整個子樹
    characterData: true, // 監(jiān)聽元素的文本是否變化
    attributes: true // 監(jiān)聽元素的屬性是否變化
  });
}

外部引入使用

import observeFirstScreenPaint from './performance';

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

DOM 渲染時間和 window.onload 時間

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

DOM 渲染的時間

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

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

window.onload 的時間

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

計算資源的緩存命中率

緩存命中率:從緩存中得到數(shù)據(jù)的請求數(shù)與所有請求數(shù)的比率

理想狀態(tài)是緩存命中率越高越好,緩存命中率越高說明網(wǎng)站的緩存策略越有效,用戶打開頁面的速度也會相應提高

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

1)通過performance.getEntries()找到所有資源的信息

2)在這些資源對象中有一個transferSize 字段,它表示獲取資源的大小,包括響應頭字段和響應數(shù)據(jù)的大小

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

4)如果這個值不為 0,但是encodedBodySize 字段為 0,說明它走的是協(xié)商緩存(encodedBodySize 表示請求響應數(shù)據(jù) body 的大小

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

所有命中緩存的數(shù)據(jù) / 總數(shù)據(jù) 就能得出緩存命中率

性能數(shù)據(jù)上報

上報方式

一般使用圖片打點的方式,通過動態(tài)創(chuàng)建 img 標簽的方式,new 出像素為1x1 pxgif Image(gif 體積最小)對象就能發(fā)起請求,可以跨域、不需要等待服務器返回數(shù)據(jù)

上報時機

可以利用requestIdleCallback,瀏覽器空閑的時候上報,好處是:不阻塞其他流程的進行

如果瀏覽器不支持該requestIdleCallback,就使用setTimeout上報

// 優(yōu)先使用requestIdleCallback
if (window.requestIdleCallback) {
  window.requestIdleCallback(
    () => {
      // 獲取瀏覽器的剩余空閑時間
      console.log(deadline.timeRemaining());
      report(data); // 上報數(shù)據(jù)
    },
    // timeout設置為1000,如果在1000ms內沒有執(zhí)行該后調,在下次空閑時間時,callback會強制執(zhí)行
    { timeout: 1000 }
  );
else {
  setTimeout(() => {
    report(data); // 上報數(shù)據(jù)
  });
}

內存分析與優(yōu)化

性能分析很火,但內存分析相比就低調很多了

舉一個我之前遇到的情況,客戶電腦配置低,打開公司開發(fā)的頁面,經常出現(xiàn)頁面崩潰

調查原因,就是因為頁面內存占用太大,客戶打開幾個頁面后,內存直接拉滿,也是經過這件事,我開始重視內存分析與優(yōu)化

下面,聊一聊內存這塊有哪些知識

Memory 工具

Memory 工具,通過內存快照的方式,分析當前頁面的內存使用情況

Memory 工具使用流程

1)打開 chrome 瀏覽器控制臺,選擇Memory工具

2)點擊左側start按鈕,刷新頁面,開始錄制的JS堆動態(tài)分配時間線,會生成頁面加載過程內存變化的柱狀統(tǒng)計圖(藍色表示未回收,灰色表示已回收)

memory.jpg

Memory 工具中的關鍵項

關鍵項

Constructor:對象的類名;
Distance:對象到根的引用層級;
Objects Count:對象的數(shù)量;
Shallow Size: 對象本身占用的內存,不包括引用的對象所占內存;
Retained Size: 對象所占總內存,包含引用的其他對象所占內存;
Retainers:對象的引用層級關系

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

// 測試代碼
class Jane {}
class Tom {
  constructor () { this.jane = new Jane();}
}
Array(1000000).fill('').map(() => new Tom())
cpudemo.jpg

shallow size 和 retained size 的區(qū)別,以用紅框里的 Tom 和 Jane 更直觀的展示

Tom 的 shallow 占了 32M,retained 占用了 56M,這是因為 retained 包括了引用的指針對應的內存大小,即 tom.jane 所占用的內存

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

retained size 可以理解為當回收掉該對象時可以釋放的內存大小,在內存調優(yōu)中具有重要參考意義

內存分析的關鍵點

找到內存最高的節(jié)點,分析這些時刻執(zhí)行了哪些代碼,發(fā)生了什么操作,盡可能去優(yōu)化它們

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

2)按照Retainers size(總內存大?。┡判?,點擊展開內存最高的哪一項,點擊展開構造函數(shù),可以看到所有構造函數(shù)相關的對象實例

3)選中構造函數(shù),底部會顯示對應源碼文件,點擊源碼文件,可以跳轉到具體的代碼,這樣我們就知道是哪里的代碼造成內存過大

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

retainedSize.jpg

點擊keyghost.js可以跳轉到具體的源碼

localkey.png

內存泄露的情況

1)意外的全局變量, 掛載到 window 上全局變量
2)遺忘的定時器,定時器沒有清除
3)閉包不當?shù)氖褂?/p>

內存分析總結

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

2)通過 JS 堆動態(tài)分配時間線,找到內存最高的時刻

3)按照 Retainers size(總內存大?。┡判颍c擊展開內存最高的前幾項,分析由于哪個函數(shù)操作導致了內存過大,甚至是內存泄露

4)結合具體的代碼,去解決或優(yōu)化內存變大的情況

chrome 內存泄露(一)、內存泄漏分析工具[29]
chrome 內存泄露(二)、內存泄漏實例[30]
JavaScript 進階-常見內存泄露及如何避免[31]

項目優(yōu)化總結

優(yōu)化的本質:響應更快,展示更快

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

經典:雅虎軍規(guī)

很多前端優(yōu)化準則都是圍繞著這個展開

雅虎35條軍規(guī).jpg

優(yōu)化建議

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

1、分析打包后的文件

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

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

比如:組件庫如elementUI的按需引入、Swiper輪播圖組件打包后的體積約 200k,看是否能替換成體積更小的插件、momentjs去掉無用的語言包等

vendors.png

2、合理處理公共資源

如果項目支持 CDN,可以配置externals,將Vue、Vue-router、Vuex、echarts等公共資源,通過 CDN 的方式引入,不打到項目里邊

如果項目不支持 CDN,可以使用DllPlugin動態(tài)鏈接庫,將業(yè)務代碼和公共資源代碼相分離,公共資源單獨打包,給這些公共資源設置強緩存(公共資源基本不會變),這樣以后可以只打包業(yè)務代碼,提升打包速度

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

preload 預加載

<link rel='preload' href='/path/style.css' as='style'>
<link rel='preload' href='/path/home.js' as='script'>

preload 預加載是告訴瀏覽器頁面必定需要的資源,瀏覽器會優(yōu)先加載這些資源;使用 link 標簽創(chuàng)建(vue 項目打包后,會將首頁所用到的資源都加上 preload)

注意:preload 只是預加載資源,但不會執(zhí)行,還需要引入具體的文件后才會執(zhí)行 <script src='/path/home.js'>

DNS 預解析

DNS Prefetch 是一種 DNS 預解析技術,當你瀏覽網(wǎng)頁時,瀏覽器會在加載網(wǎng)頁時,對網(wǎng)頁中的域名進行解析緩存

這樣在你單擊當前網(wǎng)頁中的連接時就無需進行DNS 的解析,減少用戶等待時間,提高用戶體驗

使用dns-prefetch,如<link rel='dns-prefetch' href='//img1.taobao.com'>

很多大型的網(wǎng)站,都會用N 個CDN 域名來做圖片、靜態(tài)文件等資源訪問。解析單個域名同樣的地點加上高并發(fā)難免有點堵塞,通過多個 CDN 域名來分擔高并發(fā)下的堵塞

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

方式一: defer 或 async

使用 script 標簽的defer或async屬性,這兩種方式都是異步加載 js,不會阻塞 DOM 的渲染

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

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

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

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

方式二:依賴動態(tài)引入

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

比如echart.js,只有 A 頁面使用,可以在 A 頁面的鉤子函數(shù)中動態(tài)加載,在onload事件中進行 echart 初始化

資源動態(tài)加載的代碼示例

// 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() 動態(tài)加載路由和組件,對資源進行拆分,只有使用的時候才進行動態(tài)加載

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

// 組件懶加載
// 在visible屬性為true時,動態(tài)去加載demoComponent組件
<demoComponent v-if='visible == true' />

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

5、合理利用緩存

html 資源設置協(xié)商緩存,其他 js、css、圖片等資源設置強緩存

當用戶再次打開頁面時,html 先和服務器校驗,如果該資源未變化,服務器返回 304,直接使用緩存的文件;若返回 200,則返回最新的 html 資源

6、網(wǎng)絡方面的優(yōu)化

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

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

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

7、代碼層面的優(yōu)化

1)前端長列表渲染優(yōu)化,分頁 + 虛擬列表,長列表渲染的性能效率與用戶體驗成正比

2)圖片的懶加載、圖片的動態(tài)裁剪

特點是手機端項目,圖片幾乎不需要原圖,使用七?;虬⒗镌频膭討B(tài)裁剪功能,可以將原本幾M的大小裁剪成幾k

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

使用 GPU 硬件加速包括:transform 不為none、opacity、filter、will-change

4)函數(shù)的節(jié)流和防抖,減少接口的請求次數(shù)

5)使用骨架屏優(yōu)化用戶等待體驗,可以根據(jù)不同路由配置不同的骨架

vue 項目推薦使用vue-skeleton-webpack-plugin,骨架屏原理將<div id='app'></div> 中的內容替換掉

6)大數(shù)據(jù)的渲染,如果數(shù)據(jù)不會變化,vue 項目可以使用Object.freeze()

Object.freeze()方法可以凍結一個對象,Vue 正常情況下,會將 data 中定義的是對象變成響應式,但如果判斷對象的自身屬性不可修改,就直接返回改對象,省去了遞歸遍歷對象的時間與內存消耗

7)定時器和綁定的事件,在頁面銷毀時卸載

8、webpack 優(yōu)化

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

Webpack 優(yōu)化——將你的構建效率提速翻倍[33]
帶你深度解鎖 Webpack 系列(優(yōu)化篇)[34]

性能分析總結

1)先用 Lighthouse 得到當前頁面的性能得分,了解頁面的整體情況,重點關注 Opportunities 優(yōu)化建議和 Diagnostics 診斷問題列表

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

3)利用 Memory 工具,了解頁面整體的內存使用情況,通過 JS 堆動態(tài)分配時間線,找到內存最高的時刻。結合具體的代碼,去解決或優(yōu)化內存變大的問題

前端監(jiān)控

沒有監(jiān)控的項目,就是在“裸奔”

需要通過監(jiān)控才能真正了解項目的整體情況,各項指標要通過大量的數(shù)據(jù)采集、數(shù)據(jù)分析,變得更有意義

監(jiān)控的好處:事前預警和事后分析

事前預警:設置一個閾值,當監(jiān)控的數(shù)據(jù)達到閾值時,通過各種渠道通知管理員和開發(fā),提前避免可能會造成的宕機或崩潰

事后分析:通過監(jiān)控日志文件,分析故障原因和故障發(fā)生點。從而做出修改,防止這種情況再次發(fā)生

我們可以使用市面上現(xiàn)有的工具,也可以自建 sentry 監(jiān)控,關鍵在于通過這些數(shù)據(jù),能看到這些冰冷數(shù)據(jù)背后的故事

組件庫

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

現(xiàn)在開發(fā)一個新項目,如果要求不能用現(xiàn)成的組件庫,我估計要瞬間懵逼,無從下手了,不知不覺中,我已經患上嚴重的組件庫依賴癥

如果讓我只能說出一種,快速提升編程技能的方法,我一定會推薦去看看組件庫源碼

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

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

比如最常用的彈框組件,里邊的流程控制、層級控制、組件間事件派發(fā)與廣播、遞歸查找組件等設計,相當驚艷,真沒想到一個小小的彈框組件,也是內有乾坤

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

總結

工程化,是一個非常容易出彩的方向,是展現(xiàn)一個俠者內功是否深厚的窗口

文中大部分知識點,只是入門級教程,也是我以后需要持續(xù)努力的方向

本站僅提供存儲服務,所有內容均由用戶發(fā)布,如發(fā)現(xiàn)有害或侵權內容,請點擊舉報
打開APP,閱讀全文并永久保存 查看更多類似文章
猜你喜歡
類似文章
webpack2 項目
Vue項目從2.5M優(yōu)化到200kb的全過程
入門Webpack,看這篇就夠了
webpack的入門實踐,看這篇就夠了
帶你由淺入深了解webpack4中的各種常用配置(一)
Webpack+React+ES6開發(fā)模式入門指南
更多類似文章 >>
生活服務
分享 收藏 導長圖 關注 下載文章
綁定賬號成功
后續(xù)可登錄賬號暢享VIP特權!
如果VIP功能使用有故障,
可點擊這里聯(lián)系客服!

聯(lián)系客服