我曾两次将大型JavaScript应用程序迁移到PureScript。在CitizenNet,我们用卤素代替了ANGLING,在觉醒安全,我们用PureScript Reaction代替了大部分Reaction应用程序。这两家公司的生产缺陷都出现了戏剧性的下降。
由于PureScript的REACT和REACT-BASIC库,替换REACT相对容易。Reaction心理模型非常适合像PureScript(或Reason)这样的强类型纯函数式语言,并且使用相同的底层库意味着只需很少的修改就可以在语言之间共享组件。
在Awake Security,我们共享国际化、Redux存储和中间件,以及更多内容,在代码库中,PureScript定期导入JavaScript,JavaScript定期导入PureScript。
将一个重要的应用程序从一种语言重写到另一种语言的最佳方式是在它运行时递增。起初,新语言可以接管应用程序中逻辑上孤立的部分:管理仪表板、聊天窗口或表单。但最终您必须将两种语言的组件混合在一起-例如,为了支持共享全局状态。
在这一点上,您不能让新语言接管DOM节点。您需要支持简单、清晰的混合语言功能。幸运的是,您可以将惯用的PureScript代码的接口转换为惯用的JavaScript(反之亦然)。使用REACT和REACT-Basic,您可以用PureScript编写业务逻辑,但很容易与更大的REACT生态系统和现有代码进行互操作。
在本文中,我将演示如何用用PureScript编写的简单组件替换Reaction应用程序的一部分。在此过程中,我将分享使此互操作变得方便和可靠的最佳实践。示例很简单,但同样的技术也适用于复杂的组件。
我鼓励您与本文一起编写代码;没有省略任何代码,并且固定了依赖项,以帮助确保示例是可重现的。此代码使用全局安装的Nodev11.1.0、Yarnv1.12.0和NPXv6.5.0,以及本地安装的PureScript工具。
Peter Murphy已经使用Reaction Hooks实现了本文中的想法,如果您希望看到这一点的话。
当有新帖子发布或我有重要更新时,请通知我,并帮助我决定下一步要写什么!
我们将编写一个显示一些计数器的小型Reaction应用程序,然后我们将用PureScript替换它的组件。除了导入之外,生成的JavaScript代码将无法从原始代码中区分出来,但是在幕后它将完全是PureScript。
让我们按照官方的Reaction文档使用create-action-app初始化项目,然后将源代码削减到最低限度。
我们在src下有几个源文件,但是我们的应用程序只需要其中的两个:index.js(webpack的入口点)和app.js(我们的应用程序的根组件)。我们可以删除其余部分:
#删除除入口点和#root app组件以外的所有源文件find src-type f-not\(-name';index.js';-or-name';app.js';\)-delete。
最后,让我们将这两个文件的内容替换为本文所需的最小内容。从现在开始,我将提供一些差异,您可以将这些差异提供给git application,以应用与我所做的相同的更改。
//src/App.js导入反应来自";reaction";;function App(){return(<;div>;<;h1>;my App<;/H1>;<;/div>;);}导出默认App;
让我们编写第一个反应组件:计数器。这可能是您遇到的第一个Reaction组件示例;它也是PureScript Reaction库中的第一个示例。它也足够小和简单,可以在本文过程中替换两次。
计数器将是一个按钮,用于维护其已被点击的次数。它将接受显示在按钮上的标签作为其唯一的道具。
//src/Counter.js从";REACT";;类计数器导入REACT扩展REACT。组件{构造函数(道具){超级(道具);此。state={count:0};}Render(){return(<;button onclick={()=>;this。setState({计数:此。州政府。Count+1})}>;{此。道具。标签}:{此。州政府。count}<;/button>;);}}导出默认计数器;
-a/src/App.js+b/src/App.js@@-1,9+1,13@@import反应来自";reaction";;/count&34;;函数App(){return(<;div>;<;h1>;my App<;/h1>;+<;counter label=";count&34;;函数App(){return(<;div>;<;my App<;/h1>;+<;count label=";count";)。+<;计数器标签=";交互";/>;<;/div>;);}。
有了Sile Start,我们就可以运行开发服务器并查看我们的应用程序的运行情况。
我们已经完全编写了太多的JavaScript。让我们在这个项目中也支持PureScript。我们的目标是用任何一种语言编写代码,并在两个方向上自由导入,而不会产生摩擦。为此,我们将安装PureScript工具,创建单独的PureScript源目录,并依赖编译器生成JavaScript代码。
首先,我们必须安装PureScript工具。我建议安装与本文中使用的版本相匹配的编译器和Spago(包管理器和构建工具)版本。我将使用NPX来确保所有命令都使用本地副本运行。
#随心所欲地安装编译器和Spago包管理器;#由于我们已经在REACT项目中,我将使用YarnyansAdd-D [email protected] [email protected]。
我们可以使用Spago init创建一个新的PureScript项目。从0.8.4版开始,Spago始终使用相同的包集进行初始化,这意味着您应该拥有与撰写本文时使用的包版本相同的包版本。我使用的是PSC-0.13.0-20190607套装。
#npx确保我们重新使用安装在node_module es.npx Spago init中的Spago本地副本。
Spago已经创建了一个Packages.dHall文件和一个spago.dHall文件,前者指向可以安装的软件包集合,后者列出了我们实际安装的软件包。我们现在可以安装所需的任何依赖项,并且可以确保所有版本都是兼容的。
在安装之前,让我们更新现有的.gitignore文件以涵盖PureScript。对于基于Spago的项目,这将起作用:
最后,让我们组织我们的源代码。通常将JavaScript源代码与PureScript源代码分开,除非为PureScript编写FFI文件。因为我们在这个项目中没有这样做,所以我们的源文件将完全分开。让我们将所有JavaScript代码移到javascript子目录中,并在其旁边创建一个新的purescript文件夹。
-a/src/index.js+b/src/index.js@@-1,5+1,5@@import reaction from";reaction";;import ReactDOM from";reaction-dom&34;;-从";./App";;+从";./javascript/App";;导入App";;ReactDOM.Render(<;App/>。
我们只剩下一项任务了。PureScript编译器在项目根目录中名为output的目录中生成JavaScript。但是create-action-app禁止导入src目录之外的任何内容。虽然有一些更花哨的解决方案,但对于这个项目,我们将通过将输出目录符号链接到src目录来绕过限制。
src├──index.js├──javascript│├──App.js│└──计数器.js├──Output->;../Output└──purescript。
在将JavaScript Reaction组件替换为PureScript组件时,我喜欢遵循四个简单的步骤:
为组件编写单独的互操作模块。此模块提供JavaScript接口以及PureScript和JavaScript类型和习惯用法之间的转换函数。
我们将从Awake Security使用的Reaction库开始。它类似于Reaction-Basic,但更直接地映射到底层的Reaction代码,不那么固执己见。稍后,我们将切换到Reaction-Basic,这将演示它们之间的一些区别。
当我们在这个过程中采取每一步时,我将更多地解释为什么它是必要的,以及一些需要牢记的最佳实践。让我们开始:安装Reaction库并准备编写我们的组件:
#安装purescript-reaction库arynpx Spago install act#构建项目,以便编辑人员可以选择`output`目录ynpx Spago build#创建组件源filetouch src/purescript/Counter.purs。
即使我们正在编写要从JavaScript使用的组件,我们仍然应该编写普通的PureScript。我们很快就会看到,可以只针对JavaScript调整组件的接口,而保持内部不变。如果该组件要同时由PureScript和JavaScript使用,这一点尤其重要;我们不想在这两个代码库中引入任何与互操作相关的笨拙。
下面,我编写了一个具有相同道具、状态和呈现的组件版本。将其内容复制到src/purescript/Counter.purs。
注意:在创建组件时没有必要对此进行注释,但是这样做可以提高错误的质量(如果您做错了事情)。
import Prelude import Reaction(ReactClass,ReactElement,ReactThis,Component,createLeafElement,getProps,getState,setState)import React.DOM as D import React.DOM.Props as P type props={Label::String}type State={count::int}Counter::Props->;ReactElement Counter=createLeafElement Counter Class::ReactClass Props CounterClass=Component";让Render=do state<;-getState this props<;-getProps这个纯$D。按钮[P.。onClick\_->;setState this{count:stat.。计数+1}][D.。TEXT$PROPS。标签<;>;";:";<;>;显示状态。计数]纯{状态:{计数:0},呈现}
在PureScript代码库中,这就是我们需要的全部内容;我们可以通过导入Counter并为其提供道具来使用该组件:
--对比我们的JavaScript主应用导入计数器(计数器)renderApp::ReactElement renderApp=div';[h1';[text";My App";],计数器{Label:";count";},Counter{Label:";count";}]。
我们也可以从JavaScript中使用此组件。React库将从该代码生成一个可用的Reaction组件,我们可以像导入任何其他JavaScript Reaction组件一样导入该组件。让我们先试一试,然后再做一些改进。
然后我们将导入组件。请注意,我们的实现非常接近,我们只需更改导入,而不需要更改其他内容!PureScript将在输出中生成文件,因此我们的计数器组件现在驻留在OUTPUT/COUNTER。
-a/src/javascript/App.js+b/src/javascript/App.js@@-1,5+1,5@@import反应来自";reaction";;-从";导入计数器。/count";;+从";../output/count";;函数App(){return(){return(。
运行Screen start,您应该会看到与以前完全相同的一组计数器。现在使用PureScript实现了我们的组件,我们不再需要我们的JavaScript版本:
我们很幸运,我们的组件马上就可以工作了。事实上,它之所以有效,只是因为到目前为止我们使用的是简单的JavaScript类型,并且计数器组件的用户是值得信任的,并且没有省略标签prop,我们认为这是必需的。我们可以在PureScript中强制执行正确的类型和不缺失值,但在JavaScript中不能强制执行。
嗯,将unDefined设置为标签并不好,但也没有整个应用程序崩溃那么糟糕-如果您试图对伪装为字符串的值使用PureScript函数,就会发生这种情况。问题是字符串类型不能很好地捕捉可能从JavaScript到达的值。作为一般规则,我希望人们以他们通常的方式编写JavaScript,这意味着使用内置类型、常规的非游标函数,并且有时会省略信息,而提供null或未定义的内容。这就是为什么在Awak Security,我们通常会为JavaScript代码中使用的组件提供互操作模块,这就是为什么我们要为JavaScript代码中使用的组件提供互操作模块:
提供组件中使用的PureScript类型与简单JavaScript表示形式之间的映射。
通过使用Nullable类型标记所有可能合理地为空或未定义的输入,从而添加了一层安全保护,这有助于我们的代码优雅地处理遗漏的值。
将Currated形式的函数转换为常用的JavaScript函数,并将有效的函数(在生成的代码中表示为thunk)转换为调用时立即运行的函数。
充当影响依赖JavaScript代码的PureScript代码更改的金丝雀,因此您可以格外小心。
在本文的其余部分中,我们将探索这些技术中的每一种。目前,我们需要做的就是将输入字符串标记为Null,并显式处理省略时应该发生的事情。
模块计数器.Interop WHERE IMPORT Prelude导入计数器(道具,计数器)导入数据。可能(From MMaybe)导入数据。可空(Null,toMaybe)导入反应(ReactElement)类型JSProps={Label::Nullable String}jsPropsToProps::JSProps->;Props jsPropsToProps{Label}={Label:from mMaybe";count";$toMaybe Label}。
我们已经为我们的组件JSProps创建了一个新接口,它将在JavaScript中使用,而不是在我们的PureScript接口Props中使用。我们还创建了一个在两个接口之间转换的函数,并生成了一个使用JavaScript接口而不是PureScript接口的新组件。
将标签prop标记为Nullable会使编译器意识到该字符串可能不存在。然后,它迫使我们显式处理NULL或未定义的情况,然后才能将道具视为普通字符串。我们需要处理NULL情况,以便将新的JSProps类型映射到组件的预期道具类型。为此,我们将Nullable转换为可能,然后提供一个后备值,以便在道具不存在时使用。
Nullable类型显式用于与JavaScript进行互操作,但是它的行为并不总是完全符合您的预期。它不直接映射到普通的“可能”类型。通常应该尽快将任何可为空的类型转换为。如果您想了解更多关于这方面的信息,请查看可为空的库。
让我们更改App.js中的导入,并验证省略的标签是否被正确处理。
-a/src/javascript/App.js+b/src/javascript/App.js@@-1,5+1,5@@import反应来自";reaction";;-从";../output/count";;+import{jsCounter作为计数器}从";../output/Counter.Interop";;函数App(){return(){return(){return(。
在本例中,我们的互操作模块只是将单个字段标记为Null。但是JavaScript接口与它正在转换的PureScript接口稍有不同是很常见的。通过保留单独的互操作模块,可以在不影响核心组件的情况下轻松完成此操作。
它还确保对底层组件的任何更改都反映为互操作文件中的类型错误,而不是(潜在地)破坏JavaScript代码。当您习惯于编译器警告您一个文件中的更改将在另一个文件中产生的影响时,很容易对此变得懒惰!
如果您使用TypeScript,Justin Woo写了一篇关于与来自PureScript的TypeScript透明共享类型的文章,值得一读。
让我们再次尝试替换计数器,但这一次使用更新的、更自以为是的Reaction-Basic库。在此过程中,我们将使用一些更复杂的类型,并构建更复杂的互操作模块。
import Prelude import React.Basic(JSX,createComponent,Make)import React.Basic.DOM as R import React.Basic.DOM.Events(Capture_)type PROPS={Label::String}Counter::Props->;JSX Counter=Make(createComponent";Counter";){initialState,Render}其中initialState={count:0}Render Self=R。按钮{onClick:CAPTURE_$SELF。setState\s->;s{count=s.。count+1},子项:[r.。文本$SELF。道具。标签<;>;";&34;<;>;显示自我。州政府。计数]}
这两个Reaction库不共享类型,因此我们将更改互操作模块以描述生成JSX而不是ReactElement。
-a/src/purescript/counter/Interop.purs+b/src/purescript/Counter/Interop.purs@@-5,13+5,13@@import Prelude import Counter(props,counter)import Data.Maybe(From MMaybe)import Data.Nullable(Nullable,toMaybe)-import reaction(ReactElement)+import React.Basic(JSX)type JSProps={Label::NSX。ReactElement+jsCounter::JSProps->;JSX jsCounter=计数器<;<;jsPropsToProps。
该组件在PureScript代码库中工作得很好。不过,与我们的Reaction组件不同的是,我们的Reaction-Basic组件也不会自动在JavaScript代码中工作。相反,我们需要使用make来构造一个用于PureScript的组件,并使用toReactComponent来构造一个用于JavaScript的组件。
尽管如此,这两个函数使用相同的组件规范类型,因此新的限制很容易解决。我们只需移动initialState并呈现到模块作用域。这样,我们就可以将它们直接导入到互操作模块中,以提供给toReactComponent。
-a/src/purescript/Counter.purs+b/src/purescript/Counter.purs@@-2,21+2,28@@模块计数器,其中import Prelude-import React.Basic(JSX,createComponent,make)+import React.Basic(Component,JSX,Self,createComponent,make)import React.Basic.DOM As R import React.Basic.DOM.Events(Capture_。JSX-Counter=make(createComponent";Counter";){initialState,Render}-where-initialState={count:0}--Render self=-R.button-{onclick:-Capture_$self.setState\s->;s{count=s.count+1}-,子项:-[R.text$self.pros.label<;>;";&34;<;>;show self.state.count]-}+Counter=Make Component{initialState,Render}++initialState::State+initialState={count:0}++Render::Self Props State->;JSX+Render Self=+R.Button+{onclick:+Capture_$self.setState\s{count=s.count+1}+,子对象:+[R.text$self.pros.label<;>;"。
否则我们将保持代码不变。接下来,让我们转到互操作模块。它现在应该使用toReactComponent来创建可从JavaScript使用的组件。此函数接受组件和组件规范,与make的方式完全相同,但它还接受一个额外的参数:我们的jsPropsToProps函数。
Reaction-Basic库使互操作比Reaction更加明确,但最终我们将编写几乎相同的互操作代码。
-a/src/purescript/counter/Interop.purs+b/src/purescript/counter/Interop.purs@@-2,16+2,15@@MODULE计数器。Interop where import Prelude-导入计数器(props,count)+导入计数器(props,component,initialState,Render)import Data.Maybe(From MMaybe)import Data.Nullable(Nullable,toMaybe)-import action(ReactElement)-
..