>  기사  >  웹 프론트엔드  >  실습 데모: 처음부터 자신만의 프레임워크 구축

실습 데모: 처음부터 자신만의 프레임워크 구축

PHPz
PHPz원래의
2023-09-03 09:17:091017검색

실습 데모: 처음부터 자신만의 프레임워크 구축

이 시리즈의 첫 번째 부분에서는 패싯을 사용하여 다양한 동작을 관리할 수 있는 구성 요소와 Milo가 메시징을 관리하는 방법에 대해 논의했습니다.

이 기사에서는 브라우저 애플리케이션을 개발할 때 발생하는 또 다른 일반적인 문제인 보기 연결 모델에 대해 설명합니다. 우리는 Milo에서 양방향 데이터 바인딩의 "마법" 중 일부를 발견하고 마지막으로 50줄 미만의 코드로 완전한 기능을 갖춘 할 일 애플리케이션을 구축할 것입니다.

모델(또는 Eval은 사악하지 않음)

JavaScript에 대한 몇 가지 오해가 있습니다. 많은 개발자들은 eval이 악하므로 절대 사용해서는 안 된다고 믿습니다. 이러한 믿음으로 인해 많은 개발자는 언제 eval을 사용할 수 있고 사용해야 하는지 판단할 수 없게 됩니다.

"eval is evil"과 같은 주문은 본질적으로 도구인 것을 다룰 때만 파괴적입니다. 도구는 상황에 따라 "좋음" 또는 "나쁨"일 뿐입니다. 망치가 나쁘다고는 말할 수 없겠죠? 실제로 어떻게 사용하느냐에 따라 다릅니다. 못이나 일부 가구에 사용하면 "해머는 괜찮습니다". 빵에 버터를 바르는 데 사용할 때 "망치는 좋지 않습니다."

우리는 eval에 한계(예: 성능)와 위험(특히 사용자가 입력한 코드를 평가하는 경우)이 있다는 점에 절대적으로 동의하지만, 많은 경우 eval은 원하는 기능을 달성하는 유일한 방법입니다.

예를 들어, 많은 템플릿 엔진은 템플릿을 JavaScript 함수로 컴파일하기 위해 with 연산자 범위 내에서 eval(개발자 사이에서 또 다른 큰 금기 사항)를 사용합니다.

모델에서 원하는 것이 무엇인지 생각할 때 몇 가지 접근 방식을 고려했습니다. 하나는 Backbone과 같은 얕은 모델을 사용하고 모델이 변경되면 메시지를 내보내는 것입니다. 구현하기는 쉽지만 이러한 모델은 유용성이 제한되어 있습니다. 대부분의 실제 모델은 매우 깊습니다.

우리는 최근에야 기본적으로 활성화된 Object.observe API 一起使用(这将消除实现任何模型的需要)。虽然我们的应用程序只需要与 Chrome 配合使用,但 Object.observe와 순수 JavaScript 개체를 통합하는 것을 고려했습니다. 이전에는 Chrome 플래그를 켜야 했기 때문에 배포 및 지원이 어려웠습니다.

우리는 뷰에 연결될 수 있지만 코드 한 줄 변경 없이, 모델 구조 변경 없이, 뷰 모델 변환을 명시적으로 관리하지 않고도 뷰 구조를 변경할 수 있는 모델을 원합니다. 데이터 모델에.

또한 모델을 서로 연결하고(반응형 프로그래밍 참조) 모델 변경 사항을 구독할 수 있기를 원합니다. Angular는 모델의 상태를 비교하여 모니터링을 구현하는데, 이는 크고 깊은 모델에는 매우 비효율적입니다.

몇 가지 논의 끝에 우리는 간단한 get/set API를 지원하여 이를 작동하고 변경 사항을 구독할 수 있는 모델 클래스를 구현하기로 결정했습니다.

으아악

이 API는 일반 속성 액세스와 유사하며 속성에 대한 안전한 심층 액세스를 제공해야 합니다. get 时,它返回 undefined,并且当set이 존재하지 않는 속성 경로에서 호출되면 필요에 따라 누락된 개체/배열 트리가 생성됩니다.

이 API는 구현 전에 생성되었으며 우리가 직면한 주요 알 수 없는 점은 호출 가능한 함수이기도 한 객체를 생성하는 방법이었습니다. 호출 가능한 객체를 반환하는 생성자를 생성하려면 생성자에서 이 함수를 반환하고 동시에 프로토타입이 Model 클래스의 인스턴스가 되도록 설정해야 합니다.

으아악

일반적으로 객체의 __proto__ 속성을 사용하지 않는 것이 가장 좋지만, 여전히 객체의 인스턴스 프로토타입과 생성자 프로토타입을 변경하는 유일한 방법입니다.

모델 호출 시 반환되어야 하는 ModelPath 实例(例如上面的 m('.info.name') )提出了另一个实现挑战。 ModelPath 实例应该具有在调用模型时正确设置传递给模型的模型属性的方法(在本例中为 .info.name ). 우리는 액세스할 때 문자열로 전달된 속성을 단순히 구문 분석하여 구현하는 것을 고려했지만 이로 인해 성능이 저하될 수 있음을 깨달았습니다.

대신 m('.info.name')이 객체(ModelPath) 해당 "클래스")를 구현하여 모든 접근자 메서드(<code class="inline">get, set, del splice)는 JavaScript 코드로 합성되고 m('.info.name') 返回一个对象(ModelPath 的实例)的方式来实现它们“class”),将所有访问器方法(getsetdelsplice)合成为 JavaScript 代码并使用 eval를 사용하여 JavaScript 함수로 변환됩니다.

我们还缓存了所有这些合成方法,因此一旦任何模型使用 .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);
}

事实证明这是一个很好的方法。它允许我们为我们拥有的所有访问器方法编写代码(getsetdel splice)非常模块化且可维护。

事实证明,我们开发的模型 API 非常有用且高性能。它演变为支持数组元素语法、数组的 splice 方法(以及派生方法,例如 pushpop 等)以及属性/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 共同撰写。

위 내용은 실습 데모: 처음부터 자신만의 프레임워크 구축의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
이전 기사:배열 재구성다음 기사:배열 재구성