构建前端框架;零依赖性的反应性和可组合性

发布时间 2023-09-21 11:17:51作者: 漫漫长路

 

原文链接:https://18alan.space/posts/how-hard-is-it-to-build-a-frontend-framework.html

 

 

 

homeabout

Building a Frontend Framework; Reactivity and Composability With Zero Dependencies
构建前端框架;零依赖性的反应性和可组合性

13th May, 2023 13th 5月, 2023

Before I start—to set some context—by frontend framework what I mean is, a framework that allows us to avoid having to write regular old HTML and JavaScript such as this:
在我开始之前 - 设置一些上下文 - 通过前端框架,我的意思是,一个允许我们避免编写常规旧HTML和JavaScript的框架,例如:

<p id="cool-para"></p>
<script>
  const coolPara = 'Lorem ipsum.';
  const el = document.getElementById('cool-para');
  el.innerText = coolPara;
</script>

and instead allows us to write magical HTML and JavaScript code such as this (Vue):
相反,允许我们编写神奇的HTML和JavaScript代码,例如(Vue):

<script setup>
  const coolPara = 'Lorem ipsum.';
</script>
<template>
  <p>{{ coolPara }}</p>
</template>

or this (React):
或这个(反应):

export default function Para() {
  const coolPara = 'Lorem ipsum';
  return <p>{ coolPara }</p>;
}

and the benefit of such a framework is understandable. Remembering words or phrases such as documentinnerText, and getElementById are difficult—so many syllables!
这种框架的好处是可以理解的。记住单词或短语,如 document 、 innerText 和 getElementById 是困难的——这么多音节!

Okay, syllable count isn’t the main reason.
好的,音节计数不是主要原因。

Reactivity ✨ 反应 ✨

The first main reason is that, in the second and third examples, we can just set or update the value of the variable coolPara and the markup—i.e. the <p> element—is updated without without explicitly having to set its innerText.
第一个主要原因是,在第二个和第三个示例中,我们可以只设置或更新变量 coolPara 的值,并且标记(即 <p> 元素)无需显式设置其 innerText .

This is called reactivity, the UI is tied to the data in such a way that just changing the data updates the UI.
这称为反应性,UI 以这样一种方式与数据相关联,只需更改数据即可更新 UI。

Composability ✨ 可组合性 ✨

The second main reason is the ability to define a component and reuse it without having to redefine it every time we need to use it. This is called composability.
第二个主要原因是能够定义组件并重用它,而不必每次需要使用它时都重新定义它。这称为可组合性。

Regular HTML + JavaScript does not have this by default. And so the following code does not do what it feels like it should:
常规HTML + JavaScript默认没有这个。因此,以下代码没有执行应有的操作:

<!-- Defining the component -->
<component name="cool-para">
  <p>
    <content />
  </p>
</component>

<!-- Using the component -->
<cool-para>Lorem ipsum.</cool-para>

Reactivity and composability are the two main things the usual frontend frameworks such as Vue, React, etc give us.
反应性和可组合性是通常的前端框架(如 Vue、React 等)给我们的两个主要东西。

These abstractions aren’t granted for free, one has to front-load a bunch of framework specific concepts, deal with their leakiness when things work in inexplicably magical ways, and not to mention, a whole load of failure-prone dependencies.
这些抽象不是免费授予的,人们必须预先加载一堆特定于框架的概念,当事情以莫名其妙的神奇方式工作时处理它们的泄漏,更不用说一大堆容易失败的依赖项了。

But, it turns out that using modern Web APIs these two things aren’t very hard to achieve. And most use cases we might not actually need the usual frameworks and their cacophony of complexities…
但是,事实证明,使用现代 Web API 这两件事并不难实现。大多数用例我们可能实际上并不需要通常的框架及其复杂性的杂音......

Reactivity 反应

A simple statement that explains reactivity is when the data updates, update the UI automatically.
解释反应性的简单语句是,当数据更新时,自动更新 UI。

The first part is to know when the data updates. This unfortunately is not something a regular object can do. We can't just attach a listener called ondataupdate to listen to data update events.
第一部分是知道数据何时更新。不幸的是,这不是普通对象可以做的事情。我们不能只附加一个调用 ondataupdate 来侦听数据更新事件的侦听器。

Fortunately JavaScript has just the thing that would allow us to do this, it’s called Proxy.
幸运的是,JavaScript 有允许我们这样做的东西,它被称为 Proxy .

Proxy Objects  Proxy 对象

Proxy allows us to create a proxy object from a regular object:
Proxy 允许我们从常规对象创建代理对象:

const user = { name: 'Lin' };
const proxy = new Proxy(user, {});

and this proxy object can then listen to changes to the data.
然后,此代理对象可以侦听对数据的更改。

In the example above we have a proxy object, but it is not really doing anything when it comes to know that name has changed.
在上面的例子中,我们有一个代理对象,但是当知道它 name 已经改变时,它并没有真正做任何事情。

