Web性能優(yōu)化: 使用Webpack分離數(shù)據(jù)的正確方法
制定向用戶提供文件的***方式可能是一項(xiàng)棘手的工作。 有很多不同的場(chǎng)景,不同的技術(shù),不同的術(shù)語(yǔ)。
在這篇文章中,我希望給你所有你需要的東西,這樣你就可以:
- 了解哪種文件分割策略最適合你的網(wǎng)站和用戶
- 知道怎么做
根據(jù) Webpack glossary,有兩種不同類型的文件分割。 這些術(shù)語(yǔ)聽(tīng)起來(lái)可以互換,但顯然不是。
Webpack 文件分離包括兩個(gè)部分,一個(gè)是 Bundle splitting,一個(gè)是 Code splitting:
- Bundle splitting: 創(chuàng)建更多更小的文件,并行加載,以獲得更好的緩存效果,主要作用就是使瀏覽器并行下載,提高下載速度。并且運(yùn)用瀏覽器緩存,只有代碼被修改,文件名中的哈希值改變了才會(huì)去再次加載。
- Code splitting:只加載用戶最需要的部分,其余的代碼都遵從懶加載的策略,主要的作用就是加快頁(yè)面的加載速度,不加載不必要的代碼。
第二個(gè)聽(tīng)起來(lái)更吸引人,不是嗎?事實(shí)上,關(guān)于這個(gè)問(wèn)題的許多文章似乎都假設(shè)這是制作更小的JavaScript 文件的惟一值得的情況。
但我在這里要告訴你的是,***個(gè)在很多網(wǎng)站上都更有價(jià)值,應(yīng)該是你為所有網(wǎng)站做的***件事。
就讓我們一探究竟吧。
Bundle splitting
bundle splitting 背后的思想非常簡(jiǎn)單,如果你有一個(gè)巨大的文件,并且更改了一行代碼,那么用戶必須再次下載整個(gè)文件。但是如果將其分成兩個(gè)文件,那么用戶只需要下載更改的文件,瀏覽器將從緩存中提供另一個(gè)文件。
值得注意的是,由于 bundle splitting 都是關(guān)于緩存的,所以對(duì)于***次訪問(wèn)來(lái)說(shuō)沒(méi)有什么區(qū)別。
(我認(rèn)為太多關(guān)于性能的討論都是關(guān)于***次訪問(wèn)一個(gè)站點(diǎn),或許部分原因是“***印象很重要”,部分原因是它很好、很容易衡量。
對(duì)于經(jīng)常訪問(wèn)的用戶來(lái)說(shuō),量化性能增強(qiáng)所帶來(lái)的影響可能比較棘手,但是我們必須進(jìn)行量化!
這將需要一個(gè)電子表格,因此我們需要鎖定一組非常特定的環(huán)境,我們可以針對(duì)這些環(huán)境測(cè)試每個(gè)緩存策略。
這是我在前一段中提到的情況:
- Alice 每周訪問(wèn)我們的網(wǎng)站一次,持續(xù) 10 周
- 我們每周更新一次網(wǎng)站
- 我們每周都會(huì)更新我們的“產(chǎn)品列表”頁(yè)面
- 我們也有一個(gè)“產(chǎn)品詳細(xì)信息”頁(yè)面,但我們目前還沒(méi)有開(kāi)發(fā)
- 在第 5 周,我們向站點(diǎn)添加了一個(gè)新的 npm 包
- 在第 8 周,我們更新了一個(gè)現(xiàn)有的 npm 包
某些類型的人(比如我)會(huì)嘗試讓這個(gè)場(chǎng)景盡可能的真實(shí)。不要這樣做。實(shí)際情況并不重要,稍后我們將找出原因。
基線
假設(shè)我們的 JavaScript 包的總?cè)萘渴?00 KB,目前我們將它作為一個(gè)名為 main.js 的文件加載。
我們有一個(gè) Webpack 配置如下(我省略了一些無(wú)關(guān)的配置):
- // webpack.config.js
- const path = require('path')
- module.exports = {
- entry: path.resolve(__dirame, 'src/index.js')
- output: {
- path: path.resolve(__dirname, 'dist'),
- filename: '[name].[contenthash].js'
- }
- }
對(duì)于那些新的緩存破壞:任何時(shí)候我說(shuō) main.js,我實(shí)際上是指 main.xMePWxHo.js,其中里面的字符串是文件內(nèi)容的散列。這意味著不同的文件名 當(dāng)應(yīng)用程序中的代碼發(fā)生更改時(shí),從而強(qiáng)制瀏覽器下載新文件。
每周當(dāng)我們對(duì)站點(diǎn)進(jìn)行一些新的更改時(shí),這個(gè)包的 contenthash 都會(huì)發(fā)生變化。因此,Alice 每周都要訪問(wèn)我們的站點(diǎn)并下載一個(gè)新的 400kb 文件。
如果我們把這些事件做成一張表格,它會(huì)是這樣的。
也就是10周內(nèi), 4.12 MB, 我們可以做得更好。
分解 vendor 包
讓我們將包分成 main.js 和 vendor.js 文件。
- // webpack.config.js
- const path = require('path')
- module.exports = {
- entry: path.resolve(__dirname, 'src/index.js'),
- output: {
- path: path.resolve(__dirname, 'dist'),
- filename: '[name].[contenthash].js',
- },
- optimization: {
- splitChunks: {
- chunks: 'all'
- }
- }
- }
Webpack4 為你做***的事情,而沒(méi)有告訴你想要如何拆分包。這導(dǎo)致我們對(duì) webpack 是如何分包的知之甚少,結(jié)果有人會(huì)問(wèn) “你到底在對(duì)我的包裹做什么?”
添加 optimization.splitChunks.chunks ='all'的一種說(shuō)法是 “將 node_modules 中的所有內(nèi)容放入名為 vendors~main.js 的文件中”。
有了這個(gè)基本的 bundle splitting,Alice 每次訪問(wèn)時(shí)仍然下載一個(gè)新的 200kb 的 main.js,但是在***周、第8周和第5周只下載 200kb 的 vendor.js (不是按此順序)。
總共:2.64 MB。
減少36%。 在我們的配置中添加五行代碼并不錯(cuò)。 在進(jìn)一步閱讀之前,先去做。 如果你需要從 Webpack 3 升級(jí)到 4,請(qǐng)不要擔(dān)心,它非常簡(jiǎn)單。
我認(rèn)為這種性能改進(jìn)似乎更抽象,因?yàn)樗窃?0周內(nèi)進(jìn)行的,但是它確實(shí)為忠實(shí)用戶減少了36%的字節(jié),我們應(yīng)該為自己感到自豪。
但我們可以做得更好。
分離每個(gè) npm 包
我們的 vendor.js 遇到了與我們的 main.js 文件相同的問(wèn)題——對(duì)其中一部分的更改意味著重新下載它的所有部分。
那么為什么不為每 個(gè)npm 包創(chuàng)建一個(gè)單獨(dú)的文件呢?這很容易做到。
所以把 react、lodash、redux、moment 等拆分成不同的文件:
- const path = require('path');
- const webpack = require('webpack');
- module.exports = {
- entry: path.resolve(__dirname, 'src/index.js'),
- plugins: [
- new webpack.HashedModuleIdsPlugin(), // so that file hashes don't change unexpectedly
- ],
- output: {
- path: path.resolve(__dirname, 'dist'),
- filename: '[name].[contenthash].js',
- },
- optimization: {
- runtimeChunk: 'single',
- splitChunks: {
- chunks: 'all',
- maxInitialRequests: Infinity,
- minSize: 0,
- cacheGroups: {
- vendor: {
- test: /[\\/]node_modules[\\/]/,
- name(module) {
- // get the name. E.g. node_modules/packageName/not/this/part.js
- // or node_modules/packageName
- const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1];
- // npm package names are URL-safe, but some servers don't like @ symbols
- return `npm.${packageName.replace('@', '')}`;
- },
- },
- },
- },
- },
- };
文檔將很好地解釋這里的大部分內(nèi)容,但是我將稍微解釋一下需要注意的部分,因?yàn)樗鼈兓宋姨嗟臅r(shí)間。
- Webpack 有一些不太聰明的默認(rèn)設(shè)置,比如分割輸出文件時(shí)最多3個(gè)文件,最小文件大小為30 KB(所有較小的文件將連接在一起),所以我重寫(xiě)了這些。
- cacheGroups 是我們定義 Webpack 應(yīng)該如何將數(shù)據(jù)塊分組到輸出文件中的規(guī)則的地方。這里有一個(gè)名為 “vendor” 的模塊,它將用于從 node_modules 加載的任何模塊。通常,你只需將輸出文件的名稱定義為字符串。但是我將 name 定義為一個(gè)函數(shù)(將為每個(gè)解析的文件調(diào)用這個(gè)函數(shù))。然后從模塊的路徑返回包的名稱。因此,我們將為每個(gè)包獲得一個(gè)文件,例如 npm.react-dom.899sadfhj4.js。
- NPM 包名稱必須是 URL 安全的才能發(fā)布,因此我們不需要 encodeURI 的 packageName。 但是,我遇到一個(gè).NET服務(wù)器不能提供名稱中帶有 @(來(lái)自一個(gè)限定范圍的包)的文件,所以我在這個(gè)代碼片段中替換了 @。
- 整個(gè)設(shè)置很棒,因?yàn)樗且怀刹蛔兊摹?無(wú)需維護(hù) - 不需要按名稱引用任何包。
Alice 仍然會(huì)每周重新下載 200 KB 的 main.js 文件,并且在***次訪問(wèn)時(shí)仍會(huì)下載 200 KB 的npm包,但她絕不會(huì)兩次下載相同的包。
總共: 2.24 MB.
與基線相比減少了44%,這對(duì)于一些可以從博客文章中復(fù)制/粘貼的代碼來(lái)說(shuō)非??帷?/p>
我想知道是否有可能超過(guò) 50% ? 這完全沒(méi)有問(wèn)題。
分離應(yīng)用程序代碼的區(qū)域
讓我們轉(zhuǎn)到 main.js 文件,可憐的 Alice 一次又一次地下載這個(gè)文件。
我之前提到過(guò),我們?cè)诖苏军c(diǎn)上有兩個(gè)不同的部分:產(chǎn)品列表和產(chǎn)品詳細(xì)信息頁(yè)面。 每個(gè)區(qū)域中的唯一代碼為25 KB(共享代碼為150 KB)。
我們的產(chǎn)品詳情頁(yè)面現(xiàn)在變化不大,因?yàn)槲覀冏龅锰?**了。 因此,如果我們將其做為單獨(dú)的文件,則可以在大多數(shù)時(shí)間從緩存中獲取到它。
另外,我們網(wǎng)站有一個(gè)較大的內(nèi)聯(lián)SVG文件用于渲染圖標(biāo),重量只有25 KB,而這個(gè)也是很少變化的, 我們也需要優(yōu)化它。
我們只需手動(dòng)添加一些入口點(diǎn),告訴 Webpack 為每個(gè)項(xiàng)創(chuàng)建一個(gè)文件。
- module.exports = {
- entry: {
- main: path.resolve(__dirname, 'src/index.js'),
- ProductList: path.resolve(__dirname, 'src/ProductList/ProductList.js'),
- ProductPage: path.resolve(__dirname, 'src/ProductPage/ProductPage.js'),
- Icon: path.resolve(__dirname, 'src/Icon/Icon.js'),
- },
- output: {
- path: path.resolve(__dirname, 'dist'),
- filename: '[name].[contenthash:8].js',
- },
- plugins: [
- new webpack.HashedModuleIdsPlugin(), // so that file hashes don't change unexpectedly
- ],
- optimization: {
- runtimeChunk: 'single',
- splitChunks: {
- chunks: 'all',
- maxInitialRequests: Infinity,
- minSize: 0,
- cacheGroups: {
- vendor: {
- test: /[\\/]node_modules[\\/]/,
- name(module) {
- // get the name. E.g. node_modules/packageName/not/this/part.js
- // or node_modules/packageName
- const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1];
- // npm package names are URL-safe, but some servers don't like @ symbols
- return `npm.${packageName.replace('@', '')}`;
- },
- },
- },
- },
- },
- };
Webpack 還會(huì)為 ProductList 和 ProductPage 之間共享的內(nèi)容創(chuàng)建文件,這樣我們就不會(huì)得到重復(fù)的代碼。
這將為 Alice 在大多數(shù)情況下節(jié)省 50 KB 的下載。
只有 1.815 MB!
我們已經(jīng)為 Alice 節(jié)省了高達(dá)56%的下載量,這種節(jié)省將(在我們的理論場(chǎng)景中)持續(xù)到時(shí)間結(jié)束。
所有這些都只在Webpack配置中進(jìn)行了更改——我們沒(méi)有對(duì)應(yīng)用程序代碼進(jìn)行任何更改。
我在前面提到過(guò),測(cè)試中的確切場(chǎng)景并不重要。這是因?yàn)?,無(wú)論你提出什么場(chǎng)景,結(jié)論都是一樣的:將應(yīng)用程序分割成合理的小文件,以便用戶下載更少的代碼。
很快,=將討論“code splitting”——另一種類型的文件分割——但首先我想解決你現(xiàn)在正在考慮的三個(gè)問(wèn)題。
#1:大量的網(wǎng)絡(luò)請(qǐng)求不是更慢嗎?
答案當(dāng)然是不會(huì)。
在 HTTP/1.1 時(shí)代,這曾經(jīng)是一種情況,但在 HTTP/2 時(shí)代就不是這樣了。
盡管如此,這篇2016年的文章 和 Khan Academy 2015年的文章都得出結(jié)論,即使使用 HTTP/2,下載太多的文件還是比較慢。但在這兩篇文章中,“太多”的意思都是“幾百個(gè)”。所以請(qǐng)記住,如果你有數(shù)百個(gè)文件,你可能一開(kāi)始就會(huì)遇到并發(fā)限制。
如果您想知道,對(duì) HTTP/2 的支持可以追溯到 Windows 10 上的 ie11。我做了一個(gè)詳盡的調(diào)查,每個(gè)人都使用比那更舊的設(shè)置,他們一致向我保證,他們不在乎網(wǎng)站加載有多快。
#2:每個(gè)webpack包中沒(méi)有 開(kāi)銷/引用 代碼嗎?
是的,這也是真的。
好吧,狗屎:
- more files = 更多 Webpack 引用
- more files = 不壓縮
讓我們量化一下,這樣我們就能確切地知道需要擔(dān)心多少。
好的,我剛做了一個(gè)測(cè)試,一個(gè) 190 KB 的站點(diǎn)拆分成 19 個(gè)文件,增加了大約 2%發(fā)送到瀏覽器的總字節(jié)數(shù)。
因此......在***次訪問(wèn)時(shí)增加 2%,在每次訪問(wèn)之前減少60%直到網(wǎng)站下架。
正確的擔(dān)憂是:完全沒(méi)有。
當(dāng)我測(cè)試1個(gè)文件對(duì)19個(gè)時(shí),我想我會(huì)在一些不同的網(wǎng)絡(luò)上試一試,包括HTTP / 1.1
在 3G 和4G上,這個(gè)站點(diǎn)在有19個(gè)文件的情況下加載時(shí)間減少了30%。
這是非常雜亂的數(shù)據(jù)。 例如,在運(yùn)行2號(hào) 的 4G 上,站點(diǎn)加載時(shí)間為 646ms,然后運(yùn)行兩次之后,加載時(shí)間為1116ms,比之前長(zhǎng)73%,沒(méi)有變化。因此,聲稱 HTTP/2 “快30%” 似乎有點(diǎn)鬼鬼祟祟。
我創(chuàng)建這個(gè)表是為了嘗試量化 HTTP/2 所帶來(lái)的差異,但實(shí)際上我唯一能說(shuō)的是“它可能沒(méi)有顯著的差異”。
真正令人吃驚的是***兩行。那是舊的 Windows 和 HTTP/1.1,我打賭會(huì)慢得多,我想我需把網(wǎng)速調(diào)慢一點(diǎn)。
我從微軟的網(wǎng)站上下載了一個(gè)Windows 7 虛擬機(jī)來(lái)測(cè)試這些東西。它是 IE8 自帶的,我想把它升級(jí)到IE9,所以我轉(zhuǎn)到微軟的IE9下載頁(yè)面…
關(guān)于HTTP/2 的***一個(gè)問(wèn)題,你知道它現(xiàn)在已經(jīng)內(nèi)置到 Node中了嗎?如果你想體驗(yàn)一下,我編寫(xiě)了一個(gè)帶有g(shù)zip、brotli和響應(yīng)緩存的小型100行HTTP/2服務(wù)器
,以滿足你的測(cè)試樂(lè)趣。
這就是我要講的關(guān)于 bundle splitting 的所有內(nèi)容。我認(rèn)為這種方法唯一的缺點(diǎn)是必須不斷地說(shuō)服人們加載大量的小文件是可以的。
Code splitting (加載你需要的代碼)
我說(shuō),這種特殊的方法只有在某些網(wǎng)站上才有意義。
我喜歡應(yīng)用我剛剛編造的 20/20 規(guī)則:如果你的站點(diǎn)的某個(gè)部分只有 20% 的用戶訪問(wèn),并且它大于站點(diǎn)的 JavaScript 的 20%,那么你應(yīng)該按需加載該代碼。
如何決定?
假設(shè)你有一個(gè)購(gòu)物網(wǎng)站,想知道是否應(yīng)該將“checkout”的代碼分開(kāi),因?yàn)橹挥?0%的訪問(wèn)者才會(huì)訪問(wèn)那里。
首先要做的是賣更好的東西。
第二件事是弄清楚多少代碼對(duì)于結(jié)賬功能是完全獨(dú)立的。 由于在執(zhí)行“code splitting” 之前應(yīng)始終先“bundle splitting’ ”,因此你可能已經(jīng)知道代碼的這一部分有多大。
它可能比你想象的要小,所以在你太興奮之前做一下加法。例如,如果你有一個(gè) React 站點(diǎn),那么你的 store、reducer、routing、actions 等都將在整個(gè)站點(diǎn)上共享。唯一的部分將主要是組件和它們的幫助類。
因此,你注意到你的結(jié)帳頁(yè)面完全獨(dú)特的代碼是 7KB。 該網(wǎng)站的其余部分是 300 KB。 我會(huì)看著這個(gè),然后說(shuō),我不打算把它拆分,原因如下:
- 提前加載不會(huì)變慢。記住,你是在并行加載所有這些文件。查看是否可以記錄 300KB 和 307KB 之間的加載時(shí)間差異。
* 如果你稍后加載此代碼,則用戶必須在單擊“TAKE MY MONEY”之后等待該文件 - 你希望延遲的最小的時(shí)間。
- Code splitting 需要更改應(yīng)用程序代碼。 它引入了異步邏輯,以前只有同步邏輯。 這不是火箭科學(xué),但我認(rèn)為應(yīng)該通過(guò)可感知的用戶體驗(yàn)改進(jìn)來(lái)證明其復(fù)雜性。
讓我們看兩個(gè) code splitting 的例子。
Polyfills
我將從這個(gè)開(kāi)始,因?yàn)樗m用于大多數(shù)站點(diǎn),并且是一個(gè)很好的簡(jiǎn)單介紹。
我在我的網(wǎng)站上使用了一些奇特的功能,所以我有一個(gè)文件可以導(dǎo)入我需要的所有polyfill, 它包括以下八行:
- // polyfills.js
- require('whatwg-fetch');
- require('intl');
- require('url-polyfill');
- require('core-js/web/dom-collections');
- require('core-js/es6/map');
- require('core-js/es6/string');
- require('core-js/es6/array');
- require('core-js/es6/object');
在 index.js 中導(dǎo)入這個(gè)文件。
- // index-always-poly.js
- import './polyfills';
- import React from 'react';
- import ReactDOM from 'react-dom';
- import App from './App/App';
- import './index.css';
- const render = () => {
- ReactDOM.render(<App />, document.getElementById('root'));
- }
- render(); // yes I am pointless, for now
使用 bundle splitting 的 Webpack 配置,我的 polyfills 將自動(dòng)拆分為四個(gè)不同的文件,因?yàn)檫@里有四個(gè) npm 包。 它們總共大約 25 KB,并且 90% 的瀏覽器不需要它們,因此值得動(dòng)態(tài)加載它們。
使用 Webpack 4 和 import() 語(yǔ)法(不要與 import 語(yǔ)法混淆),有條件地加載polyfill 非常容易。
- import React from 'react';
- import ReactDOM from 'react-dom';
- import App from './App/App';
- import './index.css';
- const render = () => {
- ReactDOM.render(<App />, document.getElementById('root'));
- }
- if (
- 'fetch' in window &&
- 'Intl' in window &&
- 'URL' in window &&
- 'Map' in window &&
- 'forEach' in NodeList.prototype &&
- 'startsWith' in String.prototype &&
- 'endsWith' in String.prototype &&
- 'includes' in String.prototype &&
- 'includes' in Array.prototype &&
- 'assign' in Object &&
- 'entries' in Object &&
- 'keys' in Object
- ) {
- render();
- } else {
- import('./polyfills').then(render);
- }
合理? 如果支持所有這些內(nèi)容,則渲染頁(yè)面。 否則,導(dǎo)入 polyfill 然后渲染頁(yè)面。 當(dāng)這個(gè)代碼在瀏覽器中運(yùn)行時(shí),Webpack 的運(yùn)行時(shí)將處理這四個(gè) npm 包的加載,當(dāng)它們被下載和解析時(shí),將調(diào)用 render() 并繼續(xù)進(jìn)行。
順便說(shuō)一句,要使用 import(),你需要 Babel 的動(dòng)態(tài)導(dǎo)入插件。另外,正如 Webpack 文檔解釋的那樣,import() 使用 promises,所以你需要將其與其他polyfill分開(kāi)填充。
基于路由的動(dòng)態(tài)加載(特定于React)
回到 Alice 的例子,假設(shè)站點(diǎn)現(xiàn)在有一個(gè)“管理”部分,產(chǎn)品的銷售者可以登錄并管理他們所銷售的一些沒(méi)用的記錄。
本節(jié)有許多精彩的特性、大量的圖表和來(lái)自 npm 的大型圖表庫(kù)。因?yàn)槲乙呀?jīng)在做 bundle splittin 了,我可以看到這些都是超過(guò) 100 KB 的陰影。
目前,我有一個(gè)路由設(shè)置,當(dāng)用戶查看 /admin URL時(shí),它將渲染 <AdminPage>。當(dāng)Webpack 打包所有東西時(shí),它會(huì)找到 import AdminPage from './AdminPage.js'。然后說(shuō)"嘿,我需要在初始負(fù)載中包含這個(gè)"
但我們不希望這樣,我們需要將這個(gè)引用放到一個(gè)動(dòng)態(tài)導(dǎo)入的管理頁(yè)面中,比如import('./AdminPage.js') ,這樣 Webpack 就知道動(dòng)態(tài)加載它。
它非常酷,不需要配置。
因此,不必直接引用 AdminPage,我可以創(chuàng)建另一個(gè)組件,當(dāng)用戶訪問(wèn) /admin URL時(shí)將渲染該組件,它可能是這樣的:
- // AdminPageLoader.js
- import React from 'react';
- class AdminPageLoader extends React.PureComponent {
- constructor(props) {
- super(props);
- this.state = {
- AdminPage: null,
- }
- }
- componentDidMount() {
- import('./AdminPage').then(module => {
- this.setState({ AdminPage: module.default });
- });
- }
- render() {
- const { AdminPage } = this.state;
- return AdminPage
- ? <AdminPage {...this.props} />
- : <div>Loading...</div>;
- }
- }
- export default AdminPageLoader;
這個(gè)概念很簡(jiǎn)單,對(duì)吧? 當(dāng)這個(gè)組件掛載時(shí)(意味著用戶位于 /admin URL),我們將動(dòng)態(tài)加載 ./AdminPage.js,然后在狀態(tài)中保存對(duì)該組件的引用。
在 render 方法中,我們只是在等待 <AdminPage> 加載時(shí)渲染 <div>Loading...</div>,或者在加載并存儲(chǔ)狀態(tài)時(shí)渲染 <AdminPage>。
我想自己做這個(gè)只是為了好玩,但是在現(xiàn)實(shí)世界中,你只需要使用 react-loadable ,如關(guān)于 code-splitting 的React文檔 中所述。
總結(jié)
對(duì)于上面總結(jié)以下兩點(diǎn):
- 如果有人不止一次訪問(wèn)你的網(wǎng)站,把你的代碼分成許多小文件。
- 如果你的站點(diǎn)有大部分用戶不訪問(wèn)的部分,則動(dòng)態(tài)加載該代碼。
代碼部署后可能存在的BUG沒(méi)法實(shí)時(shí)知道,事后為了解決這些BUG,花了大量的時(shí)間進(jìn)行l(wèi)og 調(diào)試,這邊順便給大家推薦一個(gè)好用的BUG監(jiān)控工具 Fundebug。