透過 babel 製作自己的第三方套件

溫靖輝
AsiaYo Engineering
Published in
12 min readSep 30, 2022

前言

一般來說,在公司專案或自己的 side project,前端都會導入一些好用的第三方套件,以 React 來說,UI Component Libraries 就有 MUIAnt Design 等,或是 hook 的集合庫 ​​react-use

上述舉例的套件都是相當普遍且實用的,但有時候我們也會有客製或修改成自己的元件或功能的時候,在 AsiaYo 團隊內就實作了許多客製元件,因此這次的需求是希望將自己的 components 額外實作在另一個 repo,以利套件的開發與維護。

由於這次在初期實作上,有點摸不清應該如何進行,經過多方的爬文(以及看別人的原始碼),才規劃出實作的方向,因此想透過撰寫文章來紀錄並且分享此次的實作經驗給正在瀏覽的各位!也希望可以透過此篇文章來幫助有需要創建第三方套件的讀者們一種實作的方式!

在本篇文章就以簡單的 React 加上 styled-components 製作元件,並且透過本文主軸 — babel 來進行編譯,使其他專案都可以導入並使用!

實作一個 UI Library 專案

(文章下方亦提供 github 連結,有興趣者可自行 clone 試試!)

安裝套件

此次示例用的套件與版本是 React 17 + babel 7 + styled-components,建立起專案並安裝下列套件:

yarn add react@17 react-dom@17 styled-componentsyarn add @babel/cli @babel/core @babel/preset-env @babel/react –D

因為此次有用到 styled-components 實作,因此也不要忘記安裝 babel-plugin-styled-components

yarn add babel-plugin-styled-components

實作元件

此範例我們實作兩個簡單的元件:buttonswitch

新增 src 資料夾並分別在底下再建各自的資料夾,內部新增 index.js

src 下的資料夾結構

src/button/index.js:

(簡單示例利用 styled-components 實作 props 的傳遞改變 css 的方式)

src/switch/index.js:

接著我們在 src 的資料夾下新增 index.js 來導出各個元件

src/index.js:

以上基礎的範例已初步完成,接下來就是要用 babel 來編譯我們的 code 了!

babel 編譯

若有不理解 babel 作用的朋友們,可以至 babel 官網 查看介紹,透過官網的介紹可得知

babel 是將採用 ECMAScript 2015+ 語法編寫的代碼轉換為向後兼容的 JavaScript 語法,以便能夠運行在當前和舊版本的瀏覽器或其他環境中。

簡而言之就是,將你的程式碼轉換為瀏覽器所看得懂的語法!

要實作到這一點,我們必須在 root 層新增全局配置用的 babel.config.js:

@babel/preset-env:直接包含大部分的相容性轉換用 plugin,前身為 babel-preset-latest (babel6),亦可以透過配置參數進行針對性的語法轉換及 polyfill 的部分引入,這裡我們不多探討。

@babel/preset-react:編譯 react 語法與 jsx 用。

babel-plugin-styled-components:提供了一些相當有用的功能,例如:通過向每個組件的樣式添加唯一標示符,避免 client side 與 server side 的 class 產生不同而導致不匹配。

接著我們在 package.json 內的 scripts 補上此段並執行:

babel [路徑] –out-dir [輸出路徑]

編譯結果會產出 lib 資料夾,並且每隻檔案都已經透過 babel 編譯完成:

(編譯完成後的結果,會產生 lib 資料夾)

再回到 package.json 修改 main 的路徑,作用是來指定專案程式的進入點:

最後,我們先打開 lib/index.js 來看看:

可以看到的是,轉換成了 exports 物件的形式,若在專案中直接使用會得到 uncaught reference error: exports is not defined的錯誤。

目前瀏覽器還不支持 exports 這個屬性,因此需要透過轉譯工具來轉譯為 es5 的語法,可以用很多打包工具去實現。

