- 原文地址:A practical guide to writing more functional JavaScript
- 原文作者:Nadeesha Cabral
- 本文永久链接:github-heyushuo-blob
- 译者:heyushuo
一切皆为函数
函数式编程很棒。随着 React 的引入,越来越多的 JavaScript 前端代码正在考虑 FP 原则。但是我们如何在我们编写的日常代码中开始使用 FP 思维模式?我将尝试使用日常代码块并逐步重构它。
我们的问题:用户来到我们的登录页面链接后会带一个redirect_to
参数。就像/login?redirect_to =%2Fmy-page
。请注意,当%2Fmy-page
被编码为 URL
的一部分时,它实际上是/ my-page
。我们需要提取此参数,并将其存储在本地存储中,以便在完成登录后,可以将用户重定向到 my-page
页面。
第 0 步:必要的方法
如果我们以最简单方式来呈现这个解决方案,我们将如何编写它?我们需要如下几个步骤
- 解析链接后参数。
- 获取 redirect_to 值。
- 解码该值。
- 将解码后的值存储在 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 步 更具可读性的组合
如果你已经完成了以上的一些重构,那么你就会遇到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);
}
复制代码
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 语法糖使这变得更好,但是这是以后的某一天。
如果发现译文存在错误或其他需要改进的地方请指出。