[Web翻译]比较React和Web组件。第2部分:组件

682 阅读20分钟

原文地址:matsu.fi/posts/compa…

原文作者:matsu.fi

发布时间:2020年8月19日

这是我比较Web Components和React系列的第二部分。你可以在这里找到第一部分

在我们开始之前,我先澄清一下上一篇文章中可能存在的一些误解。

  • OpenWC不是LitElement。LitElement是一个由Polymer团队和开源贡献者编写的开源库,而OpenWC是一个旨在为Web组件开发提供建议的社区。
  • LitElement是一个使用lit-html库的基类。
  • lit-html是一个用于创建快速html模板的模板库。

好了,现在让我们继续我们离开的地方。现在让我们继续刚才的话题。

比较组件开发

在今天的文章中,我们将比较在3个不同的环境中构建Javascript组件。

  • React
  • Lit-Element
  • HTMLEEMENTS (vanilla)

我们将通过一个小表单的简单实现,用户可以在表单中插入自己想要的随机猫咪图片的宽度和高度,然后通过我们的代码查询PlaceKitten的API来接收它

我们完成的应用程序将看起来像下面的图片。

很漂亮吧?我没有花太多时间在css上,因为只有css的用法与本帖有关,而不是它的风格化输出。

我们先来深入了解一下我们是如何在React中完成这个任务的。

React的实现

创作

对于React的实现,我在网上找到了很多关于创建React组件/库的最佳方法的不同材料。我最终选择了create-react-library,因为它在Github上有一个不错的>3千颗星和一个直接的入门套件。

我们首先从运行指示命令npx create-react-library开始,这对很多人使用的create-react-app很熟悉,包括我们在上一篇文章中。

运行这个命令,我们会创建一个项目,或者说实际上是2个独立的项目。

我们有一个项目用于开发组件,还有一个项目用于在 "真实 "环境中演示组件。

第一个项目构建组件,并将其他所需的库通过符号链接到example-directory的node_modules中。

这就允许我们的演示项目像导入普通包一样导入它。

import CatImageViewer from 'cat-image-react-viewer';

demo项目的node_modules是由顶层项目,也就是组件项目的符号链接。

> ll example/node_modules/

total 20
drwxrwxr-x 5 matsu matsu 4096 Aug 13 13:56  ./
drwxrwxr-x 5 matsu matsu 4096 Aug 13 13:56  ../
drwxrwxr-x 4 matsu matsu 4096 Aug 13 13:56 '@babel'/
drwxrwxr-x 2 matsu matsu 4096 Aug 13 13:56  .bin/
drwxrwxr-x 4 matsu matsu 4096 Aug 13 13:56  .cache/
lrwxrwxrwx 1 matsu matsu    5 Aug 13 13:56  cat-image-react-viewer -> ../../
lrwxrwxrwx 1 matsu matsu   24 Aug 13 13:56  react -> ../../node_modules/react/
lrwxrwxrwx 1 matsu matsu   28 Aug 13 13:56  react-dom -> ../../node_modules/react-dom/
lrwxrwxrwx 1 matsu matsu   32 Aug 13 13:56  react-scripts -> ../../node_modules/react-scripts/

发展

为了运行我们的开发环境,我们需要两个终端。一个导航到组件目录,为组件服务。另一个导航到示例目录,并为该项目提供服务。我知道有很多开发人员只使用VSCode终端,这就相当麻烦了。

这让我怀疑他们是否能以某种方式将这两个任务结合起来,比如Concurrently。尽管如此,从这里开始的开发体验是相当轻松的。

我们得到了一个很好的组件配置示例,我们能够轻松地开始工作。我们最终的组件源是这样的。

import React, { useState } from 'react';
import styles from './index.module.css';

const placeKittenUrl = 'http://placekitten.com/{width}/{height}';

