redux-saga 中的開源工程實踐

blank

redux-saga 中的開源工程實踐

接觸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記錄,所以初始大小較大)

blank

優勢二: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模塊。為了在一些在線運行環境(例如codesandboxobservablehq )可以通過import 'redux-saga' / require('redux-saga')的形式直接加載redux-saga UMD模塊, package.json也中設置了unpkg字段指向UMD模塊

打包為UMD 模塊的另一個作用是追踪輸出文件的大小。如下圖,對GitHub / travis-ci 進行相應配置之後,在每個PR 的檢查中,bundlesize 會報告本次輸出文件的大小(minify+gzip),以及和master 分支的對比情況。

blank

更多的自動化

本文上方已經介紹了不少工具,redux-saga同時也配置了git hooksnpm scriptstravis-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 閱讀源碼以深入理解原理。

What do you think?

Written by marketer

blank

Facebook工程師CatChen 確認出席FEDAY

blank

Webpack官方成員Sean確認出席FEDAY,發表演講