作为服务器端模板语言的React/JSX

53 阅读5分钟

上周在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字符串的函数)。所以在这个代码库上工作的工程师将不得不知道并维护:

  1. express-es6-template-engine 用于个人资料和设置页面
  2. React和JSX用于客户端应用程序
  3. 用于营销页面的自定义标签模板字面解决方案。

我们决定将其简化为一个解决方案。前台和后台都使用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>&lt;script src=&quot;hello.js&quot;&gt;&lt;/script&gt;</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 组件定义的函数中,而不是在它发生的地方硬编码替换,这一事实很重要。任何人来到代码库,看到RawTextremoveRawText ,都能更快地发现发生了什么。

本地化

在我们的客户端应用程序中,我们使用了我的朋友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会给你一个警告:)
  • ${ 需要改成 。在这次重构的过程中,我发现有一些游离的 ,渲染了好几次。😅{ $

总结

我很高兴我们现在对整个应用程序(包括前端和后端)只有一个模板解决方案。我认为这将减少应用程序的维护负担,这是一个真正的胜利。尝试东西和做实验是件好事,但回过头来把事情重构到成功的抽象中去,是制作可长期维护的应用程序的重要步骤。我希望这对你有帮助!祝您好运!