折腾|一次原生项目的最佳实践的探索

最后修改:2023-05-23

JavaScript

前言

因为一些原因,前些天,我在重构以前写的一个期末大作业项目😣。

刚开始重构项目,我就开始怀念vue了😂

我选择元素时是这样做的:

const $ = (e) => document.querySelector(e)
const $$ = (e) => document.querySelectorAll(e)
$('.box').xxx

全局暴露 $$$ 方法来方便获取元素。

emmm,这样其实也还行,对于原生项目来说……

不过,我的折腾病,可不允许我就这样。

设想

原生项目似乎没有组件的概念,我最多是见过使用 <iframe> 来达成组件效果的(我也不确定这样做是不是为了组件化)。

UI 组件化似乎没法达成了,但我想了想业务逻辑还是有可能的。

可以写一个类,用面向对象的思维,把某一个 html 范围的业务逻辑写在一个类里面。

我就暂且称之为“模块”吧。

class ExampleBox {
constructor(selector) {
    this.$el = document.querySelector(selector)
  }
}
new ExampleBox('.example-box')

这样就可以在 ExampleBox 里面使用 this.$el.querySelector('xxx') 获取对应 html 内的元素了,并且可以把该元素直接挂在 ExampleBox 实例里面。

ref 指令

但这样还是不方便……还是需要自己写 querySelector

是否可以直接在 html 上标记元素,然后就可以在 ExampleBox 实例里面拿到元素?就像 vue 的 ref 属性一样。

开搞~

设想:在 html 标签上通过 data-ref 标记该元素,然后 new ExampleBox() 时候调用 this.$el.querySelector() 获取该元素,并将 data-ref 的值作为 key 放在实例上。

实现:

<div class="example-box">
<p data-ref="question">问世间帅为何物?</p>
</div>
class Base {
constructor(selector) {
this.$el = document.querySelector(selector)
this.initRef()
}
initRef() {
const refNodes = this.getNodeAll('[data-ref]')
for (let node of refNodes) {
const key = node.dataset.ref
this[key] = node
}
}
getNode(selector) {
return this.$el.querySelector(selector)
}
getNodeAll(selector) {
return this.$el.querySelectorAll(selector)
}
}
class ExampleBox extends Base {
  constructor(selector) {
    super(selector)
  }
}
console.log(new ExampleBox('.example-box'))

打印 ExampleBox 实例就可以看到:

Alt text

看来是成功了😂。

上面写了一个 Base 的基础类可以让每一个“模块”继承以获得 ref 指令的功能。

event 指令

不用10分钟的时间就完成上面的 ref 指令功能,我已经开始飘了,我脑子🧠又产生了大量想法。

于是我又用5分钟实现了 event 指令(事件绑定)功能。

<div class="example-box">
<p data-ref="question">问世间帅为何物?</p>
<button data-event="click:showAnswer">点我查看答案</button>
</div>
// base
initEvent() {
const eventNodes = this.getNodeAll('[data-event]')
for (let node of eventNodes) {
const [eventName, methodName] = node.dataset.event.split(':')
node.addEventListener(eventName, this[methodName].bind(this))
}
}
class ExampleBox extends Base {
constructor(selector) {
super(selector)
}
showAnswer() {
this.question.innerHTML = '唯默小言独占也。'
}
}
console.log(new ExampleBox('.example-box'))

data-event 指令使用 : 来区分不同的参数。

clone & bind 指令

上面的 refevent 指令都很顺利,令我我折腾病大发作。

我继续观察我要重构的原生项目,看看还有哪些痛点,需要解决。

果不奇然,项目都是发送请求,拿到响应数据之后就开始根据数据克隆对应数量的 html 节点,然后$('...').innerHTML= [数据] 😓

如果有 clonebind 指令就可以解决这个问题了🧐。

设想:

<ul class="example-box">
<template data-clone="listItem1:true:arr">
<li data-target="listItem">
<p data-bind="innerHTML:name"></p>
<template data-clone="spanItem:false:images">
<p data-target="spanItem" data-bind="innerHTML:$item"></p>
</template>
</li>
</template>
</ul>
const dataObj = {
arr: [
{
name: 'xiaoyan-1',
images: [1, 2, 3],
},
{
name: 'xiaoyan-2',
images: [4, 5, 6],
},
{
name: 'xiaoyan-3',
images: [7, 8, 9],
},
],
}

data-clone 应该只用在 <template> 上。

从上面 html 可以看到该指令有三个参数,分别是:

data-bind 的第一个参数是元素的属性,例如:innerHTML、innerText、className、id……,第二个参数是需要绑定的数据。

例如:arr 有三个元素,就意味会会把 <li> 克隆3次,并把克隆的结果添加到父元素内。注意:arr 第一个元素的数据就对应着第一个克隆的 <li> 节点,所以 bind 指令就能正确地绑定数据。

细心的同学可能发现了 bind 指令有一个特殊的参数($item),它用于绑定对应的数组元素。这不好解释,我直接上一个效果图,估计就能看明白了。

Alt text

由于,实现这两个指令的代码有点长,就不放在这文章里面了。

感兴趣的可以查看:项目地址

结论

其实这个东西(Base类)没啥大用,不过能满足我的折腾病了。

真要用在实际项目,还是用框架方便🤣