上周在PayPal,我的一个拉动请求被合并到一个快速代码库中,该代码库将我们从一个定制的模板系统迁移到使用React函数组件和JSX。这样做的目的是为了减少在前台使用JSX的同时,还要了解和维护自定义模板系统所带来的维护费用。
这个应用程序是paypal.me。它的工作方式是,我们有100%渲染的HTML/CSS的主页、条款和支持的国家的页面(只有一小部分vanilla JS),然后个人资料和设置页面由服务器渲染为 "骨架 "html页面(与SEO相关的标签和根<div> 等),然后客户端的React应用程序开始加载页面上需要的其余数据/互动。
你应该使用Remix,而不是像我们在paypal.me上做的那样只使用骨架index.html。我们有自己的理由(凡事都有细微差别,我不打算谈这个)。当然,在我们做这件事的时候,Remix还不存在,所以🤷♂️
在我做PR之前,我们实际上有两个系统。我们使用express-es6-template-engine 对于个人资料和设置页面(实际上是同一个页面),对于营销页面,我们的一个工程师想出了一个类似于react的标签模板文字解决方案(有接受道具并返回HTML字符串的函数)。所以在这个代码库上工作的工程师将不得不知道并维护:
express-es6-template-engine用于个人资料和设置页面- React和JSX用于客户端应用程序
- 用于营销页面的自定义标签模板字面解决方案。
我们决定将其简化为一个解决方案。前台和后台都使用React和JJSX。这就是我所承担的任务。我想解释一下我在进行这一转变时遇到的一些问题和解决方案。
JSX的编译
这其实和npm install --save react react-dom 中的server 一样简单。因为paypal.me使用paypal-scripts,服务器已经编译了内置的babel配置,如果项目把react列为dep,它将自动添加必要的react插件。很好!我喜欢工具箱!
HTML结构
我面临的最大挑战是与其他PayPal模块的整合,这些模块产生的HTML需要插入到我们正在渲染的HTML中。其中一个例子是我之前写过的polyfill服务,它插入了一个脚本标签,有一些特殊的查询参数和一个服务器nonce。我们把这个作为中间件,它添加了一个res.locals.polyfill.headHTML ,这是一个需要出现在你渲染的<head> 的HTML字符串。
有了模板字面和es6-模板-引擎的东西,这就很简单了。只要在正确的地方添加${polyfill.headHTML} ,你就可以了。但在React中,这就有点棘手了。让我们来试试。让我们假设polyfill.headHTML 是<script src="hello.js"></script> 。因此,如果我们这样做:
<head>{polyfill.headHTML}</head>
这将导致HTML看起来像这样:
<head><script src="hello.js"></script></head>
这是因为React转义了渲染的插值(那些出现在{ 和} 之间的值)。这是React内置的一个跨网站脚本(XSS)保护功能。由于React这样做,我们所有的应用程序都更安全。然而,在有些情况下,它会导致问题(比如这个)。所以React给了你一个逃生舱口,你可以选择不使用这种保护。让我们来使用它:
<head>
<div dangerouslySetInnerHTML={{__html: polyfill.headHTML}} />
</head>
因此,这将导致:
<head>
<div>
<script src="hello.js" />
</div>
</head>
但这在语义上完全不准确。一个div 不应该出现在一个head 。我们也有一些meta 标签。从技术上讲,这在Chrome浏览器中是可行的,但我不知道在PayPal支持的所有浏览器中会发生什么,我不想为此破坏SEO或旧的、宽容度低的浏览器的功能。
因此,这是我想出的解决方案,我并不讨厌:
<head>
<RawText>{polyfill.headHTML}</RawText>
</head>
该RawText 组件的实现是非常简单的:
function RawText({children}) {
return <raw-text dangerouslySetInnerHTML={{__html: children}} />
}
因此,这将导致:
<head>
<raw-text>
<script src="hello.js" />
</raw-text>
</head>
这本身并不能解决这个问题。下面是我们在将页面渲染成HTML时的做法:
const htmlOutput = ReactDOMServer.renderToStaticMarkup(<Page {...options} />)
const rendered = `
<!DOCTYPE html>
${removeRawText(htmlOutput)}
`
// ...etc...
那个removeRawText 函数就定义在RawText 组件旁边,看起来像这样:
function removeRawText(string) {
return string.replace(/<\/?raw-text>/g, '')
}
因此,实际上我们的rendered 字符串看起来是这样的:
<head>
<script src="hello.js"></script>
</head>
🎉 㱮㱮
因此,我们有一个简单的组件,我们可以用它来处理任何我们想按原样插入到文档中的原始字符串,而不必在混合中添加一个额外的无意义(有时在语义上有害)的DOM节点。(注意,这个问题的真正解决方案是让React支持dangerouslySetInnerHTML 在片段上)。
**注意:**这个逻辑存在于一个紧挨着RawText 组件定义的函数中,而不是在它发生的地方硬编码替换,这一事实很重要。任何人来到代码库,看到RawText 或removeRawText ,都能更快地发现发生了什么。
本地化
在我们的客户端应用程序中,我们使用了我的朋友Jamund和我合作的本地化模块,它依赖于内容字符串的单子 "存储"。它非常好用,因为在客户端应用程序的整个生命周期中,只有一种语言是需要的。但单子在后端工作得不是很好。所以我建立了一个简单的React Context消费者和提供者,这使得使用同样的抽象来获取消息变得更容易,而不需要单子。我不打算分享它的代码,但这里是你可以使用它的方法。
<Message msgKey="marketing_pages/new_landing.title" />
它的效果很好。Message 组件渲染MessageConsumer组件,该组件将从上下文中获取内容,并检索带有给定键的消息。
其他值得注意的事情
React.Fragments当结构如此重要的时候,你会发现自己到处都在使用React的片段。我们正在使用babel 7,并且很喜欢<>和</>的新的短语法。style/className的变化。在这是直接的HTML之前,我最大的变化是所有的class="必须改为className=",这并不是所有的挑战,但我发现我忘了style="的属性需要改为style={和对象的语法。幸运的是,如果你错过了一个,React会给你一个警告:)${需要改成 。在这次重构的过程中,我发现有一些游离的 ,渲染了好几次。😅{$
总结
我很高兴我们现在对整个应用程序(包括前端和后端)只有一个模板解决方案。我认为这将减少应用程序的维护负担,这是一个真正的胜利。尝试东西和做实验是件好事,但回过头来把事情重构到成功的抽象中去,是制作可长期维护的应用程序的重要步骤。我希望这对你有帮助!祝您好运!