我们继续讲述在Odnoklassniki内部如何借助GraalVM设法与Java和JavaScript成为朋友并开始迁移到具有大量旧代码的大型系统的故事。在本文的第二部分中,我们将详细讨论新堆栈上应用程序的启动,组装和集成,深入探讨它们在客户端和服务器上的工作细节,并讨论在我们的方法中遇到的困难并描述帮助他们克服的解决方案。如果您尚未阅读第一部分我强烈建议您这样做。从中您将了解Odnoklassniki前端的历史,并了解其历史特征,通过寻找解决方案来解决我们13年项目中积累的问题,最后,您将深入了解我们所做出决策的服务器实现的技术特征。UI配置
为了编写UI代码,我们选择了最先进的工具:React以及MobX,CSS模块,ESLint,TypeScript,Lerna。所有这些都是使用Webpack收集的。
应用架构
如本文前面所述,为了实现逐步迁移,我们将在网站上将新组件插入具有自定义名称的DOM元素中,这些元素将在新的UI堆栈中使用,而对于网站的其余部分,它将看起来像是具有DOM元素的DOM元素。其API。这些元素的内容可以在服务器上呈现。它是什么?内部有一个很酷,时尚的现代MVC应用程序,它在React上运行,并向外提供标准的DOM API:属性,此DOM元素上的方法和事件。
为了运行这些组件,我们开发了一种特殊的机制。他在做什么?首先,它根据其描述初始化应用程序。其次,它将组件绑定到启动它的特定DOM节点。还有两个引擎(用于客户端和服务器)可以找到并呈现这些组件。
为什么需要这个?事实是,当整个站点都在React上构建时,通常会将站点组件呈现到页面的根元素中,而该组件无关紧要什么,而只有内部有趣。在我们的情况下,一切都变得更加复杂:许多应用程序都需要有机会在网站上告诉我们的页面“我是,我正在发生变化。”例如,日历需要引发一个事件,即用户单击了按钮,并且日期已更改,或者您需要具有外部功能,以便日历内部可以更改日期。为此,应用程序引擎在应用程序的基本功能中实现了外观。在向客户端交付组件时,旧站点的引擎必须可以启动此组件。为此,在构建期间,将收集启动所需的信息。{
"events-calendar": {
"bundleName": "events-calendar",
"js": "events-calendar-h4h5m.js",
"css": "events-calendar-h4h5m.css"
}
}
特殊标记被添加到component标记的属性中,这表示此应用程序是新类型的,其代码可以从特定的JS文件中获取。同时,它具有初始化此组件所需的自己的属性:它们形成商店中组件的初始状态。<events-calendar data-module="react-loader"
data-bundle="events-calendar.js"
date=".."
marks="[{..}]"
…
/>
对于补液,不使用应用程序状态的强制转换,而是使用属性,以节省流量。它们以规范化形式出现,通常小于应用程序创建的商店。同时,从客户机上的属性重新创建存储的时间很短,因此通常可以忽略它们。例如,对于日历,属性仅具有突出显示的日期,并且商店已经具有一个包含月份完整信息的矩阵。显然,从服务器传输它毫无意义。如何运行代码?
该概念已通过简单的功能进行了测试,这些功能要么为服务器提供一行,要么为客户端编写了innerHTML。但是在实际代码中,有模块和TypeScript。有一些针对客户端的标准解决方案,例如,使用Webpack收集代码,Webpack本身将研磨所有内容并将其以捆绑形式提供给客户端。使用GraalVM时对服务器做什么?
让我们考虑两个选项。第一种是像在Node.js中那样在JavaScript中键入TypeScript。不幸的是,当JavaScript是GraalVM中的来宾语言时,此选项在我们的配置中不起作用。在这种情况下,JavaScript没有模块化系统,甚至没有异步性。因为模块化和与异步一起使用提供了特定的运行时:NodeJS或浏览器。在我们的案例中,服务器具有只能同步执行代码的JavaScript。第二个选项-您可以简单地从为客户端收集的相同文件中的服务器代码上运行。并且此选项有效。但是存在一个问题,服务器需要多种方法的其他实现。例如,将在服务器上调用renderToString()函数来渲染组件,并在客户端上调用ReactDOM.render()。还是上一篇文章中的另一个示例:要在服务器上获取文本和设置,将调用Java提供的功能,而在客户端上它将是JS中的实现。作为此问题的解决方案,您可以使用Webpack中的别名。它们使您可以创建我们需要的类的两个实现:用于客户端和服务器。然后,在客户端和服务器的配置文件中,指定适当的实现。
但是两个配置文件是两个程序集。每次,分别收集服务器和客户端的所有内容很长,而且很难获得支持。您需要提出这样的配置,以便一次性收集所有内容。Webpack配置以在服务器和客户端上运行JS
为了找到解决该问题的方法,让我们看一下该项目包括哪些部分:
首先,该项目具有第三方运行时(供应商),客户端和服务器都相同。它几乎永远不会改变。可以将Rantime分配给用户,他将被缓存在客户端上,直到我们更新第三方库的版本为止。其次,有我们的运行时(核心),可确保启动应用程序。它为客户端和服务器提供了具有不同实现的方法。例如,获取本地化文本,设置等。该运行时也很少更改。第三,有一个组件代码。客户端和服务器都相同,这使您可以在浏览器中调试应用程序代码,而无需启动服务器。如果客户端出现问题,您可以在浏览器控制台中看到错误,并牢记所有内容,并确保在服务器上启动时不会出现错误。总共获得了三个需要组装的零件。我们想要:- 分别配置每个零件的装配。
- 放下它们之间的依赖关系,以使每个部分都不属于另一个部分。
- 一口气收集所有东西。
如何分别描述装配体组成的零件?Webpack中有多种配置:您只需放弃每个部分中包含的模块导出的数组即可。module.exports = [{
entry: './vendors.js',
}, {
entry: './core.js'
}, {
entry: './app.js'
}];
一切都会好起来的,但是在这些部分的每个部分中,该部分所依赖的模块的代码将被复制:
幸运的是,在基本的Webpack插件集中有DllPlugin,它允许您获取每个组装部件所包含的模块的列表。例如,对于供应商,您可以找出此部分中包含哪些特定模块。在构建其他部分(例如核心库)时,可以说它们取决于供应商部分。
然后,在webpack组装期间,DllPlugin会根据供应商中已有的某些库来查看核心,并且不会将其添加到核心中,而只是将链接添加到该核心中。结果,一次组装了三块并且彼此依赖。当第一个应用程序下载到客户端时,运行时库和核心库将保存在浏览器缓存中。由于Odnoklassniki是一个站点,因此用户可以“永远”打开的选项卡很少出现。在大多数情况下,随着网站新版本的发布,只会更新应用程序代码。资源交付
通过使用存储在单独数据库中的本地化文本的示例来考虑该问题。如果服务器上较早的某个位置需要组件中的文本,则可以调用该函数以获取文本。const pkg = l10n('smiles');
<div>
: { pkg.getText('title') }
</div>
在服务器上获取文本并不困难,因为服务器应用程序可以向数据库发出快速请求,甚至可以将所有文本缓存在内存中。如何在GraalVM的服务器上呈现的React组件中获取文本?如本文第一部分所述,您可以在JS上下文中将方法添加到要从JavaScript访问的全局对象中。决定使用所有可用于JavaScript的方法创建一个类。public class ServerMethods {
…
public String getText(String pkg, String key) {
…
}
…
}
然后将此类的实例放在全局JavaScript上下文中:
js.putMember("serverMethods", serverMethods);
结果,在服务器实现中的JavaScript中,我们只需调用以下函数:function getText(pkg: string, key: string): string {
return global.serverMethods.getText(pkg, key);
}
实际上,这将是Java中的函数调用,它将返回请求的文本。直接同步交互,无HTTP调用。不幸的是,在客户端上,通过HTTP接收组件中每次插入文本插入函数的每次调用都需要花费很长时间。您可以将所有文本预下载到客户端,但是仅这些文本就占了几十兆字节,并且还有其他类型的资源。
用户将厌倦于等待所有内容下载完毕后再启动应用程序。因此,此方法不合适。我只想接收特定应用程序中需要的那些文本。我们的文本分为几包。因此,您可以收集应用程序所需的软件包,并将它们与捆绑软件一起下载。当应用程序启动时,所有文本将已经在客户端缓存中。如何找出应用程序需要哪些文本?我们达成了一个协议,代码中的文本包是通过调用l10n()函数获得的,其中包名仅以字符串文字形式传输:const pkg = l10n('smiles');
<div>
{ pkg.getLMsg('title') }
</div>
我们编写了一个webpack插件,该插件通过分析组件代码的AST树,查找对l10n()函数的所有调用,并从参数中收集包名称。同样,插件收集有关应用程序所需其他资源类型的信息。在为每个应用程序组装之后的输出中,我们获得了一个包含其资源的配置:{
"events-calendar": {
"pkg": [
"calendar",
"dates"
],
"cfg": [
"config1",
"config2"
],
"bundleName": "events-calendar",
"js": "events-calendar.js",
"css": "events-calendar.css",
}
}
当然,我们一定不要忘记更新文本。因为在服务器上,所有文本始终都是最新的,并且客户端需要单独的缓存更新机制,例如观察程序或推送。新旧代码
在平稳过渡的情况下,会出现在新组件中重用旧代码的问题,因为存在大型而复杂的组件(例如视频播放器),重写将花费大量时间,并且您现在需要在新堆栈中使用它们。
有什么问题?- 旧站点和新React应用程序的生命周期完全不同。
- 如果您将旧样本的代码粘贴到React应用程序中,则该代码将不会启动,因为React不知道如何激活它。
- 由于生命周期不同,React和旧引擎可能会同时尝试修改旧代码的内容,这可能会导致令人不快的副作用。
为了解决这些问题,为包含旧代码的组件分配了一个通用的基类。该类允许继承人协调React和旧式应用程序的生命周期。export class OldCodeBase<T> extends React.Component<T> {
ref: React.RefObject<HTMLElement> = React.createRef();
componentDidMount() {
this.props.activate(this.ref.current!);
}
componentWillUnmount() {
this.props.deactivate(this.ref.current!);
}
shouldComponentUpdate() {
return false;
}
render() {
return (
<div ref={this.ref}></div>
);
}
}
该类使您可以创建按旧方式工作的代码片段,或者销毁它们,而不会同时进行交互。将旧代码粘贴到服务器上
实际上,需要包装器组件(例如,弹出窗口),其内容可以是任何内容,包括那些使用旧技术制作的内容。您需要弄清楚如何在此类组件内部的服务器上嵌入任何代码。在上一篇文章中,我们讨论了如何使用属性将参数传递给客户端和服务器上的新组件。<cool-app users="[1,2,3]" />
现在,我们仍然要在此处插入一个标记,这实际上不是属性。为此,决定使用插槽系统。<cool-app>
<ui:part id="old-code">
<div>old component</div>
</ui:part>
</cool-app>
如上例所示,在cool-app组件的代码内部,描述了一个包含旧组件的旧代码槽。然后,在react组件内部,指示您要粘贴此插槽内容的位置:render() {
return (
<div>
<UiPart id="old-code" />
</div>
);
}
服务器引擎呈现此react组件,并在<ui-part>标记中构造插槽的内容,并为其分配data-part-id =“ old-code”属性。<cool-app>
<div>
<ui-part data-part-id="old-code">
old code
</ui-part>
</div>
</cool-app>
如果GraalVM中JS的服务器端渲染不适合超时,则我们将回退到客户端渲染。为此,服务器上的引擎仅提供插槽,将它们放入模板标记中,以便浏览器不与它们的代码进行交互。<cool-app>
<template>
<ui-part data-part-id="old-code">
old code
</ui-part>
</template>
</cool-app>
客户发生了什么事?客户端引擎只需扫描组件代码,收集<ui-part>标记,以字符串形式接收其内容,然后将其与其余参数一起传递给呈现函数。var tagName = 'cool-app';
var reactComponent = components[tagName];
reactComponent.render({
tagName: tagName,
attrs: attrs,
parts: parts,
node: element
});
将插槽插入所需位置的组件代码如下:export class UiPart extends OldCodeBase<IProps> {
render() {
const id = this.props.id;
const parts = this.props.parts;
if (!parts.hasOwnProperty(id)) {
return null;
}
return React.createElement('ui-part', {
'data-part-id': id,
ref: this.ref,
dangerouslySetInnerHTML: { __html: parts[id] }
});
}
}
同时,它继承自OldCodeBase类,解决了新旧堆栈之间的交互问题。
现在,您可以编写一个弹出窗口,并使用新的堆栈填充它,或使用旧方法从服务器请求。在这种情况下,组件将正常工作。这使您可以逐步将站点组件迁移到新堆栈。只是这是新前端的主要要求之一。摘要
每个人都想知道GraalVM有多快。Odnoklassniki开发人员对React应用程序进行了各种测试。一个预热后返回字符串的简单函数大约需要1微秒。组件(再次预热后)-从0.5到6毫秒,具体取决于它们的大小。GraalVM的加速速度比V8慢。但是,由于客户端渲染的回退,在预热期间,情况得以缓解。由于用户太多,因此虚拟机会快速升温。你做了什么
- 在Classmates的Java世界中的服务器上运行JavaScript。
- 为UI制作同构代码。
- 使用所有前端供应商都知道的现代堆栈。
- 创建用于编写UI的通用平台和单一方法。
- 在不使操作复杂化且不减慢服务器渲染速度的情况下开始平稳过渡。
我们希望Odnoklassniki的经验和示例对您有所帮助,并且您会发现它们可用于您的工作。