腾讯发布新版前端组件框架 Omi,全面拥抱 Web Components
omi - 合一
下一代 web 框架,去万物糟粕,合精华为一
→ https://github.com/tencent/omi
特性
- 4kb 的代码尺寸,比小更小
- 顺势而为,顺从浏览器的发展和 api 设计
- webcomponents + jsx 相互融合为一个框架 omi
- webcomponents 也可以数据驱动视图, ui = fn(data)
- jsx 是开发体验最棒(智能提示)、的 ui 表达式
- 独创的 path updating 机制,基于 proxy 全自动化的精准更新,功耗低,*度高,性能卓越,方便集成 requestidlecallback
- 使用 store 系统不需要调用 this.udpate,它会自动化按需更新局部视图
- 看看facebook react 和 web components对比优势,omi 融合了各自的优点,而且给开发者*的选择喜爱的方式
- shadom dom 与 virtual dom 融合,omi 既使用了虚拟 dom,也是使用真实 shadom dom,让视图更新更准确更迅速
- 类似 westore 体系,99.9% 的项目不需要什么时间旅行,也不仅仅 redux 能时间旅行,请不要上来就 redux,omi store 体系可以满足所有项目
- 局部 css 最佳解决方案(shadow dom),社区为局部 css 折腾了不少框架和库(使用js或json写样式,如:radium,jsxstyle,react-style;与webpack绑定使用生成独特的classname
文件名—类名—hash值
,如:css modules,vue),都是 hack 技术;shadow dom style 是最完美的方案
对比同样开发 todoapp, omi 和 react 渲染完的 dom 结构:
左(上)边是omi,右(下)边是 react,omi 使用 shadow dom 隔离样式和语义化结构。
一个 html 完全上手
下面这个页面不需要任何构建工具就可以执行
<!doctype html> <html> <head> <meta charset="utf-8" /> <title>add omi in one minute</title> </head> <body> <script src="https://unpkg.com/omi"></script> <script> const { weelement, h, render, define } = omi class likebutton extends weelement { install() { this.data = { liked: false } } render() { if (this.data.liked) { return 'you liked this.' } return h( 'button', { onclick: () => { this.data.liked = true this.update() } }, 'like' ) } } define('like-button', likebutton) render(h('like-button'), 'body') </script> </body> </html>
getting started
install
$ npm i omi-cli -g # install cli $ omi init your_project_name # init project, you can also exec 'omi init' in an empty folder $ cd your_project_name # please ignore this command if you executed 'omi init' in an empty folder $ npm start # develop $ npm run build # release
cli 自动创建的项目脚手架是基于单页的 create-react-app 改造成多页的,有配置方面的问题可以查看 create-react-app 用户指南。
hello element
先创建一个自定义元素:
import { tag, weelement, render } from 'omi' @tag('hello-element') class helloelement extends weelement { onclick = (evt) => { //trigger customevent this.fire('abc', { name : 'dntzhang', age: 12 }) evt.stoppropagation() } css() { return ` div{ color: red; cursor: pointer; }` } render(props) { return ( <div onclick={this.onclick}> hello {props.msg} {props.propfromparent} <div>click me!</div> </div> ) } }
使用该元素:
import { tag, weelement, render } from 'omi' import './hello-element' @tag('my-app') class myapp extends weelement { static get data() { return { abc: '', passtochild: '' } } //bind customevent onabc = (evt) => { // get evt data by evt.detail this.data.abc = ' by ' + evt.detail.name this.update() } css() { return ` div{ color: green; }` } render(props, data) { return ( <div> hello {props.name} {data.abc} <hello-element onabc={this.onabc} prop-from-parent={data.passtochild} msg="weelement"></hello-element> </div> ) } } render(<my-app name='omi v4.0'></my-app>, 'body')
告诉 babel 把 jsx 转化成 omi.h() 的调用:
{ "presets": ["env", "omi"] }
需要安装下面两个 npm 包支持上面的配置:
"babel-preset-env": "^1.6.0", "babel-preset-omi": "^0.1.1",
如果不想把 css 写在 js 里,你可以使用 , 比如下面配置:
{ test: /[\\|\/]_[\s]*\.css$/, use: [ 'to-string-loader', 'css-loader' ] }
如果你的 css 文件以 _
开头, css 会使用 to-string-loader. 如:
import { tag, weelement render } from 'omi' //typeof cssstr is string import cssstr from './_index.css' @tag('my-app') class myapp extends weelement { css() { return cssstr } ... ... ...
todoapp
下面列举一个相对完整的 todoapp 的例子:
import { tag, weelement, render } from 'omi' @tag('todo-list') class todolist extends weelement { render(props) { return ( <ul> {props.items.map(item => ( <li key={item.id}>{item.text}</li> ))} </ul> ); } } @tag('todo-app') class todoapp extends weelement { static get data() { return { items: [], text: '' } } render() { return ( <div> <h3>todo</h3> <todo-list items={this.data.items} /> <form onsubmit={this.handlesubmit}> <input id="new-todo" onchange={this.handlechange} value={this.data.text} /> <button> add #{this.data.items.length + 1} </button> </form> </div> ); } handlechange = (e) => { this.data.text = e.target.value } handlesubmit = (e) => { e.preventdefault(); if (!this.data.text.trim().length) { return; } this.data.items.push({ text: this.data.text, id: date.now() }) this.data.text = '' } } render(<todo-app></todo-app>, 'body')
store
使用 store 体系可以告别 update 方法,基于 proxy 的全自动属性追踪和更新机制。强大的 store 体系是高性能的原因,除了靠 props 决定组件状态的组件,其余组件所有 data 都挂载在 store 上,
export default { data: { items: [], text: '', firstname: 'dnt', lastname: 'zhang', fullname: function () { return this.firstname + this.lastname }, globalproptest: 'abc', //更改我会刷新所有页面,不需要再组件和页面声明data依赖 ccc: { ddd: 1 } //更改我会刷新所有页面,不需要再组件和页面声明data依赖 }, globaldata: ['globalproptest', 'ccc.ddd'], add: function () { if (!this.data.text.trim().length) { return; } this.data.items.push({ text: this.data.text, id: date.now() }) this.data.text = '' } //默认 false,为 true 会无脑更新所有实例 //updateall: true }
自定义 element 需要声明依赖的 data,这样 omi store 根据自定义组件上声明的 data 计算依赖 path 并会按需局部更新。如:
class todoapp extends weelement { static get data() { //如果你用了 store,这个只是用来声明依赖,按需 path updating return { items: [], text: '' } } ... ... ... handlechange = (e) => { this.store.data.text = e.target.value } handlesubmit = (e) => { e.preventdefault() this.store.add() } }
- 数据的逻辑都封装在了 store 定义的方法里 (如 store.add)
- 视图只负责传递数据给 store (如上面调用 store.add 或设置 store.data.text)
需要在 render 的时候从根节点注入 store 才能在所有自定义 element 里使用 this.store:
render(<todo-app></todo-app>, 'body', store)
总结一下:
- store.data 用来列出所有属性和默认值(除去 props 决定的视图的组件)
- 组件和页面的 data 用来列出依赖的 store.data 的属性 (omi会记录path),按需更新
- 如果页面简单组件很少,可以 updateall 设置成 true,并且组件和页面不需要声明 data,也就不会按需更新
- globaldata 里声明的 path,只要修改了对应 path 的值,就会刷新所有页面和组件,globaldata 可以用来列出所有页面或大部分公共的属性 path
文档
my first element
import { weelement, tag, render } from 'omi' @tag('my-first-element') class myfirstelement extends weelement { render() { return ( <h1>hello, world!</h1> ) } } render(<my-first-element></my-first-element>, 'body')
在 html 开发者工具里看看渲染得到的结构:
除了渲染到 body,你可以在其他任意自定义元素中使用 my-first-element
。
props
import { weelement, tag, render } from 'omi' @tag('my-first-element') class myfirstelement extends weelement { render(props) { return ( <h1>hello, {props.name}!</h1> ) } } render(<my-first-element name="world"></my-first-element>, 'body')
你也可以传任意类型的数据给 props:
import { weelement, tag, render } from 'omi' @tag('my-first-element') class myfirstelement extends weelement { render(props) { return ( <h1>hello, {props.myobj.name}!</h1> ) } } render(<my-first-element my-obj={{ name: 'world' }}></my-first-element>, 'body')
my-obj
将映射到 myobj,驼峰的方式。
event
class myfirstelement extends weelement { onclick = (evt) => { alert('hello omi!') } render() { return ( <h1 onclick={this.onclick}>hello, wrold!</h1> ) } }
custom event
@tag('my-first-element') class myfirstelement extends weelement { onclick = (evt) => { this.fire('myevent', { name: 'abc' }) } render(props) { return ( <h1 onclick={this.onclick}>hello, world!</h1> ) } } render(<my-first-element onmyevent={(evt) => { alert(evt.detail.name) }}></my-first-element>, 'body')
通过 this.fire
触发自定义事件,fire 第一个参数是事件名称,第二个参数是传递的数据。通过 evt.detail
可以获取到传递的数据。
ref
@tag('my-first-element') class myfirstelement extends weelement { onclick = (evt) => { console.log(this.h1) } render(props) { return ( <div> <h1 ref={e => { this.h1 = e }} onclick={this.onclick}>hello, world!</h1> </div> ) } } render(<my-first-element></my-first-element>, 'body')
在元素上添加 ref={e => { this.anynameyouwant = e }}
,然后你就可以 js 代码里使用 this.anynameyouwant
访问该元素。
store system
import { weelement, tag, render } from 'omi' @tag('my-first-element') class myfirstelement extends weelement { //you must declare data here for view updating static get data() { return { name: null } } onclick = () => { //auto update the view this.store.data.name = 'abc' } render(props, data) { //data === this.store.data when using store stystem return ( <h1 onclick={this.onclick}>hello, {data.name}!</h1> ) } } const store = { data: { name: 'omi' } } render(<my-first-element name="world"></my-first-element>, 'body', store)
当使用 store 体系是,static get data
就仅仅被用来声明依赖,举个例子:
static get data() { return { a: null, b: null, c: { d: [] }, e: [] } }
会被转换成:
{ a: true, b: true, 'c.d':true, e: true }
举例说明 path 命中规则:
diffresult | updatepath | 是否更新 |
---|---|---|
abc | abc | 更新 |
abc[1] | abc | 更新 |
abc.a | abc | 更新 |
abc | abc.a | 不更新 |
abc | abc[1] | 不更新 |
abc | abc[1].c | 不更新 |
abc.b | abc.b | 更新 |
以上只要命中一个条件就可以进行更新!
总结就是只要等于 updatepath 或者在 updatepath 子节点下都进行更新!
看可以看到 store 体系是中心化的体系?那么怎么做到部分组件去中心化?使用 tag 的第二个参数:
@tag('my-first-element', true)
纯元素!不会注入 store!
生命周期
lifecycle method | when it gets called |
---|---|
install |
before the component gets mounted to the dom |
installed |
after the component gets mounted to the dom |
uninstall |
prior to removal from the dom |
beforeupdate |
before render()
|
afterupdate |
after render()
|
生态
在里面查找你想要的组件,直接使用,或者花几分钟就能转换成 omi element(把模板拷贝到 render 方法,style拷贝到 css 方法)。
浏览器兼容
omi 4.0+ works in the latest two versions of all major browsers: safari 10+, ie 11+, and the evergreen chrome, firefox, and edge.
由于需要使用 proxy 的原因,放弃ie!
star & fork
license
mit © tencent
上一篇: input 属性为 number,maxlength不起作用如何解决?
下一篇: 校园里飘出滴爆笑声