redux-saga 中的開源工程實踐
來自專欄前端外刊評論39 人贊了文章
接觸 redux-saga 也有好長一段時間了,這篇文章將主要介紹 redux-saga 中用到的開源工具。維護開源項目是相當耗費精力的一件事,維護者除了需要寫代碼之外,還需要花時間在文檔、教程、解決 issue、答疑等方面上。這些工作包含了大量重複勞動,redux-saga 配置了各種各樣的開源工具,通過自動化的方式減少大量的重複工作。而這其中許多工具也可以應用在我們普通開發者的日常開發中,提升工作效率。
依賴管理與啟動腳本 —— npm
npm 已經是前端的標配了。大部分時候,我們將前端項目從 GitHub 克隆下來之後,便施展一套熟練的起手式: npm install && npm start
。redux-saga 也是一樣,package.json 文件詳細記錄了項目的信息:名稱、版本、開源協議、代碼倉庫地址等,以及一個長長的依賴列表。後面介紹的工具都會出現在該依賴列表中,運行 npm install
時,npm 會將這些依賴安裝到項目的 node_modules 文件夾下。
不同前端項目開啟調試環境(例如開啟 webpack-dev-server)的方式不盡相同,一般開發者會將開啟項目調試環境的命令寫入 start 腳本,npm start
也成為了通用的啟動調試環境的腳本。redux-saga 自帶若干示例項目,我們可以通過在示例目錄下運行 npm start
來運行各個示例。
多 package 管理 —— lerna
與我們的日常項目不一樣的是,redux-saga 倉庫中包含多個 package。其中兩個需要發布在 npm 上:
- 一個 redux-saga 類庫本身,npm 的包名即為 redux-saga
- 另一個是 babel-plugin-redux-saga,用於提升 saga 報錯信息的可讀性
其他 package 為倉庫中的示例項目,不會發布在 npm 中(即 package.json 中 private 欄位為 true
)。每個示例項目都是獨立且完整的 node/npm 工程,用戶可以進入示例目錄運行 npm install && npm start
來查看示例的實際效果。
redux-saga 倉庫下各個 package 目錄結構大致如下:
<redux-saga倉庫> package.json lerna.json packages/ core/ pacakge.json src/ test/ ... babel-plugin-redux-saga/ pacakge.json src/ test/ ... exmaples/ async/ pacakge.json src/ test/ cancellable-counter/ pacakge.json src/ test/ ..... 其他示例項目
從上面的目錄我們可以看出來整個倉庫中有多個 package.json,且分布在不同的目錄下。如果我們手動進入每個目錄並運行 npm install
來安裝依賴,那將是相當繁瑣的。lerna 提供了 bootstrap 命令來解決這個問題,在倉庫目錄執行 lerna bootstrap
,lerna 將會為所有的 package 安裝依賴。當然有的同學會問我用 shell 腳本加上一個循環也可以做到這個啊,為什麼一定要使用 lerna 呢?下面我們來看 「lerna bootstrap」相比於「依次 npm install」的優勢。
優勢一:lerna bootstrap 有更快的安裝速度,更少的依賴佔用空間
在 redux-saga 中共有 8 個示例項目,其中 6 個使用 webpack 來進行打包構建,且不同示例項目使用的 webpack 版本相同。如果我們依次安裝依賴,那麼我們將會安裝 6 份 webpack,每一份安裝位於各個示例的 node_modules 目錄下。顯然,這 6 份安裝中有 5 份都是多餘的。根據 node 查找模塊的演算法,我們只需要在這些模塊的父文件夾(即倉庫文件夾)安裝一份 webpack 即可。
當使用 --hoist 參數 時,lerna 會分析出不同模塊的公共依賴,並將這些公共依賴安裝在倉庫文件夾。當公共依賴版本不一致時(例如上述例子中 5 份 webpack 版本要求為 4.x,另外一份 webpack 要求 3.x),lerna 會將最常用的版本安裝在倉庫文件夾,不一致的那些版本仍會被安裝在各自的模塊文件夾中。
下圖列舉了不同情況下整個 redux-saga 項目的大小和文件數量,我們可以看到使用 hoist 可以減少約 60% 的空間佔用。(下圖中因為初始情況下也包含了完整的 git 記錄,所以初始大小較大)
優勢二:lerna bootstrap 會為 package 之間的相互引用創建符號鏈接
例如我們的示例項目 example/async 依賴於 redux-saga package,並在 package.json 添加了 redux-saga 依賴這一行。那麼在執行 lerna bootstrap 時,lerna 不會再去下載 npm registry 中的版本,而是直接在 example/async/node_modules 文件夾創建一個符號鏈接,指向 packages/core,這樣我們就可以在示例項目中用到最新的 redux-saga 版本。值得一提的是,npm/yarn 也提供了 link 的功能,方便用戶調試自己本地的 package。
redux-saga 中的示例項目都帶有一定的測試,在使用符號鏈接的情況下,這些測試都將使用最新的 redux-saga 版本,幫助我們發現最新版本出現的問題。為了確保測試時使用的是最新版本,redux-saga 將 npm pretest 腳本設置為 npm run build
,確保每次測試都使用最新打包出來的文件。
代碼風格 —— prettier & ESLint
代碼風格本應是仁者見仁智者見智的一件事,但當開發人員較多,且開發人員不可控(redux-saga 社區活躍,不知道誰在什麼時候會貢獻代碼)的時候,選擇偏向性更強、規則更嚴的格式化工具更為合適。redux-saga 使用 prettier 作為格式化工具,並設置了 lint-staged 工具確保所有代碼在提交時都會經過格式化。prettier 是一個 opinionated 的格式化工具,工具自帶一套代碼風格,可供開發者配置的選項並不多;prettier 也是一個非常嚴格的代碼格式化工具,只要代碼的 AST(抽象語法樹)相同,使用該工具就能得到相同的輸出(除了少數空行、換行等例外)。
ESLint 則是一個功能豐富且強大的靜態檢查工具,提供了武裝到牙齒的配置。ESLint 默認包含了 250+ 不同的規則,每個規則擁有若干選項來對單個規則進行配置;ESLint 的插件機制允許開發者安裝插件來使用其他規則,例如非常流行的 eslint-react-plugin 提供了約 80 個 react/JSX 相關規則。
ESLint 規則的粒度非常細緻,例如規則 generator-star-spacing 可用來配置「生成器函數的星號兩邊是否需要空格」,該規則允許我們選擇 before
/ after
/ both
/ neither
中的其中一種,此外,該規則還允許我們針對不同的生成器聲明方式(命名函數 / 匿名函數 / 方法)單獨設置上述空格配置 _(:з」∠)_。
從零開始配置 ESLint 是一件很繁瑣的事情,好在 ESLint 提供了拓展機制,允許我們基於已有的規則集合進行二次配置。ESLint 也提供了 eslint:recommended,該規則集合包含了針對一些常見的錯誤(未定義的變數,無法到達的代碼等)的規則。 redux-saga 使用了 eslint:recommended 與 plugin:react/recommended,這兩個集合基本能夠覆蓋代碼檢查需求。
自動化測試 —— tape
自動化測試這個詞我們已經聽過好多遍,幾乎每本講編程的書,都會有那麼幾個小節介紹自動化測試以及其帶來的好處。自動化測試其實也挺講究,我個人認為測試質量有如下幾個階段:
第一階段,從無到有:我們開始書寫測試用例,我們會寫一些簡單的測試覆蓋一些常見的情況。即使這些測試很簡單,但通過這個測試,我們至少能夠保證代碼在大部分情況下將正常運行。
第二階段,從低覆蓋率到高覆蓋率:我們開始關注一些不太常見的情況,並構造用例來測試代碼在一些邊界條件下是否正常運行。一些工具(例如 jest 所使用的 istanbul)會生成測試覆蓋率(語句覆蓋率,行覆蓋率,分支覆蓋率)報告,會告訴我們每一行代碼是否被執行,執行了多少次。通過這些工具我們不斷補充缺失的測試用例,直至覆蓋率達到一個較高的值。
第三階段,從寫測試到設計測試:我們開始思考如何更好地設計測試用例,我們開始考慮以下這些問題「測試是否足夠小,小到恰好測試我們想測試的代碼單元?」,「測試是否足夠直觀,輸入輸出的可讀性如何,單元測試是否易於構造?」…… 我們不再滿足於「讓代碼通過測試」,而是像設計軟體一樣去設計測試用例,並像核心代碼一樣去維護測試代碼。
單元測試對於基礎類庫是必不可少的。redux-saga 包含了非常完善的自動化測試,每一個 effect 類型都有若干相應的用例來保證其在不同情況下運行正常,同時豐富的測試還涵蓋了 sagaHelper(例如 takeEvery、takeLatest)、數據結構(例如 buffer 與 channel)、typescript 類型、saga monitor 等方面。測試用例一般會在實現功能時就準備好(和功能代碼放在同一個 pull request 中),也會在日常的維護中被不斷改進。
redux-saga 使用 tape 作為自動化測試工具。tape 是一個非常簡單的測試工具,我們需要在測試文件引入 tape,然後使用其提供的函數來書寫測試用例。tape 只是一個簡單的 node 模塊,也沒有什麼魔法,故測試文件都是能夠獨立運行的 JavaScript 文件,我們可以直接使用 node 來運行測試文件。當測試文件較多時,我們可以新建一個文件(例如叫做 index.js),並在該文件中 require 其他測試文件,然後運行 index.js 便能運行所有測試。
編譯與打包 —— babel & rollup
redux-saga 源碼用到了一些尚未進入 ECMAScript 標準的特性,例如 object-rest-spread 特性,所以在發布代碼之前需要配置 babel 對這些代碼進行編譯。一些較新的語言特性也無法運行在低版本的 node 或瀏覽器中,所以 babel 中也配置了 preset-env 來編譯這些語言特性。
考慮到有時用戶也需要直接在瀏覽器(不經過 webpack 打包)中運行 redux-saga,redux-saga 也配置了 rollup 來將代碼直接打包為瀏覽器可用的 UMD 模塊。為了在一些在線運行環境(例如 codesandbox、observablehq)可以通過 import redux-saga
/ require(redux-saga)
的形式直接載入 redux-saga UMD 模塊,package.json 也中設置了 unpkg 欄位指向 UMD 模塊。
打包為 UMD 模塊的另一個作用是追蹤輸出文件的大小。如下圖,對 GitHub / travis-ci 進行相應配置之後,在每個 PR 的檢查中,bundlesize 會報告本次輸出文件的大小(minify+gzip),以及和 master 分支的對比情況。
更多的自動化
本文上方已經介紹了不少工具,redux-saga 同時也配置了 git hooks、npm scripts、travis-ci 來自動化運行這些工具。一些典型的場景如下:
- 將 npm postinstall 腳本設置為
lerna bootstrap
?? 每次安裝依賴之後,自動運行該命令 - 在 git pre-commit 中運行 prettier 和 ESLint ?? 確保每次提交到 git 倉庫的代碼都符合代碼風格
- 在 git pre-push 中運行單元測試 ?? 保證推送至 GitHub 的提交都通過單元測試
- 配置 travis-ci,使用獨立的全新安裝的環境來運行 redux-saga 中的各項測試 ?? 防止「在我這裡上明明可以運行,怎麼到你那裡就不行了」的情況
- 將 npm preset 腳本設置為
npm run build
?? 確保示例項目的測試運行時使用的是最新構建的 redux-saga
redux-saga 中其他的自動化腳本還有很多,這裡就不再一一闡述,感興趣的小夥伴可以直接去看 redux-saga 的 package.json 文件。其實每個自動化腳本背後都有相應的出發點和使用場景,一個一個地分析腳本,並發現其出發點是一件很有趣的事情。
其他工具
redux-saga 還用到了其他許多工具,例如使用 gitbook 從 markdown 文件中生成文檔網站 https://redux-saga.js.org ,例如使用 lint-staged 通過只對提交的文件運行腳本以減少 pre-commit hook 的運行時間,再例如配置了 issue template 和 pull-request template 以保證 issue/PR 儘可能地規範化。
本文中介紹的所有工具對於開源項目都是免費的,其中大部分工具在 GitHub 上開源。網上有許多關於這些工具的教程,一些工具(例如 prettier)的配置成本也不高,我們可以根據自己的需求選擇合適的工具。我覺得我們生活在一個幸福的時代,開源給我們開發者帶來了太多東西,我們能夠免費使用各類工具,隨便一搜就能搜出豐富的教程和資源,也可以在 GitHub 閱讀源碼以深入理解原理。
推薦閱讀:
※開源5個版本basline|遊戲玩家付費金額預測大賽
※GitHub開源項目2018-06-13更新精選
※李開源:鄉村風水案例
※開源PaaS Rainbond發布3.7.0-rc1版本
※Eggjs 和 SOFA 的跨語言互調