一、前言
從 0 到 1 學習的朋友可參考前置學習文章:
前置文章 學習 Webpack5 之路(基礎篇) 對 webpack 的概念做了簡單介紹,學習 Webpack5 之路(實踐篇) 則從配置著手,用 webpack 搭建了一個 SASS + TS + React 的專案。
本篇將從優化開發體驗、加快編譯速度、減小打包體積、加快載入速度 4 個角度出發,介紹如何對 webpack 專案進行優化。
本文依賴的 webpack 版本資訊如下:
- webpack-cli@4.7.2
- webpack@5.46.0
二、優化效率工具
在優化開始之前,需要做一些準備工作。
安裝以下 webpack 外掛,幫助我們分析優化效率:
- progress-bar-webpack-plugin:檢視編譯進度;
- speed-measure-webpack-plugin:檢視編譯速度;
- webpack-bundle-analyzer:打包體積分析。
1. 編譯進度條
一般來說,中型專案的首次編譯時間為 5-20s,沒個進度條等得多著急,通過 progress-bar-webpack-plugin 外掛檢視編譯進度,方便我們掌握編譯情況。
安裝:
npm i -D progress-bar-webpack-plugin
webpack.common.js
配置方式如下:
const chalk = require("chalk");
const ProgressBarPlugin = require("progress-bar-webpack-plugin");
module.exports = {
plugins: [
// 進度條
new ProgressBarPlugin({
format: ` :msg [:bar] ${chalk.green.bold(":percent")} (:elapsed s)`,
}),
],
};
貼心的為進度百分比新增了加粗和綠色高亮態樣式。
包含內容、進度條、進度百分比、消耗時間,進度條效果如下:
2. 編譯速度分析
優化 webpack 構建速度,首先需要知道是哪些外掛、哪些 loader 耗時長,方便我們針對性的優化。
通過 speed-measure-webpack-plugin 外掛進行構建速度分析,可以看到各個 loader、plugin 的構建時長,後續可針對耗時 loader、plugin 進行優化。
安裝:
npm i -D speed-measure-webpack-plugin
webpack.dev.js
配置方式如下:
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();
module.exports = smp.wrap({
// ...webpack config...
});
包含各工具的構建耗時,效果如下:
注意:這些灰色文字的樣式,是因為我在 vscode 終端執行的,導致有顏色的字型都顯示為灰色,換個終端就好了,如 iTerm2。
3. 打包體積分析
同樣,優化打包體積,也需要先分析各個 bundle 檔案的佔比大小,來進行鍼對優化。
使用 webpack-bundle-analyzer 檢視打包後生成的 bundle 體積分析,將 bundle 內容展示為一個便捷的、互動式、可縮放的樹狀圖形式。幫助我們分析輸出結果來檢查模組在何處結束。
安裝:
npm i -D webpack-bundle-analyzer
webpack.prod.js
配置方式如下:
const BundleAnalyzerPlugin =
require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
module.exports = {
plugins: [
// 打包體積分析
new BundleAnalyzerPlugin(),
],
};
包含各個 bundle 的體積分析,效果如下:
三、優化開發體驗
1. 自動更新
自動更新 指的是,在開發過程中,修改程式碼後,無需手動再次編譯,可以自動編譯程式碼更新編譯後程式碼的功能。
webpack 提供了以下幾種可選方式,實現自動更新功能:
webpack 官方推薦的方式是 webpack-dev-server
,在 學習 Webpack5 之路(實踐篇) - DevServer 章節 已經介紹了 webpack-dev-server 幫助我們在程式碼發生變化後自動編譯程式碼實現自動更新的用法,在這裡不重複贅述。
這是針對開發環境的優化,修改
webpack.dev.js
配置。
2. 熱更新
熱更新 指的是,在開發過程中,修改程式碼後,僅更新修改部分的內容,無需重新整理整個頁面。
2.1 修改 webpack-dev-server 配置
使用 webpack 內建的 HMR 外掛,更新 webpack-dev-server 配置。
webpack.dev.js
配置方式如下:
module.export = {
devServer: {
contentBase: "./dist",
hot: true, // 熱更新
},
};
2.2 引入 react-refresh-webpack-plugin
使用 react-refresh-webpack-plugin 熱更新 react 元件。
安裝:
npm install -D @pmmmwh/react-refresh-webpack-plugin react-refresh
webpack.dev.js
配置方式如下:
const ReactRefreshWebpackPlugin = require("@pmmmwh/react-refresh-webpack-plugin");
module.exports = {
plugins: [
new webpack.HotModuleReplacementPlugin(),
new ReactRefreshWebpackPlugin(),
],
};
遇到的問題:
配置了 SpeedMeasurePlugin 後,熱更新就無效了,會提示 runtime is undefined
。
解決方案:
僅在分析構建速度時開啟 SpeedMeasurePlugin 外掛,這裡我們先關閉 SpeedMeasurePlugin 的使用,來檢視熱更新效果。
最終效果:
更新 react 元件程式碼時,無需重新整理頁面,僅更新元件部分。
四、加快構建速度
1. 更新版本
1.1 webpack 版本
使用最新的 webpack 版本,通過 webpack 自身的迭代優化,來加快構建速度。
這一點是非常有效的,如 webpack5 較於 webpack4,新增了持久化快取、改進快取演算法等優化,webpack5 新特性可檢視 參考資料。
1.2 包管理工具版本
將 Node.js 、package 管理工具(例如 npm
或者 yarn
)更新到最新版本,也有助於提高效能。較新的版本能夠建立更高效的模組樹以及提高解析速度。
本文依賴的版本資訊如下:
webpack@5.46.0
node@14.15.0
npm@6.14.8
2. 快取
2.1 cache
通過配置 webpack 持久化快取 cache: filesystem
,來快取生成的 webpack 模組和 chunk,改善構建速度。
簡單來說,通過 cache: filesystem
可以將構建過程的 webpack 模板進行快取,大幅提升二次構建速度、打包速度,當構建突然中斷,二次進行構建時,可以直接從快取中拉取,可提速 90% 左右。
webpack.common.js
配置方式如下:
module.exports = {
cache: {
type: "filesystem", // 使用檔案快取
},
};
引入快取後,首次構建時間將增加 15%,二次構建時間將減少 90%,效果如下:
2.2 dll ❌
在 webpack 官網構建效能 中看到關於 dll 的介紹:
dll 可以為更改不頻繁的程式碼生成單獨的編譯結果。可以提高應用程式的編譯速度。
我興沖沖的開始尋找 dll 的相關配置說明,太複雜了,接著找到了一個輔助配置 dll 的外掛 autodll-webpack-plugin,結果上面直接寫了 webpack5 開箱即用的持久快取是比 dll 更優的解決方案。
所以,不用再配置 dll 了,上面介紹的 cache 明顯更香。
2.3 cache-loader ❌
沒錯,cache-loader 也不需要引入了,上面的 cache 已經幫助我們快取了。
3. 減少 loader、plugins
每個的 loader、plugin 都有其啟動時間。儘量少地使用工具,將非必須的 loader、plugins 刪除。
3.1 指定 include
為 loader 指定 include,減少 loader 應用範圍,僅應用於最少數量的必要模組,。
rule.exclude 可以排除模組範圍,也可用於減少 loader 應用範圍.
webpack.common.js
配置方式如下:
module.exports = {
rules: [
{
test: /\.(js|ts|jsx|tsx)$/,
include: paths.appSrc,
use: [
{
loader: "esbuild-loader",
options: {
loader: "tsx",
target: "es2015",
},
},
],
},
],
};
定義 loader 的 include 後,構建時間將減少 12%,效果如下:
3.2 管理資源
使用 webpack 資源模組 (asset module) 代替舊的 assets loader(如 file-loader
/url-loader
/raw-loader
等),減少 loader 配置數量。
配置方式如下:
module.exports = {
rules: [
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
include: [paths.appSrc],
type: "asset/resource",
},
],
};
引入資源模組後,構建時間將減少 7%,效果如下:
4. 優化 resolve 配置
resolve 用來配置 webpack 如何解析模組,可通過優化 resolve 配置來覆蓋預設配置項,減少解析範圍。
4.1 alias
alias 可以建立 import
或 require
的別名,用來簡化模組引入。
webpack.common.js
配置方式如下:
module.exports = {
resolve: {
alias: {
"@": paths.appSrc, // @ 代表 src 路徑
},
},
};
4.2 extensions
extensions 表示需要解析的檔案型別列表。
根據專案中的檔案型別,定義 extensions,以覆蓋 webpack 預設的 extensions,加快解析速度。
由於 webpack 的解析順序是從左到右,因此要將使用頻率高的檔案型別放在左側,如下我將 tsx
放在最左側。
webpack.common.js
配置方式如下:
module.exports = {
resolve: {
extensions: [".tsx", ".js"], // 因為我的專案只有這兩種型別的檔案,如果有其他型別,需要新增進去。
},
};
4.3 modules
modules 表示 webpack 解析模組時需要解析的目錄。
指定目錄可縮小 webpack 解析範圍,加快構建速度。
webpack.common.js
配置方式如下:
module.exports = {
modules: ["node_modules", paths.appSrc],
};
4.4 symlinks
如果專案不使用 symlinks(例如 npm link
或者 yarn link
),可以設定 resolve.symlinks: false
,減少解析工作量。
webpack.common.js
配置方式如下:
module.exports = {
resolve: {
symlinks: false,
},
}
優化 resolve 配置後,構建時間將減少 1.5%,效果如下:
5. 多程式
上述可以看到 sass-loader 的構建時間有 1.56s,佔據了整個構建過程的 60%,那麼有沒有方法來加快 sass-loader 的構建速度呢?
可以通過多程式來實現,試想將 sass-loader 放在一個獨立的 worker 池中執行,就不會阻礙其他 loader 的構建了,可以大大加快構建速度。
5.1 thread-loader
通過 thread-loader 將耗時的 loader 放在一個獨立的 worker 池中執行,加快 loader 構建速度。
安裝:
npm i -D thread-loader
webpack.common.js
配置方式如下:
module.exports = {
rules: [
{
test: /\.module\.(scss|sass)$/,
include: paths.appSrc,
use: [
"style-loader",
{
loader: "css-loader",
options: {
modules: true,
importLoaders: 2,
},
},
{
loader: "postcss-loader",
options: {
postcssOptions: {
plugins: [["postcss-preset-env"]],
},
},
},
{
loader: "thread-loader",
options: {
workerParallelJobs: 2,
},
},
"sass-loader",
].filter(Boolean),
},
],
};
webpack 官網 提到
node-sass
中有個來自 Node.js 執行緒池的阻塞執行緒的 bug。 當使用thread-loader
時,需要設定workerParallelJobs: 2
。
由於 thread-loader 引入後,需要 0.6s 左右的時間開啟新的 node 程式,本專案程式碼量小,可見引入 thread-loader 後,構建時間反而增加了 0.19s。
因此,我們應該僅在非常耗時的 loader 前引入 thread-loader。
效果如下:
5.2 happypack ❌
happypack 同樣是用來設定多執行緒,但是在 webpack5 就不要再使用 happypack 了,官方也已經不再維護了,推薦使用上文介紹的 thread-loader。
6. 區分環境
在 學習 Webpack5 之路(實踐篇) - 模式(mode) 章節 已經介紹了 webpack 的不同模式的內建優化。
在開發過程中,切忌在開發環境使用生產環境才會用到的工具,如在開發環境下,應該排除 [fullhash]
/[chunkhash]
/[contenthash]
等工具。
同樣,在生產環境,也應該避免使用開發環境才會用到的工具,如 webpack-dev-server 等外掛。
7. 其他
7.1 devtool
不同的 devtool
設定,會導致效能差異。
在大多數情況下,最佳選擇是 eval-cheap-module-source-map
。
詳細區分可至 webpack devtool 檢視。
webpack.dev.js
配置方式如下:
export.module = {
devtool: 'eval-cheap-module-source-map',
}
7.2 輸出結果不攜帶路徑資訊
預設 webpack 會在輸出的 bundle 中生成路徑資訊,將路徑資訊刪除可小幅提升構建速度。
module.exports = {
output: {
pathinfo: false,
},
};
}
四、減小打包體積
1. 程式碼壓縮
體積優化第一步是壓縮程式碼,通過 webpack 外掛,將 JS、CSS 等檔案進行壓縮。
1.1 JS 壓縮
使用 TerserWebpackPlugin 來壓縮 JavaScript。
webpack5 自帶最新的 terser-webpack-plugin
,無需手動安裝。
terser-webpack-plugin
預設開啟了 parallel: true
配置,併發執行的預設數量: os.cpus().length - 1
,本文配置的 parallel 數量為 4,使用多程式併發執行壓縮以提高構建速度。
webpack.prod.js
配置方式如下:
const TerserPlugin = require("terser-webpack-plugin");
module.exports = {
optimization: {
minimizer: [
new TerserPlugin({
parallel: 4,
terserOptions: {
parse: {
ecma: 8,
},
compress: {
ecma: 5,
warnings: false,
comparisons: false,
inline: 2,
},
mangle: {
safari10: true,
},
output: {
ecma: 5,
comments: false,
ascii_only: true,
},
},
}),
],
},
};
體積減小 10%,效果如下:
1.1 ParallelUglifyPlugin ❌
你可能有聽過 ParallelUglifyPlugin 外掛,它可以幫助我們多程式壓縮 JS,webpack5 的 TerserWebpackPlugin 預設就開啟了多程式和快取,無需再引入 ParallelUglifyPlugin。
1.2 CSS 壓縮
使用 CssMinimizerWebpackPlugin 壓縮 CSS 檔案。
和 optimize-css-assets-webpack-plugin 相比,css-minimizer-webpack-plugin 在 source maps 和 assets 中使用查詢字串會更加準確,而且支援快取和併發模式下執行。
CssMinimizerWebpackPlugin
將在 Webpack 構建期間搜尋 CSS 檔案,優化、壓縮 CSS。
安裝:
npm install -D css-minimizer-webpack-plugin
webpack.prod.js
配置方式如下:
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
module.exports = {
optimization: {
minimizer: [
new CssMinimizerPlugin({
parallel: 4,
}),
],
},
};
由於 CSS 預設是放在 JS 檔案中,因此本示例是基於下章節將 CSS 程式碼分離後的效果。
2. 程式碼分離
程式碼分離能夠把程式碼分離到不同的 bundle 中,然後可以按需載入或並行載入這些檔案。程式碼分離可以用於獲取更小的 bundle,以及控制資源載入優先順序,可以縮短頁面載入時間。
2.1 抽離重複程式碼
SplitChunksPlugin 外掛開箱即用,可以將公共的依賴模組提取到已有的入口 chunk 中,或者提取到一個新生成的 chunk。
webpack 將根據以下條件自動拆分 chunks:
- 新的 chunk 可以被共享,或者模組來自於
node_modules
資料夾; - 新的 chunk 體積大於 20kb(在進行 min+gz 之前的體積);
- 當按需載入 chunks 時,並行請求的最大數量小於或等於 30;
- 當載入初始化頁面時,併發請求的最大數量小於或等於 30;
通過 splitChunks 把 react 等公共庫抽離出來,不重複引入佔用體積。
注意:切記不要為 cacheGroups 定義固定的 name,因為 cacheGroups.name 指定字串或始終返回相同字串的函式時,會將所有常見模組和 vendor 合併為一個 chunk。這會導致更大的初始下載量並減慢頁面載入速度。
webpack.prod.js
配置方式如下:
module.exports = {
splitChunks: {
// include all types of chunks
chunks: "all",
// 重複打包問題
cacheGroups: {
vendors: {
// node_modules裡的程式碼
test: /[\\/]node_modules[\\/]/,
chunks: "all",
// name: 'vendors', 一定不要定義固定的name
priority: 10, // 優先順序
enforce: true,
},
},
},
};
將公共的模組單獨打包,不再重複引入,效果如下:
2.2 CSS 檔案分離
MiniCssExtractPlugin 外掛將 CSS 提取到單獨的檔案中,為每個包含 CSS 的 JS 檔案建立一個 CSS 檔案,並且支援 CSS 和 SourceMaps 的按需載入。
安裝:
npm install -D mini-css-extract-plugin
webpack.common.js
配置方式如下:
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
plugins: [new MiniCssExtractPlugin()],
module: {
rules: [
{
test: /\.module\.(scss|sass)$/,
include: paths.appSrc,
use: [
"style-loader",
isEnvProduction && MiniCssExtractPlugin.loader, // 僅生產環境
{
loader: "css-loader",
options: {
modules: true,
importLoaders: 2,
},
},
{
loader: "postcss-loader",
options: {
postcssOptions: {
plugins: [["postcss-preset-env"]],
},
},
},
{
loader: "thread-loader",
options: {
workerParallelJobs: 2,
},
},
"sass-loader",
].filter(Boolean),
},
],
},
};
注意:MiniCssExtractPlugin.loader 要放在 style-loader 後面。
效果如下:
2.3 最小化 entry chunk
通過配置 optimization.runtimeChunk = true
,為執行時程式碼建立一個額外的 chunk,減少 entry chunk 體積,提高效能。
webpack.prod.js
配置方式如下:
module.exports = {
optimization: {
runtimeChunk: true,
},
};
}
效果如下:
3. Tree Shaking(搖樹)
搖樹,顧名思義,就是將枯黃的落葉搖下來,只留下樹上活的葉子。枯黃的落葉代表專案中未引用的無用程式碼,活的樹葉代表專案中實際用到的原始碼。
3.1 JS
JS Tree Shaking 將 JavaScript 上下文中的未引用程式碼(Dead Code)移除,通過 package.json
的 "sideEffects"
屬性作為標記,向 compiler 提供提示,表明專案中的哪些檔案是 "pure(純正 ES2015 模組)",由此可以安全地刪除檔案中未使用的部分。
Dead Code 一般具有以下幾個特徵:
- 程式碼不會被執行,不可到達;
- 程式碼執行的結果不會被用到;
- 程式碼只會影響死變數(只寫不讀)。
3.1.1 webpack5 sideEffects
通過 package.json 的 "sideEffects"
屬性,來實現這種方式。
{
"name": "your-project",
"sideEffects": false
}
需注意的是,當程式碼有副作用時,需要將 sideEffects
改為提供一個陣列,新增有副作用程式碼的檔案路徑:
{
"name": "your-project",
"sideEffects": ["./src/some-side-effectful-file.js"]
}
新增 TreeShaking 後,未引用的程式碼,將不會被打包,效果如下:
3.1.2 對元件庫引用的優化
webpack5 sideEffects 只能清除無副作用的引用,而有副作用的引用則只能通過優化引用方式來進行 Tree Shaking
。
1. lodash
類似 import { throttle } from 'lodash'
就屬於有副作用的引用,會將整個 lodash 檔案進行打包。
優化方式是使用 import { throttle } from 'lodash-es'
代替 import { throttle } from 'lodash'
,lodash-es 將 Lodash 庫匯出為 ES 模組,支援基於 ES modules 的 tree shaking,實現按需引入。
2. ant-design
ant-design 預設支援基於 ES modules 的 tree shaking,對於 js 部分,直接引入 import { Button } from 'antd'
就會有按需載入的效果。
假如專案中僅引入少部分元件,import { Button } from 'antd'
也屬於有副作用,webpack 不能把其他元件進行 tree-shaking。這時可以縮小引用範圍,將引入方式修改為 import { Button } from 'antd/lib/button'
來進一步優化。
3.2 CSS
上述對 JS 程式碼做了 Tree Shaking 操作,同樣,CSS 程式碼也需要搖搖樹,打包時把沒有用的 CSS 程式碼搖走,可以大幅減少打包後的 CSS 檔案大小。
使用 purgecss-webpack-plugin 對 CSS Tree Shaking。
安裝:
npm i purgecss-webpack-plugin -D
因為打包時 CSS 預設放在 JS 檔案內,因此要結合 webpack 分離 CSS 檔案外掛 mini-css-extract-plugin
一起使用,先將 CSS 檔案分離,再進行 CSS Tree Shaking。
webpack.prod.js
配置方式如下:
const glob = require("glob");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const PurgeCSSPlugin = require("purgecss-webpack-plugin");
const paths = require("paths");
module.exports = {
plugins: [
// 打包體積分析
new BundleAnalyzerPlugin(),
// 提取 CSS
new MiniCssExtractPlugin({
filename: "[name].css",
}),
// CSS Tree Shaking
new PurgeCSSPlugin({
paths: glob.sync(`${paths.appSrc}/**/*`, { nodir: true }),
}),
],
};
上面為了測試 CSS 壓縮效果,我引入了大量無效 CSS 程式碼,因此 Tree Shaking 效果也非常明顯,效果如下:
3. CDN
上述是對 webpack 配置的優化,另一方面還可以通過 CDN 來減小打包體積。
這裡引入 CDN 的首要目的為了減少打包體積,因此僅僅將一部分大的靜態資源手動上傳至 CDN,並修改本地引入路徑。下文的加快載入速度,將介紹另一種 CDN 優化手段。
將大的靜態資源上傳至 CDN:
- 字型:壓縮並上傳至 CDN;
- 圖片:壓縮並上傳至 CDN。
五、加快載入速度
1. 按需載入
通過 webpack 提供的 import() 語法 動態匯入 功能進行程式碼分離,通過按需載入,大大提升網頁載入速度。
使用方式如下:
export default function App() {
return (
<div>
hello react 111
<Hello />
<button onClick={() => import("lodash")}>載入lodash</button>
</div>
);
}
效果如下:
2. 瀏覽器快取
瀏覽器快取,就是進入某個網站後,載入的靜態資源被瀏覽器快取,再次進入該網站後,將直接拉取快取資源,加快載入速度。
webpack 支援根據資源內容,建立 hash id,當資源內容發生變化時,將會建立新的 hash id。
配置 JS bundle hash,webpack.common.js
配置方式如下:
module.exports = {
// 輸出
output: {
// 僅在生產環境新增 hash
filename: ctx.isEnvProduction
? "[name].[contenthash].bundle.js"
: "[name].bundle.js",
},
};
配置 CSS bundle hash,webpack.prod.js
配置方式如下:
module.exports = {
plugins: [
// 提取 CSS
new MiniCssExtractPlugin({
filename: "[hash].[name].css",
}),
],
};
配置 optimization.moduleIds,讓公共包 splitChunks 的 hash 不因為新的依賴而改變,減少非必要的 hash 變動,webpack.prod.js
配置方式如下:
module.exports = {
optimization: {
moduleIds: "deterministic",
},
};
通過配置 contenthash/hash,瀏覽器快取了未改動的檔案,僅重新載入有改動的檔案,大大加快載入速度。
3. CDN
將所有的靜態資源,上傳至 CDN,通過 CDN 加速來提升載入速度。
webpack.common.js
配置方式如下:
export.modules = {
output: {
publicPath: ctx.isEnvProduction ? 'https://xxx.com' : '', // CDN 域名
},
}
六、優化前後對比
在倉庫程式碼僅 webpack 配置不同的情況下,檢視優化前後對比。
1. 構建速度
型別 | 首次構建 | 未修改內容二次構建 | 修改內容二次構建 |
---|---|---|---|
優化前 | 2.7s | 2.7s | 2.7s |
優化後 | 2.7s | 0.5s | 0.3s |
2. 打包體積
型別 | 體積大小 |
---|---|
優化前 | 250 kb |
優化後 | 231 kb |
七、總結
從上章節 [優化前後對比] 可知,在小型專案中,新增過多的優化配置,作用不大,反而會因為額外的 loader、plugin 增加構建時間。
在加快構建時間方面,作用最大的是配置 cache,可大大加快二次構建速度。
在減小打包體積方面,作用最大的是壓縮程式碼、分離重複程式碼、Tree Shaking,可最大幅度減小打包體積。
在加快載入速度方面,按需載入、瀏覽器快取、CDN 效果都很顯著。
本篇就介紹到這兒啦,有更好的 webpack 優化方式歡迎評論區告訴我哦~
本文原始碼:
希望能對你有所幫助,感謝閱讀~
別忘了點個贊鼓勵一下我哦,筆芯 ❤️
參考資料
- Tree-Shaking 效能優化實踐 - 原理篇
- Tree-Shaking 效能優化實踐 - 實踐篇
- 三十分鐘掌握 Webpack 效能優化
- 玩轉 webpack,使你的打包速度提升 90%
- 帶你深度解鎖 Webpack 系列(優化篇)
- Webpack 5 中的新特性
- 辛辛苦苦學會的 webpack dll 配置,可能已經過時了
歡迎關注凹凸實驗室部落格:aotu.io
或者關注凹凸實驗室公眾號(AOTULabs),不定時推送文章: