Im ersten Teil dieser Serie haben wir Komponenten besprochen, die es Ihnen ermöglichen, unterschiedliche Verhaltensweisen mithilfe von Facetten zu verwalten, und wie Milo Nachrichten verwaltet.
In diesem Artikel besprechen wir ein weiteres häufiges Problem bei der Entwicklung von Browseranwendungen: die Verbindung zwischen Modell und Anzeige. Wir werden etwas von der „Magie“ der bidirektionalen Datenbindung in Milo entdecken und schließlich eine voll funktionsfähige To-Do-Anwendung in weniger als 50 Codezeilen erstellen.
Es gibt mehrere Missverständnisse über JavaScript. Viele Entwickler glauben, dass eval böse ist und niemals verwendet werden sollte. Diese Überzeugung führt dazu, dass viele Entwickler nicht bestimmen können, wann Evaluierung verwendet werden kann und sollte.
Zaubersprüche wie „eval
ist böse“ sind nur dann destruktiv, wenn wir es mit etwas zu tun haben, das im Wesentlichen ein Werkzeug ist. Werkzeuge sind je nach Kontext nur „gut“ oder „schlecht“. Du würdest doch nicht sagen, dass der Hammer böse ist, oder? Es hängt wirklich davon ab, wie Sie es verwenden. „Der Hammer ist in Ordnung“, wenn er mit Nägeln und einigen Möbeln verwendet wird. „Hämmer nützen nichts“, wenn sie zum Buttern von Brot verwendet werden.
Obwohl wir absolut der Meinung sind, dass eval
seine Grenzen (z. B. Leistung) und Risiken (insbesondere wenn wir vom Benutzer eingegebenen Code auswerten) hat, ist die Evaluierung in vielen Fällen die einzige Möglichkeit, die gewünschte Funktionalität zu erreichen.
Zum Beispiel verwenden viele Template-Engines eval
im Rahmen des with-Operators (ein weiteres großes Tabu unter Entwicklern), um Vorlagen in JavaScript-Funktionen zu kompilieren.
Als wir darüber nachdachten, was wir von dem Modell erwarten, haben wir mehrere Ansätze in Betracht gezogen. Eine besteht darin, ein flaches Modell wie Backbone zu verwenden und Nachrichten auszugeben, wenn sich das Modell ändert. Obwohl diese Modelle einfach zu implementieren sind, haben sie nur begrenzten Nutzen – die meisten realen Modelle sind sehr tiefgreifend.
Wir haben darüber nachgedacht, reine JavaScript-Objekte mit Object.observe
API 一起使用(这将消除实现任何模型的需要)。虽然我们的应用程序只需要与 Chrome 配合使用,但 Object.observe
zu integrieren, was erst seit Kurzem standardmäßig aktiviert ist – zuvor war es erforderlich, dass ein Chrome-Flag aktiviert war, was die Bereitstellung und Unterstützung erschweren würde.
Wir wollen ein Modell, das mit einer Ansicht verbunden werden kann, aber so, dass wir die Struktur der Ansicht ändern können, ohne eine einzige Codezeile zu ändern, ohne die Struktur des Modells zu ändern und ohne die Konvertierung des Ansichtsmodells explizit zu verwalten zum Datenmodell.
Wir möchten auch in der Lage sein, Modelle miteinander zu verbinden (siehe reaktive Programmierung) und Modelländerungen zu abonnieren. Angular implementiert die Überwachung durch den Vergleich des Zustands von Modellen, was bei großen und tiefen Modellen sehr ineffizient wird.
Nach einiger Diskussion haben wir beschlossen, unsere Modellklasse zu implementieren, die eine einfache Get/Set-API unterstützt, um sie zu bedienen und das Abonnieren von Änderungen in ihnen zu ermöglichen:
var m = new Model; m('.info.name').set('angular'); console.log(m('.info').get()); // logs: {name: 'angular'} m.on('.info.name', onNameChange); function onNameChange(msg, data) { console.log('Name changed from', data.oldValue, 'to', data.newValue); } m('.info.name').set('milo'); // logs: Name changed from angular to milo console.log(m.get()); // logs: { info: { name: 'milo' } } console.log(m('.info').get()); // logs: { name: 'milo' }
Diese API ähnelt dem normalen Eigenschaftszugriff und sollte einen sicheren Tiefenzugriff auf Eigenschaften ermöglichen – wenn get
时,它返回 undefined
,并且当set
auf einem nicht vorhandenen Eigenschaftspfad aufgerufen wird, erstellt es nach Bedarf den fehlenden Objekt-/Array-Baum.
Diese API wurde vor der Implementierung erstellt und die größte Unbekannte, mit der wir konfrontiert waren, war, wie man ein Objekt erstellt, das auch eine aufrufbare Funktion ist. Es stellt sich heraus, dass Sie zum Erstellen eines Konstruktors, der ein aufrufbares Objekt zurückgibt, diese Funktion vom Konstruktor zurückgeben und gleichzeitig ihren Prototyp so festlegen müssen, dass er eine Instanz der Model
-Klasse ist:
function Model(data) { // modelPath should return a ModelPath object // with methods to get/set model properties, // to subscribe to property changes, etc. var model = function modelPath(path) { return new ModelPath(model, path); } model.__proto__ = Model.prototype; model._data = data; model._messenger = new Messenger(model, Messenger.defaultMethods); return model; } Model.prototype.__proto__ = Model.__proto__;
Während es im Allgemeinen am besten ist, die Verwendung der Eigenschaft __proto__
eines Objekts zu vermeiden, ist dies dennoch die einzige Möglichkeit, den Instanzprototyp und den Konstruktorprototyp eines Objekts zu ändern.
Das ModelPath
实例(例如上面的 m('.info.name')
)提出了另一个实现挑战。 ModelPath
实例应该具有在调用模型时正确设置传递给模型的模型属性的方法(在本例中为 .info.name
, das beim Aufruf des Modells zurückgegeben werden sollte). Wir haben darüber nachgedacht, sie zu implementieren, indem wir einfach die beim Zugriff als Zeichenfolgen übergebenen Eigenschaften analysieren, aber wir haben festgestellt, dass dies zu einer schlechten Leistung führen würde.
Stattdessen haben wir uns dafür entschieden, es so zu machen, dass m('.info.name')
ein Objekt (eine Instanz von ModelPath<) zurückgibt /code>) implementieren ihre „Klasse“) und konvertieren alle Zugriffsmethoden (<code class="inline">get
, set
, del
und splice
) werden in JavaScript-Code synthetisiert und mithilfe von m('.info.name')
返回一个对象(ModelPath
的实例)的方式来实现它们“class”),将所有访问器方法(get
、set
、del
和 splice
)合成为 JavaScript 代码并使用 eval
in JavaScript-Funktionen umgewandelt.
我们还缓存了所有这些合成方法,因此一旦任何模型使用 .info.name
,该“属性路径”的所有访问器方法都会被缓存,并且可以重用于任何其他模型。
get 方法的第一个实现如下所示:
function synthesizeGetter(path, parsedPath) { var getter; var getterCode = 'getter = function value() ' + '{\n var m = ' + modelAccessPrefix + ';\n return '; var modelDataProperty = 'm'; for (var i=0, count = parsedPath.length-1; i < count; i++) { modelDataProperty += parsedPath[i].property; getterCode += modelDataProperty + ' && '; } getterCode += modelDataProperty + parsedPath[count].property + ';\n };'; try { eval(getterCode); } catch (e) { throw ModelError('ModelPath getter error; path: ' + path + ', code: ' + getterCode); } return getter; }
但是 set
方法看起来更糟糕,并且非常难以遵循、阅读和维护,因为创建的方法的代码大量散布在生成该方法的代码中。因此,我们改用 doT 模板引擎来生成访问器方法的代码。
这是切换到使用模板后的 getter:
var dotDef = { modelAccessPrefix: 'this._model._data', }; var getterTemplate = 'method = function value() { \ var m = {{# def.modelAccessPrefix }}; \ {{ var modelDataProperty = "m"; }} \ return {{ \ for (var i = 0, count = it.parsedPath.length-1; \ i < count; i++) { \ modelDataProperty+=it.parsedPath[i].property; \ }} {{=modelDataProperty}} && {{ \ } \ }} {{=modelDataProperty}}{{=it.parsedPath[count].property}}; \ }'; var getterSynthesizer = dot.compile(getterTemplate, dotDef); function synthesizeMethod(synthesizer, path, parsedPath) { var method , methodCode = synthesizer({ parsedPath: parsedPath }); try { eval(methodCode); } catch (e) { throw Error('ModelPath method compilation error; path: ' + path + ', code: ' + methodCode); } return method; } function synthesizeGetter(path, parsedPath) { return synthesizeMethod(getterSynthesizer, path, parsedPath); }
事实证明这是一个很好的方法。它允许我们为我们拥有的所有访问器方法编写代码(get
、set
、del
和 splice
)非常模块化且可维护。
事实证明,我们开发的模型 API 非常有用且高性能。它演变为支持数组元素语法、数组的 splice
方法(以及派生方法,例如 push
、pop
等)以及属性/item 访问插值。
引入后者是为了避免当唯一改变的是某些属性或项目索引时合成访问器方法(这是访问属性或项目慢得多的操作)。如果模型内的数组元素必须在循环中更新,就会发生这种情况。
考虑这个例子:
for (var i = 0; i < 100; i++) { var mPath = m('.list[' + i + '].name'); var name = mPath.get(); mPath.set(capitalize(name)); }
在每次迭代中,都会创建一个 ModelPath
实例来访问和更新模型中数组元素的 name 属性。所有实例都有不同的属性路径,并且需要使用 eval
为 100 个元素中的每一个元素合成四个访问器方法。这将是一个相当慢的操作。
通过属性访问插值,此示例中的第二行可以更改为:
var mPath = m('.list[$1].name', i);
它不仅看起来更具可读性,而且速度更快。虽然我们仍然在此循环中创建 100 个 ModelPath
实例,但它们都将共享相同的访问器方法,因此我们只合成四种方法,而不是 400 个。
欢迎您估计这些示例之间的性能差异。
Milo 使用可观察模型实现了反应式编程,只要其任何属性发生变化,这些模型就会向自身发出通知。这使我们能够使用以下 API 实现反应式数据连接:
var connector = minder(m1, '<<<->>>', m2('.info')); // creates bi-directional reactive connection // between model m1 and property “.info” of model m2 // with the depth of 2 (properties and sub-properties // of models are connected).
从上面一行可以看出,由 <code class="inline">m2('.info')
返回的 ModelPath 应该具有与模型相同的 API,这意味着具有与模型相同的消息 API,也是一个函数:
var mPath = m('.info); mPath('.name').set(''); // sets poperty '.info.name' in m mPath.on('.name', onNameChange); // same as m('.info.name').on('', onNameChange) // same as m.on('.info.name', onNameChange);
以类似的方式,我们可以将模型连接到视图。组件(请参阅本系列的第一部分)可以有一个数据方面,用作 API 来操作 DOM,就好像它是一个模型一样。它具有与模型相同的 API,可以在反应式连接中使用。
例如,此代码将 DOM 视图连接到模型:
var connector = minder(m, ‘<<<->>>’, comp.data);
下面将在示例待办事项应用程序中对其进行更详细的演示。
这个连接器如何工作?在底层,连接器只是订阅连接两侧数据源中的更改,并将从一个数据源接收到的更改传递到另一个数据源。数据源可以是模型、模型路径、组件的数据方面或实现与模型相同的消息传递 API 的任何其他对象。
连接器的第一个实现非常简单:
// ds1 and ds2 – connected datasources // mode defines the direction and the depth of connection function Connector(ds1, mode, ds2) { var parsedMode = mode.match(/^(\<*)\-+(\>*)$/); _.extend(this, { ds1: ds1, ds2: ds2, mode: mode, depth1: parsedMode[1].length, depth2: parsedMode[2].length, isOn: false }); this.on(); } _.extendProto(Connector, { on: on, off: off }); function on() { var subscriptionPath = this._subscriptionPath = new Array(this.depth1 || this.depth2).join('*'); var self = this; if (this.depth1) linkDataSource('_link1', '_link2', this.ds1, this.ds2, subscriptionPath); if (this.depth2) linkDataSource('_link2', '_link1', this.ds2, this.ds1, subscriptionPath); this.isOn = true; function linkDataSource(linkName, stopLink, linkToDS, linkedDS, subscriptionPath) { var onData = function onData(path, data) { // prevents endless message loop // for bi-directional connections if (onData.__stopLink) return; var dsPath = linkToDS.path(path); if (dsPath) { self[stopLink].__stopLink = true; dsPath.set(data.newValue); delete self[stopLink].__stopLink } }; linkedDS.on(subscriptionPath, onData); self[linkName] = onData; return onData; } } function off() { var self = this; unlinkDataSource(this.ds1, '_link2'); unlinkDataSource(this.ds2, '_link1'); this.isOn = false; function unlinkDataSource(linkedDS, linkName) { if (self[linkName]) { linkedDS.off(self._subscriptionPath, self[linkName]); delete self[linkName]; } } }
到目前为止,milo 中的反应式连接已经有了很大的发展 - 它们可以更改数据结构、更改数据本身,还可以执行数据验证。这使我们能够创建一个非常强大的 UI/表单生成器,我们也计划将其开源。
你们中的许多人都会知道 TodoMVC 项目:使用各种不同的 MV* 框架制作的待办应用程序实现的集合。 To-Do 应用程序是对任何框架的完美测试,因为它的构建和比较相当简单,但需要相当广泛的功能,包括 CRUD(创建、读取、更新和删除)操作、DOM 交互和视图/模型仅举几例绑定。
在 Milo 开发的各个阶段,我们尝试构建简单的待办事项应用程序,并且毫无失败地突出了框架错误或缺点。即使深入我们的主项目,当 Milo 用于支持更复杂的应用程序时,我们也通过这种方式发现了小错误。到目前为止,该框架涵盖了 Web 应用程序开发所需的大部分领域,我们发现构建待办事项应用程序所需的代码非常简洁且具有声明性。
首先,我们有 HTML 标记。它是一个标准的 HTML 样板,带有一些样式来管理选中的项目。在正文中,我们有一个 ml-bind
属性来声明待办事项列表,这只是一个添加了 list
方面的简单组件。如果我们想要有多个列表,我们可能应该为此列表定义一个组件类。
列表中是我们的示例项,它是使用自定义 Todo
类声明的。虽然声明类不是必需的,但它使组件子组件的管理变得更加简单和模块化。
<html> <head> <script src="../../milo.bundle.js"></script> <script src="todo.js"></script> <link rel="stylesheet" type="text/css" href="todo.css"> <style> /* Style for checked items */ .todo-item-checked { color: #888; text-decoration: line-through; } </style> </head> <body> <!-- An HTML input managed by a component with a `data` facet --> <input ml-bind="[data]:newTodo" /> <!-- A button with an `events` facet --> <button ml-bind="[events]:addBtn">Add</button> <h3>To-Do's</h3> <!-- Since we have only one list it makes sense to declare it like this. To manage multiple lists, a list class should be setup like this: ml-bind="MyList:todos" --> <ul ml-bind="[list]:todos"> <!-- A single todo item in the list. Every list requires one child with an item facet. This is basically milo's ng-repeat, except that we manage lists and items separately and you can include any other markup in here that you need. --> <li ml-bind="Todo:todo"> <!-- And each list has the following markup and child components that it manages. --> <input ml-bind="[data]:checked" type="checkbox"> <!-- Notice the `contenteditable`. This works, out-of-the-box with `data` facet to fire off changes to the `minder`. --> <span ml-bind="[data]:text" contenteditable="true"></span> <button ml-bind="[events]:deleteBtn">X</button> </li> </ul> <!-- This component is only to show the contents of the model --> <h3>Model</h3> <div ml-bind="[data]:modelView"></div> </body>
为了让我们现在运行 milo.binder()
,我们首先需要定义 Todo
类。该类需要具有 item
方面,并且基本上负责管理每个 Todo
上的删除按钮和复选框。
在组件对其子组件进行操作之前,它需要首先等待对其触发 childrenbound
事件。有关组件生命周期的更多信息,请查看文档(链接到组件文档)。
// Creating a new facetted component class with the `item` facet. // This would usually be defined in it's own file. // Note: The item facet will `require` in // the `container`, `data` and `dom` facets var Todo = _.createSubclass(milo.Component, 'Todo'); milo.registry.components.add(Todo); // Adding our own custom init method _.extendProto(Todo, { init: Todo$init }); function Todo$init() { // Calling the inherited init method. milo.Component.prototype.init.apply(this, arguments); // Listening for `childrenbound` which is fired after binder // has finished with all children of this component. this.on('childrenbound', function() { // We get the scope (the child components live here) var scope = this.container.scope; // And setup two subscriptions, one to the data of the checkbox // The subscription syntax allows for context to be passed scope.checked.data.on('', { subscriber: checkTodo, context: this }); // and one to the delete button's `click` event. scope.deleteBtn.events.on('click', { subscriber: removeTodo, context: this }); }); // When checkbox changes, we'll set the class of the Todo accordingly function checkTodo(path, data) { this.el.classList.toggle('todo-item-checked', data.newValue); } // To remove the item, we use the `removeItem` method of the `item` facet function removeTodo(eventType, event) { this.item.removeItem(); } }
现在我们已经完成了设置,我们可以调用绑定器将组件附加到 DOM 元素,创建一个通过其数据方面与列表进行双向连接的新模型。
// Milo ready function, works like jQuery's ready function. milo(function() { // Call binder on the document. // It attaches components to DOM elements with ml-bind attribute var scope = milo.binder(); // Get access to our components via the scope object var todos = scope.todos // Todos list , newTodo = scope.newTodo // New todo input , addBtn = scope.addBtn // Add button , modelView = scope.modelView; // Where we print out model // Setup our model, this will hold the array of todos var m = new milo.Model; // This subscription will show us the contents of the // model at all times below the todos m.on(/.*/, function showModel(msg, data) { modelView.data.set(JSON.stringify(m.get())); }); // Create a deep two-way bind between our model and the todos list data facet. // The innermost chevrons show connection direction (can also be one way), // the rest define connection depth - 2 levels in this case, to include // the properties of array items. milo.minder(m, '<<<->>>', todos.data); // Subscription to click event of add button addBtn.events.on('click', addTodo); // Click handler of add button function addTodo() { // We package the `newTodo` input up as an object // The property `text` corresponds to the item markup. var itemData = { text: newTodo.data.get() }; // We push that data into the model. // The view will be updated automatically! m.push(itemData); // And finally set the input to blank again. newTodo.data.set(''); } });
此示例可在 jsfiddle 中找到。
待办事项示例非常简单,它仅显示了 Milo 强大功能的一小部分。 Milo 具有本文和之前的文章中未涵盖的许多功能,包括拖放、本地存储、http 和 websockets 实用程序、高级 DOM 实用程序等。
如今,milo 为 dailymail.co.uk 的新 CMS 提供支持(该 CMS 拥有数万个前端 JavaScript 代码,每天用于创建超过 500 篇文章)。
p>
Milo 是开源的,仍处于测试阶段,因此现在是尝试它甚至做出贡献的好时机。我们希望得到您的反馈。
请注意,本文由 Jason Green 和 Evgeny Poberezkin 共同撰写。
Das obige ist der detaillierte Inhalt vonPraktische Demo: Erstellen Sie Ihr eigenes Framework von Grund auf. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!