也许可以不用vue模板和jsx,在js当中就可以灵活、优雅、简洁的编写UI

101 阅读3分钟

前言

大家好,今天给各位推荐一下我写的一个前端轮子,名叫microway,主要功能的是在JS文件中编写UI,现在前端编写UI界面主要有两个方向,模板和JSX,两者的优劣本文就不讨论了,感兴趣的可以自行搜索,两种方案都是要在node环境经过编译之后,再生成js代码,而想要使用他们编写原生环境代码,又需要使用和主流有差异的语法(主要是使用模板字符串或者vnode),本轮子的终极目标是在JS当中,找到一种灵活、优雅、又简洁的语法,用来代替目前所有主流的UI编写方式,让原生环境和node环境使用同样的语法来编写UI,

使用教程

吹牛环节结束,现在开始轮子教程环节

首先引入

import { init, l } from 'microway'

const { fragment } = init()

调用 fragment 函数,在参数括号内使用 l 函数来编写 html 标签,正如其名,fragment 函数返回一个 DocumentFragment,然后来把 fgm 挂载到界面

const fgm = fragment(
  l`div ddd span='14'`(
    l`a href='https://baidu.com/' target="_blank"`('百度'),
    456
  )
)

document.querySelector('#app')!.appendChild(fgm)

接下来是在标签上引入js变量,让我们再引入 baseModule 然后再 init 时传入 baseModule 插件

import { init, l, baseModule } from 'microway'

const { fragment } = init(baseModule)

使用变量

const pd = '25px'
const num = 123

const on = {
  click() {
    console.log('hello microway')
  }
}

const fgm = fragment(
  l`div ddd span='14'`(
    l`a ${{ href: 'https://baidu.com/' }} target="_blank"`('百度'),
    l`div style=${{ 'padding-top': pd }}`(l`span`('fff'), pd),
    num
  ),
  l`div ${{ on, style: { border: '1px solid red' } }}`('btn')
)

响应式

前面的教程都属于静态部分,因为 fragment 只会编译一次,想要动态内容,就得使用响应式框架来实现,本轮子不提供响应式框架实现,但是可以通过配置引入其他框架,接下来使用 @vue/reactivity 来演示

import { isRef, effect, stop, computed, ref } from '@vue/reactivity'
import { init, l, baseModule, signalModule } from 'microway'

const { fragment } = init(
  {
    _signal: {
      is: isRef,
      get: obj => (isRef(obj) ? obj.value : obj),
      memo: computed,
      effect,
      stop
    }
  },
  baseModule,
  signalModule
)

使用

const pd = ref('25px')
const href = ref('https://baidu.com/')
const num = ref(2)

const on = {
  click() {
    num.value++
    console.log('add: ', num.value)
  }
}

const setPd = () => {
  pd.value = '10px'
}

const fgm = fragment(
  l`div ddd span='14'`(
    l`a href=${href} target="_blank"`('百度'),
    l`div style=${{ 'padding-top': pd }} onclick=${setPd}`(pd),
    456
  ),
  l`div ${{ on, style: { border: '1px solid red' } }}`('hello'),
  l`div`(() => [...new Array(num.value).keys()].map(i => l`div style='color: blue'`(i)))
)

解释和思考

使用环节结束了,可以看到主要用来编写UI的是 l 函数,使用方式是这样的

l`a href='https://baidu.com/' target="_blank"`('百度')

l 第一部分是模板字符串,和主流使用方式不同,在这里的模板字符串只描述一个标签的头部,也就是 tag 和 attr 两个部分,将子元素放到了第2部分,也就是小括号当中,这样做有两个好处, 即提供了与HTML差不多的语法(把尖括号换成模板字符串首尾的`, 即可无缝适应新语法),又保留了js的灵活,简直完美

大家对这样的语法有什么见解呢,欢迎在评论区指点,接下来让我们来看一下 l 函数的实现

const l = (strs: TemplateStringsArray, ...vals: any[]) => {
  const rv = {
    [Symbol.toStringTag]: 'LTag',
    strs,
    vals
  }

  return Object.assign((...children: any[]) => ({ ...rv, children }), rv)
}

可以看到实现非常简单,因为 l 函数只保存数据,将数据编译成 dom 的过程在 fragment 当中 值得一提的是返回值部分,l 是一个标签函数,在返回值里我们返回一个函数以及模板数据合并到一起返回, 当调用该函数插入子元素时,再把模板数据和子元素一起返回

接下来介绍一下插入 js 变量,引入变量的情况分为在等号后引入,此时 style 为属性名

l`div style=${{ 'padding-top': pd }}`

以及直接引入对象字面量,此时 href 为属性名

l`a ${{ href: 'https://baidu.com/' }} target="_blank"`

然后介绍一下子元素部分,首先 l 函数都会被编译成 Element,string和number变量会编译成 Text,init 函数只支持这3种子元素数据

在引入 signalModule 插件后,fragment 会将 signal.value 为 string 和 number 的 signal 看作动态的 Text 节点, 插入在 l 函数模板字符串里的 signal 也会响应式更新 dom, 副作用保存在 dom.effects 当中,遇到函数时把函数看作动态节点,使用 fragment 编译函数的返回值, 再插入到函数的位置,函数会用为 memo 的参数,所以只要函数里的响应式数据有更改, 函数部分的节点都会重新渲染一次,当遇到函数时会在dom的位置前后插入Common节点, 用来定位函数编译后的节点插入的位置,signal 的副作用会保存在 dom.effects 当中,函数动态节点的 副作用则保存在 endCommon.effects 当中

到此,本轮子已经介绍结束,如果有什么建议和指点,欢迎评论区探讨,如果各位想扩展功能,欢迎各位往史山加shi,也可以编写插件,baseModule 和 signalModule 就是轮子默认提供的插件(本来应该叫 plugin 的,在写代码的时候不知道为啥给写成了 module),如果有大佬可以给我的轮子写一个 babel 插件的话那就更加欢迎了哈,因为是主要介绍的是语法,就不再深入介绍代码了,感兴趣的同学也欢迎参观源码,有机会的话会出下一章 fragment 的讲解 以及如何编写插件

敬请期待