如何在端口交互中协调渲染(Elm 0.17)

8
我希望将Elm与Javascript库集成,使Elm能够动态创建“单元格”(html divs),并向Javascript提供它们的id以便执行自定义操作。我想要实现的顺序是:
  1. Elm创建一个单元格(并分配id)
  2. 发送带有id的消息到端口
  3. Javascript接收消息并执行其操作
这是我最初实现的方式(完整源代码):
port onCellAdded : CellID -> Cmd msg

update : Msg -> Model -> (Model, Cmd Msg)
update message ({cells} as model) =
  case message of

    Push ->
      let
        uid = List.length cells
      in
      ({ model
        | cells = [uid] ++ cells
      }, onCellAdded uid)

问题在于另一侧的Javascript。
var container = document.getElementById('app');
var demoApp = Elm.RenderDemo.embed(container);

demoApp.ports.onCellAdded.subscribe(function(cellID) {
   if(document.getElementById('cell:' + cellID) === null) { window.alert("Cannot find cell " + cellID) }    
});

抱怨说找不到这样的id。显然视图尚未呈现。

因此,我向Elm应用程序添加了另一个状态(OnCellAdded),希望流程如下:

  1. Elm创建单元格(在Push上)并请求(Task.perform)异步任务OnCellAdded
  2. 这里渲染视图
  3. OnCellAdded被调用,并将带有id的消息发送到端口
  4. Javascript接收消息并执行其操作

实现看起来像这样(diff)(full source):

update message ({cells} as model) =
  case message of

    Push ->
      let
        uid = List.length cells
      in
      ({ model
        | cells = [uid] ++ cells
      }, msgToCmd (OnCellAdded uid))

    OnCellAdded counter ->
      (model, onCellAdded counter)

msgToCmd : msg -> Cmd msg
msgToCmd msg =
      Task.perform identity identity (Task.succeed msg)

但是仍然会在 Push 后立即处理 OnCellAdded,而模型之间不会被渲染。

我最后的尝试是使用 Update.andThen (差异) (完整源代码)

Push ->
  let
    uid = List.length cells
  in
  ({ model
    | cells = [uid] ++ cells
  }, Cmd.none)
  |> Update.andThen update (OnCellAdded uid)

仍然不起作用。我需要帮助。

3个回答

9

使用requestAnimationFrame()的实现

在我的经验中,这似乎是目前最清晰的解决方案。

var container = document.getElementById('app');
var demoApp = Elm.RenderDemo.embed(container);
var requestAnimationFrame = 
       window.requestAnimationFrame ||
       window.mozRequestAnimationFrame || 
       window.webkitRequestAnimationFrame || 
       window.msRequestAnimationFrame;   //Cross browser support

var myPerfectlyTimedFunc = function(cellID) {
   requestAnimationFrame(function() { 
       if(document.getElementById('cell:' + cellID) === null) { 
          window.alert("Cannot find cell " + cellID) 
       }
   })
}

demoApp.ports.onCellAdded.subscribe(myPerfectlyTimedFunc);

点击此处查看具有多个页面和需要重新渲染JS图形的SPA类型设置。还可以更新图形中的数据值。(控制台日志消息也可能很有启发性。)

如果您想知道如何在Elm端而不是html/js端实现这一点,请参见Elm Defer Command库。

正如您所描述的,问题是:

  1. Javascript被加载并寻找尚未创建的元素。
  2. Elm在此搜索之后呈现DOM,您需要的元素出现。
  3. 通过端口发送的任何Elm命令也将在渲染时或之前发生,因此由端口订阅调用的任何javascript都将遭受相同的问题。
Elm使用requestAnimationFrame(rAF)作为排队DOM渲染的一种方式,这样做是有好处的。假设Elm在不到1/60秒内进行了多个DOM操作,而不是单独渲染每个操作 - 这将非常低效 - Elm将它们传递给浏览器的rAF,后者将充当整体DOM渲染的缓冲区/队列。换句话说,在update之后的动画帧上调用view,因此不会在每个update之后始终发生view调用。

过去人们会使用:

setInterval(someAnimationFunc, 16.6) //16.6ms for 60fps

requestAnimationFrame被引入作为浏览器保持队列的一种方式,它以60fps的速度进行管理和循环。这带来了许多改进:

  • 浏览器可以优化渲染,使动画更加流畅
  • 非活动标签页中的动画将停止,从而使CPU得到休息
  • 更省电

有关rAF的更多信息在这里在这里,以及Google的一个视频在这里

我的个人经历始于我尝试将Chartist.js图形呈现到最初在Elm中创建的div中。我还想要多个页面(SPA风格),当div元素在各种页面更改时需要重新呈现图表。

我在index中直接编写了div标签,但这样做阻碍了我所期望的SPA功能。我还使用了端口和订阅,例如$(window).load(tellElmToReRender),并尝试使用Arrive.js,但每种方法都导致了各种错误和缺乏所需的功能。我稍微调整了一下rAF,但是使用位置和方式不正确。听了ElmTown - Episode 4 - JS Interop之后,我恍然大悟,意识到应该如何真正使用它。

嗨cDitch,你能告诉我在ElmTown #4中哪个部分让你恍然大悟吗?我正在正确使用rAF(我想),我的问题来自其他来源。在嵌入式小部件呈现后,您是否遇到过“Uncaught TypeError:Cannot read property 'childNodes' of undefined”错误?该小部件对DOM所做的某些更新导致了此问题。 除非初始化后实际的DOM元素获取了一些新元素,否则一切都很好。但是添加/删除类或样式之类的东西完全正常。 使用Html.Keyed和Html.Lazy也没有帮助:( - gothy
gothy - 我也遇到过“childNodes”错误。在 Ellie 代码片段中复现这个错误困难吗?我会仔细查看代码,看看发生了什么。顺便说一句,感谢您提及 Html.Keyed,我知道这个功能存在,但不知道它的名称。你提到它帮助我解决了一个我花了很多小时才解决的错误! - cDitch
cDitch,我在Ellie上尝试了一个更小的示例来抛出这个异常,但似乎它更深层次...无论如何,我已经找到了一种方法来暂时解决我的应用程序中的问题。很高兴Html.Keyed有助于解决您的问题 :) - gothy

5

昨天我想做类似的事情,将MorrisJS集成到由Elm生成的div中。

最终,我发现了Arrive JS,它使用大多数现代浏览器中可用的新MutationObserver来监视DOM的变化。

因此,在我的情况下,代码看起来像这样(简化):

$(document).ready(() => {
  $(document).arrive('.morris-chart', function () {
    Morris.Bar({
      element: this,
      data: [
        { y: '2006', a: 100, b: 90 },
        { y: '2007', a: 75, b: 65 },
        { y: '2008', a: 50, b: 40 },
        { y: '2009', a: 75, b: 65 },
        { y: '2010', a: 50, b: 40 },
        { y: '2011', a: 75, b: 65 },
        { y: '2012', a: 100, b: 90 }
      ],
      xkey: 'y',
      ykeys: ['a', 'b'],
      labels: ['Series A', 'Series B']
    })
  })
})

这段代码会监控所有带有 .morris-chart 类名的新元素,一旦发现,就会在该元素上创建图表。因此,只有在 Elm 运行 view 函数并重新生成 DOM 后才会调用它。也许像这样的代码可以满足您的需求。

这是一个不错的模式。 - Lukasz Guminski

4
截至版本0.17.1,目前还没有很好的方法可以实现这一点。最简单的建议是使用setTimeout等待至少60毫秒或者等到下一个requestAnimationFrame
考虑以下示例:
demoApp.ports.onCellAdded.subscribe(function(cellID) {
   setTimeout(function() {
      if(document.getElementById('cell:' + cellID) === null) {
         window.alert("Cannot find cell " + cellID)
      }
   }, 60);
});

有一个功能请求#19,希望添加一个挂钩(hook),以便知道HTML节点何时在DOM中。

您可以在此处跟踪进度,很可能会在即将发布的版本中实现。


好的,谢谢! - Lukasz Guminski
1
如果您选择使用requestAnimationFrame路径,有一个软件包可以让您订阅这些事件http://package.elm-lang.org/packages/elm-lang/animation-frame/1.0.0/AnimationFrame - robertjlooby
@robertjlooby 暂时我决定在Javascript端创建div,即Elm只管理模型,Javascript负责视图。这样我就避免了Elm视图和Javascript视图之间的同步问题,可以这么说。希望这是一个临时的解决方案。 - Lukasz Guminski

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接