函數式編程在前端已經成爲了一個很是熱門的話題。在最近幾年裏,有很是多的應用以及工具庫在大量使用着函數式編程思想。好比 react
的高階函數(HOC)、咱們今天的主角 Ramda
等。javascript
同時 typescript
在最近也是很是火的,衆多的應用以及工具庫都在使用 typescript
進行重構,vue3.0
全面擁抱 typescript
,react
也能夠和 typescript
很好的結合在一塊兒。html
因此爲了更好的學習 typescript
以及函數式編程,咱們將使用 typescript
重構 Ramda
工具庫。咱們將從如何構建工具庫開始,直到發佈本身的 npm
包。前端
你須要具有必定的 npm
、typescript
的知識。vue
lerna
java
咱們將每個函數都發布成一個單獨的包,因此咱們使用 lerna
作統一的管理。固然你能夠選擇所有發佈成一個包, 使用 babel
或者 webpack
開始 treeshaking
來處理。這裏稍後咱們會詳細講到。react
rollup
用來編譯、打包咱們的工具庫。webpack
jest
用來作單元測試git
eslint
用來代碼校驗,結合 @typescript-eslint/eslint-plugin
、@typescript-eslint/parser
來校驗 typscript
, 代替 tslint
。github
prettier
結合 eslint-config-prettier
、eslint-plugin-prettier
來美化咱們的代碼。web
commitizen
、husky
、lint-staged
等來規範咱們的 commit
提交信息,便於咱們生成 changelog
。
我相信一說到函數式編程不少同窗都能說出一些概念出來,好比:純函數、高階函數、函數柯里化、函數組合等等。其實你們日常也會用到函數式編程,好比常見的 map
、filter
、reduce
等函數。那麼到底什麼是函數式編程呢?
函數式編程是一種編程範式(聲明式、命令式)。主要是利用函數將運算過程封裝起來,經過組合各類函數來計算結果。
其餘的概念本文不在羅列,感興趣的同窗能夠參考一下文章:
這裏想重點講一下 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
包的話,那就要重複上面的過程。但這裏實際上是有一些問題的:
這其實就是 multirepo
傳統的作法, 即按模塊分紅多個代碼庫。與之對應就是 monorepo
, 將全部的模塊放在同一個 repo 中,每一個模塊單獨發佈,但全部的模塊使用與該 repo 統一的版本號(例如 React
、Babel
、vue-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
的用法,如何建立一個模塊、如何處理依賴、如何下載依賴(全局依賴,各個模塊不一樣的依賴)、如何發佈等等。