Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
heyushuo opened this issue Dec 27, 2019 · 0 comments
Open

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

heyushuo opened this issue Dec 27, 2019 · 0 comments

Comments

@heyushuo
Copy link
Owner

[译] 编写函数式的 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 语法糖使这变得更好,但是这是以后的某一天。

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant