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 裡,你可以使用 to-string-loader, 比如下面配置:
{
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