暴肝!7000 字的前端性能優化總結 | 乾貨建議收藏


爲什麼要做性能優化?性能優化到底有多重要? 網站的性能優化對於用戶的留存率、轉化率有很大的影響,所以對於前端開發來說性能優化能力也是重要的考察點。

性能優化的點非常的多,有的小夥伴覺得記起來非常的麻煩,所以這裏主要梳理出一條線來幫助記憶。

可以將性能優化分爲兩個大的分類:

  • 加載時優化
  • 運行時優化

加載時性能

顧名思義加載時優化 主要解決的就是讓一個網站加載過程更快,比如壓縮文件大小、使用CDN加速等方式可以優化加載性能。檢查加載性能的指標一般看:白屏時間和首屏時間:

  • 白屏時間:指的是從輸入網址, 到頁面開始顯示內容的時間。
  • 首屏時間:指從輸入網址, 到首屏頁面內容渲染完畢的時間。

白屏時間計算

將代碼腳本放在 </head> 前面就能獲取白屏時間:

<script>
    new Date().getTime() - performance.timing.navigationStart
</script>

首屏時間計算

window.onload事件中執行以下代碼,可以獲取首屏時間:

new Date().getTime() - performance.timing.navigationStart

運行時性能

運行時性能是指頁面運行時的性能表現,而不是頁面加載時的性能。可以通過chrome開發者工具中的 Performance 面板來分析頁面的運行時性能。關於chrome開發者工具具體如何操作以及如何查看性能,可以看這篇文章性能優化篇——運行時性能分析

接下來就從加載時性能和運行時性能兩個方面來討論網站優化具體應該怎麼做。

加載時性能優化

我們知道瀏覽器如果輸入的是一個網址,首先要交給DNS域名解析 -> 找到對應的IP地址 -> 然後進行TCP連接 -> 瀏覽器發送HTTP請求 -> 服務器接收請求 -> 服務器處理請求並返回HTTP報文 -> 以及瀏覽器接收並解析渲染頁面。從這一過程中,其實就可以挖出優化點,縮短請求的時間,從而去加快網站的訪問速度,提升性能。

這個過程中可以提升性能的優化的點:

  1. DNS解析優化,瀏覽器訪問DNS的時間就可以縮短
  2. 使用HTTP2
  3. 減少HTTP請求數量
  4. 減少http請求大小
  5. 服務器端渲染
  6. 靜態資源使用CDN
  7. 資源緩存,不重複加載相同的資源

從上面幾個優化點出發,有以下幾種實現性能優化的方式。

1.DNS 預解析

DNS 作爲互聯網的基礎協議,其解析的速度似乎容易被網站優化人員忽視。現在大多數新瀏覽器已經針對DNS解析進行了優化,典型的一次DNS解析耗費20-120毫秒,減少DNS解析時間和次數是個很好的優化方式。DNS Prefetching是具有此屬性的域名不需要用戶點擊鏈接就在後臺解析,而域名解析和內容載入是串行的網絡操作,所以這個方式能減少用戶的等待時間,提升用戶體驗。

瀏覽器對網站第一次的域名DNS解析查找流程依次爲:

瀏覽器緩存 ->系統緩存 ->路由器緩存 ->ISP DNS緩存 ->遞歸搜索

DNS預解析的實現:

用meta信息來告知瀏覽器, 當前頁面要做DNS預解析:

<meta http-equiv="x-dns-prefetch-control" content="on" />

在頁面header中使用link標籤來強制對DNS預解析:

<link rel="dns-prefetch" href="http://bdimg.share.baidu.com" />

注意:dns-prefetch需慎用,多頁面重複DNS預解析會增加重複DNS查詢次數。

2.使用HTTP2

HTTP2帶來了非常大的加載優化,所以在做優化上首先就想到了用HTTP2代替HTTP1。

HTTP2相對於HTTP1有這些優點:

解析速度快

服務器解析 HTTP1.1 的請求時,必須不斷地讀入字節,直到遇到分隔符 CRLF 爲止。而解析 HTTP2 的請求就不用這麼麻煩,因爲 HTTP2 是基於幀的協議,每個幀都有表示幀長度的字段。

多路複用

在 HTTP2 上,多個請求可以共用一個 TCP 連接,這稱爲多路複用。

當然HTTP1.1有一個可選的Pipelining技術,說的意思是當一個HTTP連接在等待接收響應時可以通過這個連接發送其他請求。聽起來很棒,其實這裏有一個坑,處理響應是按照順序的,也就是後發的請求有可能被先發的阻塞住,也正因此很多瀏覽器默認是不開啓Pipelining的。

HTTP1 的Pipelining技術會有阻塞的問題,HTTP/2的多路複用可以粗略的理解爲非阻塞版的Pipelining。即可以同時通過一個HTTP連接發送多個請求,誰先響應就先處理誰,這樣就充分的壓榨了TCP這個全雙工管道的性能。加載性能會是HTTP1的幾倍,需要加載的資源越多越明顯。當然多路複用是建立在加載的資源在同一域名下,不同域名神仙也複用不了。

首部壓縮

HTTP2 提供了首部壓縮功能。(這部分了解一下就行)

HTTP 1.1請求的大小變得越來越大,有時甚至會大於TCP窗口的初始大小,因爲它們需要等待帶着ACK的響應回來以後才能繼續被髮送。HTTP/2對消息頭採用HPACK(專爲http/2頭部設計的壓縮格式)進行壓縮傳輸,能夠節省消息頭佔用的網絡的流量。而HTTP/1.x每次請求,都會攜帶大量冗餘頭信息,浪費了很多帶寬資源。

服務器推送

服務端可以在發送頁面HTML時主動推送其它資源,而不用等到瀏覽器解析到相應位置,發起請求再響應。

3.減少HTTP請求數量

HTTP請求建立和釋放需要時間。

HTTP請求從建立到關閉一共經過以下步驟:

  1. 客戶端連接到Web服務器
  2. 發送HTTP請求
  3. 服務器接受請求並返回HTTP響應
  4. 釋放連接TCP鏈接

這些步驟都是需要花費時間的,在網絡情況差的情況下,花費的時間更長。如果頁面的資源非常碎片化,每個HTTP請求只帶回來幾K甚至不到1K的數據(比如各種小圖標)那性能是非常浪費的。

4.壓縮、合併文件

  • 壓縮文件 -> 減少HTTP請求大小,可以減少請求時間
  • 文件合併 -> 減少HTTP請求數量。

我們可以對html、css、js以及圖片資源進行壓縮處理,現在可以很方便的使用 webpack 實現文件的壓縮:

  • js壓縮:UglifyPlugin
  • CSS壓縮:MiniCssExtractPlugin
  • HTML壓縮:HtmlWebpackPlugin
  • 圖片壓縮:image-webpack-loader

提取公共代碼

合併文件雖然能減少HTTP請求數量, 但是並不是文件合併越多越好,還可以考慮按需加載方式(後面第6點有講到)。什麼樣的文件可以合併呢?可以提取項目中多次使用到的公共代碼進行提取,打包成公共模塊。

可以使用 webpack4 的 splitChunk 插件 cacheGroups 選項。

optimization: {
      runtimeChunk: {
        name'manifest' // 將 webpack 的 runtime 代碼拆分爲一個單獨的 chunk。
    },
    splitChunks: {
        cacheGroups: {
            vendor: {
                name'chunk-vendors',
                test/[\\/]node_modules[\\/]/,
                priority-10,
                chunks'initial'
            },
            common: {
                name'chunk-common',
                minChunks2,
                priority-20,
                chunks'initial',
                reuseExistingChunktrue
            }
        },
    }
},

5.採用svg圖片或者字體圖標

因爲字體圖標或者SVG是矢量圖,代碼編寫出來的,放大不會失真,而且渲染速度快。字體圖標使用時就跟字體一樣,可以設置屬性,例如 font-size、color 等等,非常方便,還有一個優點是生成的文件特別小。

6.按需加載代碼,減少冗餘代碼

按需加載

在開發SPA項目時,項目中經常存在十幾個甚至更多的路由頁面, 如果將這些頁面都打包進一個JS文件, 雖然減少了HTTP請求數量, 但是會導致文件比較大,同時加載了大量首頁不需要的代碼,有些得不償失,這時候就可以使用按需加載, 將每個路由頁面單獨打包爲一個文件,當然不僅僅是路由可以按需加載。

根據文件內容生成文件名,結合 import 動態引入組件實現按需加載:

通過配置 output 的 filename 屬性可以實現這個需求。filename 屬性的值選項中有一個 [contenthash],它將根據文件內容創建出唯一 hash。當文件內容發生變化時,[contenthash] 也會發生變化。

output: {
    filename'[name].[contenthash].js',
    chunkFilename'[name].[contenthash].js',
    path: path.resolve(__dirname, '../dist'),
},

減少冗餘代碼

一方面避免不必要的轉義:babel-loaderincludeexclude 來幫我們避免不必要的轉譯,不轉譯node_moudules中的js文件,其次在緩存當前轉譯的js文件,設置loader: 'babel-loader?cacheDirectory=true'

其次減少ES6 轉爲 ES5 的冗餘代碼:Babel 轉化後的代碼想要實現和原來代碼一樣的功能需要藉助一些幫助函數,比如:

class Person {}

會被轉換爲:

"use strict";

function _classCallCheck(instance, Constructor{
  if (!(instance instanceof Constructor)) {
    throw new TypeError("Cannot call a class as a function");
  }
}

var Person = function Person({
  _classCallCheck(this, Person);
};

這裏 _classCallCheck 就是一個 helper 函數,如果在很多文件裏都聲明瞭類,那麼就會產生很多個這樣的 helper 函數。

這裏的 @babel/runtime 包就聲明瞭所有需要用到的幫助函數,而 @babel/plugin-transform-runtime 的作用就是將所有需要 helper 函數的文件,從 @babel/runtime包 引進來:

"use strict";
var _classCallCheck2 = require("@babel/runtime/helpers/classCallCheck");
var _classCallCheck3 = _interopRequireDefault(_classCallCheck2);

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

var Person = function Person({
  (0, _classCallCheck3.default)(this, Person);
};

這裏就沒有再編譯出 helper 函數 classCallCheck 了,而是直接引用了@babel/runtime 中的 helpers/classCallCheck

  • 安裝

npm i -D @babel/plugin-transform-runtime @babel/runtime使用 在 .babelrc 文件中

"plugins": [
        "@babel/plugin-transform-runtime"
]

7.服務器端渲染

客戶端渲染: 獲取 HTML 文件,根據需要下載 JavaScript 文件,運行文件,生成 DOM,再渲染。

服務端渲染:服務端返回 HTML 文件,客戶端只需解析 HTML。

優點:首屏渲染快,SEO 好。缺點:配置麻煩,增加了服務器的計算壓力。

8. 使用 Defer 加載JS

儘量將 CSS 放在文件頭部,JavaScript 文件放在底部

所有放在 head 標籤裏的 CSS 和 JS 文件都會堵塞渲染。如果這些 CSS 和 JS 需要加載和解析很久的話,那麼頁面就空白了。所以 JS 文件要放在底部,等 HTML 解析完了再加載 JS 文件。

那爲什麼 CSS 文件還要放在頭部呢?

因爲先加載 HTML 再加載 CSS,會讓用戶第一時間看到的頁面是沒有樣式的、“醜陋”的,爲了避免這種情況發生,就要將 CSS 文件放在頭部了。

另外,JS 文件也不是不可以放在頭部,只要給 script 標籤加上 defer 屬性就可以了,異步下載,延遲執行。

9. 靜態資源使用 CDN

用戶與服務器的物理距離對響應時間也有影響。把內容部署在多個地理位置分散的服務器上能讓用戶更快地載入頁面, CDN就是爲了解決這一問題,在多個位置部署服務器,讓用戶離服務器更近,從而縮短請求時間。

10. 圖片優化

雪碧圖

圖片可以合併麼?當然。最爲常用的圖片合併場景就是雪碧圖(Sprite)。

在網站上通常會有很多小的圖標,不經優化的話,最直接的方式就是將這些小圖標保存爲一個個獨立的圖片文件,然後通過 CSS 將對應元素的背景圖片設置爲對應的圖標圖片。這麼做的一個重要問題在於,頁面加載時可能會同時請求非常多的小圖標圖片,這就會受到瀏覽器併發 HTTP 請求數的限制。

雪碧圖的核心原理在於設置不同的背景偏移量,大致包含兩點:

  • 不同的圖標元素都會將 background-url 設置爲合併後的雪碧圖的 uri;
  • 不同的圖標通過設置對應的 background-position 來展示大圖中對應的圖標部分。你可以用 Photoshop 這類工具自己製作雪碧圖。當然比較推薦的還是將雪碧圖的生成集成到前端自動化構建工具中,例如在 webpack 中使用 webpack-spritesmith,或者在 gulp 中使用 gulp.spritesmith。它們兩者都是基於 spritesmith 這個庫。

圖片懶加載

一般來說,我們訪問網站頁面時,其實很多圖片並不在首屏中,如果我們都加載的話,相當於是加載了用戶不一定會看到圖片, 這顯然是一種浪費。解決的核心思路就是懶加載:實現方式就是先不給圖片設置路徑,當圖片出現在瀏覽器可視區域時才設置真正的圖片路徑。

實現上就是先將圖片路徑設置給original-src,當頁面不可見時,圖片不會加載:

<img original-src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9eb06680a16044feb794f40fc3b1ac3d~tplv-k3u1fbpfcp-watermark.image" />

通過監聽頁面滾動,等頁面可見時設置圖片src:

const img = document.querySelector('img')
img.src = img.getAttribute("original-src")

如果想使用懶加載,還可以藉助一些已有的工具庫,例如 aFarkas/lazysizes、verlok/lazyload、tuupola/lazyload 等。

css中圖片懶加載

除了對於 <img> 元素的圖片進行來加載,在 CSS 中使用的圖片一樣可以懶加載,最常見的場景就是 background-url

.login {
    background-urlurl(/static/img/login.png);
}

對於上面這個樣式規則,如果不應用到具體的元素,瀏覽器不會去下載該圖片。所以你可以通過切換 className 的方式,放心得進行 CSS 中圖片的懶加載。

運行時性能優化

1. 減少重繪與重排

有前端經驗的開發者對這個概念一定不會陌生,瀏覽器下載完頁面需要的所有資源後, 就開始渲染頁面,主要經歷這5個過程:

  1. 解析HTML生成DOM樹
  2. 解析CSS生成CSSOM規則樹
  3. 將DOM樹與CSSOM規則樹合併生成Render(渲染)樹
  4. 遍歷Render(渲染)樹開始佈局, 計算每一個節點的位置大小信息
  5. 將渲染樹每個節點繪製到屏幕上
瀏覽器渲染過程

重排

當改變DOM元素位置或者大小時, 會導致瀏覽器重新生成Render樹, 這個過程叫重排

重繪

當重新生成渲染樹後, 將要將渲染樹每個節點繪製到屏幕, 這個過程叫重繪。

重排觸發時機

重排發生後的根本原理就是元素的幾何屬性發生改變, 所以從能夠改變幾何屬性的角度入手:

  • 添加|刪除可見的DOM元素
  • 元素位置發生改變
  • 元素本省的尺寸發生改變
  • 內容變化
  • 頁面渲染器初始化
  • 瀏覽器窗口大小發生改變

二者關係:重排會導致重繪, 但是重繪不會導致重排

瞭解了重排和重繪這兩個概念,我們還要知道重排和重繪的開銷都是非常昂貴的,如果不停的改變頁面的佈局,就會造成瀏覽器消耗大量的開銷在進行頁面的計算上,這樣容易造成頁面卡頓。那麼回到我們的問題如何減少重繪與重排呢?

1.1 避免table佈局

  • 不要使用table佈局,可能很小的一個改動會造成整個table重新佈局

1.2 分離讀寫操作

DOM 的多個讀操作(或多個寫操作),應該放在一起。不要兩個讀操作之間,加入一個寫操作。

// bad 強制刷新 觸發四次重排+重繪
div.style.left = div.offsetLeft + 1 + 'px';
div.style.top = div.offsetTop + 1 + 'px';
div.style.right = div.offsetRight + 1 + 'px';
div.style.bottom = div.offsetBottom + 1 + 'px';


// good 緩存佈局信息 相當於讀寫分離 觸發一次重排+重繪
var curLeft = div.offsetLeft;
var curTop = div.offsetTop;
var curRight = div.offsetRight;
var curBottom = div.offsetBottom;

div.style.left = curLeft + 1 + 'px';
div.style.top = curTop + 1 + 'px';
div.style.right = curRight + 1 + 'px';
div.style.bottom = curBottom + 1 + 'px';

1.3 樣式集中改變

不要頻發的操作樣式,雖然現在大部分瀏覽器有渲染隊列優化,但是在一些老版本的瀏覽器仍然存在效率低下的問題:

// 三次重排
div.style.left = '10px';
div.style.top = '10px';
div.style.width = '20px';

// 一次重排
el.style.cssText = 'left: 10px;top: 10px; width: 20px';

或者可以採用更改類名而不是修改樣式的方式。

1.4 position屬性爲absolute或fixed

使用絕對定位會使的該元素單獨成爲渲染樹中 body 的一個子元素,重排開銷比較小,不會對其它節點造成太多影響。當你在這些節點上放置這個元素時,一些其它在這個區域內的節點可能需要重繪,但是不需要重排。

2. 避免頁面卡頓

我們目前大多數屏幕的刷新率-60次/s,瀏覽器渲染更新頁面的標準幀率也爲60次/s --60FPS(frames/pre second), 那麼每一幀的預算時間約爲16.6ms ≈ 1s/60,瀏覽器在這個時間內要完成所有的整理工作,如果無法符合此預算, 幀率將下降,內容會在屏幕抖動, 此現象通常稱爲卡頓

瀏覽器需要做的工作包含下面這個流程:

首先你用js做了些邏輯,還觸發了樣式變化,style把應用的樣式規則計算好之後,把影響到的頁面元素進行重新佈局,叫做layout,再把它畫到內存的一個畫布裏面,paint成了像素,最後把這個畫布刷到屏幕上去,叫做composite,形成一幀。

這幾項的任何一項如果執行時間太長了,就會導致渲染這一幀的時間太長,平均幀率就會掉。假設這一幀花了50ms,那麼此時的幀率就爲1s / 50ms = 20fps.

當然上面的過程並不一定每一步都會執行,例如:

  • 你的js只是做一些運算,並沒有增刪DOM或改變CSS,那麼後續幾步就不會執行
  • style只改了顏色等不需要重新layout的屬性就不用執行 layout這一步
  • style改了transform屬性,在blink和edge瀏覽器裏面不需要layout和paint

3. 長列表優化

有時會有這樣的需求, 需要在頁面上展示包含上百個元素的列表(例如一個Feed流)。每個列表元素還有着複雜的內部結構,這顯然提高了頁面渲染的成本。當你使用了React時,長列表的問題就會被進一步的放大。那麼怎麼來優化長列表呢?

1.1 實現虛擬列表

虛擬列表是一種用來優化長列表的技術。它可以保證在列表元素不斷增加,或者列表元素很多的情況下,依然擁有很好的滾動、瀏覽性能。它的核心思想在於:只渲染可見區域附近的列表元素。下圖左邊就是虛擬列表的效果,可以看到只有視口內和臨近視口的上下區域內的元素會被渲染。

Virtual List.png

具體實現步驟如下所示:

  • 首先確定長列表所在父元素的大小,父元素的大小決定了可視區的寬和高
  • 確定長列表每一個列表元素的寬和高,同時初始的條件下計算好長列表每一個元素相對於父元素的位置,並用一個數組來保存所有列表元素的位置信息
  • 首次渲染時,只展示相對於父元素可視區內的子列表元素,在滾動時,根據父元素的滾動的 offset重新計算應該在可視區內的子列表元素。這樣保證了無論如何滾動,真實渲染出的dom節點只有可視區內的列表元素。
  • 假設可視區內能展示5個子列表元素,及時長列表總共有1000個元素,但是每時每刻,真實渲染出來的dom節點只有5個。
  • 補充說明,這種情況下,父元素一般使用 position:relative,子元素的定位一般使用: position:absolutesticky

除了自己實現外, 常用的框架也有不錯的開源實現, 例如:

  • 基於React的 react-virtualized
  • 基於Vue 的 vue-virtual-scroll-list
  • 基於Angular的 ngx-virtual-scroller

4. 滾動事件性能優化

前端最容易碰到的性能問題的場景之一就是監聽滾動事件並進行相應的操作。由於滾動事件發生非常頻繁,所以頻繁地執行監聽回調就容易造成JavaScript執行與頁面渲染之間互相阻塞的情況。

對應滾動這個場景,可以採用防抖節流來處理。

當一個事件頻繁觸發,而我們希望間隔一定的時間再觸發相應的函數時, 就可以使用節流(throttle)來處理。比如判斷頁面是否滾動到底部,然後展示相應的內容;就可以使用節流,在滾動時每300ms進行一次計算判斷是否滾動到底部的邏輯,而不用無時無刻地計算。

當一個事件頻繁觸發,而我們希望在事件觸發結束一段時間後(此段時間內不再有觸發)才實際觸發響應函數時會使用防抖(debounce)。例如用戶一直點擊按鈕,但你不希望頻繁發送請求,你就可以設置當點擊後 200ms 內用戶不再點擊時才發送請求。

對節流和防抖不太瞭解的可以看這篇文章:老生常談的防抖與節流https://mp.weixin.qq.com/s/HVkV7F1U77GvXbEI9MWA6g

5. 使用 Web Workers

前面提到了大量數據的渲染環節我們可以採用虛擬列表的方式實現,但是大量數據的計算環節依然會產生瀏覽器假死或者卡頓的情況.

通常情況下我們CPU密集型的任務都是交給後端計算的,但是有些時候我們需要處理一些離線場景或者解放後端壓力,這個時候此方法就不奏效了.

還有一種方法是計算切片,使用 setTimeout 拆分密集型任務,但是有些計算無法利用此方法拆解,同時還可能產生副作用,這個方法需要視具體場景而動.

最後一種方法也是目前比較奏效的方法就是利用Web Worker 進行多線程編程.

Web Worker 是一個獨立的線程(獨立的執行環境),這就意味着它可以完全和 UI 線程(主線程)並行的執行 js 代碼,從而不會阻塞 UI,它和主線程是通過 onmessage 和 postMessage 接口進行通信的。

Web Worker 使得網頁中進行多線程編程成爲可能。當主線程在處理界面事件時,worker 可以在後臺運行,幫你處理大量的數據計算,當計算完成,將計算結果返回給主線程,由主線程更新 DOM 元素。

6. 寫代碼時的優化點

提升性能,有時候在我們寫代碼時注意一些細節也是有效果的。

6.1 使用事件委託

看一下下面這段代碼:

<ul>
  <li>字節跳動</li>
  <li>阿里</li>
  <li>騰訊</li>
  <li>京東</li>
</ul>

/
/ good
document.querySelector('ul').onclick = (event) => {
  const target = event.target
  if (target.nodeName === 'LI') {
    console.log(target.innerHTML)
  }
}

/
/ bad
document.querySelectorAll('li').forEach((e) => {
  e.onclick = function() {
    console.log(this.innerHTML)
  }
}) 

綁定的事件越多, 瀏覽器內存佔有就越多,從而影響性能,利用事件代理的方式就可節省一些內存。

6.2 if-else 對比 switch

當判定條件越來越多時, 越傾向於使用switch,而不是if-else:

if (state ==0) {
    console.log("待開通")
else if (state == 1) {
    console.log("學習中")
else if (state == 2) {
    console.log("休學中")
else if (state == 3) {
    console.log("已過期")
} esle if (state ==4){
    console.log("未購買")
}

switch (state) {
    case 0:

        break
    case 1:

        break
    case 2:

        break
    case 3:

        break
    case 4:

        break
}

向上面這種情況使用switch更好, 假設state爲4,那麼if-else語句就要進行4次判定,switch只要進行一次即可。

但是有的情況下switch也做不到if-else的事情, 例如有多個判斷條件的情況下,無法使用switch

6.3 佈局上使用flexbox

在早期的 CSS 佈局方式中我們能對元素實行絕對定位、相對定位或浮動定位。而現在,我們有了新的佈局方式 flexbox,它比起早期的佈局方式來說有個優勢,那就是性能比較好。

關於前端性能優化就寫到這裏了,相信還有很多在代碼細節上注意就能進行性能優化的點,大家可以到公衆號【程序員成長指北】後臺留言, 後期也可以繼續完善文章,謝謝

參考文章

https://juejin.cn/post/6844903506906710024#comment

https://learnku.com/docs/f2e-performance-rules/reduce-the-number-of-http-requests/6369


 
    
    
    

關注下方公衆號 “全棧修煉”,回覆 “電子書” 即可以獲得下面 300 本技術精華書籍哦,貓哥 wx:CB834301747 。


本文分享自微信公衆號 - 全棧修煉(QuanZhanXiuLian)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章