騰訊釋出新版前端元件框架 Omi,全面擁抱 Web Components

【當耐特】發表於2018-10-18

Omi - 合一

下一代 Web 框架,去萬物糟粕,合精華為一

omi

→ 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,全面擁抱 Web Components 騰訊釋出新版前端元件框架 Omi,全面擁抱 Web Components

左(上)邊是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 完整的程式碼

總結一下:

  • 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 開發者工具裡看看渲染得到的結構:

騰訊釋出新版前端元件框架 Omi,全面擁抱 Web Components

除了渲染到 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.

Browsers Support

→ polyfills

由於需要使用 Proxy 的原因,放棄IE!

Star & Fork

License

MIT © Tencent

相關文章