Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

尤大 3 天前发在 GitHub 上的 vue-lit 是啥? #41

Open
axuebin opened this issue Sep 22, 2020 · 0 comments
Open

尤大 3 天前发在 GitHub 上的 vue-lit 是啥? #41

axuebin opened this issue Sep 22, 2020 · 0 comments

Comments

@axuebin
Copy link
Owner

axuebin commented Sep 22, 2020

未经授权,不得转载。

写在前面

我在尤大的 GitHub 上发现了一个有趣的东西 vue-lit,直觉告诉我这又是一个啥面向未来的下一代 xxx,所以我就点进去看了一眼是啥新玩具。

Hello World

Proof of concept mini custom elements framework powered by @vue/reactivity and lit-html.

看上去是尤大的一个验证性的尝试,看到 custom elementlit-html,盲猜一把,是一个可以直接在浏览器中渲染 vue 写法的 Web Component 的工具。

这里提到了 lit-html,后面会专门介绍一下。

按照尤大给的 Demo,我们来试一下 Hello World

<!DOCTYPE html>
<html lang="en">
  <head>
    <script type="module">
      import {
        defineComponent,
        reactive,
        html,
        onMounted
      } from 'https://unpkg.com/@vue/lit@0.0.2';
  
      defineComponent('my-component', () => {
        const state = reactive({
          text: 'Hello World',
        });
        
        function onClick() {
          alert('cliked!');
        }
  
        onMounted(() => {
          console.log('mounted');
        });
  
        return () => html`
          <p>
            <button @click=${onClick}>Click me</button>
            ${state.text}
          </p>
        `;
      })
    </script>
  </head>
  <body>
    <my-component />
  </body>
</html>

不用任何编译打包工具,直接打开这个 index.html,看上去没毛病:

!

可以看到,这里渲染出来的是一个 Web Component,并且 mounted 生命周期也触发了。

关于 lit-html 和 lit-element

vue-lit 之前,我们先了解一下 lit-htmllit-ement,这两个东西其实已经出来很久了,可能并不是所有人都了解。

lit-html

lit-html 可能很多人并不熟悉,甚至没有见过。

所以是啥?答案是 HTML 模板引擎

如果没有体感,我问一个问题,React 核心的东西有哪些?大家都会回答:jsxVirtual-DOMdiff,没错,就是这些东西构成了 UI = f(data)React

来看看 jsx 的语法:

function App() {
  const msg = 'Hello World';
  return <div>{msg}</div>;
}

再看看 lit-html 的语法:

function App() {
  const msg = 'Hello World';
  return html`
    <div>${msg}</div>
  `;
}

我们知道 jsx 是需要编译的它的底层最终还是 createElement....。而 lit-html 就不一样了,它是基于 tagged template 的,使得它不用编译就可以在浏览器上运行,并且和 HTML Template 结合想怎么玩怎么玩,扩展能力更强,不香吗?

当然,无论是 jsx 还是 lint-html,这个 App 都是需要 render 到真实 DOM 上。

lint-html 实现一个 Button 组件

直接上代码(省略样式代码):

<!DOCTYPE html>
<html lang="en">
<head>
  <script type="module">
    import { html, render } from 'https://unpkg.com/lit-html?module';

    const Button = (text, props = {
      type: 'default',
      borderRadius: '2px'
    }, onClick) => {
      // 点击事件
      const clickHandler = {
        handleEvent(e) { 
          alert('inner clicked!');
          if (onClick) {
            onClick();
          }
        },
        capture: true,
      };

      return html`
        <div class="btn btn-${props.type}" @click=${clickHandler}>
          ${text}
        </div>
      `
    };
    render(Button('Defualt'), document.getElementById('button1'));
    render(Button('Primary', { type: 'primary' }, () => alert('outer clicked!')), document.getElementById('button2'));
    render(Button('Error', { type: 'error' }), document.getElementById('button3'));
  </script>
</head>
<body>
  <div id="button1"></div>
  <div id="button2"></div>
  <div id="button3"></div>
</body>
</html>

效果:

性能

lit-html 会比 React 性能更好吗?这里我没仔细看过源码,也没进行过相关实验,无法下定论。

但是可以大胆猜测一下,lit-html 没有使用类 diff 算法而是直接基于相同 template 的更新,看上去这种方式会更轻量一点。

但是,我们常问的一个问题 “在渲染列表的时候,key 有什么用?”,这个在 lit-html 是不是没法解决了。我如果删除了长列表中的其中一项,按照 lit-html 的基于相同 template 的更新,整个长列表都会更新一次,这个性能就差很多了啊。

// TODO:埋个坑,以后看

lit-element

lit-element 这又是啥呢?

关键词:web components

例子

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

class MyElement extends LitElement {
  static get properties() {
    return {
      msg: { type: String },
    };
  }
  constructor() {
    super();
    this.msg = 'Hello World';
  }
  render() {
    return html`
      <p>${this.msg}</p>
    `;
  }
}

customElements.define('my-element', MyElement);

效果

结论:可以用类 React 的语法写 Web Component

so, lit-element 是一个可以创建 Web Componentbase class。分析一下上面的 Demo,lit-element 做了什么事情:

  1. static get properties: 可以 setterstate
  2. constructor: 初始化 state
  3. render: 通过 lit-html 渲染元素,并且会创建 ShadowDOM

总之,lit-element 遵守 Web Components 标准,它是一个 class,基于它可以快速创建 Web Component

更多关于如何使用 lit-element 进行开发,在这里就不展开说了。

Web Components

浏览器原生能力香吗?

Web Components 之前我想先问问大家,大家还记得 jQuery 吗,它方便的选择器让人难忘。但是后来 document.querySelector 这个 API 的出现并且广泛使用,大家似乎就慢慢地淡忘了 jQuery

浏览器原生 API 已经足够好用,我们并不需要为了操作 DOM 而使用 jQuery

You Dont Need jQuery

再后来,是不是很久没有直接操作过 DOM 了?

是的,由于 React / Vue 等框架(库)的出现,帮我们做了很多事情,我们可以不用再通过复杂的 DOM API 来操作 DOM

我想表达的是,是不是有一天,如果浏览器原生能力足够好用的时候,React 等是不是也会像 jQuery 一样被浏览器原生能力替代?

组件化

React / Vue 等框架(库)都做了同样的事情,在之前浏览器的原生能力是实现不了的,比如创建一个可复用的组件,可以渲染在 DOM 中的任意位置。

现在呢?我们似乎可以不使用任意的框架和库,甚至不用打包编译,仅是通过 Web Components 这样的浏览器原生能力就可以创建可复用的组件,是不是未来的某一天我们就抛弃了现在所谓的框架和库,直接使用原生 API 或者是使用基于 Web Components 标准的框架和库来开发了?

当然,未来是不可知的

我不是一个 Web Components 的无脑吹,只不过,我们需要面向未来编程。

来看看 Web Components 的一些主要功能吧。

Custom elements: 自定义元素

自定义元素顾名思义就是用户可以自定义 HTML 元素,通过 CustomElementRegistrydefine 来定义,比如:

window.customElements.define('my-element', MyElement);

然后就可以直接通过 <my-element /> 使用了。

根据规范,有两种 Custom elements

  • Autonomous custom elements: 独立的元素,不继承任何 HTML 元素,使用时可以直接 <my-element />
  • Customized buld-in elements: 继承自 HTML 元素,比如通过 { extends: 'p' } 来标识继承自 p 元素,使用时需要 <p is="my-element"></p>

两种 Custom elements 在实现的时候也有所区别:

// Autonomous custom elements
class MyElement extends HTMLElement {
  constructor() {
    super();
  }
}

// Customized buld-in elements:继承自 p 元素
class MyElement extends HTMLParagraphElement {
  constructor() {
    super();
  }
}

更多关于 Custom elements

生命周期函数

Custom elements 的构造函数中,可以指定多个回调函数,它们将会在元素的不同生命时期被调用。

  • connectedCallback:元素首次被插入文档 DOM
  • disconnectedCallback:元素从文档 DOM 中删除时
  • adoptedCallback:元素被移动到新的文档时
  • attributeChangedCallback: 元素增加、删除、修改自身属性时

我们这里留意一下 attributeChangedCallback,是每当元素的属性发生变化时,就会执行这个回调函数,并且获得元素的相关信息:

attributeChangedCallback(name, oldValue, newValue) {
  // TODO
}

