騰訊發佈新版前端組件框架 Omi,全面擁抱 Web Components

Omi - 合一

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

omi

→ https://github.com/Tencent/omihtml

特性

  • 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 結構:react

左(上)邊是Omi,右(下)邊是 React,Omi 使用 Shadow DOM 隔離樣式和語義化結構。webpack


一個 HTML 徹底上手

下面這個頁面不須要任何構建工具就能夠執行git

<!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 用戶指南github

Hello Element

先建立一個自定義元素:web

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

使用該元素:npm

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() 的調用:json

{
    "presets": ["env", "omi"]
}

須要安裝下面兩個 npm 包支持上面的配置:redux

"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 開發者工具裏看看渲染獲得的結構:

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

相關文章
相關標籤/搜索