使用 typescript 重寫 Ramda 基於函數式編程思想的工具庫 (一)

前言

函數式編程在前端已經成爲了一個很是熱門的話題。在最近幾年裏,有很是多的應用以及工具庫在大量使用着函數式編程思想。好比 react 的高階函數(HOC)、咱們今天的主角 Ramda 等。javascript

同時 typescript 在最近也是很是火的,衆多的應用以及工具庫都在使用 typescript 進行重構,vue3.0 全面擁抱 typescriptreact 也能夠和 typescript 很好的結合在一塊兒。html

因此爲了更好的學習 typescript 以及函數式編程,咱們將使用 typescript 重構 Ramda 工具庫。咱們將從如何構建工具庫開始,直到發佈本身的 npm 包。前端

1、所涉及到的技術

你須要具有必定的 npmtypescript 的知識。vue

  • lernajava

    咱們將每個函數都發布成一個單獨的包,因此咱們使用 lerna 作統一的管理。固然你能夠選擇所有發佈成一個包, 使用 babel 或者 webpack 開始 treeshaking 來處理。這裏稍後咱們會詳細講到。react

  • rollup 用來編譯、打包咱們的工具庫。webpack

  • jest 用來作單元測試git

  • eslint 用來代碼校驗,結合 @typescript-eslint/eslint-plugin@typescript-eslint/parser 來校驗 typscript , 代替 tslintgithub

  • prettier 結合 eslint-config-prettiereslint-plugin-prettier 來美化咱們的代碼。web

  • commitizenhuskylint-staged 等來規範咱們的 commit 提交信息,便於咱們生成 changelog

2、一些常見的概念

我相信一說到函數式編程不少同窗都能說出一些概念出來,好比:純函數、高階函數、函數柯里化、函數組合等等。其實你們日常也會用到函數式編程,好比常見的 mapfilterreduce 等函數。那麼到底什麼是函數式編程呢?

函數式編程是一種編程範式(聲明式、命令式)。主要是利用函數將運算過程封裝起來,經過組合各類函數來計算結果。

其餘的概念本文不在羅列,感興趣的同窗能夠參考一下文章:

這裏想重點講一下 Pointfree, 不使用所要處理的值,只合成運算過程。 中文能夠譯爲無值風格。 咱們直接經過例子來理解 Pointfree, 部分例子拷貝自 Scott Sauyet 的文章 《Favoring Curry》,那篇文章能幫助你深刻理解柯里化,強烈推薦閱讀。

下面是一段服務器返回的 JSON 數據。

var data = {
    result: "SUCCESS",
    interfaceVersion: "1.0.3",
    requested: "10/17/2013 15:31:20",
    lastUpdated: "10/16/2013 10:52:39",
    tasks: [
        {id: 104, complete: false,            priority: "high",
                  dueDate: "2013-11-29",      username: "Scott",
                  title: "Do something",      created: "9/22/2013"},
        {id: 105, complete: false,            priority: "medium",
                  dueDate: "2013-11-22",      username: "Lena",
                  title: "Do something else", created: "9/22/2013"},
        {id: 107, complete: true,             priority: "high",
                  dueDate: "2013-11-22",      username: "Mike",
                  title: "Fix the foo",       created: "9/22/2013"},
        {id: 108, complete: false,            priority: "low",
                  dueDate: "2013-11-15",      username: "Punam",
                  title: "Adjust the bar",    created: "9/25/2013"},
        {id: 110, complete: false,            priority: "medium",
                  dueDate: "2013-11-15",      username: "Scott",
                  title: "Rename everything", created: "10/2/2013"},
        {id: 112, complete: true,             priority: "high",
                  dueDate: "2013-11-27",      username: "Lena",
                  title: "Alter all quuxes",  created: "10/5/2013"}
        // , ...
    ]
};
複製代碼

如今的要求是,找到用戶 Scott 的全部未完成的任務,並按照日期的升序排序, 而且只返回必要的數據。

[
    {id: 110, title: "Rename everything", dueDate: "2013-11-15", priority: "medium"},
    {id: 104, title: "Do something", dueDate: "2013-11-29", priority: "high"}
]
複製代碼

過程式編程以下

const getIncompleteTaskSummaries = function (membername) {
    return fetchData()
        .then(data => {
            const tasks = data.tasks;
            // 這裏咱們就不使用filter等函數了畢竟filter等函數也屬於函數式(哈哈)
            const results = [];
            for (let i = 0; i < tasks.length; i++) {
                if (tasks[i].username === membername && !tasks[i].complete) {
                    results.push({
                        id: tasks[i].id,
                        dueDate: tasks[i].dueDate,
                        title: tasks[i].title,
                        priority: tasks[i].priority
                    });
                }
            }
            
            return results.sort((a, b) => a.dueDate - b.dueDate);
        });
}
複製代碼

上面的代碼不只可讀性差並且是脆弱的,很容易出錯。

咱們使用 Ramda 提供的函數,使用 Pointfree 風格改寫一下:

const getIncompleteTaskSummaries = function (membername) {
    return fetchData()
        .then(R.prop('tasks'))
        .then(R.filter(R.propEq('username', membername)))
        .then(R.reject(R.propEq('complete', true)))
        .then(R.map(R.pick(['id', 'dueDate', 'title', 'priority'])))
        .then(R.sortBy(R.prop('dueDate')));
}
複製代碼

另一種寫法就是利用函數組合把各個 then 的函數組合在一塊兒:

// 獲取 tasks 屬性
const selectTasks = R.prop('tasks');
// 過濾指定的用戶
const filterMember = member => R.filter(R.propEq('username', member));
// 排除已經完成的任務
const excludeCompletedTasks = R.reject(R.propEq('complete', true));
// 選取指定的屬性
const selectFields = R.map(R.pick(['id', 'dueDate', 'title', 'priority']));
// 根據日期升序排序
const sortByDueDate = R.sortBy(R.prop('dueDate'));

const getIncompleteTaskSummaries = function (membername) {
    return fetchData().then(
        R.pipe(
            selectTasks,
            filterMember(membername),
            excludeCompletedTasks,
            selectFields,
            sortByDueDate
        )
    );
}
複製代碼

上面的代碼也很一目瞭然。這裏有的同窗會有一些疑問,函數組合不是 compose 嗎,這裏怎麼使用 pipe,其實他們的原理是同樣的。只不過 compose 組合的函數的執行順序是從右到左的:

// 超級簡單版 compose
const compose = (f, g) => x => f(g(x));
const add1 = x => x + 1;
const mul5 = x => x * 5;
compose(mul5, add1)(2)  // => 15
compose(add1, mul5)(2)  // => 11
複製代碼

這樣的話多少有一點很差理解,pipe 讓組合函數的執行順序變成從左到右,加強可讀性。

有的同窗又可能會說我可使用 ES6 寫出更簡單的方法:

const getIncompleteTaskSummaries = function (membername) {
    return fetchData().then(data => {
        const tasks = data.tasks;
        const filterByName = tasks.filter(t => t.username === membername);
        const filterIncomplete = filterByName.filter(t => !t.complete);
        const selectFields = filterIncomplete.map(m => ({
            id: m.id,
            dueDate: m.dueDate,
            title: m.title,
            priority: m.priority
        }));
        
        return selectFields.sort((a, b) => a - b);
    });
}
複製代碼

但其實你有沒有發現這裏面也用到了函數式編程的思想,但他仍是有一些問題的,它的可讀性仍是不夠好,同時如何咱們的需求有變更不在是獲取 Tasks 了,那麼下面的全部的代碼都會有問題,由於他們每一步都用到了上一步的變量。而函數式則不一樣,只須要改動函數組合的部分就能夠了。

從上面的例子中咱們也能夠看到函數式編程,須要定義不少單一功能的函數,而後經過函數組合來知足不一樣的需求,在工做中頻繁定義這些函數也是不現實的,Ramda 爲咱們提供了不少這些的函數,方便咱們的平常開發。

好了,回到主題,咱們將一步步使用 ts 重構 Ramda 工具庫。

使用 lerna 初始化工程

通常咱們初始化一個工程的話是這樣的:

mkdir lib-demo
cd lib-demo
npm init
複製代碼

而後根據命令一步一步執行,若是咱們要把 Ramda 的每一個函數都發布成一個 npm 包的話,那就要重複上面的過程。但這裏實際上是有一些問題的:

  • issue 管理混亂
  • changelog難以整合,須要人工梳理變更的 repo, 在加以整合。
  • 版本更新麻煩,須要同步依賴

這其實就是 multirepo 傳統的作法, 即按模塊分紅多個代碼庫。與之對應就是 monorepo, 將全部的模塊放在同一個 repo 中,每一個模塊單獨發佈,但全部的模塊使用與該 repo 統一的版本號(例如 ReactBabelvue-cli)。

Lerna is a tool that optimizes the workflow around managing multi-package repositories with git and npm.

多模塊管理工具,用來幫助維護monorepo。

如何使用 lerna 來初始化一個工程呢?

// 安裝 lerna
npm i lerna -g
// or
yarn global add lerna

mkdir lib-demo
cd lib-demo
lerna init
複製代碼

根據命令一步一步便可。lerna 初始化後的目錄結構是

├── lerna.json # lerna配置文件
├── package.json
└── packages # 包存放文件夾
複製代碼

若是能夠獲得上面的結構,說明你已經初始化完成了。

下一篇文章咱們將繼續學習更多 lerna 的用法,如何建立一個模塊、如何處理依賴、如何下載依賴(全局依賴,各個模塊不一樣的依賴)、如何發佈等等。

參考文章

相關文章
相關標籤/搜索