ficusjs系列(一) ficusjs入门使用

前端又双叒叕来新玩具了

ficusjs 系列

ficusjs

老规矩还是先介绍一下 ficusjs。 文档地址:docs-ficusjs

说 ficusjs 之前,就不得不提 Web Components

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

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

而 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" οnclick="${this.handleClick}">Click me</button>
        </div>
      `
    }
  })
</script>

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

引入

文档地址 https://docs.ficusjs.org/installation/

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>
  • 部分引入
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'

// 其余模块同理 ...

ficusjs - 使用 npm 构建

npm install ficusjs

剩下的引入就也类似了

import { createComponent, use } from 'ficusjs'

创建一个组件

从最基础的 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" οnclick="${this.handleClick}">Click me</button>
      </div>
    `
  }
})

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

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

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

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

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
  }
}
属性是否必填
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>

因为接收的并不是真正的 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>

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

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

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

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

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>
  • 第一次运行后效果

这次的触发是因为在 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}

Q1: 如何重新赋值?

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

  • 直接赋值

直接赋值弊端:

  1. 页面更新是异步更新的,如果后面的步骤依赖于赋值后的效果,那直接赋值不适合
  2. 直接赋值不能指定数组的某一项进行赋值(setState 也不行)
this.state.count++
  • 使用 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('渲染完成回调')
)

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')
  }
}

生命周期也没几个

名称触发时间
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" οnclick="${this.handleClick}">Click me</button>
          <div>${this.state.count}</div>
        </div>
      `)
      console.log('resolve end', new Date())
    }, 1000)
  })
}

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

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


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


removed 生命周期

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

ficusjs - renderer 函数

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

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

// 等同于

createComponent('hello-world', {
  renderer(what, where) {
    console.log(what, where)
    renderer(what, where)
  }
})

从截图也能看出,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>
      `
  })
}

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


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

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

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

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

【知识点来了】

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

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

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

ficusjs - Rendering 渲染方式

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

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

我就随便挑了一个 htm 来试下

引入还是一如既往的方便(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'

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

renderer(what, where) {
  console.log(what, where)
  render(what, where)
},
render() {
  return html`
        <div>
          <p>FicusJS hello world</p>
        </div>
      `
  })
}

渲染结果是一模一样的,区别就是 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 οnclick="${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" οnclick="${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. 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>

ficusjs - 插槽 Slots

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

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

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

可以看到,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>
  `
}

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


最后

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

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

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值