需要特别注意的是,如果需要在元素某个属性变化后,触发 attributeChangedCallback() 回调函数,你必须监听这个属性

class MyElement extends HTMLElement {
  static get observedAttributes() {
    return ['my-name'];
  }
  constructor() {
    super();
  }
}

元素的 my-name 属性发生变化时,就会触发回调方法。

Shadow DOM

Web Components 一个非常重要的特性,可以将结构、样式封装在组件内部,与页面上其它代码隔离,这个特性就是通过 Shadow DOM 实现。

关于 Shadow DOM,这里主要想说一下 CSS 样式隔离的特性。Shadow DOM 里外的 selector 是相互获取不到的,所以也没办法在内部使用外部定义的样式,当然外部也没法获取到内部定义的样式。

这样有什么好处呢?划重点,样式隔离,Shadow DOM 通过局部的 HTMLCSS,解决了样式上的一些问题,类似 vuescope 的感觉,元素内部不用关心 selectorCSS rule 会不会被别人覆盖了,会不会不小心把别人的样式给覆盖了。所以,元素的 selector 非常简单:title / item 等,不需要任何的工具或者命名的约束。

更多关于 Shadow DOM

Templates: 模板

可以通过 <template> 来添加一个 Web ComponentShadow DOM 里的 HTML 内容:

<body>
  <template id="my-paragraph">
    <style>
      p {
        color: white;
        background-color: #666;
        padding: 5px;
      }
    </style>
    <p>My paragraph</p>
  </template>
  <script>
    customElements.define('my-paragraph',
      class extends HTMLElement {
        constructor() {
          super();
          let template = document.getElementById('my-paragraph');
          let templateContent = template.content;

          const shadowRoot = this.attachShadow({mode: 'open'}).appendChild(templateContent.cloneNode(true));
        }
      }
    )
  </script>
  <my-paragraph></my-paragraph>
</body>

效果:

我们知道,<template> 是不会直接被渲染的,所以我们是不是可以定义多个 <template> 然后在自定义元素时根据不同的条件选择渲染不同的 <template>?答案当然是:可以。

更多关于 Templates

vue-lit

介绍了 lit-html/elementWeb Components,我们回到尤大这个 vue-lit

首先我们看到在 Vue 3.0Release 里有这么一段:

The @vue/reactivity module exports functions that provide direct access to Vue's reactivity system, and can be used as a standalone package. It can be used to pair with other templating solutions (e.g. lit-html) or even in non-UI scenarios.

意思大概就是说 @vue/reactivity 模块和类似 lit-html 的方案配合,也能设计出一个直接访问 Vue 响应式系统的解决方案。

巧了不是,对上了,这不就是 vue-lit 吗?

源码解析

import { render } from 'https://unpkg.com/lit-html?module'
import {
  shallowReactive,
  effect
} from 'https://unpkg.com/@vue/reactivity/dist/reactivity.esm-browser.js'
  • lit-html 提供核心 render 能力
  • @vue/reactiity 提供 Vue 响应式系统的能力

这里稍带解释一下 shallowReactiveeffect,不展开:

shallowReactive:简单理解就是“浅响应”,类似于“浅拷贝”,它仅仅是响应数据的第一层

const state = shallowReactive({
  a: 1,
  b: {
    c: 2,
  },
})

state.a++ // 响应式
state.b.c++ // 非响应式

effect:简单理解就是 watcher

const state = reactive({
  name: "前端试炼",
});
console.log(state); // 这里返回的是Proxy代理后的对象
effect(() => {
  console.log(state.name); // 每当name数据变化将会导致effect重新执行
});

接着往下看:

export function defineComponent(name, propDefs, factory) {
  // propDefs
  // 如果是函数,则直接当作工厂函数
  // 如果是数组,则监听他们,触发 attributeChangedCallback 回调函数
  if (typeof propDefs === 'function') {
    factory = propDefs
    propDefs = []
  }
  // 调用 Web Components 创建 Custom Elements 的函数
  customElements.define(
    name,
    class extends HTMLElement {
      // 监听 propDefs
      static get observedAttributes() {
        return propDefs
      }
      constructor() {
        super()
        // 创建一个浅响应
        const props = (this._props = shallowReactive({}))
        currentInstance = this
        const template = factory.call(this, props)
        currentInstance = null
        // beforeMount 生命周期
        this._bm && this._bm.forEach((cb) => cb())
        // 定义一个 Shadow root,并且内部实现无法被 JavaScript 访问及修改,类似 <video> 标签
        const root = this.attachShadow({ mode: 'closed' })
        let isMounted = false
        // watcher
        effect(() => {
          if (!isMounted) {
            // beforeUpdate 生命周期
            this._bu && this._bu.forEach((cb) => cb())
          }
          // 调用 lit-html 的核心渲染能力,参考上文 lit-html 的 Demo
          render(template(), root)
          if (isMounted) {
            // update 生命周期
            this._u && this._u.forEach((cb) => cb())
          } else {
            // 渲染完成,将 isMounted 置为 true
            isMounted = true
          }
        })
      }
      connectedCallback() {
        // mounted 生命周期
        this._m && this._m.forEach((cb) => cb())
      }
      disconnectedCallback() {
        // unMounted 生命周期
        this._um && this._um.forEach((cb) => cb())
      }
      attributeChangedCallback(name, oldValue, newValue) {
        // 每次修改 propDefs 里的参数都会触发
        this._props[name] = newValue
      }
    }
  )
}

// 挂载生命周期
function createLifecycleMethod(name) {
  return (cb) => {
    if (currentInstance) {
      ;(currentInstance[name] || (currentInstance[name] = [])).push(cb)
    }
  }
}

// 导出生命周期
export const onBeforeMount = createLifecycleMethod('_bm')
export const onMounted = createLifecycleMethod('_m')
export const onBeforeUpdate = createLifecycleMethod('_bu')
export const onUpdated = createLifecycleMethod('_u')
export const onUnmounted = createLifecycleMethod('_um')

// 导出 lit-hteml 和 @vue/reactivity 的所有 API
export * from 'https://unpkg.com/lit-html?module'
export * from 'https://unpkg.com/@vue/reactivity/dist/reactivity.esm-browser.js'

简化版有助于理解

整体看下来,为了更好地理解,我们不考虑生命周期之后可以简化一下:

import { render } from 'https://unpkg.com/lit-html?module'
import {
  shallowReactive,
  effect
} from 'https://unpkg.com/@vue/reactivity/dist/reactivity.esm-browser.js'

export function defineComponent(name, factory) {
  customElements.define(
    name,
    class extends HTMLElement {
      constructor() {
        super()
        const root = this.attachShadow({ mode: 'closed' })
        effect(() => {
          render(factory(), root)
        })
      }
    }
  )
}

也就这几个流程:

  1. 创建 Web ComponentsCustom Elements
  2. 创建一个 Shadow DOMShadowRoot 节点
  3. 将传入的 factory 和内部创建的 ShadowRoot 节点交给 lit-htmlrender 渲染出来

回过头来看尤大提供的 DEMO:

import {
  defineComponent,
  reactive,
  html,
} from 'https://unpkg.com/@vue/lit'

defineComponent('my-component', () => {
  const msg = 'Hello World'
  const state = reactive({
    show: true
  })
  const toggle = () => {
    state.show = !state.show
  }
  
  return () => html`
    <button @click=${toggle}>toggle child</button>
    ${state.show ? html`<my-child msg=${msg}></my-child>` : ``}
  `
})

my-component 是传入的 name,第二个是一个函数,也就是传入的 factory,其实就是 lit-html 的第一个参数,只不过引入了 @vue/reactivityreactive 能力,把 state 变成了响应式。

没毛病,和 Vue 3.0 Release 里说的一致,@vue/reactivity 可以和 lit-html 配合,使得 VueWeb Components 结合到一块儿了,是不是还挺有意思。

写在最后

可能尤大只是一时兴起,写了这个小玩具,但是可以见得这可能真的是一种大趋势。

猜测不久将来这些关键词会突然就爆发:Unbundled / ES Modules / Web components / Custom Element / Shadow DOM...

是不是值得期待一下?

思考可能还比较浅,文笔有限,不足之处欢迎大家指出。

招聘

阿里国际化团队基础架构组招聘前端 P6/P7,base 杭州,基础设施建设,业务赋能... 很多事情可以做。

要求熟悉 工程化/ Node/ React... 可直接发送简历至 yibin.xb@alibaba-inc.com

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant