如何在用于编辑 HTML 的 Javascript 组件中实现视图模型分离?

Posted

技术标签:

【中文标题】如何在用于编辑 HTML 的 Javascript 组件中实现视图模型分离?【英文标题】:How can I achieve view-model separation in a Javascript component for editing HTML? 【发布时间】:2015-09-26 00:27:41 【问题描述】:

我需要为特定的 html 子集构建一个浏览器内 WYSI(或多或少)WYG 编辑器。这要求模型 HTML 元素可以用额外的标记进行修饰以支持编辑。出于编辑的目的,可能必须完全替换某些模型 HTML 元素。

我想避免在编辑器标记 HTML 和输出标记 HTML 之间来回转换,因为在之前的组件化身中证明这很容易出错。

相反,我想要一个清晰的模型和视图分离,最好使用客户端 MVC 框架之一,例如 React.js。

如何做到这一点?到目前为止,我想出了以下几个想法:

将模型表示为浏览器 DOM,并将其用作生成编辑器标记(和行为)的视图的模型 使用 React 的虚拟 DOM 表示模型(这甚至可能吗?) 使用 javascript 的数据类型滚动我自己的模型表示。但是,这将需要编写解析器和序列化程序,我非常希望避免这样做。

这是如何在其他编辑器组件中完成的?这种方法是否可行?

编辑:为了更清楚地说明用例,我并不是在寻找支持像<strong><a> 等内联元素的常规编辑的架构。但是结合了内联元素的就地编辑(我正在考虑为此使用CKEditor 之类的东西)以及更多的结构编辑,例如克隆<div>s 和布局<table>s 并移动它们。

【问题讨论】:

很遗憾,要求我们推荐或查找书籍、工具、软件库、教程或其他非现场资源的问题在此处是题外话。但是,您可能会发现更好的运气SoftwareRecs.SE。记得阅读their question requirements,因为他们比这个网站更严格。 这是题外话,但是因为“跨浏览器兼容性和良好的性能”而不使用浏览器的 DOM 听起来很奇怪。浏览器是围绕 DOM 构建的,它上面的任何东西很可能比原生 DOM 慢得多,而且它更有可能增加而不是减少兼容性问题,因为现在您需要担心额外的兼容性层(非浏览器 DOM 库与浏览器的兼容性。) @Juhana,浏览器的 DOM 是出了名的慢(这是 React 使用虚拟 DOM 的全部原因),因为它需要检查是否需要重新渲染内容。我不需要渲染,我只想操作模型。 但只要您不操作文档中的元素,那么浏览器就不会进行重新渲染检查。你不需要图书馆来做到这一点。 您引用的指南还指出“相反,请描述问题以及迄今为止为解决该问题所做的工作” - 我认为 OP 已经做到了。这是一个真正的编程问题,他提出了这个问题(然后继续提出可能的解决方案)。如果问题是“什么是伪造 DOM 的最佳库?”,那么可以肯定,这个问题会跑题。 【参考方案1】:

如果我能正确理解您的问题,您想在单个页面中编辑多个元素吗?

在这种情况下,我建议使用 angularjs 指令编写此“模块”(通过属性连接,因此您对 html 的语义与它根本不应用于文档相同)。该指令(当加载 Angular 应用程序时)应更改元素的“可编辑”属性,该属性将允许用户编辑元素的内容。所以让我们说模板是这样的:

<div id="title-editor" my-editor>...</div>

这可能是当用户模糊这个 div 然后在指令中触发事件并将内容发送到后端的场景。

这是工作指令,可能是审查它的候选人https://github.com/TerryMooreII/angular-wysiwyg

最重要的是,您可以在页面中拥有多个“占位符”,因此您可以定义布局以及将哪些 Web 部件放置在页面中的位置,其他事情则由页面版主掌握。

还有一点需要注意的是,为每个占位符命名,这样您就可以在后端识别它们并在您的情况下填充数据,或者您可以使用 jquery/angular 在页面加载时填充它们。问题是你喜欢什么方式:)

在后端,您可以使用 redis 或 mongo 来存储这个占位符逻辑结构,而不是尝试表示完整的 DOM 结构并在每次从后端读取它时重新创建它。所以这样加载会快很多。

对于您想要在页面上拖放的部分,您将需要一些占位符,但这是不同类型的“编辑器”,因此您需要额外的角度指令。这里还有一个可以帮助您了解如何做的知识。 http://codef0rmer.github.io/angular-dragdrop.

将这两者结合起来,您可以得到接近您需要的东西。再次。您必须(作为开发人员)负责页面布局,因为您必须至少定义***(拖放)占位符的位置。

【讨论】:

【参考方案2】:

据我了解,这不是使用哪个库或使用哪种模式的问题。它更像是一个基于角色的架构。假设您可以在编辑器中创建和编辑的元素之一是 <strong> 标签。最终用户会看到

<strong class="some-class">Some content</strong>

虽然您的编辑器会看到相同的元素(因此所见即所得)但具有其他样式或控件,但假设它会显示为红色并有一个“删除”按钮。

<strong class="some-class">Some content <sup>Remove</sup></strong>

我们可以利用 CSS 来实现这些关于元素视觉方面的差异,只需在编辑器画布上使用一个额外的类。

.canvas strong
    font-weight: bold;

.canvas strong sup
    display:none;


.editing.canvas strong
    color:red;

.editing.canvas strong sup
    display:inline;


// your end users will see
<div class="canvas">
    // but the <sup> element will be display:none; by default
    <strong class="some-class">Some content <sup>Remove</sup></strong>
</div>

// your editors will see
<div class="canvas editing">
    // here, the 'editing' class will make the <sup> visible and change the color to red
    <strong class="some-class">Some content <sup>Remove</sup></strong>
</div>

当然,这是一个非常简单的示例,但您可以看到更复杂的元素如何融入此架构。现在是 js 部分。

假设我们的强元素会在悬停时向最终用户发出警报,而对于我们的编辑器来说,它将处理对“删除”元素的点击。

function StrongElement()
    var id,
        el,
        data = 
           content: 'default content',
           classname: 'default classname'
        ;

    function setId(id)
        id = id;
        render();
    

    function setContent(content)
        data.content = content;
        render();
    

    (function create()
        // this is done only once!
        el = document.createElement('strong'); // this in more complex elements would be a template
        var remove = document.createElement('sup');
        remove.innerHTML = 'remove';
        el.appendChild(remove);
    )();

    function render()
        el.id = id;
        el.className = data.classname;
        el.innerHTML = data.content;
        return el;
    

    function onHover()
        alert('Im strong and I like it');
    

    function remove()
        // more on editor later*
        if (editor.isInEditingMode())
            editor.remove(id);
        
    

    // expose the public methods
    return 
        render: render
    ;

我们已经定义了一个 js 'class' 来处理这个元素的任何交互。它可能更复杂,元素可能有嵌套元素。普通 html 插件的附加功能是这些元素有一个 id,并且某些方法仅在变量 editor 存在时才有效。我们来看看编辑器:

function Editor()
    var canvas = document.getElementById('canvas'), // note that the canvas can be a text area, a div, or even a html5 canvas where your elements are rendered graphically instead of the DOM
        elements = [],
        editingMode = true;

    function addElement(element)
        // keep track of the js instance of the element
        elements.push(element);
        // give it an id that both the element and the editor know
        element.setId(elements.length);
        // append the graphic representation of the instance (some html tags) into the graphic representation of the editor
        canvas.appendChild(element.render());
        // we return the id so that anyone who added an element can keep track of it
        return elements.length; // the id, will be the index in the array of elements
    

    function removeElement(id)
        // this could be way better but its just for demo purposes
        elements[id] = null;
    

    function refresh()
        // elements are already in the canvas, so we iterate over all of them 
        for (var i in elements)
            // and re render them
            elements[i].render();
        
    

    function setEditingMode(mode)
        editingMode = mode;
        if (editingMode)
             canvas.className = 'editing';
        else
             canvas.className = '';
        
    

    function isInEditingMode()
        return editingMode;
    

    return 
        isInEditingMode: isInEditingMode,
        addElement: addElement,
        removeElement: removeElement,
        refresh: refresh
    

这是一个非常简单的编辑器,要使其达到生产就绪级别,您需要处理每个元素的位置,在这种情况下,我们可以假设元素的显示顺序与添加它们的顺序相同。但是,当表示在像素画布上绘制的元素的树结构或 x、y 和 z 位置时,这会变得更有趣。该位置数据将被添加到每个元素中,并且每个元素中的渲染函数应该能够参考父编辑器来处理它。

这个超级简单的编辑器可以这样工作:

<div id="canvas"></div>

var editor = new Editor(),
    element = new Strong();

element.setContent("I'm Stronger");
editor.addElement( element );
editor.refresh();
editor.setEditingMode(true); // this should add the 'editing' class and you should now see the remove button and red colored content
editor.setEditingMode(false); // this should return the element to a end user state and if you hover you should get an alert

演示:http://jsfiddle.net/pzef52gh/1/

基本上,我要为最终用户设计和实现元素,包括样式和功能。之后,我会在这些元素之上构建您处于编辑模式时的附加样式和功能。您可以使用任何语言和任何框架来执行此操作。这个示例对于主干js 用户来说可能看起来很熟悉,但如果我今天必须做这样的事情,我会使用角度,并且每个“元素”都将是一个具有编辑/非编辑状态的指令。希望对您有所帮助!

【讨论】:

感谢您提供非常详细的回答。不幸的是,这种方法意味着我将在编辑后的 ​​HTML 中有额外的编辑器支持元素,这是我试图避免的(我在上面已经说过)。我不能在由编辑器组件保存到数据库的 HTML 中包含任何额外的编辑器元素。【参考方案3】:

我建议使用 ReactJS 研究 Flux 实现。这样,您的模型可以很容易地抽象为使用 Flux 操作调用的服务,而这些服务又可以在您的应用内数据可以改变的商店中被监听。这样,您的视图逻辑在组件中,而您的业务逻辑在其他地方抽象。

【讨论】:

感谢您的回答,但是,我觉得它太笼统了。我将如何将其应用于我的特定用例? Flux 如何帮助我处理 DOM 本身的模型? 我相信这就是您想要的。选择一个实现通量架构的库来对您在视图中使用的数据进行建模。当你想操纵数据时......称之为行动。 @cwbutler 可能是的,事实上我现在正朝着这个方向努力,但我正在寻找更具体的指导,更适合我的情况和我提出的具体问题。【参考方案4】:

不要使用外部工具,自己制作。更多控制,更少无用代码。

MVC : model 是编辑后的数据,view 是显示的 html,controller 是 SQL I/O 和保存/编辑功能。

这有什么问题?

有条不紊、清晰明了,少即是多,你会成功的。并且完全掌握它。

【讨论】:

问题是模型也是 DOM,我真的很难理解这将如何在 MVC 的上下文中工作,因为 DOM 是高度分层且不可观察的。你能详细说明如何解决这个问题吗? 我不确定您的问题到底是什么。也许我太容易了,但编辑 HTML 既是编辑代码(长文本),又是渲染预览(“运行” $myframe.html(mylongtext) 中的代码)。非常快的说道。你有什么问题 ?你知道当一个人在问题中头脑清醒时很难表达困难,缩小并简单地解释,综合。谢谢 所以,在这种情况下,我不仅要渲染预览,还要渲染交互式预览。应该有选择框,div 上的悬停按钮和类似的东西。这意味着编辑器视图与我要编辑的实际 HTML 代码不同。这意味着我将有一个视图转换 f(mylongtext),以借用您的示例。如果我现在非常快速地更改模型(考虑范围滑块更改一些数字属性,如颜色或边框半径),则可以保证连续重新渲染视图会破坏性能。这就是我正在考虑的问题。 好的。更清楚了,谢谢。你肯定不会得到很好的性能。美丽的项目,我不禁抱歉。我必须说,对我来说,这根本不是 MVC 的问题。非常没有。当然 MVC 可以改进这一切,但它更多的是关于设计/概念。完全出于 *** 的目标,不是代码问题,而是架构问题。 题外话 架构就是代码,至少对我来说是这样。但是感谢您推动我澄清问题,这已经有很大帮助。

以上是关于如何在用于编辑 HTML 的 Javascript 组件中实现视图模型分离?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Firebug 中编辑 JavaScript?

如何在 Eclipse ADT 中编辑 HTML/CSS/Javascript [关闭]

如何使用 Javascript 获取选定文本中的所有 HTML 选择标签?

如何在HTML / Javascript中创建可编辑的组合框?

如何在Javascript中检测Safari 10浏览器?

在 Atom 编辑器中突出显示 HTML 中的 javascript