在 Vue 和 React 中使用 LuLu UI

8,742 阅读7分钟

上个月 LuLu UI Edge 主题对外的时候发了条微博,由于 LuLu UI 是基于原生 DOM 开发的,因此,就有人认为是不支持 Vue 的,这个就属于误会了,LuLu UI 在 Vue 中的支持非常好。

所以,我觉得有必要专门写篇文章讲下这个问题。

一、原生语言的魅力

LuLu UI 的 Pure 主题(IE9+)和 Edge 主题(非IE)都是基于 原生 JavaScript 语法开发的,没有刻意和任何框架接近,这就保证了其作为跨项目、跨团队的统一解决方案成为了可能。

例如团队中 A 产品使用的是 Vue,B 产品使用的是 React(或Preact),此时,领导想要这两个产品统一 UI 组件库,那种根植于 Vue 规则或者 React 规则的 UI 组件库肯定是无法完成这个使命的。

只有原生语言开发,保持自身个性的 UI 组件库才能完成这样一件事情。

其实道理很简单,无论是 Vue 还是 React,底层也都是使用原生 JavaScript 语言开发的,本是同根生,相兼不用急。

数据驱动修改的还是 DOM

LuLu UI  是基于 HTML 元素开发的,一切都是围绕 HTML 元素的属性变化展开,这个和数据驱动一点也不冲突,因为数据驱动最终修改的还是 DOM。

只是,LuLu UI 中 DOM 修改后的 UI 同步采用的是自己内置的观察器,不是 Vue 的,也不是 React的,也就是虽然不依赖 Vue 和 React,但是就组件思想而言,和 Vue、React 等框架是一脉相承的,因此,可以完全兼容。

举个例子,在 LuLu UI 中,分页组件的总页数和当前页数都是通过 HTML 属性控制的,例如下面代码中的 total 和 per:

<ui-pagination total="100" per="8"></ui-pagination>

如果是在 jQuery 开发项目中,我们会直接操作 DOM 属性的值使分页发生变化,例如:

$('ui-pagination').attr('total', 200)

功能 OK,来了来了,症结点来了,看多人看到这里就误认为 LuLu UI 组件使用的时候需要直接操作 DOM,错啦,不是的啊!

注意,注意,请注意,我们直接使用数据驱动,同样也是功能 OK的,例如:

<ui-pagination total="{{ total }}" per="{{ per }}"></ui-pagination>