在此我們使用 minimal-react-webpack-babel-setup 此 repo 快速 clone 一個專案下來,只需額外在 clone 下來的 package.json 補上 dependency 即可利用裡頭寫好的 webpack 來測試看看我們的套件啦!(寫好不要忘記下 yarn install

minimal-react-webpack-babel-setup 此 repo 的 package.json 補上我們的 ui-library

實測結果

在 clone 下來的 repo 的 App.js 試著引用我們的 ui-library 並使用:

(專案透過 webpack 打包後顯示的結果)

到了這階段,基本上已經是完成了我們想達成的需求了👏!

延伸思考

只是既然都做到這個份上了,我們不妨會再延伸思考一件事情,就是實際上引用進來之後,所佔的 bundle 大小是多少呢?

目前我們是基於 es module 進行開發,使用的專案透過 webpack 打包的時候,會希望做到 tree shaking 的功能,因此我們來試試透過 webpack-bundle-analyzer 來查看看我們的 ui-library 實際上的 bundle 狀況。

不熟悉 tree shaking 的朋友可至 webpack 官方文件 內查閱,
或者是參考這篇「原來程式碼打包也有這麼多眉角 - 淺談 Tree Shaking 機制」!

首先在測試專案內 yarn add webpack-bundle-analyzer -D,接著在 webpack.config.js 補上下列程式碼。

(測試 repo 的) webpack.config.js:

新增 require & 在 plugins 補上 BundleAnalyzerPlugin

以及在 package.json 補上 scripts "build": "webpack --mode=production",因為正常看打包的時機點還是著重在 prod mode 居多。

先試個範例,只在 App.js 下 import Button 這個元件並使用,接著執行 yarn build,打包出來會出現怎麼樣的狀況:

webpack-bundle-analyzer 解析出來的結果圖

果不其然的把 Switch 也拉進來了!雖然目前看起來不會是個很嚴重的情況,但實際狀況一定不會是區區兩三個元件這麼簡單,因此我們必須要做到活用 es module 的 tree shaking 才算真正的做到了優化!

回到上面的議題,雖然提到是基於 es module 進行開發,但是實際上透過 babel 進行編譯的結果是預設的 cjs 格式,而非預期要的 esm 格式,因此我們要將 babel 轉譯的 config 設定好才可以達成目標。

@babel/preset-env 中有個 modules 參數,可以看到官網對於這個參數的介紹:

(擷自此處)

意指將 es 模塊語法轉為設置的 type,default 為auto,其實即為 commonjs (cjs) 格式,而設置為false的話代表不會轉換 es 模塊,這就是我們想要達成的!因此來修改一下 package.json 裡面的 script 指令。

package.json:

設置了兩種指令,build:cjs build:es,同時再用 build 來依序執行兩個指令,為了配合這個改法,接著來修改 babel.config.js的設置。

babel.config.js:

使用 npm_lifecycle_event 來查出我們目前跑的是哪一條 script,來決定是否修改 @babel/preset-env 的 modules 參數,如此一來執行 yarn build 的時候就可以根據上述的判斷來將兩種格式分別編譯出來了!

lib 基本上不會有任何異動,但我們來看看新增出來的 esm 吧!

(新增指令後產生出的 esm 資料夾)

esm/index.js:

居然跟原先 src/index.js 長得一樣!這是因為我們沒有將 code 轉譯成 cjs 格式,所以就照常輸出了!我們可以看看 esm 下的 button 長什麼樣子。

esm/button/index.js:

從這個檔案的輸出可以得知,依舊保留了 es module 的 import / export 寫法,但也一定程度的編譯了 code。

在處理 babel 編譯的問題過後,那麼我們要怎麼樣讓使用這個套件的專案可以真正的載入 es module 呢?回過頭來看看 package.json,我們設定過 “main” 的路徑,先前有提及其作用是「指定專案程式的進入點」,再描述清楚一點就是:main 定義了 npm 的入口文件,在 browser 環境和 node 環境均可使用

一般來說定義main即可,但其實還有個定義,即是modulemodule 是用來定義 npm 套件中 esm 規範的入口文件,同樣在browser 環境node 環境均可使用,因此我們必須要充分的利用它!

package.json 新增如下:

但即使這樣設置了,要怎麼保證開發的專案再引用自製的套件時,是採用 esm 的路徑呢?這就要再深入探討到 npm 套件的加載優先級的議題了,可以至此文章深度研究,在此容許我不過多的深討,直接向大家簡述答案~

依照正常的使用情境應該為 web browser + webpack bundle + esm 引入
因此使用的入口優先順序即為:browser = browser+mjs > module > browser+cjs > main

可以知道在我們的使用情境下,會優先走向的路徑即為我們設置的 module 路徑啦~!(撒花

提醒:由於還是得兼容不支援的 node 版本以及 bundler,因此還是得保留 main 的出口設置,而不是只導出 module 路徑。

馬上來試試看新推上去的套件有沒有完美的引入 esm!(由於我們沒有上傳至 npm 及控管版本號,記得要將 lock 檔移除,如:yarn.lock,再重新 yarn install)

(修改專案後再次透過 webpack-bundle-analyzer 檢查後的結果,原始圖片過長,為了保留完整性就沒有特別修剪圖片,請見諒)

可以看到依舊在只有 import Button 的時候,的確是載入了 esm 路徑下的檔案,但到底為什麼 Switch 依舊跟著出來亂?說好的 tree shaking 呢!?

讓我們再度查回 webpack 的官方文件中對於 tree shaking 的解釋,透過解釋發現,原來是我們要在 package.json 中加入 sideEffects 屬性作為標記,可以表明哪些文件是 es 模塊,才可以安全的刪除文件中未使用的部分!

因此我們在 package.json 補上以下代碼:

在此要特別解釋一下為什麼是 false,在官方文件中提及,如果你保證所有的代碼都不包含副作用,可將屬性直接標記為 false 即可。

例如若是有類似載入css 的情況發生:

import "global.css"

上述狀況是有引用進來的,只是程式並沒有特別的針對這個 import 再去做「實際」的使用。所謂「實際」的使用就像我們示例所撰寫:

import { Button } from "ui-library"

並且我們真的有在 jsx 文件內使用到 Button,這種情況下就不會被視為可移除的代碼

如果你的代碼確實有一些副作用,sideEffects 應該更改為提供 array 的方式。(例如 antd 的 package.json 下有特別標出樣式的部分)

(擷自 ant-design repo)

照著上述的步驟去執行,最後再來重新安裝一次我們的套件來看看 bundle analyzer 的結果:

(final 版本)

終於達到我們想要的具有 es module tree shaking 效果的套件啦!(大撒花~~~

本文採用的是一步一步的引導完成一個小 repo,再逐步地去優化,因此非常感謝您願意閱讀!

如有任何問題,都歡迎留言批評指教!

最後,來個工商服務時間!
AsiaYo Mid/Sr Frontend Engineer & Mid/Sr UI/UX Designer 招募中! 我們的工程師文化是:

  • Technique enthusiasts 熱愛技術,精進自我
  • Play fun 運用所學做出又酷又好玩的東西
  • Problem solving 成為一個 problem solver
  • Team working 透過團隊集思廣益,選擇最好的開發語言及系統架構
  • Give it a try 擁抱改變,錯了就修正

歡迎來了解職缺內容,成為我們的夥伴(CakeResume

--

--