For that we need a handler, which is an object that tells the proxy object what to do when the data is updated.
为此,我们需要一个处理程序,它是一个对象,它告诉代理对象在数据更新时要做什么。

// Handler that listens to data assignment operations
const handler = {
  set(user, value, property) {
    console.log(`${property} is being updated`);
    return Reflect.set(user, value, property);
  },
};

// Creating a proxy with the handler
const user = { name: 'Lin' };
const proxy = new Proxy(user, handler);

Now whenever we update name using the proxy object, we’ll get a message saying "name is being updated".
现在,每当我们使用该 proxy 对象进行更新 name 时,我们都会收到一条消息,说 "name is being updated" .

If you’re wondering, What’s the big deal, I could’ve done this using a regular old setter, I’ll tell you the deal:
如果你想知道,有什么大不了的,我可以用一个普通的老二传手来做这件事,我会告诉你交易:

  • The proxy method is generalized, and handlers can be reused, which means that…
    代理方法是通用的,并且处理程序可以重用,这意味着...
  • Any value you set on a proxied object can be recursively converted into a proxy, which means that…
    您在代理对象上设置的任何值都可以递归转换为代理,这意味着...
  • You now have this magical object with the ability to react to data updates no matter how nested it is.
    您现在拥有了这个神奇的对象,无论它多么嵌套,它都能够对数据更新做出反应。

Other than this you can handle several other access events such as when a property is readupdateddeleted, etc.
除此之外,您还可以处理其他几个访问事件,例如读取、更新、删除属性等。

Now that we have the ability to listen to listen to operations, we need to react to them in a meaningful way.
现在我们有能力倾听操作,我们需要以有意义的方式对它们做出反应。

Updating the UI 更新用户界面

If you recall, The second part of reactivity was update the UI automatically. For this we need to fetch the appropriate UI element to be updated. But before that that we need to first mark a UI element as appropriate.
如果您还记得,反应性的第二部分是自动更新 UI。为此,我们需要获取要更新的相应 UI 元素。但在此之前,我们需要首先根据需要标记 UI 元素。

To do this we’ll use data-attributes, a feature that allows us to set arbitrary values on an element:
为此,我们将使用 data-attributes,该功能允许我们在元素上设置任意值:

<div>
  <!-- Mark the h1 as appropriate for when "name" changes -->
  <h1 data-mark="name"></h1>
</div>

The nicety of data-attributes are that we can now find all the appropriate elements using:
数据属性的优点在于,我们现在可以使用以下方法找到所有合适的元素:

document.querySelectorAll('[data-mark="name"]');

Now we just set the innerText of all the appropriate elements:
现在我们只需设置所有适当元素 innerText :

const handler = {
  set(user, value, property) {
    const query = `[data-mark="${property}"]`;
    const elements = document.querySelectorAll(query);

    for (const el of elements) {
      el.innerText = value;
    }

    return Reflect.set(user, value, property);
  },
};

// Regular object is omitted cause it's not needed.
const user = new Proxy({ name: 'Lin' }, handler);

That’s it, that’s the crux of reactivity!
就是这样,这就是反应性的关键!

Because of the general nature of our handler, for any property of user that is set, all the appropriate UI elements will be updated.
由于我们的 handler 一般性质,对于设置的任何属性 user ,所有适当的 UI 元素都将更新。

That’s how powerful the JavaScript Proxy features are, with zero dependencies and some cleverness it can give us these magical reactive objects.
这就是 JavaScript Proxy 功能的强大之处,零依赖性和一些聪明,它可以为我们提供这些神奇的反应对象。

Now onto the second main thing…
现在进入第二个主要问题...

Composibility 可组合性

Turns out, browsers already have an entire feature dedicated to this called Web Components, who knew!
事实证明,浏览器已经有一个专门用于此的完整功能,称为 Web 组件,谁知道呢!

Few use it cause it’s a bit of a pain in the ass to use (and also because most reach out for the usual frameworks as a default when starting a project, irrespective of the scope).
很少有人使用它,因为它使用起来有点痛苦(也因为大多数人在启动项目时都会将常用框架作为默认值,无论范围如何)。

For composability we first need to define the components.
对于可组合性,我们首先需要定义组件。

Defining components using template and slot
使用 和 slot 定义 template 组件

The <template> tags are used to contain markup which is not rendered by the browser. For instance, you can add the following markup in your HTML:
标记 <template> 用于包含浏览器未呈现的标记。例如,您可以在 HTML 中添加以下标记:

<template>
  <h1>Will not render!</h1>
</template>

and it won’t be rendered. You can think of them as invisible containers for your components.
并且它不会被渲染。您可以将它们视为组件的不可见容器。

The next building block is the <slot> element which defines where the content of a component will be placed in it. This enables a component to be reused with different content, i.e it becomes composable.
下一个构建块是定义组件内容将放置在其中的位置的 <slot> 元素。这使得组件能够与不同的内容重用,即它变得可组合。