{
  data() {
    return {
      total: 100,
      per: 8
    }
}

this.data.total = 200;

分页组件的 UI 也是会自动变化的。

原因很简单,LuLu UI 中的组件关心的是 DOM 上属性的变化和 DOM 元素自身的变化,至于这个 DOM 元素是直接选择器获取发生的变化,还是在虚拟 DOM 中处理发生的变化,LuLu UI 并不关心。

所以,在 Vue 和 React 中使用 LuLu UI 是完全没问题的,当然,在原生环境或者 jQuery 项目中使用也是没问题的。

二、原生能力的魅力

LuLu UI 是如何实现 DOM 属性变化组件 UI 同步变化的呢?

这就要讲讲浏览器原生的组件能力了。

1. 原本就有的组件

本质上,输入框、单复选框等表单控件就是一个组件:

<input class="ui-input">

此时,我们只需要使用 CSS 对整个 input 元素进行美化,一个功能完好的输入框 UI 组件就好了,value 赋值内容会变化,输入行为发生会 触发 change、input 等事件,无论何时,只要该 DOM 元素和页面建立联系,其 UI 表现就会瞬时渲染。

开发者无需关心组件本身,只需要专注在业务逻辑就好了。

2. 内置自定义元素

然而,有部分 UI 组件无法直接使用 CSS 进行彻底的美化,例如 下拉框组件: <select></select> 此时,就需要借助 JavaScript 进行全新的 DOM 构造,然后使用 CSS 实现最终的美化后的下拉框。 问题来了,构造是没问题的,如何让  的值和选中项发生变化的时候,构造的 DOM UI 也同步发生变化呢?

LuLu UI 是使用的 Customized built-in element 实现的,兼容性如下(Safari 可以引入一段 Polyfill 进行支持):

语法是使用 is 属性,属性值为自定义的组件名称:

<select is="ui-select"></select>

Customized built-in element 本质上也是 Web Components,无论是生命周期函数,还是对属性的观察与检测都是浏览器天然支持的。

无需开发者自己去处理何时初始化,以及自己写事件函数去识别 HTML 属性变化。

3. 标准自定义元素

例如,上面提到的  就是自定义元素,开发者可以任意定义 HTML 属性作为组件的 API 接口。

在 LuLu UI 中有很多组件都是 Web Components 自定义元素组件,例如下拉、选项卡、轻提示组件等。

由于非本文重点,不展开。

4. 自定义属性的观察

在 LuLu UI 中,标准 HTML 元素也能拥有组件的能力,只需要添加一个自定义属性即可,例如 Drop 下拉效果,只需要设置一个 is-drop 属性,就拥有了下拉选择的能力。

<a href="javascript:" is-drop="t2">click我</a>

这是使用 MutationObserver 实现的,也是 Pure 主题实现 HTML 属性驱动的核心方法。

正是借助上述浏览器的能力,使得 LuLu UI 中的组件可以和数据驱动无缝对接,因为 HTML 属性值其实就是数据。

三、在 Vue 中使用 LuLu UI

LuLu UI 和 Vue 可以 100% 无障碍使用,这里有一些使用指南与建议。

1. 使用template元素

Vue 支持常规的 HTML 元素作为渲染的一部分,例如下面这样:

<div id="event-handling">
  <p>{{ message }}</p>
  <select is="ui-select" @change="showMessage">
      <option value="青龙">青龙</option>
      <option value="玄武">玄武</option>
  </select>
</div>

但是在 LuLu UI 中,会有问题,因为会存在 <select> 元素二次渲染的问题。

当页面载入完毕,此时 <select> 是一个符合 LuLu UI 规则的 UI 组件,则会执行组件渲染,当再次执行 Vue 的 DOM 渲染,<select> 元素又会重新被赋予到文档中,会触发第二次的组件渲染。

因此,在 Vue 中使用 LuLu UI,总是建议将组件元素放在 <template> 元素中,这样,LuLu 的 UI 组件只会在 Vue 完成 DOM 绘制后进行唯一一次的渲染,功能就完全正常了。

当然,这只是建议,对于LuLu UI 组件,即使两次渲染,功能也是没有任何影响的。

2. 静态组件直接使用

由于静态组件是基于 CSS 渲染,因此,静态组件当做原生的标准 HTML 元素在 Vue 中使用就可以了,无需专门放在 <template> 元素中。例如:

<div id="static-components">
    <button type="primary" is="ui-button">主按钮</button>
    <button class="ui-button" data-type="primary">主按钮</button>
    <p><input is="ui-input"> <input class="ui-input"></p>
    <input type="checkbox" is="ui-checkbox"> <input type="radio" is="ui-radio">
    <p><textarea is="ui-textarea"></textarea> <textarea class="ui-textarea"></textarea></p>
    <input type="checkbox" is="ui-switch"> <progress is="ui-progress" value=".5"></progress>
</div>

Vue.createApp({}).mount('#static-components')

效果如下图所示:

3. 内置自定义元素使用

内置自定义元素的 is 属性在 Vue 中并无特殊含义,因此可以无障碍使用,例如:

<div id="buildin-components">
    <template v-if="1">
        <input type="range" is="ui-range" data-tips="${value}%"><br>
        <input type="color" is="ui-color" value="#ff0000"><br>
        <input type="date" is="ui-datetime"><br>
    </template>
</div>

Vue.createApp({}).mount('#buildin-components')

4. 自定义元素使用

类似 <xx-xx> 的自定义元素在 Vue 中有着特殊含义,通常代表和 Vue 语法集成的 Web 组件,因此,LuLu UI 中的自定义元素组件在 Vue 中会发生警告,需要大家配置下。

const app = Vue.createApp({})
app.config.compilerOptions.isCustomElement = tag => tag.startsWith('ui-')
app.mount('#app')

也可以在 Vite 或 Vue CLI 中配置,详见官方文档

这个时候就可以自如使用了:

<ui-pagination total="100" per="8"></ui-pagination>

5. 事件的处理

LuLu UI 中的组件采用的是浏览器元素的 DOM 事件机制,也正好是Vue 的事件执行机制,因此,LuLu UI 的自定义事件可以和 Vue 的事件处理无缝对接。

例如下面这个表单验证的案例:

<div id="formApp">
    <template v-if="1">
        <form is-validate @valid="setSuccess">
            <input type="search" class="ui-input" required>
            <button type="primary" class="ui-button">搜索</button>
        </form>
    </template>
</div>

上面的 @valid 就是 LuLu UI 的自定义事件,在表单验证全部通过后触发。

配合下面的 Vue 代码,就可以在验证成功后显示提示弹框:

// 表单事件
const FormHandling = {
  methods: {
    setSuccess() {
      new Dialog().alert('验证通过');
    }
  }
}
Vue.createApp(FormHandling).mount('#formApp')

Gif 录屏效果如下所示:

6. 参数的传递

LuLu UI 中,有部分参数是 DOM 对象属性(非 HTML 属性),此时,数据驱动的赋值就会遇到阻碍,此时,可以借助 LuLu UI 所有组件都内置的 connected 生命周期事件进行处理。

例如,给一个数据列表传递列表数组数据:

<div id="listApp">
    <template v-if="1">
        <input class="ui-input" results="5" is="ui-datalist" @connected="setListData">
    </template>                
</div>

// 传参
const ListHandle = {
  methods: {
    setListData(e) {
      e.target.params.data = [...];
    }
  }
}
Vue.createApp(ListHandle).mount('#listApp')

这样,传参的逻辑就在 Vue 的业务逻辑代码中,就和使用 Vue 组件的感觉一样。

7. 在 Vue 框架中使用

LuLu UI 也支持在 Vue 开发框架中使用,使用方式和常规的 UI 组件库类似,分为安装和调用两步。

npm install lu2

<style src="lu2/theme/edge/css/common/ui/Dialog.css"></style>

<script>
    import Dialog from 'lu2/theme/edge/js/common/ui/Dialog'
</script>

接下来的使用就按照各个 UI 组件对应的文档提示和本教程进行就好了。

不过 LuLu  UI 官方建议 LuLu UI 根植于业务代码中,而不是作为一个 node_modules 库,因为 LuLu UI 本质上是层 UI 皮肤,不是语言类库,应该和项目融合在一起迭代。

四、在 React / Preact 中使用 LuLu UI

整体来看,LuLu UI 在 React 的支持情况要比 Vue 逊色一些,主要原因在于 React 自己构造一整套的事件处理机制,导致 LuLu UI 中部分 UI 组件的自定义事情没法直接套用 React 语法,需要借助 ref 手动添加事件,还有部分原因在于 React 有不少自己定义的规则。

而 Preact 无论是 HTML 属性规则还是事件处理,都是基于浏览器原生的那套实现的,因此,LuLu UI 和 Preact 是 100% 支持的,无论是 Web 免安装环境,还是 CLI 框架中。

1. Preact 与 LuLu UI
这里通过一个简单的例子演示下事件与传参:

<div id="listApp"></div>

<script type="module">
import { html, render } from 'https://unpkg.com/htm/preact/index.mjs?module'
// 传参设置
function setListData (event) {
    event.target.params.data = [...]
};
render(html`<input class="ui-input" is="ui-datalist" onconnected="${setListData}"/>`, listApp);</script>

Preact 也是使用的浏览器自带的事件机制,因此,当在渲染的 HTML 字符串中使用 onconnected 是可以触发 LuLu UI 中自定义的 connected 事件的,从而实现在业务代码中精准设置组件的参数。

实现效果如下 Gif 录屏:

2. React 与 LuLu UI

虽然 LuLu UI 的气质和 React 不太相符,但是,绝大多数组件依然是可以无障碍使用的,并且相比 Vue 还有优势,那就是自定义元素组件天然支持。例如下面的 <ui-drop> 自定义元素组件在 React 中可以正常渲染,但是在 Vue 中会被警告处理:

<ui-drop target="t2" eventtype="hover">hover我</ui-drop>

React 中使用 LuLu UI 最大的问题是部分 UI 组件的自定义事件处理问题,尤其在 Edge 主题中,几乎所有的回调处理都基于浏览器事件来完成。

而 React 自造了一套事件处理机制,这使得,对于部分 LuLu UI 组件,需要借助 ref,才能在 React 中自如使用。

还是数据列表的例子,如果是在 React 框架开发环境中,则需要这么处理:

<div id="root"></div>

import "lu2/theme/edge/css/common/ui/Input.css";
import "lu2/theme/edge/css/common/ui/Datalist.css";
import "lu2/theme/edge/js/common/ui/Datalist.js";

const myRef = React.createRef();
const datalist = <input class="ui-input" results="5" is="ui-datalist" ref={myRef}/>
// 组件 DOM 渲染
ReactDOM.render(datalist, document.getElementById('root'));

// 传参,此时 myRef.current 就是组件元素
const data = [...];

if (myRef.current.isConnectedCallback) {
  myRef.current.params.data = data;
} else {
  myRef.current.addEventListener('connected', function () {
    this.params.data = data
  });
}

通过 ref 匹配到组件元素,再进行参数设置,这显然就没有 Vue 和 Preact 那么优雅了。

效果如下图所示(result="5"限制最多出现5项列表):

不过好在,LuLu UI 中,大多数的 UI 组件是不需要通过 DOM 对象传参的,依然是可以无障碍使用的。

关于在 React 中使用 LuLu UI 更多案例,可以参考这个教程:在 React 中使用 LuLu UI

五、关于 LuLu UI

LuLu UI 是阅文集团荣誉出品的前端 UI 组件库。

形象气质如下图,更柔软,更亲近,同时简单灵活,对用户侧非常友好,非常适合面向外部用户的 PC 网站

Github项目地址是:github.com/yued-fe/lul…

官网地址是:l-ui.com

LuLu UI 的竞争力与局限

LuLu UI 的优势在于自由、灵活、跨端跨项目、上手简单、即插即用,严格遵循追本溯源,无为而治的设计准则,个性鲜明,特立独行,也注定会更能够穿越历史的沉浮,更加长寿。

但是距离成为顶级的 UI 组件库还有一段距离,例如案例、教程、周边、插件、社区等都还是待开发的土地,由于厂子的主要精力还是聚焦在业务上,所以,注定不会有那么多人财物投入在上面。

所以,希望有多多的同行一同参与建设,肯定还有很多可以变得更好的地方,欢迎试用、反馈与交流。

哪怕随手 Star 下,也是一种参与建设

以上~

感谢您的阅读,ღ( ´・ᴗ・` )比心 !