函数式编程在前端已经成为了一个很是热门的话题。在最近几年里,有很是多的应用以及工具库在大量使用着函数式编程思想。好比 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
的用法,如何建立一个模块、如何处理依赖、如何下载依赖(全局依赖,各个模块不一样的依赖)、如何发布等等。