让 JSX, Web Components, Proxy, Store, Path Updating 在一起

  • 小巧的尺寸(gzip压缩后仅4kb)
  • 真正的 , 拥有 mappingjs 强力支持
  • 支持
  • 响应式数据绑定
  • 增强了 CSS, ,基于 750 屏幕宽度
  • 基于 Shadow Dom 设计
  • 利用轻松调试,从 Chrome 应用商店安装
  • 符合浏览器的发展趋势以及API设计理念
  • + JSX 相互融合为一个框架 Omi
  • 内置 observe 制作响应式视图(免去 this.update)
  • Web Components 也可以数据驱动视图, UI = fn(data)
  • JSX 是开发体验最棒(智能提示)、、图灵完备的 UI 表达式,模板引擎不完备,模板字符串完备但是语法噪音太大
  • 独创的 Path Updating 机制,基于 Proxy 全自动化的精准更新,功耗低,自由度高,性能卓越,方便集成 requestIdleCallback
  • 对 this.update 说再见吧!只要使用 store 系统,它就会自动化按需更新局部视图
  • 看看Facebook React 和 Web Components对比优势,Omi 融合了各自的优点,而且给开发者自由的选择喜爱的方式
  • Shadow DOMVirtual DOM 融合,Omi 既使用了虚拟 DOM,也是使用真实 Shadow DOM,让视图更新更准确更迅速
  • 99.9% 的项目不需要什么时间旅行,也不需要时间旅行调试(Time travel debugging),而且也不仅仅 redux 能时间旅行,请不要上来就 redux,Omi store 系统可以满足所有项目。
  • 局部 CSS 最佳解决方案(Shadow DOM),社区为局部 CSS 折腾了不少框架和库(使用js或json写样式,如:Radiumjsxstylereact-style;与webpack绑定使用生成独特的className文件名—类名—hash值,如:CSS ModulesVue),还有运行时注入scoped atrr 的方式,都是 hack 技术;Shadow DOM Style 是最完美的方案

对比同样开发 TodoApp, Omi 和 React 渲染完的 DOM 结构,Omi 使用 Shadow DOM 隔离样式和语义化结构:

Omi 生态

项目 描述
omi-docs Omi 官方文档
MVVM 王者归来, mappingjs 强力加持。
Using htm in omi.
30 秒理解一段有用的 Omi 代码片段.
omi-canvas Web Components, JSX 和 Canvas 的完美融合
通过微信小程序开发和生成 Web 单页应用(H5 SPA)
omi-router Omi 官方路由。
omi-devtools 谷歌浏览器开发工具扩展
项目脚手架工具,支持 Javascript 和 Typescript
omi-transform Omi 和 完美结合. 让 css3 transform 在你的 Omi项目中变得超级简单.
omi-tap2 Omi 原生支持 tap 事件(omi v4.0.24+)
让 Omi 项目轻松支持 tap 事件
omi-finger Omi 官方手势库
丝般顺滑的触摸运动
omi-mobx Omi Mobx 适配器
跟 React hooks 类似的方式定义纯组件
omi-native 把 web components 渲染到 native,比如 IOS 、Android
小程序解决方案 westore,与 Omi 互相启发
omi-weui for Omi by @132yse.
Omi 国际化解决方案
omi-page 基于 的 Omi 路由

omi-mp

看下官方模板的转换例子:

因为 Web 里拉取不到用户登录态,更换了用户头像和名称。

必须收藏的资源


目录

一个 HTML 完全上手

下面这个页面不需要任何构建工具就可以执行

通过上面脚本的执行,你已经定义好了一个自定义标签,可以不使用 render 方法,直接使用 like-button 标签:

  1. <body>
  2. <like-button></like-button>
  3. </body>

你也可以使用现代化的 JS 语法,快速构建 Omi 项目:

  1. import { render, WeElement, tag, observe } from "omi"
  2. @observe
  3. @tag("my-counter")
  4. class MyCounter extends WeElement {
  5. data = {
  6. count: 0
  7. }
  8. sub = () => {
  9. this.data.count--
  10. }
  11. add = () => {
  12. this.data.count++
  13. }
  14. render() {
  15. return (
  16. <div>
  17. <button onClick={this.sub}>-</button>
  18. <span>{this.data.count}</span>
  19. <button onClick={this.add}>+</button>
  20. </div>
  21. )
  22. }
  23. }
  24. render(<my-counter />, "body")

