ficusjs使用

5/9/2021 ficusjs

前端又双叒叕来新玩具了

# ficusjs 系列

# ficusjs

老规矩还是先介绍一下 ficusjs (opens new window)。 文档地址:docs-ficusjs (opens new window)

说 ficusjs 之前,就不得不提 Web Components (opens new window)

Web Components 旨在解决这些问题 — 它由三项主要技术组成,它们可以一起使用来创建封装功能的定制元素,可以在你喜欢的任何地方重用,不必担心代码冲突。

在我理解,这是下一代的 web 前端技术,让前端的组件化更进一步!目前做前端组件化都是基于框架(比如 vue,react 之类的),而 web components 则是提供了天然的隔离,并且可以在 html 界面写自定义的标签了!而且也有组件自己的生命周期等,最绝的就是,写完自定义标签后,用法和 div 一模一样,只能说有过之而无不及!包括 document.querySelector 一样能找到对应的元素。兼容性也还算 OK~ caniuse - web components (opens new window)

而 ficusjs 则是基于 web components 的下一代前端框架

# hello world

不管学习什么,hello world 不能忘

直接拿官网的 demo 来看看先

 





 















<hello-world></hello-world>

<script type="module">
  import { html, renderer } from 'https://cdn.skypack.dev/@ficusjs/renderers@3/htm'
  import { createComponent } from 'https://cdn.skypack.dev/ficusjs@3/component'

  createComponent('hello-world', {
    renderer,
    handleClick() {
      window.alert('Hello to you!')
    },
    render() {
      return html`
        <div>
          <p>FicusJS hello world</p>
          <button type="button" onclick="${this.handleClick}">Click me</button>
        </div>
      `
    }
  })
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

看下效果:hello-world 标签是直接渲染出来了,(不像 vue 和 react 最后还是把代码转换成 div)

# 引入

文档地址 https://docs.ficusjs.org/installation/ (opens new window)

# ficusjs - CDN 引入

cdn 的引入特别有意思,很有 deno 的味道

注意使用 type="module" 来引入。对于低版本,不支持 type="module" 的浏览器,还是用 npm 引入,使用 webpack 打包

  • 全部功能引入
<script type="module">
  import {
    // components
    createComponent,

    // extending components
    withStateTransactions,
    withStore,
    withEventBus,
    withStyles,
    withLazyRender,

    // event bus
    createEventBus,
    getEventBus,

    // app state
    createAppState,
    getAppState,
    createPersist,

    // stores - DEPRECATED
    createStore,
    getStore,

    // modules
    use
  } from 'https://cdn.skypack.dev/ficusjs@3'
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
  • 部分引入
import { createComponent, use } from 'https://cdn.skypack.dev/ficusjs@3/component'
import { withStateTransactions } from 'https://cdn.skypack.dev/ficusjs@3/with-state-transactions'
import { withEventBus } from 'https://cdn.skypack.dev/ficusjs@3/with-event-bus'
import { withStore } from 'https://cdn.skypack.dev/ficusjs@3/with-store'
import { withStyles } from 'https://cdn.skypack.dev/ficusjs@3/with-styles'
import { withLazyRender } from 'https://cdn.skypack.dev/ficusjs@3/with-lazy-render'

// 其余模块同理 ...
1
2
3
4
5
6
7
8

# ficusjs - 使用 npm 构建

npm install ficusjs
1

剩下的引入就也类似了

import { createComponent, use } from 'ficusjs'
1

# 创建一个组件

从最基础的 demo 入手,看到引入了 html、renderer、createComponent 方法。那么就来看看这到底是怎么用的

# ficusjs - createComponent

createComponent('hello-world', {
  renderer,
  handleClick() {
    window.alert('Hello to you!')
  },
  render() {
    return html`
      <div>
        <p>FicusJS hello world</p>
        <button type="button" onclick="${this.handleClick}">Click me</button>
      </div>
    `
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14

首先执行的就是 createComponent 。接收 2 个参数,一个是组件的名称,第二个参数就是一些配置项了

以下表格来自官方文档的翻译(翻译的不准不要打我)

属性 是否必填 类型 描述
renderer 必填 function 一个函数,用于渲染从render函数返回的内容。
render 必填 function 一个必须返回一个 可以传递给 render 的响应的函数
root string 设置组件的根定义
props object 这里的 props 就和 vue 很像了,接收组件的参数
computed object 这个和 vue 的 computed 也是很像!用于返回数据的
state function 返回一个包含初始状态的对象的函数。状态是组件中的内部变量(和 vue 的data函数很像)
* function 组件中的任何方法,都可以写这里面,然后通过 this.xxx 调用
created function 生命周期 - 当组件被创建时,在它被连接到 DOM 之前被调用
mounted function 生命周期 - DOM 挂载后
updated function 生命周期 - 组件更时
removed function 生命周期 - 组件销毁时

# ficusjs - 一个必须返回一个 可以传递给 render 的响应的函数

说一下这个的意思,render 函数中,return html`` 注意这个 html不是用()调用,而且直接接上字符串类型,使用 html 处理后,才能传递给 renderer 函数

# ficusjs - root 的解析

root 接收的是一个 string 类型的东西。一共就 3 个值:

描述
standard 一个普通的 html 节点
shadow 一个开放的 Shadow DOM 节点(见下图)
shadow:closed 一个闭合的 Shadow DOM 节点

关于 Shadow DOM ,我也不太了解,自己看看把~ Shadow DOM (opens new window)

# ficusjs - props 参数

props 的参数使用和 vue 也是很像很像,只有一丢丢的小区别

props: {
  className: {
    type: String,
    default: 'btn',
    required: true, // is this required?
    observed: false // turn off observing changes to this prop
  },
  required: {
    type: Boolean,
    default: false
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
属性 是否必填
type 这必须是字符串、数字、布尔值或对象中的一种(StringNumberBooleanObject
default 如果没有设置默认值,则设置一个默认值
required 在使用该组件时,这个参数是否必填 (true/false)
observed 是否监听这个参数的变化,如果设置为 false,即使父组件更新了参数的值,组件也不会有响应

Q1:在 html 代码中,我怎么传递 Object 对象?

不小心看到了源码,对于 Object 类型的,只是简单的做了 JSON.parse 处理。

<!-- 这段代码可以 -->
<hello-world user-info='{"name":"Jioho"}'></hello-world>

<!-- 这段代码不行!! -->
<hello-world user-info='{name:"Jioho"}'></hello-world>
1
2
3
4
5

因为接收的并不是真正的 Object 对象,所以 {name:"Jioho"} 是不能通过 JSON.parse 转义的


Q2:type 中貌似不支持数组(Array)

数组类型按不严格来说,也是属于对象类型,所以只要我们把数组 JSON.stringify 后传入,然后type设置为Object 就可以收到数组对象了


Q3:在 html 代码中,使用驼峰的参数名不能识别

如果想在参数中使用 userInfo 这种驼峰命名,那么在 html 中需要用 - 来声明,即user-info。可是在定义参数 props 和 获取值的时候,还是使用驼峰来获取

<hello-world user-info='{"name":"Jioho"}'></hello-world>

<script>
  // 省略一堆代码
  console.log(this.props.userInfo) // {name:"Jioho"}
</script>
1
2
3
4
5
6

Q4:既然是 props ,js 如何修改这些参数,修改后是否又会立刻响应?

  • 参数是响应式的,修改是立即生效的

  • 既然参数是直接写入到 html 的,所以修改参数的方式和修改 html 的属性方式是一致的:

const helloWorld = document.querySelector('hello-world')
helloWorld.setAttribute('user-info', JSON.stringify({ name: 'Jioho2' }))
1
2

# ficusjs - Computed

计算属性,emmm 还是那句,和 vue 效果一样。也只是效果一样,功能完全不一样!!

  • 【不同】ficusjs 的 Computed 并不会进行依赖收集,只要是 state 或者 props 的参数变化,他都会重新执行
  • 【相同】ficusjs 的 Computed 也会有计算缓存,只要参数没变化,重新获取的时候也是拿计算好的值,不会重复计算

稍微准备了一个 demo 演示下:

<body>
  <hello-world person-name="person-name" user-info='{"name":"Jioho"}'></hello-world>
  <button id="btn">更新person-name</button>
  <button id="btn_user">更新user-info</button>
</body>

<script type="module">
  import { html, renderer } from 'https://cdn.skypack.dev/@ficusjs/renderers/lit-html'
  import { createComponent } from 'https://cdn.skypack.dev/ficusjs/component'

  createComponent('hello-world', {
    renderer,
    props: {
      personName: {
        type: String,
        required: true
      },
      userInfo: {
        type: Object,
        required: true
      }
    },
    handleClick(e) {
      console.log(this.props.userInfo, 'userInfo')
      console.log(this.myName, 'myName')
    },
    computed: {
      myName() {
        console.log('触发 computed-myName')
        let propName = this.props.userInfo.name
        return propName + '_'
      }
    },
    state() {
      return {
        count: 0
      }
    },
    render() {
      return html`
        <div>
          <button type="button" @click="${this.handleClick}">Click me!</button>
        </div>
        <div>${this.props.userInfo.name}</div>
        <div>${this.myName}</div>
      `
    }
  })

  document.getElementById('btn').onclick = function() {
    document.querySelector('hello-world').setAttribute('person-name', '随机' + new Date().getTime())
  }

  document.getElementById('btn_user').onclick = function() {
    document.querySelector('hello-world').setAttribute('user-info', JSON.stringify({ name: 'Jioho222' }))
  }
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
  • 第一次运行后效果

这次的触发是因为在 html 中用到了 myName 这个属性


  • 接着更新 userInfo

点击按钮后,页面确实开始更新了,然后也重新触发了 computed


  • 点击 更新 person-name 按钮

区别:在更新 person-name 时,代码并没有用到 person-name 属性,可是 computed 还是重新触发了,这和 vue 收集依赖在触发是有点不同的!


  • 点击 click me !

在按钮点击事件中,重新获取了 this.myName 属性,可以看到 computed 没有触发,不过也输出了对应的值,这和 vue 的计算后的缓存又有点类似~

包括后面重新改了一下 demo,输出多次名称,也只会触发一次的计算量

# ficusjs - State

页面的数据,react 也叫state,vue 则是用data 表示。state 也是一个函数,然后 return 对应的数据,至于为什么要用函数,盲猜一波和 vue 的原理类似 vue 组件的 data 为什么必须是函数

{
  state () {
    return {
      count: 0,
      myName: 'state-myName',
      list: [{ name: 'Jioho', age: 18 }, { name: 'Jioho2', age: 20 }, { name: 'Jioho3', age: 22 }]
    }
  }
}
// 页面上使用的时候如下:
// ${this.state.count}
1
2
3
4
5
6
7
8
9
10
11

Q1: 如何重新赋值?

赋值有 2 种方式,一个是直接赋值,一个是用 setState 方法

  • 直接赋值

直接赋值弊端:

  1. 页面更新是异步更新的,如果后面的步骤依赖于赋值后的效果,那直接赋值不适合
  2. 直接赋值不能指定数组的某一项进行赋值(setState 也不行)
this.state.count++
1
  • 使用 setState 函数

setState 函数写法比较奇特,和小程序和 react 类似,但不能说一样~

  1. 支持赋值后的回调
  2. return 函数中只需要返回需要更新的值即可,无须返回整个 state
  3. 注意第一个参数必须是一个函数,函数中的返回值才是需要更新的内容
this.setState(
  state => {
    let _list = state.list
    _list[0].name = 'Jioho_update'
    return {
      list: _list
    }
  },
  () => console.log('渲染完成回调')
)
1
2
3
4
5
6
7
8
9
10

Q2: 必须要通过 this.state.xxx 取值吗? this.xxx 取值是否可以? 能否和 computed 同名?

必须通过this.state.xxx取值,this.xxx 是取不到数据的。也正因为这个特性,和 computed 可以重名(vue 就不能重名)

而且 computed 取值规范一点是 this.get.xxx 的方式取值,经试验不写 .get 也没有影响。

# ficusjs - Method 方法

方法就没啥需要多说的了,写法都一样

# ficusjs - 生命周期

{
  created() {
    console.log('created')
  },
  mounted() {
    console.log('mounted')
  },
  updated() {
    console.log('updated')
  },
  removed() {
    console.log('removed')
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

生命周期也没几个

名称 触发时间
created 创建节点后还没挂载前
mounted 挂载到页面上
updated 数据触发更新(第一次挂载也会触发)
removed 组件被移除时

created 和 mounted 之间

这个问题很值得说一说。看到文档后面会发现,render 函数支持 Promise 作为返回值。那像下面这段 render 是可以运行的,猜猜看生命周期是如何的?

render() {
  return new Promise((resolve, reject) => {
    console.log('开始运行', new Date())
    setTimeout(() => {
      resolve(html`
        <div>
          <p>FicusJS hello world</p>
          <button type="button" onclick="${this.handleClick}">Click me</button>
          <div>${this.state.count}</div>
        </div>
      `)
      console.log('resolve end', new Date())
    }, 1000)
  })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

!!你准备看到答案了!!

created 在 JS 资源加载完成后就开始运行了,然后就是进入了 1s 的等待,才执行了 mounted。值得说的就是 created 是比 render 函数早执行的


updated 生命周期
如上图所示,节点挂载后没做任何操作时,他也会触发updated(渲染静态节点也会触发更新)


removed 生命周期

document.querySelector('hello-world').remove()
1

# ficusjs - renderer 函数

可能写法上他一直都是一个缩写。renderer 是从 JS 库引入的。在 createComponent 中的 renderer 是类似这个组件的一个回调函数

createComponent('hello-world', {
  renderer
})

// 等同于

createComponent('hello-world', {
  renderer(what, where) {
    console.log(what, where)
    renderer(what, where)
  }
})
1
2
3
4
5
6
7
8
9
10
11
12

从截图也能看出,renderer 的执行也是在 createdmounted 之间,并且在 render 函数返回之后才执行

每次更新组件的值的执行顺序:render函数 -> renderer函数 -> updated

说一说 renderer 回调函数的 2 个值,和 renderer 方法
注意区分下面哪个说的是回调函数。哪个是 renderer方法

# ficusjs - 回调函数回调了 2 个参数whatwhere

  • whatrender函数处理完返回的 html 结构,上面的图打印也能看到,打印出来是一个 dom 节点

所以,在节点 mounetd 之前,或者 update 之前,你完全可以伪造一个节点!!比如像下面这种

renderer(what, where) {
  console.log(what, where)
  let myDiv = document.createElement('div')
  myDiv.innerText = 'hello world'
  renderer(myDiv, where)
},
render() {
  return html`
        <div>
          <p>FicusJS hello world</p>
        </div>
      `
  })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

虽然 render 函数返回的是 FicusJS hello world 。可以最后挂载到节点上的内容,是 hello world


  • where 则是要挂载的节点(这里有知识点!!)

要挂载的节点这个就更加好理解了,默认我们是挂载到标签写的位置,那么我们可以指定他挂载在界面上的其他地方。比如在 html 添加一个id=app的新节点。挂载函数也改一改,获取 app 节点,然后传入 renderer 函数中

<div id="app"></div>
<hello-world></hello-world>
1
2
renderer(what, where) {
  console.log(what, where)
  let myDiv = document.createElement('div')
  myDiv.innerText = 'hello world'
  let app = document.getElementById('app')
  renderer(myDiv, app)
},
1
2
3
4
5
6
7

是的,myDiv 的内容挂载到了 #app 中。虽然节点已经被转移到#app,可是数据还是响应式的

【知识点来了】

你以为就这么就结束了吗?还记得 remove 生命周期吗?在这种情况下,我们把hello-world节点移除

  • 触发 removed 生命周期
  • 点击按钮更新 count -> render 函数,updated 等都正常运行!

(也不知道是 bug 还是特性,反正咋也不敢问)

# ficusjs - Rendering 渲染方式

有了上面的 renderer 认识基础,再来看 Rendering 就好理解很多了

看看文档原话(大意就是说,处理 html 支持多种渲染引擎,uhtml (opens new window),lit-html (opens new window),htm (opens new window))等一系列渲染引擎:

我就随便挑了一个 htm (opens new window) 来试下

引入还是一如既往的方便(npm 引入的就自己看文档把)

// 把之前ficusjs的注释掉
// import { html, renderer } from 'https://cdn.skypack.dev/@ficusjs/renderers@3/htm'

// 替换为新的渲染引擎(注意这里是 render,而不是 rendered 了)
import { html, render } from 'https://unpkg.com/htm/preact/standalone.module.js'
import { createComponent } from 'https://cdn.skypack.dev/ficusjs@3/component'
1
2
3
4
5
6

html 倒是没变,所以 render 函数不用改,注意下面的 render 方法,用的就是 htm 的方法渲染了


 











renderer(what, where) {
  console.log(what, where)
  render(what, where)
},
render() {
  return html`
        <div>
          <p>FicusJS hello world</p>
        </div>
      `
  })
}
1
2
3
4
5
6
7
8
9
10
11
12

渲染结果是一模一样的,区别就是 what 就是 render 函数返回的数据,之前返回的是 html 节点,现在返回的是虚拟节点,然后在通过 render 方法转换成对应的节点,在挂载到页面上去。其他的渲染引擎框架也同理了。还有一些别的框架特性也自己去摸索了

# ficusjs - 事件派发 emit

派发事件用的是 this.emit(eventName,eventData)。接受 2 个参数(eventName 和 eventData)

  • eventName 派发事件的名称,到时候监听也是监听这个名称
  • eventData 派发出去的数据,是个对象类型。在对应事件的 e.detail 对象里面可以获取到对应的值

看下具体用法:

附上一个 demo





















 















 








 

















 





<body>
  <div id="app"></div>

  <hello-world></hello-world>
  <button id="btn">remove</button>

  <script type="module">
    import { html, renderer } from 'https://cdn.skypack.dev/@ficusjs/renderers@3/htm'
    import { createComponent } from 'https://cdn.skypack.dev/ficusjs@3/component'

    // 准备一个子组件
    createComponent('my-count', {
      renderer,
      state() {
        return {
          count: 0
        }
      },
      addCount() {
        this.state.count++
        this.emit('changeCount', { count: this.state.count })
      },
      render() {
        return html`
          <p>${this.state.count}</p>
          <button onclick="${this.addCount}">add count</button>
        `
      }
    })

    createComponent('hello-world', {
      renderer(what, where) {
        renderer(what, where)
      },
      handleClick() {
        this.state.count++
        this.emit('updatecount', { count: this.state.count, detail: { name: 'Jioho' } })
      },
      root: 'shadow',
      state() {
        return {
          count: 0
        }
      },
      childrenChange(e) {
        console.log('父组件监听到 my-count', e)
      },
      render() {
        return html`
          <div>
            <p>FicusJS hello world</p>
            <button type="button" onclick="${this.handleClick}">Click me</button>
            <div>${this.state.count}</div>
            <my-count onchangeCount="${this.childrenChange}"></my-count>
          </div>
        `
      }
    })

    document.getElementById('btn').addEventListener('click', function() {
      document.querySelector('hello-world').remove()
    })

    document.querySelector('hello-world').addEventListener('updatecount', function(e) {
      console.log('body 监听 hello-world', e)
    })
  </script>
</body>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68

运行效果和监听点击

说几个比较重要的:

  1. emit 事件后,如果是在框架内(createComponent 内部渲染的组件)可以直接用 on+事件名 进行监听
  2. 如果不是框架内,比如组件创建后就在 body 节点,这时候不能简单的在 <hello-world> 上使用 onupdatecount。这种是无效的
<!-- 无效监听 -->
<hello-world onupdatecount="updatecount"></hello-world>

<script>
  // 也不会触发这里
  function updatecount(e) {
    console.log(e)
  }

  // 有效绑定
  document.querySelector('hello-world').addEventListener('updatecount', function(e) {
    console.log('body 监听 hello-world', e)
  })
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# ficusjs - 插槽 Slots

组件化的时代,插槽是必不可少的特性之一

  • 默认插槽: this.slots.default
  • 具名插槽:this.slots.slotName (slotName) 为自定义名称
// 省略很多代码

return html`
  <div>${this.slots.default}</div>
  <div>${this.slots.button}</div>
`
1
2
3
4
5
6
<hello-world>
  <div>这些是给默认插槽的内容</div>
  <button slot="actions">插槽的按钮</button>
  <div>这些是给默认插槽的内容2</div>
</hello-world>
1
2
3
4
5

可以看到,slot=actions 是渲染到了指定的位置,其余的内容统一都归为了 default 的插槽,如果在组件中并没有使用 this.slots.default。那么另外的 2 个 div 写了也不会渲染


由于我平时工作多数都是用 vue,以下为个人观点进行的一些小试验和想法感受

  1. 吐槽 default 插槽

默认插槽中,把所有的换行符,空格,等都识别出来了。相比具名插槽,就是指定的 div,这控制起来会好很多。
默认插槽我还得循环判空等一系列操作,才能拿到里面的我可能想要的子节点

其次就是,哪怕我的默认插槽是空的,通过编辑器格式化后标签换行了,默认插槽又会有值~感觉这很容易误判,万一以后有开发者以为默认插槽的 [0] 就是他的节点,然后硬编码写了 this.slots.default[0].xxxx,然后下一个接手的人代码一格式化后标签换行了,那 bug 就很难排查了


  1. 插槽无法传值

有时候插槽里面也想获得组件内的一些数据,vue 的话提供了 slot-scope ,在这个框架想做这个操作并不是不行,但也没那么简单。

比如还是上图,拿到具名插槽后(千万别用默认插槽去做这件事,这种情况还是指定名称的好),使用 JS 原生方法,设置标签的属性值

render() {
  this.slots.actions.setAttribute('slot-scope', JSON.stringify({ couunt: this.state.count }))
  return html`
    <div>${this.slots.default}</div>
    <div>${this.slots.button}</div>
  `
}
1
2
3
4
5
6
7

如此一来,如果插槽的内容也是自定义的组件,那正好接上自身的 props 。如果不是的话,只能在组件的 updated 周期派发事件,通知外部的业务逻辑进行对应的操作了。


# 最后

不过比起html原生、web components 已经是一个非常大的飞跃了。ficusjs 也算是一个刚起步的阶段,这是我学习 ficusjs 的第一篇笔记,后面还有非常多有用的特性也会陆续更新~

希望 web components 能快点发展起来,ficusjs也快点成长起来,这样就再也不用考虑是 vue 还是 react 了~

Last Updated: 5/9/2021, 10:48:13 PM