一切都是函数。

函数式编程很棒。随着 React 的推进,越来越多的 JavaScript 前端代码开始基于 FP 原则开发。怎样在日常的编码中使用 FP 呢?让我们来一起写一些日常代码,然后一步步重构它。

场景: 用户来到 /login 页,可能带有 redirect_to 参数,比如 /login?redirect_to=%2Fmy-page。注意 %2Fmy-page 其实是 /my-page 做为 URL 的部分编码后的样子。我们需要将请求参数取出来,然后存储到 local Storage 里,这样登录成功,用户就可以直接跳转到 my-page 页了。

第 0 步:迫切的方法

如果需要紧急上线一个解决方案,应该怎么写呢?需求如下:

  1. 解析请求参数
  2. 得到 redirect_to 的值
  3. 解码
  4. 存储到 localStorage

对于可能会抛出异常的函数,还要用 try catch 语句嵌套起来。综上,代码如下:

function persistRedirectToParam() {
  let parsedQueryParam;
  
  try {
    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;
    }
    
    return decodedPath;
  }
  
  return null;
}

第 1 步:每一步都改为函数

此刻,先忘掉 try catch 语句,把一切都改写成函数。

// let's declare all of the functions we need to have

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() {
  // and let's call them
  
  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 步: 尝试组合函数

现在,presisRedirectToParams 看起来更像是其它 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 步: 可读性更强

如果用过 redux 或者 recompose,那么你应该知道 compose。 Compose 是可以接受多个函数的工具函数,它依次调用传入的函数,最终返回一个函数。关于 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);
}

需要注意的是 compse 从右向左执行函数,也就是说,第一个被 compose 链调用的反而是最后一个函数。

站在数学角度讲这不难理解,就像概念描述的那样,从右向左很自然,但对于重构代码的我们来说,更希望是从左向右的顺序。

第 4 步: Piping 以及 flattening

万幸,有 pipepipecompose 做同样的事,它的调用顺序更直观,调用链里的第一个函数首先被执行。

同样的,如果 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
)

// to invoke, persistRedirectToParam(window.location.search);

好事将近,别忘了,我们之前剥离了 try-catch 语句块,无视了一些风险,现在需要以某种方式再将其引入进来。qs.parsestoreRedirectToQuery 是不安全的,方法之一是把他们嵌套在一个 try-catch 语句块内,另一个更函数式的方法是将 try-catch 做为一个函数。

第 5 步:处理函数的异常

有很多库可以搞定它,但是现在需要自己动手。

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

函数的参数是一个 opt 对象,包含了 trypercatcher 函数。当传参时调用 tryer ,异常时调用 catcher,然后返回。现在,不安全的操作可以把它放在 tryer 部分里,失败时处理异常,在 catcher 里给出一个备用的结果 ( 即使是出错了)。

第 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);

这就是最终想要的,但是为了确保可读性以及易于测试,还可以抽离出不抛异常的函数。

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,
)

// to invoke, persistRedirectToParam(window.location.search);

现在我们实现了一下更庞大的函数,同时也是四个高内聚、低耦合、可测试、可复用、高容错、声明式的函数(当然,也更易读)。

还可以使用一些 FP 语法糖来做到更优雅,改天再聊。

原文链接:A practical guide to writing more functional JavaScript,作者:Nadeesha Cabral