前端又双叒叕来新玩具了
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 的响应的函数 |
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
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 | 是 | 这必须是字符串、数字、布尔值或对象中的一种(String 、Number 、Boolean 、Object ) |
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
方法
- 直接赋值
直接赋值弊端:
- 页面更新是异步更新的,如果后面的步骤依赖于赋值后的效果,那直接赋值不适合
- 直接赋值不能指定数组的某一项进行赋值(setState 也不行)
this.state.count++
- 使用 setState 函数
setState 函数写法比较奇特,和小程序和 react 类似,但不能说一样~
- 支持赋值后的回调
- return 函数中只需要返回需要更新的值即可,无须返回整个 state
- 注意第一个参数必须是一个函数,函数中的返回值才是需要更新的内容
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
的执行也是在 created
和 mounted
之间,并且在 render
函数返回之后才执行
每次更新组件的值的执行顺序:render函数
-> renderer函数
-> updated
说一说 renderer 回调函数的 2 个值,和 renderer 方法
注意区分下面哪个说的是回调函数
。哪个是renderer方法
ficusjs - 回调函数回调了 2 个参数what
和where
- what 是
render
函数处理完返回的 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>
运行效果和监听点击
说几个比较重要的:
- emit 事件后,如果是在框架内(createComponent 内部渲染的组件)可以直接用
on
+事件名 进行监听 - 如果不是框架内,比如组件创建后就在 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,以下为个人观点进行的一些小试验和想法感受
- 吐槽 default 插槽
默认插槽中,把所有的换行符,空格,等都识别出来了。相比具名插槽
,就是指定的 div,这控制起来会好很多。
默认插槽我还得循环判空等一系列操作,才能拿到里面的我可能想要的子节点
其次就是,哪怕我的默认插槽是空的,通过编辑器格式化后标签换行了,默认插槽又会有值~感觉这很容易误判,万一以后有开发者以为默认插槽的 [0] 就是他的节点,然后硬编码写了 this.slots.default[0].xxxx
,然后下一个接手的人代码一格式化后标签换行了,那 bug 就很难排查了
- 插槽无法传值
有时候插槽里面也想获得组件内的一些数据,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 了~