Maison > Article > interface Web > Démo pratique : créez votre propre framework à partir de zéro
Dans la première partie de cette série, nous avons discuté des composants qui vous permettent de gérer différents comportements à l'aide de facettes, et de la manière dont Milo gère la messagerie.
Dans cet article, nous aborderons un autre problème courant lors du développement d'applications de navigateur : le modèle pour afficher la connexion. Nous découvrirons une partie de la « magie » de la liaison de données bidirectionnelle dans Milo et, enfin, nous créerons une application de tâches entièrement fonctionnelle en moins de 50 lignes de code.
Il existe plusieurs idées fausses à propos de JavaScript. De nombreux développeurs pensent qu'eval est mauvais et ne devrait jamais être utilisé. Cette croyance fait que de nombreux développeurs sont incapables de déterminer quand eval peut et doit être utilisé.
Les sorts comme "eval
est le mal" ne sont destructeurs que lorsqu'il s'agit de quelque chose qui est essentiellement un outil. Les outils ne sont que « bons » ou « mauvais » selon le contexte. Vous ne diriez pas que le marteau est mauvais, n'est-ce pas ? Cela dépend vraiment de la façon dont vous l'utilisez. "Le marteau est très bien" lorsqu'il est utilisé avec des clous et certains meubles. "Les marteaux ne servent à rien" lorsqu'ils sont utilisés pour beurrer du pain.
Bien que nous soyons tout à fait d'accord sur le fait que eval
a ses limites (par exemple, ses performances) et ses risques (surtout si nous évaluons le code saisi par l'utilisateur), dans de nombreux cas, l'évaluation est le seul moyen d'obtenir la fonctionnalité souhaitée.
Par exemple, de nombreux moteurs de modèles utilisent eval
dans le cadre de l'opérateur with (un autre grand non-non parmi les développeurs) pour compiler des modèles en fonctions JavaScript.
Lorsque nous avons réfléchi à ce que nous attendions du modèle, nous avons envisagé plusieurs approches. La première consiste à utiliser un modèle peu profond comme Backbone et à émettre des messages lorsque le modèle change. Bien que faciles à mettre en œuvre, ces modèles ont une utilité limitée : la plupart des modèles réels sont très approfondis.
Nous avons envisagé d'intégrer des objets JavaScript purs avec Object.observe
API 一起使用(这将消除实现任何模型的需要)。虽然我们的应用程序只需要与 Chrome 配合使用,但 Object.observe
qui n'était activé que récemment par défaut - auparavant, il nécessitait l'activation d'un indicateur Chrome, ce qui rendrait le déploiement et la prise en charge difficiles.
Nous voulons un modèle qui puisse être connecté à une vue, mais de manière à pouvoir changer la structure de la vue sans changer une seule ligne de code, sans changer la structure du modèle, et sans gérer explicitement la conversion du modèle de vue au modèle de données.
Nous souhaitons également pouvoir connecter des modèles entre eux (voir programmation réactive) et souscrire aux modifications de modèles. Angular implémente la surveillance en comparant l'état des modèles, ce qui devient très inefficace pour les modèles volumineux et profonds.
Après quelques discussions, nous avons décidé d'implémenter notre classe modèle qui prendra en charge une simple API get/set pour les faire fonctionner et permettre de s'abonner à leurs modifications :
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' }
Cette API ressemble à un accès normal aux propriétés et devrait fournir un accès approfondi et sécurisé aux propriétés - lorsque get
时,它返回 undefined
,并且当set
est appelé sur un chemin de propriété qui n'existe pas, il crée l'arborescence d'objets/tableaux manquants selon les besoins.
Cette API a été créée avant la mise en œuvre et la principale inconnue à laquelle nous avons été confrontés était de savoir comment créer un objet qui était également une fonction appelable. Il s'avère que pour créer un constructeur qui renvoie un objet appelable, vous devez renvoyer cette fonction du constructeur et en même temps définir son prototype pour qu'il soit une instance de la classe Model
:
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__;
Bien qu'il soit généralement préférable d'éviter d'utiliser la propriété __proto__
d'un objet, c'est toujours le seul moyen de modifier le prototype d'instance et le prototype de constructeur d'un objet.
Le ModelPath
实例(例如上面的 m('.info.name')
)提出了另一个实现挑战。 ModelPath
实例应该具有在调用模型时正确设置传递给模型的模型属性的方法(在本例中为 .info.name
qui doit être renvoyé lors de l'appel du modèle). Nous avons envisagé de les implémenter en analysant simplement les propriétés transmises sous forme de chaînes lors de l'accès, mais nous avons réalisé que cela entraînerait de mauvaises performances.
Au lieu de cela, nous avons décidé de le faire de manière à ce que m('.info.name')
renvoie un objet (une instance de ModelPath) implémentent leur "classe"), en convertissant toutes les méthodes d'accès (<code class="inline">get
, set
, del
et splice
) sont synthétisés en code JavaScript et convertis en fonctions JavaScript à l'aide de m('.info.name')
返回一个对象(ModelPath
的实例)的方式来实现它们“class”),将所有访问器方法(get
、set
、del
和 splice
)合成为 JavaScript 代码并使用 eval
.
我们还缓存了所有这些合成方法,因此一旦任何模型使用 .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 共同撰写。
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!