
簡介ztools是一個面向JavaScript開發(fā)者的輕量前端工具集圍繞異步編程、模板渲染與依賴管理三個方向提供實用能力內置兼容IE舊版本的ES6 Promise方案便于在老舊瀏覽器中編寫現代異步代碼Plato模板引擎以簡潔的方式完成數據與DOM的綁定適合快速搭建視圖層Eidos依賴注入封裝則有助于降低模塊耦合提升代碼可測試性與復用性。壓縮包共17個文件以js源碼為主同時包含html示例、README說明、package.json配置等結構清晰便于按模塊閱讀與調試整體僅13KB適合學習或直接引入項目。目前已有308人學習下載。通過閱讀源碼與示例讀者可以了解Promise polyfill的實現思路、簡易模板引擎的解析過程以及依賴注入容器的設計方式對于想深入前端工程化與工具封裝的開發(fā)者是份不錯的參考資料。 接手團隊那會兒我翻了一遍現有代碼庫發(fā)現一個很真實的現象deepClone至少有三個版本在兩個模塊里各寫各的防抖函數有四個人用自己的實現日期格式化更是五花八門有的返回字符串、有的返回數組還有的干脆直接報錯。這些代碼本身沒問題但維護的人換了一茬又一茬風格已經割裂到沒法看了。所以就有了ztools這個前端工具集項目。它不是要做一個“什么都有”的大雜燴包而是把團隊里反復出現、已經驗證過的工具函數和邏輯沉淀成一套統一、可測試、可按需引入的基礎庫。如果你也需要把散落的公共代碼整合起來或者是想搭建自己的第一個前端工具庫這篇內容應該能給你一些可以直接抄作業(yè)的思路。1. 為什么需要一套前端工具集1.1 團隊代碼里那些“復制粘貼”之痛一個中大型前端項目跑兩三年之后公共邏輯的重復率會高得嚇人。最典型的癥狀就是每個新同學入職第一個任務大概率是“把這里的請求封裝改成統一的”然后你會發(fā)現項目里已經有四套request封裝三份localStorage讀寫工具還有兩個行為互相矛盾的 cookie 操作函數。重復代碼的問題不只是浪費幾行字節(jié)真正可怕的是“改不動”和“不敢刪”。當你發(fā)現線上有個日期格式化的 bug你要在所有用到格式化的地方逐個排查因為每一個實現的行為都可能略有不同。而當你試圖刪掉其中一個工具函數時又怕某個隱晦的調用點突然報錯。這種狀態(tài)下任何重構都是在走鋼絲。我建ztools的初衷就是把這些重復邏輯撈出來給它們一個統一的歸宿。它解決的問題不是“代碼少寫幾行”而是讓團隊對“公共能力”只有一個認知來源、一份測試用例、一個維護入口。1.2 工具集的設計目標與邊界工具集不是框架它的定位要非??酥?。我在項目規(guī)劃階段就定下了幾條設計原則只做基礎能力不做業(yè)務邏輯。通用的函數、hooks、類型定義可以收進來但跟具體業(yè)務綁定的數據解析、權限判斷、接口封裝一律不進。按需引入不能拖累主包體積。用戶引一個debounce不能被迫加載整個工具集。類型完整用法統一。所有函數都要有精確的 TypeScript 類型所有命名都要符合一套規(guī)范調用方式保持一致。必須經過測試。工具函數是最容易被大家依賴的底層代碼沒有測試覆蓋出了問題就是全線崩潰。這些邊界約束了工具集的發(fā)展方向也幫我在后續(xù)無數次“要不要把這個也放進來”的討論中快速做出判斷。工具集的價值不在于大而在于清晰。2. 技術選型與整體架構2.1 為什么用 TypeScript 加雙格式構建ztools的技術棧選擇不算激進但都是經過實際驗證的。整個工具集用 TypeScript 編寫構建產物同時輸出 ESMES Module和 CJSCommonJS兩種格式部分工具還附帶瀏覽器直接可用的 IIFE 版本。TypeScript 的核心收益不是“有類型”而是“讓使用方在編譯期就拿到提示”。工具函數一旦在團隊內廣泛使用類型定義就是隱形的文檔。比如debounce函數如果沒有類型約束調用方很容易搞混wait和immediate參數的順序而有了類型提示這種問題幾乎不可能發(fā)生。雙格式構建的原因更務實現在前端項目基本都跑在 Vite 這類現代構建工具上ESM 是主流但還有一些老項目用的是 webpack 4 甚至直接是 Node 端的 CommonJS 引用如果只有 ESM 產物它們在require(ztools)的時候會直接報錯。所以我在package.json里用exports字段做了條件導出{ name: ztools, version: 0.3.2, type: module, main: ./dist/index.cjs, module: ./dist/index.js, types: ./dist/index.d.ts, exports: { .: { types: ./dist/index.d.ts, import: ./dist/index.js, require: ./dist/index.cjs }, ./utils/*: { types: ./dist/utils/*.d.ts, import: ./dist/utils/*.js, require: ./dist/utils/*.cjs } } }同樣地很多場景下按需引入也依賴 ESM 的tree-shaking。如果你只用了ztools里的formatDate構建工具應該有能力把其他函數全部搖掉讓最終的包體積增加量幾乎可以忽略。這一點我在第 5 節(jié)會展開講。2.2 目錄結構與模塊劃分ztools的目錄結構從一開始就是按“領域”劃分的而不是按“類型”堆在一起。這樣做的好處是使用方一看到路徑就能猜到功能歸屬維護的人也知道該往哪里加代碼。ztools/ ├── src/ │ ├── utils/ # 基礎函數工具 │ │ ├── debounce.ts │ │ ├── throttle.ts │ │ ├── deepClone.ts │ │ ├── formatDate.ts │ │ ├── formatNumber.ts │ │ ├── getUrlParam.ts │ │ └── storage.ts │ ├── hooks/ # React Hooks │ │ ├── useDebounce.ts │ │ ├── useThrottle.ts │ │ ├── useLocalStorage.ts │ │ └── usePrevious.ts │ ├── dom/ # 瀏覽器 DOM 操作 │ │ ├── scrollToBottom.ts │ │ └── copyToClipboard.ts │ ├── types/ # 公共類型定義 │ │ └── index.ts │ └── index.ts # 入口統一導出 ├── tests/ ├── docs/ ├── package.json └── tsup.config.ts入口文件index.ts會統一導出所有公共 API但每個子目錄也支持單獨路徑引用這樣既能兼顧“一次性引入全部”的方便也能滿足“只引一個函數”的精準訴求。子路徑導出在package.json的exports里已經做了映射不需要額外配置。2.3 tree-shaking 與按需引入的設計很多工具庫明明功能很少但打出來的包卻有幾百 KB核心原因就是沒有做按需設計。ztools在這個問題上做了幾個層級的控制內部模塊拆分除入口文件外每個工具函數一個文件互不依賴。這樣任何構建工具在分析依賴圖時都能把未用到的模塊隔離掉。保持較少的內部依賴每個函數盡可能不依賴工具集內其他函數避免“引一個函數拖進來一串”的連鎖效應。聲明sideEffects: false在package.json中明確告知構建工具這個包里的文件不會在 import 時產生副作用可以放心刪除未使用的導出。這一點經常有人漏掉但少了它 tree-shaking 可能就失效了。{ sideEffects: false }3. 核心實現與漸進搭建過程3.1 已實現的工具類目與典型實現ztools目前積累了幾十種工具按使用頻率分為三類。第一類是高頻基礎函數比如debounce、throttle、deepClone、formatDate、getUrlParam第二類是 React Hooks比如useDebounce、useThrottle、useLocalStorage第三類是瀏覽器環(huán)境下的輔助函數比如copyToClipboard、scrollToBottom。以debounce為例拋開各種邊界情況不談它的核心實現其實只有十幾行。但真正的難點在于類型定義、參數兼容、以及this上下文的保持。我參考了業(yè)界常見的實現最后寫出來是這個樣子export function debounceA extends unknown[], R( fn: (...args: A) R, wait 300, immediate false ) { let timer: ReturnTypetypeof setTimeout | null null; let result: R | undefined; const debounced function(this: unknown, ...args: A) { const later () { timer null; if (!immediate) { result fn.apply(this, args); } }; const callNow immediate timer null; if (timer ! null) { clearTimeout(timer); } timer setTimeout(later, wait); if (callNow) { result fn.apply(this, args); } return result as R; }; debounced.cancel function() { if (timer ! null) { clearTimeout(timer); timer null; } }; return debounced; }實現完之后還有兩件事必須做一個是讓copyToClipboard這類函數兼容瀏覽器對剪貼板權限的限制——在不支持navigator.clipboard的環(huán)境下自動降級到document.execCommand(copy)另一個是給所有函數補充 JSDoc 注釋明確參數含義、返回值、使用示例和注意事項。后面這一件事當時覺得耽誤時間后來發(fā)現文檔的價值比代碼本身還大。3.2 工具函數的單元測試與質量保障一個工具函數如果沒有測試那它和臨時腳本沒有本質區(qū)別。ztools的測試選的是 Vitest理由很直接它跟 Vite 的配置天然打通跑起來快而且對 TypeScript 的支持不需要額外配置。測試用例的覆蓋范圍我一般會遵循“正常值 邊界值 異常值”的思路。拿formatDate來說正常值就是傳一個時間戳或 Date 對象期望返回格式化的字符串邊界值要覆蓋0時間戳、跨年的日期、閏年 2 月 29 日異常值要覆蓋undefined、null、非法字符串等這個時候最好能讓函數拋出一個明確的錯誤而不是靜默返回一個詭異結果。import { describe, expect, it } from vitest; import { formatDate } from ../src/utils/formatDate; describe(formatDate, () { it(formats timestamp correctly, () { const timestamp new Date(2024-03-15T08:30:00).getTime(); expect(formatDate(timestamp, YYYY-MM-DD HH:mm)).toBe(2024-03-15 08:30); }); it(handles invalid input by throwing, () { expect(() formatDate(not-a-date, YYYY-MM-DD)).toThrow(); }); });剛開始補測試的時候我會覺得進度變慢了但后來發(fā)現測試真正發(fā)揮作用的時刻是“別人來改你的函數”。沒有測試罩著別人動代碼你心里是懸的有測試罩著他改壞了 CI 第一個跳出來比你在代碼 review 里耳提面命一百遍都管用。3.3 文檔站點與 npm 發(fā)布工具集的另一半價值在于“讓人愿意用、用得明白”。我一開始只在 README 里寫了幾個示例后來被同事反復問“這個函數怎么用、參數是什么”才意識到文檔必須跟上。ztools的文檔方案沒有搞得很重。我選了一個輕量的靜態(tài)文檔生成器把函數說明、示例代碼、參數表、變更記錄集中在一個站點上。每個函數都配一個可折疊的示例區(qū)塊方便讀者直接復制。文檔的源碼放在docs/目錄下和代碼庫同步維護提交代碼時如果改了公共 APICI 會檢查對應文檔是否更新避免出現“代碼改了文檔沒改”的脫節(jié)。npm 發(fā)布流程則完全交給 GitHub Actions。每次打v*標簽自動觸發(fā)構建、跑測試、生成類型聲明然后發(fā)布到配置好的 registry。發(fā)布之后還會同步生成一份最新版 CHANGELOG日志里的版本號、feature、fix 全部從 Git 提交記錄里提取不需要手寫。這個流程一開始搭的時候花了半天但之后每次發(fā)版都是推個 tag 的事省心很多。4. 使用場景與接入方式4.1 在業(yè)務項目中接入業(yè)務項目接入ztools的方式取決于它使用的模塊體系。新項目基本走 ESM 按需引入import { debounce } from ztools; import { useDebounce } from ztools/hooks; const onSearch debounce((keyword: string) { // 搜索請求 }, 500);老項目如果還在用 CommonJS也可以直接const { debounce } require(ztools)因為我前面提到的雙格式構建已經做了兼容。這樣團隊在做技術棧升級遷移期間不需要等所有項目都切到 ESM 才能開始復用工具集。4.2 團隊協作與版本管理工具集既然是給團隊用的版本管理和發(fā)布策略就得有章法。我的做法是采用語義化版本SemVer新增工具函數加minor版本修復 bug 或優(yōu)化實現加patch版本發(fā)生 breaking change 才升major版本。breaking change盡量少出如果非要出必須提前一個版本在文檔和 CHANGELOG 里標注棄用信息給使用方留出遷移時間。比較重要的是要建立“工具集不是某個人的私有物”的共識。任何人想往里面加東西都要發(fā)起 MR說明用途、實現方案、調研過哪些已有方案并附上測試用例。我自己作為維護者一開始會花比較多精力在 review 這些 MR 上但等大家習慣了這套流程工具集會越滾越健康。4.3 后續(xù)擴展與生態(tài)方向ztools目前的規(guī)劃是繼續(xù)往更細分的場景做擴展。一個方向是增加更多 React Hooks比如useEventListener、useMediaQuery、useAsync這些都是業(yè)務里反復出現的需求。另一個方向是提供一些輕量的“配置化”能力比如統一的錯誤捕獲上報入口、統一的日志格式但這類能力要謹慎因為它容易滑向業(yè)務邏輯。還有一個想法是把工具集按領域拆成獨立包比如ztools-utils、ztools-hooks、ztools-dom由同一個 monorepo 管理、統一發(fā)布。這樣團隊里某個項目如果只需要 Hooks可以只裝ztools-hooks。不過這個分解動作會帶來不小的維護成本目前看必要性不高先記在規(guī)劃里。5. 常見問題與排查技巧實錄5.1 tree-shaking 失效打包體積沒降下來這是我被問過最多的問題。癥狀是業(yè)務項目明明只引了一個函數打包產物體積卻增大了幾百 KB。絕大多數情況下原因有三個package.json缺sideEffects: false聲明構建工具不敢動任何模塊。入口文件把所有工具都export出去了雖然理論上 ESM 可以 tree-shaking但如果構建工具配置不當或產物格式不理想還是會失效。使用方可能用了import * as ztools from ztools。這種寫法會保留整個模塊對象導致所有函數都被打包進去。排查思路是先用vite --debug或webpack-bundle-analyzer看產物結構確認哪些模塊被打進去了再一個個排除原因。我實際處理過的一個案例就是某業(yè)務項目把import * as ztools改成具名導入后體積直接少了近 200 KB。5.2 類型聲明丟失或和實際 API 不匹配發(fā)布之后發(fā)現使用方在 TypeScript 里拿不到類型提示或者提示的老類型和實際函數不匹配。這個問題的根源通常是我在發(fā)版時沒有成功生成最新的.d.ts文件或者exports字段里的types路徑指向不對。我的處理方式是構建腳本里顯式用tsc --emitDeclarationOnly生成類型聲明而不是依賴打包工具的附帶產物發(fā)布前在本地用npm pack打一次 tarball檢查里面的文件結構是否符合預期再用一個模擬業(yè)務項目通過npm link做一次真實引用測試。這套檢查做完基本就不會再出現“發(fā)出去之后發(fā)現類型不對”的尷尬了。5.3 瀏覽器兼容性問題集中爆發(fā)部分工具函數在不同瀏覽器里表現不一致比如Intl.DateTimeFormat在部分舊瀏覽器里對中文 locale 支持不完整structuredClone在更早期環(huán)境里根本不存在。工具集必須在代碼里做兼容降級而不能默認使用方瀏覽器都是最新版。我給的策略是在函數實現層面做能力檢測如果環(huán)境不支持基準 API就降級為簡單的模擬實現或拋出明確的警告。同時一定要在文檔里寫清楚每個函數的瀏覽器支持范圍避免業(yè)務在低版本瀏覽器上排查問題到頭來發(fā)現是工具集的問題。5.4 發(fā)布到內部 registry 后安裝失敗這個坑我踩過一次。當時配置了私有 npm registry但發(fā)布流程里沒有正確處理 registry 的認證信息結果 CI 構建能過業(yè)務項目卻怎么都拉不到包。后來我在發(fā)布腳本里顯式指定了 registry 地址和認證環(huán)境變量并在 CI 里加了“安裝驗證”這一步驟發(fā)布完成后立刻在一個臨時目錄里執(zhí)行一次npm install ztools確保安裝鏈路完全暢通再通知團隊使用。經驗就是凡是自動化發(fā)布的流程都要在流程末尾加一個“自檢”環(huán)節(jié)機器不會“覺得沒問題”只有驗證過才是真的沒問題。5.5 工具函數行為不統一導致線上問題有時候不同函數對同一類參數的解析方式不一致比如formatDate會用本地時區(qū)解析時間字符串而getUrlParam里的時間處理用了 UTC 時區(qū)兩個函數聯動時就會出現幾小時的偏差。所以我后來在ztools里立了一條規(guī)矩所有時間相關的函數必須在文檔里明確寫清楚默認時區(qū)并且提供統一的時區(qū)參數入口。這類隱性約定靠代碼 review 很難發(fā)現靠測試用例才能把行為固定下來。最后再說一點維護心得工具集這件事做起來容易堅持維護下去難。我見過不少團隊的工具庫熱度過了之后沒人維護新需求各寫各的慢慢又退化成“歷史遺留代碼”。我的經驗是工具集的生命力不在于代碼多炫而在于邊界清晰、文檔完整、測試覆蓋到位。每次有人提“再加一個函數”的時候先問三個問題——這個邏輯真的通用嗎團隊里有沒有已經在寫的重復實現它能不能配齊測試和文檔如果答案都是肯定的再收進來如果有一個是否定的就先緩一緩。如果你也在規(guī)劃自己的前端工具集建議不用一上來就追求大而全先從業(yè)務項目里撈兩個高頻復用的函數配好測試、寫好文檔、發(fā)布一版讓團隊先“用起來”。跑順了流程再慢慢迭代你會發(fā)現工具集這東西真的是越早做越劃算。本文還有配套的精品資源點擊獲取