阅读 685

[译] 编写函数式的 JavaScript 实用指南

一切皆为函数

函数式编程很棒。随着 React 的引入,越来越多的 JavaScript 前端代码正在考虑 FP 原则。但是我们如何在我们编写的日常代码中开始使用 FP 思维模式?我将尝试使用日常代码块并逐步重构它。

我们的问题:用户来到我们的登录页面链接后会带一个redirect_to 参数。就像/login?redirect_to =%2Fmy-page。请注意,当%2Fmy-page 被编码为 URL 的一部分时,它实际上是/ my-page。我们需要提取此参数,并将其存储在本地存储中,以便在完成登录后,可以将用户重定向到 my-page页面。

第 0 步:必要的方法

如果我们以最简单方式来呈现这个解决方案,我们将如何编写它?我们需要如下几个步骤

  1. 解析链接后参数。
  2. 获取 redirect_to 值。
  3. 解码该值。
  4. 将解码后的值存储在 localStorage 中。

我们还必须将不安全的函数放到try catch块中。有了这些,我们的代码将如下所示:

function persistRedirectToParam() {
  let parsedQueryParam;
  try {
    //获取连接后的参数{redirect_to:'/my-page'}
    parsedQueryParam = qs.parse(window.location.search); // https://www.npmjs.com/package/qs
  } catch (e) {
    console.log(e);
    return null;
  }
  //获取到参数
  const redirectToParam = parsedQueryParam.redirect_to;
  if (redirectToParam) {
    const decodedPath = decodeURIComponent(redirectToParam);
    try {
      localStorage.setItem('REDIRECT_TO', decodedPath);
    } catch (e) {
      console.log(e);
      return null;
    }
    //返回  my-page
    return decodedPath;
  }
  return null;
}
复制代码

第 1 步:将每一步写为函数

暂时,让我们忘记 try catch 块并尝试将所有内容表达为函数。

// // 让我们声明所有我们需要的函数

const parseQueryParams = query => qs.parse(query);

const getRedirectToParam = parsedQuery => parsedQuery.redirect_to;

const decodeString = string => decodeURIComponent(string);

const storeRedirectToQuery = redirectTo => localStorage.setItem('REDIRECT_TO', redirectTo);

function persistRedirectToParam() {
 //使用它们

  const parsed = parseQueryParams(window.location.search);

  const redirectTo = getRedirectToParam(parsed);

  const decoded = decodeString(redirectTo);

  storeRedirectToQuery(decoded);

  return decoded;
}
复制代码

当我们开始将所有“结果”用函数的方式表示时,我们会看到我们可以从主函数体中重构的内容。这样处理后,我们的函数变得更容易理解,并且更容易测试。

早些时候,我们将测试主要函数作为一个整体。但是现在,我们有 4 个较小的函数,其中一些只是代理其他函数,因此需要测试的足迹要小得多。

让我们识别这些代理函数,并删除代理,这样我们就可以减少一些代码。

const getRedirectToParam = parsedQuery => parsedQuery.redirect_to;

const storeRedirectToQuery = redirectTo => localStorage.setItem('REDIRECT_TO', redirectTo);

function persistRedirectToParam() {
  const parsed = qs.parse(window.location.search);

  const redirectTo = getRedirectToParam(parsed);

  const decoded = decodeURIComponent(redirectTo);

  storeRedirectToQuery(decoded);

  return decoded;
}
复制代码

第 2 步 尝试编写函数式

好的。现在,似乎 persistRedirectToParam 函数是 4 个其他函数的“组合”让我们看看我们是否可以将此函数编写为合成,从而消除我们存储为 const 的中间结果。

const getRedirectToParam = (parsedQuery) => parsedQuery.redirect_to;

// we have to re-write this a bit to return a result.
const storeRedirectToQuery = (redirectTo) => {
  localStorage.setItem("REDIRECT_TO", redirectTo)
  return redirectTo;
};

function persistRedirectToParam() {
  const decoded = storeRedirectToQuery(
    decodeURIComponent(
      getRedirectToParam(
        qs.parse(window.location.search)
      )
    )
  )

  return decoded;
}
复制代码

这很好。但是我同情读取这个嵌套函数调用的人。如果有办法解开这个混乱,那就太棒了。

第 3 步 更具可读性的组合

如果你已经完成了以上的一些重构,那么你就会遇到composeCompose 是一个实用函数,它接受多个函数,并返回一个逐个调用底层函数的函数。还有其他很好的资源来学习 composition,所以我不会在这里详细介绍。

使用 compose,我们的代码将如下所示:

const compose = require('lodash/fp/compose');
const qs = require('qs');

const getRedirectToParam = parsedQuery => parsedQuery.redirect_to;

const storeRedirectToQuery = redirectTo => {
  localStorage.setItem('REDIRECT_TO', redirectTo);
  return redirectTo;
};