export default function CatImageViewer() {
    const [imageWidth, setImageWidth] = useState(0);
    const [imageHeight, setImageHeight] = useState(0);
    const [currentImage, setCurrentImage] = useState(null);

    const getNewCatImage = () => {
        const searchUrl = placeKittenUrl
            .replace('{width}', imageWidth)
            .replace('{height}', imageHeight);
        setCurrentImage(searchUrl);
    };

    return (
        <div className={styles.formWrapper}>
            <div>
                <p>Enter the dimensions of the desired cat image</p>
                <input
                    type="number"
                    placeholder="Width"
                    id="image-width"
                    onInput={(e) => setImageWidth(e.target.value)}
                />
                <input
                    type="number"
                    placeholder="Height"
                    id="image-height"
                    onInput={(e) => setImageHeight(e.target.value)}
                />
                <button type="button" onClick={getNewCatImage}>
                    Search
                </button>
            </div>
            {currentImage && <img alt="" src={currentImage} />}
        </div>
    );
}

css模块是这样的。

.formWrapper div {
    display: flex;
    flex-direction: column;
}

.formWrapper input,
.formWrapper button {
    font-size: 2rem;
    margin: 0 0 1rem;
    width: 10rem;
}

我使用React Hooks创建了这个组件,因为它们似乎是目前React世界中最热门的东西。这让我们能够相当简洁地创建组件。

我绝不是一个React大师,但这对于模拟一个新的开发者想要创建他们的第一个组件的开发经验来说是完美的。

启动器从一开始就设置了一个index.module.css,从我看过的React项目来看,为组件单独设置css文件是相当常见的(与Web组件在js中设置css的方式相比)。

你可能还记得在上一篇文章中,create-react-app在vanilla安装中的包大小有一个巨大的足迹。我很惊讶也很高兴,因为我注意到在构建之后,我们并没有看到一个500KB的包。

> npm run build

> du -sh dist/
24K     dist/

我猜测这部分是由于我们的组件项目没有构建任何的依赖关系。在package.json中,我们没有将库列为依赖项。然而,我们将React列为peerDependency,这意味着我们的包永远注定只能被其他React项目运行。

快速查看一下 dist 文件夹中的内容也解释了为什么包的大小是 24K:该项目是以 commonjs 和 as 和 es 包的形式构建的。

> ll dist/

total 28
drwxrwxr-x 2 matsu matsu 4096 Aug 13 13:55 ./
drwxrwxr-x 7 matsu matsu 4096 Aug 13 13:56 ../
-rw-rw-r-- 1 matsu matsu  149 Aug 19 17:12 index.css
-rw-rw-r-- 1 matsu matsu 1833 Aug 19 17:12 index.js
-rw-rw-r-- 1 matsu matsu 2595 Aug 19 17:12 index.js.map
-rw-rw-r-- 1 matsu matsu 1607 Aug 19 17:12 index.modern.js
-rw-rw-r-- 1 matsu matsu 2584 Aug 19 17:12 index.modern.js.map

这是相当方便的,但也使包的大小增加了一倍,在这种情况下,这两个版本都会被发布。

如果我们看一下我们的示例文件夹,我们可以看到在React环境下如何导入项目。

import React from 'react';

import CatImageViewer from 'cat-image-react-viewer';
import 'cat-image-react-viewer/dist/index.css';

const App = () => {
    return <CatImageViewer />;
};

export default App;

我们注意到,为了使用我们组件的样式,我们被迫导入css包作为自己的导入。这意味着,要构建这个项目,我们需要在项目中拥有一个css捆绑器。

然而React通常是用Webpack或Rollup这样的东西来构建的,所以这应该不会造成太多的开销。不过在无构建的环境中,这会造成一两个头疼的问题。

总的来说,在找到并设置好环境后,开发是相当简单的。启动器为我们提供了发布包的命令。这里有一个缺点就像我说的,现在只有在项目中使用了React,才能使用这个包。

输出

最后我们来看看构建过程的输出。

如前所述,我们有5个文件,从这些文件中可以看到

  1. 是一个css文件
  2. 是js文件
  3. 是js地图文件

我们先从最简单的说起,看看css文件里面的内容。

._index-module__formWrapper__r8sB5 div {
    display: flex;
    flex-direction: column;
}

._index-module__formWrapper__r8sB5 input,
._index-module__formWrapper__r8sB5 button {
    font-size: 2rem;
    margin: 0 0 1rem;
    width: 10rem;
}

我们可以看到,除了类名之外,其他的样式都没有被修改。由于我们从未在调用 styles.formWrapper 之外命名过一个 css 选择器,我们可以看到构建过程为该类生成了一个唯一的标识符。

我在这里看到两种情况,我想强调一下。

  • 创建复杂的类名会使测试自动化更难创建可读的测试,而这些测试在新的构建过程中又不会被破坏。
  • index-module__formWrapper__r8sB5如果从浏览器看源码的话,其实并不像form-wrapper那样有描述性的阅读。

接下来我们来看看index.js文件。

function _interopDefault(ex) {
    return ex && typeof ex === 'object' && 'default' in ex ? ex['default'] : ex;
}

var React = require('react');
var React__default = _interopDefault(React);

var styles = { formWrapper: '_index-module__formWrapper__r8sB5' };

var placeKittenUrl = 'http://placekitten.com/{width}/{height}';
function CatImageViewer() {
    var _useState = React.useState(0),
        imageWidth = _useState[0],
        setImageWidth = _useState[1];

    var _useState2 = React.useState(0),
        imageHeight = _useState2[0],
        setImageHeight = _useState2[1];

    var _useState3 = React.useState(null),
        currentImage = _useState3[0],
        setCurrentImage = _useState3[1];

    var getNewCatImage = function getNewCatImage() {
        var searchUrl = placeKittenUrl
            .replace('{width}', imageWidth)
            .replace('{height}', imageHeight);
        setCurrentImage(searchUrl);
    };

    return /*#__PURE__*/ React__default.createElement(
        'div',
        {
            className: styles.formWrapper,
        },
        /*#__PURE__*/ React__default.createElement(
            'div',
            null,
            /*#__PURE__*/ React__default.createElement(
                'p',
                null,
                'Enter the dimensions of the desired cat image'
            ),
            /*#__PURE__*/ React__default.createElement('input', {
                type: 'number',
                placeholder: 'Width',
                id: 'image-width',
                onInput: function onInput(e) {
                    return setImageWidth(e.target.value);
                },
            }),
            /*#__PURE__*/ React__default.createElement('input', {
                type: 'number',
                placeholder: 'Height',
                id: 'image-height',
                onInput: function onInput(e) {
                    return setImageHeight(e.target.value);
                },
            }),
            /*#__PURE__*/ React__default.createElement(
                'button',
                {
                    type: 'button',
                    onClick: getNewCatImage,
                },
                'Search'
            )
        ),
        currentImage &&
            /*#__PURE__*/ React__default.createElement('img', {
                alt: '',
                src: currentImage,
            })
    );
}

module.exports = CatImageViewer;
//# sourceMappingURL=index.js.map

好了,在我们克服了这段代码很难读的事实之后,尤其是渲染部分,我们可以看一下几个部分。

  • 为什么我们所有的const都变成了vars?在所有的可能性中,一个var.const是所有浏览器都支持的。
    • const是所有浏览器都支持的,直到IE11(顺便说一句,现在已经不支持了)。
  • 如果开发人员在不了解React工作原理的情况下,直接从源头检查这个元素,就很难知道钩子是怎么回事。
    • 在浏览器中,可以通过提供文件的源码图来解决这个问题,前提是你的浏览器支持源码图并且启用了它。
    • 源码图并不是万无一失的,可能会在调试时引入一些问题。

接下来让我们看看另一个生成的javascript文件里面:index.modern.js

import React, { useState } from 'react';

var styles = { formWrapper: '_index-module__formWrapper__r8sB5' };

const placeKittenUrl = 'http://placekitten.com/{width}/{height}';
function CatImageViewer() {
    const [imageWidth, setImageWidth] = useState(0);
    const [imageHeight, setImageHeight] = useState(0);
    const [currentImage, setCurrentImage] = useState(null);

    const getNewCatImage = () => {
        const searchUrl = placeKittenUrl
            .replace('{width}', imageWidth)
            .replace('{height}', imageHeight);
        setCurrentImage(searchUrl);
    };

    return /*#__PURE__*/ React.createElement(
        'div',
        {
            className: styles.formWrapper,
        },
        /*#__PURE__*/ React.createElement(
            'div',
            null,
            /*#__PURE__*/ React.createElement(
                'p',
                null,
                'Enter the dimensions of the desired cat image'
            ),
            /*#__PURE__*/ React.createElement('input', {
                type: 'number',
                placeholder: 'Width',
                id: 'image-width',
                onInput: (e) => setImageWidth(e.target.value),
            }),
            /*#__PURE__*/ React.createElement('input', {
                type: 'number',
                placeholder: 'Height',
                id: 'image-height',
                onInput: (e) => setImageHeight(e.target.value),
            }),
            /*#__PURE__*/ React.createElement(
                'button',
                {
                    type: 'button',
                    onClick: getNewCatImage,
                },
                'Search'
            )
        ),
        currentImage &&
            /*#__PURE__*/ React.createElement('img', {
                alt: '',
                src: currentImage,
            })
    );
}

export default CatImageViewer;
//# sourceMappingURL=index.modern.js.map

现在这个看起来更像我们写的,这主要是因为这是es模块版本的构建代码。我们看到这次我们的consts没有被篡改。

我们的项目相当简单,所以源码无法深入分析,但我想在更复杂的系统中,你可以找到更多的东西来谈论这些构建文件。

接下来我们就来看看Lit Element的一个比较平凡的方法吧。

Lit-Element的实现

创作

接着到Lit Element,我们可以使用我们在创建示例项目时使用的相同的Open WC init命令。初始化器将提示我们创建一个应用程序或一个组件。在我们的例子中,我们显然要选择Web Component -选择。

npm init @open-wc

在运行初始化器,并选择我们的配置后,我们就有了一个漂亮的示例项目,就像我们在React组件示例中一样。不同的是,我们只需要运行一个npm start就可以进行开发,而不是运行我们单独的demo实例。变化在于我们必须相对地调用我们的组件,而不是把它放在node_modules中。

开发

运行npm start启动es-dev-server,并提供一个演示index.html文件,附加我们新初始化的Web组件。在我们要创建一个Typescript组件的情况下,该命令将使用concurrently并行运行es-dev-server和tsc。这是非常好的,允许用户只用一个命令就可以运行整个环境。

在运行项目和清理示例文件后,我们最终写出了我们的LitElement实现,如下所示。

import { html, css, LitElement } from 'lit-element';

const placeKittenUrl = 'http://placekitten.com/{width}/{height}';

export class CatImageLitViewer extends LitElement {
    static get properties() {
        return {
            imageWidth: { type: Number },
            imageHeight: { type: Number },
            currentImage: { type: String },
        };
    }

    static styles = css`
        .form-wrapper {
            display: flex;
            flex-direction: column;
        }
        button,
        input {
            font-size: 2rem;
            margin: 0 0 1rem;
            width: 10rem;
        }
    `;

    constructor() {
        super();
        this.imageWidth = 0;
        this.imageHeight = 0;
        this.currentImage = null;
    }

    getNewCatImage() {
        const searchUrl = placeKittenUrl
            .replace('{width}', this.imageWidth)
            .replace('{height}', this.imageHeight);
        this.currentImage = searchUrl;
    }

    render() {
        return html`
            <div class="form-wrapper">
                <p>Enter the dimensions of the desired cat image</p>
                <input
                    type="number"
                    placeholder="Width"
                    id="image-width"
                    @input=${(e) => {
                        this.imageWidth = e.target.value;
                    }}
                />
                <input
                    type="number"
                    placeholder="Height"
                    id="image-height"
                    @input=${(e) => {
                        this.imageHeight = e.target.value;
                    }}
                />
                <button type="button" @click=${this.getNewCatImage}>
                    Search
                </button>
            </div>
            ${this.currentImage && html`<img alt="" src="${this.currentImage}" />`}
        `;
    }
}

customElements.define('cat-image-lit-viewer', CatImageLitViewer);

和我们的React例子相比,LitElement版本相当的相似。不同之处主要在于状态管理。

在React中,我们有钩子来处理我们的状态,而在LitElement中,我们将状态对象保存为类的属性,然后在构造函数中初始化它们。在LitElement中,我们将状态对象保存为类的属性,然后在构造函数中初始化它们。这虽然多了几行代码,但是也给我们的组件提供了一个很好的类型化的API,即使是在Javascript中。

接下来的区别是,我们的样式在js文件里面,不像在React中我们使用css模块文件。我们的样式选择器也更宽松一些,因为我们是把组件开发成影子DOM,允许对样式进行封装处理。

另外,我们与react项目的不同之处还在于由此产生的DOM树。在React项目中,我们被迫用一个div-element来封装组件中的所有内容。在Web Component版本中,我们将有一个名为Web Component的元素包装,使得包装div的需要变得多余。

<!-- React -->
<div class="_index-module__formWrapper__r8sB5">
    <div>
        <p>Enter the dimensions of the desired cat image</p>
        <input type="number" placeholder="Width" id="image-width" />
        <input type="number" placeholder="Height" id="image-height" />
        <button type="button">
            Search
        </button>
    </div>
</div>

<!-- LitElement -->
<cat-image-lit-viewer>
    <div class="form-wrapper">
        <p>Enter the dimensions of the desired cat image</p>
        <input type="number" placeholder="Width" id="image-width" />
        <input type="number" placeholder="Height" id="image-height" />
        <button type="button">
            Search
        </button>
    </div>
</cat-image-lit-viewer>

这主要是代码方面的区别。我们的两个实现都有一个很好的结构化渲染,并在其中加入了一些条件渲染,我们的组件用事件监听器属性处理事件(用React的onInputonClick@input@click on LitElement)。

现在我们有了我们的组件,我们应该准备好构建了。

输出

...... 除了我们不会这样做。

读了open-wc关于构建的建议,在关于创建组件的部分,它说:"如果你正在构建一个可重用的组件或库,我们建议发布不需要修改就能在最新的浏览器上运行的代码。

如果你要构建一个可重用的组件或库 我们建议你发布的代码要能在最新的浏览器上运行而不需要修改。你不应该捆绑任何依赖关系或polyfills,也不应该构建到一个非常老的JavaScript版本,比如ES5。这样一来,消费项目可以根据浏览器的支持来决定加载哪些polyfills和针对哪个JavaScript版本。

在实际操作中,这意味着发布标准的ES模块和现代浏览器中实现的标准JavaScript功能,如Chrome、Safari、Firefox和Edge。我们建议采用无构建的开发方式,所以除非你使用的是非常前沿的功能,否则实际上你可以直接按原样发布你的源代码。

简而言之就是我们可以原封不动地发布我们的Web组件。是不是很酷?

另外由于我们不是在构建我们的包,所以我们不需要运送任何源码图。

如果我们要发布我们的Web组件,我们只需要发布一个js文件,并标记LitElement作为依赖。LitElement是一个相当小的库(23.2kB minified, 7.4kB minified + Gzipped),如果我们不压缩任何东西,我们的项目总大小将在30kB左右。

> du -sh src/CatImageLitViewer.js

4.0K    CatImageLitViewer.js

如果我们还没有在项目中使用LitElement,那么就可以这样做。如果我们已经在写一个LitElement项目,那么我们的新包只会把这4KB带入项目中。而这是不统一的。

作为对比。如果我们想在一个非react项目中添加一个React组件,最小的设置需要React和React-DOM,它们的总重量为6.3kB,2.6 gzipped + 114.6kB,35.9kB gzipped,总计约120kB未压缩。

另外,LitElement的做法还有一个好处,那就是我们也不用在实现端建立项目,因为css已经内置在js文件中,因此不需要css加载器(不像React版本)。

总的来说,在使用LitElement创建Web Components时,我们最终的包大小减少了6倍(4kb vs 总24kb),加上依赖关系,总大小减少了约9倍。

接下来我们就来看看使用浏览器自带的HTMLElement来实现完全无构建和无依赖。

HTMLElement的实现

创作

由于我们正在尽可能地构建一个项目,所以我们并不需要初始化器来实现。

我们只需创建一个文件夹,运行npm init并创建必要的文件就可以了。

mkdir cat-image-vanilla-viewer
cd cat-image-vanilla-viewer
npm init
touch index.html index.js

