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

一切皆为函数javascript

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

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

第 0 步:必要的方法

若是咱们以最简单方式来呈现这个解决方案,咱们将如何编写它?咱们须要以下几个步骤git

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

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

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 块并尝试将全部内容表达为函数。npm

// // 让咱们声明全部咱们须要的函数

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 的中间结果。bash

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 语法糖使这变得更好,可是这是之后的某一天。

若是发现译文存在错误或其余须要改进的地方请指出。

相关文章
相关标签/搜索