function persistRedirectToParam() {
  const op = compose(
    storeRedirectToQuery,
    decodeURIComponent,
    getRedirectToParam,
    qs.parse
  );

  return op(window.location.search);
}
复制代码

compose 内的函数执行顺序为从右向左,即最右边的函数(最后一个参数)最先执行,执行完的结果作为参数传递给前一个函数。因此,在 compose 链中调用的第一个函数是最后一个函数。

如果你是一名数学家并且熟悉这个概念,这对你来说不是一个问题,所以你自然会从右到左阅读。但对于熟悉命令式代码的其他人来说,我们想从左到右阅读。

第 4 步 pipe(管道)和扁平化

幸运的是这里有pipe(管道)compose 做了同样的事情,但是执行顺序和 compose 是相反的,因此链中的第一个函数最先执行,执行完的结果作为参数传递给下一个函数。

而且,似乎我们的 persistRedirectToParams 函数已经成为另一个我们称之为 op 的函数的包装器。换句话说,它所做的只是执行op。我们可以摆脱包装并“扁平化”我们的函数。

const pipe = require('lodash/fp/pipe');
const qs = require('qs');

const getRedirectToParam = parsedQuery => parsedQuery.redirect_to;

const storeRedirectToQuery = redirectTo => {
  localStorage.setItem('REDIRECT_TO', redirectTo);
  return redirectTo;
};

const persistRedirectToParam = fp.pipe(
  qs.parse,
  getRedirectToParam,
  decodeURIComponent,
  storeRedirectToQuery
);
复制代码

差不多了。请记住,我们适当地将 try-catch 块留在后面,以使其达到正确的状态?好的接下来,我们需要一些方式来介绍它。qs.parse 和 storeRedirectToQuery 都是不安全。一种选择是使它们成为包装函数并将它们放在 try-catch 块中。另一种函数式方式是将 try-catch 表示为一种函数。

第 5 步 作为函数的异常处理

有一些实用程序做到了这一点,但让我们自己尝试写一些东西。

function tryCatch(opts) {
  return args => {
    try {
      return opts.tryer(args);
    } catch (e) {
      return opts.catcher(args, e);
    }
  };
}
复制代码

我们的函数在这里需要一个包含 tryer 和 catcher 函数的 opts 对象。它将返回一个函数,当使用参数调用时,使用所述参数调用 tryer 并在失败时调用 catcher。现在,当我们有不安全的操作时,我们可以将它们放入 tryer 部分,如果它们失败,则从捕获器部分进行救援并提供安全结果(甚至记录错误)。

第 6 步 把所有东西放在一起

因此,考虑到这一点,我们的最终代码如下:

const pipe = require('lodash/fp/pipe');
const qs = require('qs');

const getRedirectToParam = parsedQuery => parsedQuery.redirect_to;

const storeRedirectToQuery = redirectTo => {
  localStorage.setItem('REDIRECT_TO', redirectTo);
  return redirectTo;
};

const persistRedirectToParam = fp.pipe(
  tryCatch({
    tryer: qs.parse,
    catcher: () => {
      return {
        redirect_to: null // we should always give back a consistent result to the subsequent function
      };
    }
  }),
  getRedirectToParam,
  decodeURIComponent,
  tryCatch({
    tryer: storeRedirectToQuery,
    catcher: () => null // if localstorage fails, we get null back
  })
);

// to invoke, persistRedirectToParam(window.location.search);
复制代码

这或多或少是我们想要的。但是为了确保代码的可读性和可测试性得到改善,我们也可以将“安全”函数(tryCatch 函数)分解出来。

const pipe = require('lodash/fp/pipe');
const qs = require('qs');

const getRedirectToParam = parsedQuery => parsedQuery.redirect_to;

const storeRedirectToQuery = redirectTo => {
  localStorage.setItem('REDIRECT_TO', redirectTo);
  return redirectTo;
};

const safeParse = tryCatch({
  tryer: qs.parse,
  catcher: () => {
    return {
      redirect_to: null // we should always give back a consistent result to the subsequent function
    };
  }
});

const safeStore = tryCatch({
  tryer: storeRedirectToQuery,
  catcher: () => null // if localstorage fails, we get null back
});

const persistRedirectToParam = fp.pipe(
  safeParse,
  getRedirectToParam,
  decodeURIComponent,
  safeStore
);
复制代码

现在,我们得到的是一个更强大功能的函数,由 4 个独立的函数组成,这些函数具有高度内聚性,松散耦合,可以独立测试,可以独立重用,考虑异常场景,并且具有高度声明性。

有一些 FP 语法糖使这变得更好,但是这是以后的某一天。

如果发现译文存在错误或其他需要改进的地方请指出。

关注下面的标签,发现更多相似文章
评论