原文链接: medium.com/javascript-...
作者:Eric Elliott
想象一下,你正在构建一个 React 应用。你在这个应用的视图上有相当多的事情想要做。
- 检查和更新用户的身份认证状态
- 检查当前活跃的特性,以决定渲染哪些特性(需要持续交付【CD】)
- 打印出每个页面的组件数量
- 渲染一个标准的布局(像导航、边框等等)
像这样的事情通常被称为横切关注点,起初,你并不好这样想他们,你只是厌烦了将一堆模版代码复制粘贴进每个组件中去。比如下面这些代码。
js
const MyPage = ({ user = {}, signIn, features = [], log }) => {
// Check and update user authentication status
useEffect(() => {
if (!user.isSignedIn) {
signIn();
}
}, [user]); // Log each page component mount
useEffect(() => {
log({
type: 'page',
name: 'MyPage',
user: user.id,
});
}, []); return <>
{
/* render the standard layout */
user.isSignedIn ?
<NavHeader>
<NavBar />
{
features.includes('new-nav-feature')
&& <NewNavFeature />
}
</NavHeader>
<div className="content">
{/* our actual page content... */}
</div>
<Footer /> :
<SignInComponent />
}
</>;
};
我们可以通过将所有的函数分别抽象为 Provider 组件来摆脱上面那种令人讨厌的写法。然后我们的页面看起来就像下面一样:
html
const MyPage = ({ user = {}, signIn, features = [], log }) => {
return (
<>
<AuthStatusProvider>
<FeatureProvider>
<LogProvider>
<StandardLayout>
<div className="content">{/* our actual page content... */}</div>
</StandardLayout>
</LogProvider>
</FeatureProvider>
</AuthStatusProvider>
</>
);
};
不过我们还是有一些问题,如果我们的标准横切关注点发生了变化,我们需要在每个页面上更改它们,并使所有页面保持同步。我们还必须记住将 Provider 组件添加到每个页面。
高阶组件
一个更好的解决办法是用高阶组件去包裹我们的页面组件。这是一个函数,他接受一个组件,然后返回一个新的组件。这个组件会渲染新的组件,但是会增加一些额外的功能。我们可以用它来用我们需要的Provider组件包裹我们的页面
js
const MyPage = ({ user = {}, signIn, features = [], log }) => {
return <>{/* our actual page content... */}</>;
};const MyPageWithProviders = withProviders(MyPage);
让我们来看看 logger 用 HOC 实现会看起来像啥样子:
js
const withLogger = (WrappedComponent) => {
return function LoggingProvider ({user, ...props}) {
useEffect(() => {
log({
type: 'page',
name: 'MyPage',
user: user.id
});
}, []);
return <WrappedComponent {...props} />
}
}
函数组合
为了让我们的Provider函数在一起工作的更好,我们可以用函数组合将它们结合起来,放入一个单个的 HOC 里面。函数组合是一个结合两个或多个函数并生成一个新的函数的过程。他是一个很有力量的概念,可以被用来构建复杂的应用。
函数组合是将一个函数应用于另一个函数的返回值。在代数中,它由函数复合运算符表示
scss
(f ∘ g)(x) = f(g(x))
在 Javascript 中,我们可以写一个叫 compose
的函数,并且用它去组成更高阶的组件。
js
const compose = (...fns) => (x) => fns.reduceRight((y, f) => fn(y), x);
const withProviders = compose(
withUser,
withFeatures,
withLogger,
withLayout
)
export default withProviders
现在你可以在任何你需要的地方引入 withProviders
了。不过我们还没做完呢。大多数的应用有很多不同的页面,并且不同的页面有时会有不同的需求。比如说,我们有时不想要展示页脚(例如,在具有无限内容流的页面上)
函数柯里化
柯里化函数是一个一次接受多个参数的函数,通过返回一系列函数,每个函数接受下一个参数。
js
// Add two numbers, curried:
const add = (a) => (b) => a + b;// Now we can specialize the function to add 1 to any number:
const increment = add(1);
这是一个很小的例子,但是柯里化有助于函数组合,因为一个函数只能返回一个值。如果我们想要自定义布局函数以获取额外的参数,最好的解决方案就是将它柯里化。
js
const withLayout = ({ showFooter = true }) =>
(WrappedComponent) => {
return function LayoutProvider ({ features, ...props}) {
return (
<>
<NavHeader>
<NavBar />
{
features.includes('new-nav-feature')
&& <NewNavFeature />
}
</NavHeader>
<div className="content">
<WrappedComponent features={features} {...props} />
</div>
{ showFooter && <Footer /> }
</>
);
};
};
但是我们不能只柯里化布局函数,我们还需要柯里化withProviders
函数:
js
const withProviders = (options) =>
compose(
withUser,
withFeatures,
withLogger,
withLayout(options)
);
现在我们就可以用 withPrivider 去包裹我们想要应用Provider组件的任何页面组件,并且对每个页面的布局做定制化。
js
const MyPage = ({ user = {}, signIn, features = [], log }) => {
return <>{/* our actual page content... */}</>;
};
const MyPageWithProviders = withProviders({
showFooter: false
})(MyPage);