For example, here’s an h1 element that colors its text red.
例如,下面是一个将其文本着色为红色的 h1 元素。

<template>
  <h1 style="color: red">
    <slot />
  </h1>
</template>

Before we get to using our components—like the red h1 above, we need to register them.
在使用组件之前(如上面的红色 h1),我们需要注册它们。

Registering the Components
注册组件

Before we can register our red h1 component, we need a name to register it by. We can just use the name attribute for that:
在注册红色 h1 组件之前,我们需要一个名称来注册它。我们可以为此使用该 name 属性:

<template name="red-h1">
  <h1 style="color: red">
    <slot />
  </h1>
</template>

And now, using some JavaScript we can get the component and its name:
现在,使用一些JavaScript,我们可以获取组件及其名称:

const template = document.getElementsByTagName('template')[0];
const componentName = template.getAttribute('name');

and then finally register it using customElements.define:
最后使用: customElements.define

customElements.define(
  componentName,
  class extends HTMLElement {
    constructor() {
      super();
      const component = template.content.children[0].cloneNode(true);
      this.attachShadow({ mode: 'open' }).appendChild(component);
    }
  }
);

There is a lot going on in the block above:
上面的块中发生了很多事情:

  • We are calling customElements.define with two arguments.
    我们用两个论点来呼吁 customElements.define 。
  • First argument is the component name (i.e. "red-h1").
    第一个参数是组件名称(即 "red-h1" )。
  • Second argument is a class that defines our custom component as an HTMLElement.
    第二个参数是一个将自定义组件定义为 HTMLElement .

What we are doing in the class constructor is using a copy of the template red-h1 to set the shadow DOM tree.
我们在类构造函数中所做的是使用模板 red-h1 的副本来设置影子 DOM 树。

What’s the Shadow DOM?
什么是影子 DOM?

 

 

 

Calling customElements.define will allow us to use the defined component like a regular HTML element.
调用将 customElements.define 允许我们像使用常规 HTML 元素一样使用定义的组件。

<red-h1>This will render in red!</red-h1>

Onto putting these two concepts together!
把这两个概念放在一起!

Composability + Reactivity
可组合性 + 反应性

A quick recap, we did two things:
快速回顾一下,我们做了两件事:

  1. We created a reactive data structure i.e. the proxy objects which on setting a value can update any element we have marked as appropriate.
    我们创建了一个反应式数据结构,即代理对象,在设置值时可以更新我们标记的任何元素。
  2. We defined a custom component red-h1 which will render it’s content as a red h1.
    我们定义了一个自定义组件,该组件 red-h1 将其内容呈现为红色 h1。

We can now put them both together:
现在我们可以将它们放在一起:

<div>
  <red-h1 data-mark="name"></red-h1>
</div>

<script>
  const user = new Proxy({}, handler);
  user.name = 'Lin';
</script>

and have a custom component render our data and update the UI when we change the data.
并让自定义组件呈现我们的数据并在我们更改数据时更新 UI。


Of course the usual frontend frameworks don’t just do this, they have specialized syntax such the template syntax in Vue, and JSX in React that makes writing complex frontends relatively more concise that it otherwise would be.
当然,通常的前端框架不只是这样做,它们有专门的语法,比如 Vue 中的模板语法和 React 中的 JSX,这使得编写复杂的前端相对更简洁。

Since this specialized syntax is not regular JavaScript or HTML, it is not parsable by a browser and so they all need specialized tools to compile them down to regular JavaScript, HTML, and CSS before the browser can understand them. And so, no body writes JavaScript any more.
由于这种专门的语法不是常规的JavaScript或HTML,因此浏览器无法解析它,因此它们都需要专门的工具来将它们编译为常规的JavaScript,HTML和CSS,然后浏览器才能理解它们。因此,没有人再写JavaScript了。

Even without specialized syntax, you can do a lot of what the usual frontend framework does—with similar conciseness—just by using Proxy and WebComponents.
即使没有专门的语法,您也可以使用 Proxy 和 WebComponents 来完成通常的前端框架所做的很多事情 - 具有类似的简洁性。

The code here is an over simplification and to convert it into a framework you’d have to flesh it out. Here’s my attempt at doing just that: a framework called Strawberry.
这里的代码过于简化,要将其转换为框架,您必须充实它。这是我尝试这样做的:一个名为Strawberry的框架。

As I develop this, I plan on maintaining two hard constraints:
在开发此功能时,我计划维护两个硬约束:

  1. No dependencies. 没有依赖关系。
  2. No build-step before it can be used.
    在可以使用之前没有构建步骤。

And a soft constraint of keeping the code base tiny. At the time of writing it’s just a single file with fewer than 400 CLOC, let’s see where it goes. ✌️
以及保持代码库最小的软约束。在撰写本文时,它只是一个少于 400 CLOC 的单个文件,让我们看看它的去向。✌️

Also, here's the HN discussion for this post.
另外,这是这篇文章的 HN 讨论。