使用 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 的用法,如何建立一个模块、如何处理依赖、如何下载依赖(全局依赖,各个模块不一样的依赖)、如何发布等等。

参考文章

相关文章
相关标签/搜索