现在我们可以用开发所需的最小设置来填充index.html。

<html>
    <head> </head>
    <body>
        <cat-image-vanilla-viewer></cat-image-vanilla-viewer>
        <script src="./index.js" type="module"></script>
    </body>
</html>

真正的简单和光滑。就像香草冰淇淋一样。

我们可以用任何http服务器实现来服务我们的项目,从http-serveres-dev-server,因为它只是一个简单的html+js文件设置。在这篇文章中,我只是添加了es-dev-server作为开发依赖,因为它不会影响包的大小。

我们的package.json现在也是最小化设置。

{
    "name": "cat-image-vanilla-viewer",
    "version": "1.0.0",
    "description": "",
    "main": "index.js",
    "scripts": {
        "start": "es-dev-server --app-index index.html --node-resolve --watch"
    },
    "author": "",
    "license": "ISC",
    "devDependencies": {
        "es-dev-server": "^1.57.2"
    }
}

现在,我们只需运行npm start,就能获得不错的、热加载的开发者体验。

开发

由于我们使用的是es-dev-server,所以开发设置和工作流程与Lit-Element的例子非常相似。

现在我们有了环境设置,就可以写我们的组件了。实现的过程只是比我们用库做的稍微长一点。

const template = document.createElement('template');
template.innerHTML = `
<style>
    .form-wrapper {
        display: flex;
        flex-direction: column;
    }
    button,
    input {
        font-size: 2rem;
        margin: 0 0 1rem;
        width: 10rem;
    }
</style>
<div class="form-wrapper">
    <p>Enter the dimensions of the desired cat image</p>
    <input
        type="number"
        placeholder="Width"
        id="image-width"
    />
    <input
        type="number"
        placeholder="Height"
        id="image-height"
    />
    <button type="button" >Search</button>
</div>
`;

const placeKittenUrl = 'http://placekitten.com/{width}/{height}';

export default class CatImageVanillaViewer extends HTMLElement {
    constructor() {
        super();
        this.imageWidth = 0;
        this.imageHeight = 0;
        this.currentImage = null;

        const root = this.attachShadow({ mode: 'open' });
        root.appendChild(template.content.cloneNode(true));
    }

    connectedCallback() {
        this.shadowRoot
            .querySelector('#image-width')
            .addEventListener('input', (e) => (this.imageWidth = e.target.value));

        this.shadowRoot
            .querySelector('#image-height')
            .addEventListener('input', (e) => (this.imageHeight = e.target.value));

        this.shadowRoot
            .querySelector('button')
            .addEventListener('click', this.getNewCatImage.bind(this));
    }

    getNewCatImage() {
        const searchUrl = placeKittenUrl
            .replace('{width}', this.imageWidth)
            .replace('{height}', this.imageHeight);
        this.currentImage = searchUrl;

        let imageElem = this.shadowRoot.querySelector('img');
        if (!imageElem) {
            imageElem = document.createElement('img');
            this.shadowRoot.appendChild(imageElem);
        }
        imageElem.src = searchUrl;
    }
}

customElements.define('cat-image-vanilla-viewer', CatImageVanillaViewer);

让我们开始看看这个实现有什么不同。正如你从一开始看到的那样,有相当多的变化。

首先,我们正在使用template。模板在lit-html中也有使用,但是在这里我们是手动创建模板,并在这里填充内容,而不是在render函数中,我们的组件没有渲染函数。

模板是 "一种机制,用于保存在页面加载时不需要立即渲染的HTML,但可以在随后的运行时使用Javascript实例化"(来自模板MDN页面)。

把模板看作是一个内容片段,它被存储起来,以便在文档中后续使用。虽然解析器在加载页面时确实会处理<template>元素的内容,但它只是为了确保这些内容是有效的;然而,该元素的内容不会被渲染。

模板元素使我们能够编写标记模板,这些模板不会立即被渲染,然后可以多次重复使用,作为自定义元素结构的基础。

在模板元素中,我们声明了我们的样式,也声明了我们组件的HTML结构。内容基本上就是我们通常放在render -function里面的内容。