你会发现 MyCounter 从未使用过,所以你可以使用下面代码达到同样效果并且避免 Eslint 提示错误:

  1. import { render, WeElement, define } from 'omi'
  2. define('my-counter', class extends WeElement {
  3. static observe = true
  4. data = {
  5. count: 1
  6. }
  7. sub = () => {
  8. this.data.count--
  9. }
  10. add = () => {
  11. this.data.count++
  12. }
  13. render() {
  14. return (
  15. <div>
  16. <button onClick={this.sub}>-</button>
  17. <span>{this.data.count}</span>
  18. <button onClick={this.add}>+</button>
  19. </div>
  20. )
  21. }
  22. })
  23. render(<my-counter />, 'body')

你也可以定义成纯函数的形式:

  1. import { define, render } from 'omi'
  2. define('my-counter', function() {
  3. const [count, setCount] = this.use({
  4. data: 0,
  5. effect: function() {
  6. document.title = `The num is ${this.data}.`
  7. }
  8. })
  9. this.useCss(`button{ color: red; }`)
  10. return (
  11. <div>
  12. <button onClick={() => setCount(count - 1)}>-</button>
  13. <span>{count}</span>
  14. <button onClick={() => setCount(count + 1)}>+</button>
  15. </div>
  16. )
  17. })
  18. render(<my-counter />, 'body')

如果你不需要 effect 方法, 可以直接使用 useData:

  1. const [count, setCount] = this.useData(0)

快速入门

安装

  1. $ npm i omi-cli -g # install cli
  2. $ omi init my-app # init project, you can also exec 'omi init' in an empty folder
  3. $ cd my-app # please ignore this command if you executed 'omi init' in an empty folder
  4. $ npm start # develop
  5. $ npm run build # release

目录说明:

  1. ├─ config
  2. ├─ public
  3. ├─ scripts
  4. ├─ src
  5. ├─ assets
  6. ├─ elements //存放所有 custom elements
  7. ├─ store //存放所有页面的 store
  8. ├─ admin.js //入口文件,会 build 成 admin.html
  9. └─ index.js //入口文件,会 build 成 index.html

比如在 windows 下:

在 mac os 中:

  1. "scripts": {
  2. "_build": "node scripts/build.js",
  3. "build":"PUBLIC_URL=https://fe.wxpay.oa.com/dv npm run _build",
  4. "fix": "eslint src --fix"
  5. },

项目模板

Template Type Command Describe
Base Template omi init my-app 基础模板
TypeScript Template(omi-cli v3.0.5+) omi init-ts my-app 使用 TypeScript 的模板
(omi-cli v3.0.10+) omi init-spa my-app 使用 omi-router 单页应用的模板
omi-mp Template(omi-cli v3.0.13+) omi init-mp my-app 小程序开发 Web 的模板
MVVM Template(omi-cli v3.0.22+) omi init-mvvm my-app MVVM 模板

Cli 自动创建的项目脚手架是基于单页的 create-react-app 改造成多页的,有配置方面的问题可以查看 create-react-app 用户指南

先创建一个自定义元素:

  1. import { define, WeElement } from 'omi'
  2. define('hello-element', class extends WeElement {
  3. onClick = evt => {
  4. // trigger CustomEvent
  5. this.fire('abc', { name: 'dntzhang', age: 12 })
  6. evt.stopPropagation()
  7. }
  8. css() {
  9. return `
  10. div {
  11. color: red;
  12. cursor: pointer;
  13. }`
  14. }
  15. render(props) {
  16. return (
  17. <div onClick={this.onClick}>
  18. Hello {props.msg} {props.propFromParent}
  19. <div>Click Me!</div>
  20. </div>
  21. )
  22. }
  23. })

使用该元素:

  1. import { define, render, WeElement } from 'omi'
  2. import './hello-element'
  3. define('my-app', class extends WeElement {
  4. data = { abc: 'abc', passToChild: 123 }
  5. // define CustomEvent Handler
  6. onAbc = evt => {
  7. // get evt data by evt.detail
  8. this.data.abc = ' by ' + evt.detail.name
  9. this.data.passToChild = 1234
  10. this.update()
  11. }
  12. css() {
  13. return `
  14. div{
  15. color: green;
  16. }`
  17. }
  18. render(props, data) {
  19. return (
  20. <div>
  21. Hello {props.name} {data.abc}
  22. <hello-element
  23. onAbc={this.onAbc}
  24. prop-from-parent={data.passToChild}
  25. msg="WeElement"
  26. />
  27. </div>
  28. )
  29. }
  30. })
  31. render(<my-app name="Omi v4.0" />, 'body')

告诉 Babel 把 JSX 转化成 Omi.h() 的调用:

  1. {
  2. "presets": ["env", "omi"]
  3. }

需要安装下面两个 npm 包支持上面的配置:

  1. "babel-preset-env": "^1.6.0",
  2. "babel-preset-omi": "^0.1.1",

如果你使用 babel7,也可以使用如下包和配置:

  1. npm install --save-dev @babel/preset-env
  2. npm install --save-dev @babel/preset-react
  1. {
  2. "presets": [
  3. "@babel/preset-env",
  4. [
  5. "@babel/preset-react",
  6. {
  7. "pragma": "Omi.h",
  8. }
  9. ]
  10. ]
  11. }

如果不想把 css 写在 js 里,你可以使用 webpack , 比如下面配置:

如果你的 css 文件以 _ 开头, css 会使用 to-string-loader. 如:

  1. import { tag, WeElement render } from 'omi'
  2. //typeof cssStr is string
  3. import cssStr from './_index.css'
  4. @tag('my-app')
  5. class MyApp extends WeElement {
  6. css() {
  7. return cssStr
  8. }
  9. ...
  10. ...
  11. ...

你也可以忘掉这一对繁琐的配置直接使用 omi-cli,不需要你配置任何东西。

TodoApp

下面列举一个相对完整的 TodoApp 的例子:

  1. import { define, render, WeElement } from 'omi'
  2. define('todo-list', function(props) {
  3. return (
  4. <ul>
  5. {props.items.map(item => (
  6. <li key={item.id}>{item.text}</li>
  7. ))}
  8. </ul>
  9. )
  10. define('todo-app', class extends WeElement {
  11. static observe = true
  12. data = { items: [], text: '' }
  13. render() {
  14. return (
  15. <div>
  16. <h3>TODO</h3>
  17. <todo-list items={this.data.items} />
  18. <input
  19. id="new-todo"
  20. onChange={this.handleChange}
  21. value={this.data.text}
  22. />
  23. <button>Add #{this.data.items.length + 1}</button>
  24. </form>
  25. </div>
  26. )
  27. }
  28. handleChange = e => {
  29. this.data.text = e.target.value
  30. }
  31. handleSubmit = e => {
  32. e.preventDefault()
  33. if (!this.data.text.trim().length) {
  34. return
  35. }
  36. this.data.items.push({
  37. text: this.data.text,
  38. id: Date.now()
  39. })
  40. this.data.text = ''
  41. }
  42. })
  43. render(<todo-app />, 'body')

Store

使用 Store 体系可以告别 update 方法,基于 Proxy 的全自动属性追踪和更新机制。强大的 Store 体系是高性能的原因,除了靠 props 决定组件状态的组件,其余组件所有 data 都挂载在 store 上,

  1. export default {
  2. data: {
  3. items: [],
  4. text: '',
  5. firstName: 'dnt',
  6. lastName: 'zhang',
  7. fullName: function () {
  8. return this.firstName + this.lastName
  9. },
  10. globalPropTest: 'abc', //更改我会刷新所有页面,不需要再组件和页面声明data依赖
  11. ccc: { ddd: 1 } //更改我会刷新所有页面,不需要再组件和页面声明data依赖
  12. },
  13. globalData: ['globalPropTest', 'ccc.ddd'],
  14. add: function () {
  15. if (!this.data.text.trim().length) {
  16. return;
  17. }
  18. this.data.items.push({
  19. text: this.data.text,
  20. id: Date.now()
  21. })
  22. this.data.text = ''
  23. }
  24. //默认 false,为 true 会无脑更新所有实例
  25. //updateAll: true
  26. }

自定义 Element 需要声明依赖的 data,这样 Omi store 根据自定义组件上声明的 data 计算依赖 path 并会按需局部更新。如:

  1. define('todo-app', class extends WeElement {
  2. static get data() {
  3. //如果你用了 store,这个只是用来声明依赖,按需 Path Updating
  4. return { items: [], text: '' }
  5. }
  6. ...
  7. ...
  8. ...
  9. handleChange = (e) => {
  10. this.store.data.text = e.target.value
  11. }
  12. handleSubmit = (e) => {
  13. e.preventDefault()
  14. this.store.add()
  15. }
  16. })
  • 数据的逻辑都封装在了 store 定义的方法里 (如 store.add)
  • 视图只负责传递数据给 store (如上面调用 store.add 或设置 store.data.text)

需要在 render 的时候从根节点注入 store 才能在所有自定义 Element 里使用 this.store:

  1. render(<todo-app></todo-app>, 'body', store)

总结一下:

  • store.data 用来列出所有属性和默认值(除去 props 决定的视图的组件)
  • 组件和页面的 data 用来列出依赖的 store.data 的属性 (omi会记录path),按需更新
  • 如果页面简单组件很少,可以 updateAll 设置成 true,并且组件和页面不需要声明 data,也就不会按需更新
  • globalData 里声明的 path,只要修改了对应 path 的值,就会刷新所有页面和组件,globalData 可以用来列出所有页面或大部分公共的属性 Path

Mitt

如果不想使用 store 的 data 体系,也可以使用发布订阅模式。比如在 Omi 中使用 跨组件通讯:

Observe

你可以为那些不需要 store 的自定义元素使用 observe 创建响应式视图,比如:

  1. import { define, WeElement } from "omi"
  2. define("my-app", class extends WeElement {
  3. static observe = true
  4. install() {
  5. this.data.name = "omi"
  6. }
  7. onClick = () => {
  8. this.data.name = "Omi V4.0"
  9. }
  10. render(props, data) {
  11. return (
  12. <div onClick={this.onClick}>
  13. <h1>Welcome to {data.name}</h1>
  14. </div>
  15. )
  16. }
  17. })

如果你想要兼容 IE11,请使用 omi-mobx 代替 omi 自带的 observe,往下看..

Omi Mobx

  1. import { tag, WeElement } from "omi"
  2. import { observe } from "omi-mobx"
  3. @observe
  4. @tag("my-app")
  5. class MyApp extends WeElement {
  6. install() {
  7. this.data.name = "omi"
  8. }
  9. onClick = () => {
  10. this.data.name = "Omi V4.0"
  11. }
  12. render(props, data) {
  13. return (
  14. <div onClick={this.onClick}>
  15. <h1>Welcome to {data.name}</h1>
  16. </div>
  17. )
  18. }
  19. }

生命周期

调试工具

使用 可以非常简单地调试和管理你的 UI。不需要任何配置,你只要安装然后就能调试。

既然 Omi 使用了 Web Components 和 Shadow-DOM, 所以不需要像 React 和 Vue 一样安装其他元素面板,只需要使用 Chrome 自带的 Elements’ sidebar 便可,它和 React and Vue 开发者工具一样强大。

Omi DevTools

举个例子,下面是吧 weui react 的 button 转成 weui omi 的 button 的例子 :

浏览器兼容

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

贡献者们


README - 图14README - 图16
README - 图18README - 图20README - 图22
README - 图24README - 图26
README - 图28README - 图30README - 图32
README - 图34README - 图36
README - 图38README - 图40README - 图42
README - 图44README - 图46

问答

感谢

MIT © Tencent