接下来我们创建类本身。我们扩展了HTMLElement,它是Javascript Web API的一部分。任何扩展HTMLElement的类都代表一个HTML元素。一些Web Components可能直接实现这个接口,而其他的可能实现另一个接口,继承这个接口。

我们的Web组件有自己内置的生命周期回调,我们在这个例子中使用了一些。这些回调的完整列表如下。

  • connectedCallback: 当自定义元素第一次连接到文档的DOM时被调用。
  • disconnectedCallback: 当自定义元素被断开时调用。当自定义元素与文档的DOM断开连接时调用。
  • adoptedCallback.当自定义元素被移动到文档的DOM中时被调用。当自定义元素被移动到一个新的文档中时被调用。
  • attributeChangedCallback。当自定义元素的一个属性被添加、删除或更改时调用。

资料来源:MDN

在我们的构造函数中,我们用默认值初始化我们的属性。我们还将影子DOM附加到我们的元素上,为我们的Web组件创建封装。在创建了shadowRoot之后,我们将我们的模板的副本附加在它里面。

connectCallback被调用之后,我们可以确定我们的元素已经打到了DOM上,使得我们的元素可以安全的附加事件。所以这就是我们在connectedCallback里面做的事情。

我们可以在线处理onclick和oninput事件,但在创建vanilla组件时,我更喜欢在javascript中手动附加元素,而不是将它们在线附加到html中。

之后我们就只剩下对猫咪图片的追加了。与其他情况不同的是,在这里我们不能轻易地有条件地渲染我们的元素,至少不像在其他库中那么容易。

在我们的例子中,在第一次添加图片的时候,我们只需要将图片元素追加到元素的DOM树中就可以了。之后,我们只需querySelector我们的图片元素,并手动操作它的src元素。

輸出

再次,由于这是完全的vanilla,所以我们的组件不需要构建就能得到浏览器的支持。

让我们来看看我们的包的大小,我们将与其他包进行比较。

> du -sh index.js

4.0K    index.js

看看这个。它的大小和我们的LitElement实现一样,但是没有添加LitElement和lit-html库到你的项目中(如果你还没有使用它们)。

就像我们在LitElement中一样,在vanilla中,我们不需要提供源码地图,因为我们将发布一个未构建的、未最小化的版本的包。

在完全的vanilla中构建包也允许我们轻松地将其导入到任何我们想要的页面中,只需从像unpkg这样的JS CDN导入包,然后将HTML元素添加到页面中即可。不需要任何外部工具。

当然,这也有一些缺点,从这种简单的包来看,这些缺点并不重要。

有了Vanilla Web Components

  • 你不能通过HTML传递除字符串以外的其他数据。
    • 但你可以通过在javascript中传递属性来实现。
// Example
const profileObject = { id: 1, name: 'Matsu' };
document.querySelector('my-element').profile = profileObject;
  • 你需要自己注册属性变更回调,使用类似attributechangedcallback的回调。
  • 你可能会混淆属性和特性
    • 但这个只要研究一下就能解决,最后也不是那么复杂的问题
  • 你可能需要写一些自定义的更新逻辑

但是对于创建像我们创建的简单的Web组件来说,其中的一些问题可能根本不会出现。

结论

所以总结一下,你应该写一个X组件,当... ...

  • 如果你没有其他选择,就写一个React组件。
  • 如果你要构建任何更复杂的东西,并且能够强迫你的组件开发者使用像Rollup这样的小工具,那就写一个LitElement组件。
    • ...至少在全球支持裸模块指定器之前是这样的。
  • 如果你正在构建一个简单的组件,不需要一些更复杂的库的功能,并且希望你的组件能够绝对(几乎)在任何地方运行,那就写一个Vanilla组件。

所以,下一次你在构建一个Javascript组件的时候,请花一秒钟的时间来评估一下你想在哪个规模上构建它,以及你想把你的用户绑在哪个级别的依赖关系上。

希望大家喜欢这个系列。如果我在写手@matsuuu_说错了什么,请给我提供关于帖子的反馈,并纠正我,如果你喜欢这个系列,请给我发DM,让我知道:)

直到下次,我们将讨论更多的Web组件。


www.deepl.com 翻译