omi/tutorial/kbone.md

10 KiB
Raw Blame History

多端统一框架 kbone 深度体验 - 支持 Omi、React、Vue 和原生JS

kbone 是小程序官方出的多端统一框架,市面上就很多跨端开发框架,但是 kbone 是最彻底的一款。因为:

kbone 不仅可以开发小程序和 Web而且可以使用任意前端框架开发小程序和 Web。

阅读本文你可以收获:

  • Kbone 基础原理
  • 使用 preact + kbone 开发小程序 Counter
  • 使用 vue + kbone 开发小程序 Counter
  • 使用 omis + kbone 开发小程序 TodoApp
  • 使用 omis + kbone 开发小程序游戏贪吃蛇
  • 领域驱动设计在前端的集成
  • 理解 MVC、MVP、MVVM 模式
  • 使用 DOM 编写小程序游戏(并非小游戏)
  • 游戏主帧率和局部帧率控制

Kbone 基础原理

打开 Kbone 官方编译出的小程序的 index.js修改其中的代码

const mp = require('miniprogram-render')
const config = require('../../config')

function init(window, document) {
    require('../../common/vendors~index.js')(window, document);

    const ele = document.createElement('div')
    ele.innerHTML = 'Hello Kbone!'
    document.body.appendChild(ele)
}
...

运行效果如下:

上面的代码运行在小程序里。可以窥见其一二:

  • Kbone 实现了完整的 DOM/BOM 对象模型,即官方的 miniprogram-render
  • Kbone 允许 react、omi 和 vue 的完整 runtime 嵌入在小程序中

还有看不见的,比如:

  • Kbone 利用自定义组件渲染所有 DOM 节点
  • 自定义组件可以自引用来描述完整 DOM 树

该自定义组件就是官方封装的 miniprogram-element

{
  "usingComponents": {
		"element": "miniprogram-element"
	}
}
<element data-private-node-id="{{nodeId}}" data-private-page-id="{{pageId}}"></element>

pageId 和 nodeId 两个参数缺一不可,组件内部会根据传入的 pageId 找到对应的 window/document然后根据 nodeId 找到对应的 dom 节点进行渲染。 上面说了miniprogram-render 实现了轻量的 DOM 对象模型所以不管是框架还是原生js执行之后输出一些节点信息也算是虚拟 DOM比如嵌套的 childNodes。miniprogram-element 可以根据节点信息作为自定义组件的 data并且遍历生产 WXML 组件的节点树。

其中 v-dom 相当于数据(这里可能有点绕dom 作为 dom 渲染的数据,但事实就是如此) mp-element 相当于模板,数据+模板完成渲染。其中前面三个步骤都是运行在小程序逻辑层(JSCore)当中,使用逻辑层自己模拟出来的 DOM/BOM API也就是官方的 miniprogram-render。

实战 TodoApp

快速开始

npm i omi-cli -g
omi init-kbone my-app
cd my-app
npm start        //开发小程序
npm run web      //开发 web
npm run build    //发布 web

也支持一条命令 npx omi-cli init-kbone my-app (npm v5.2.0+)

目录说明

├─ build
│  ├─ mp     //微信开发者工具指向的目录,用于生产环境
│  ├─ web    //web 编译出的文件,用于生产环境
├─ config
├─ public
├─ scripts
├─ src
│  ├─ assets
│  ├─ components    //存放所有组件
│  ├─ log.js        //入口文件,会 build 成  log.html
│  └─ index.js      //入口文件,会 build 成  index.html

定义结构:

const Todo = (props, { clear, filter, textInput, inputText, todo, left, type, newTodo, done, toggle, deleteItem }) => {
  return (
    <div class="container">
      <div class="title">todos</div>
      <div class="form">
        <input class="new-todo" onInput={textInput} value={inputText} placeholder="下一步行动计划是?" autofocus=""></input>
        <button class="add-btn" onClick={newTodo}>确定</button>
      </div>

      <div class="todo-list">
        {todo.map(item => (
          (type === 'all' || (type === 'active' && !item.done) || (type === 'done' && item.done)) && <div class={`todo-item${item.done ? ' done' : ''}`}>
            <div class="toggle" data-id={item.id} onClick={toggle}></div>
            <text >{item.text} </text>
            <div class="delete" data-id={item.id} onClick={deleteItem}></div>
          </div>
        ))}
      </div>

      <TodoFooter onFilter={filter} onClear={clear} left={left} done={done} type={type} ></TodoFooter>
    </div>
  )
}

定义 store:


Todo.store = _ => {
  return {

    todo: [{ text: '学习 Kbone', id: 0 }, { text: '学习 Omi', id: 1 }],
    id: 1,
    inputText: '',
    left: 2,
    type: 'all',
    done: 0,


    textInput(evt) {
      this.inputText = evt.target.value
    },

    gotoAbout() {
      wx.navigateTo({
        url: '../about/index'
      })
    },

    toggle(evt) {
      for (let i = 0, len = this.todo.length; i < len; i++) {
        const item = this.todo[i]
        if (item.id === Number(evt.target.dataset.id)) {
          item.done = !item.done
          this.computeCount()
          _.update()
          break
        }
      }
    },

    computeCount() {
      this.left = 0
      this.done = 0
      for (let i = 0, len = this.todo.length; i < len; i++) {
        this.todo[i].done ? this.done++ : this.left++
      }
    },

    deleteItem(evt) {
      for (let i = 0, len = this.todo.length; i < len; i++) {
        const item = this.todo[i]
        if (item.id === Number(evt.target.dataset.id)) {
          this.todo.splice(i, 1)
          this.computeCount()
          _.update()
          break
        }
      }
    },

    newTodo() {
      if (this.inputText.trim() === '') {
        wx.showToast({
          title: '内容不能为空',
          icon: 'none',
          duration: 2000
        })

        return
      }

      this.todo.unshift({
        text: this.inputText,
        id: ++this.id,
        done: false,
        createTime: new Date()
      })
      this.computeCount()
      this.inputText = ''
      _.update()

    },

    filter(type) {
      //因为是自定义事件
      //注意这里的 this 指向,不能直接 this.type = type
      _.store.type = type
      _.update()
    },

    clear(evt) {
      //因为是自定义事件
      //注意这里的 this 指向
      const self = _.store
      wx.showModal({
        title: '提示',
        content: '确定清空已完成任务?',
        success: (res) => {
          if (res.confirm) {
            for (let i = 0, len = self.todo.length; i < len; i++) {
              const item = self.todo[i]
              if (item.done) {
                self.todo.splice(i, 1)
                len--
                i--
              }
            }
            self.done = 0
            _.update()

          } else if (res.cancel) {
            console.log('用户点击取消')
          }
        }
      })

    },

    gotoAbout() {
      wx.navigateTo({
        url: '../about/index'
      })
    },

    clickHandle() {
      if ("undefined" != typeof wx && wx.getSystemInfoSync) {
        wx.navigateTo({
          url: '../log/index?id=1'
        })
      } else {
        location.href = 'log.html'
      }
    }
  }
}

抽取中 todo-footer 组件:

import { h } from 'omis'
import './index.css'

const TodoFooter = ({ left, type, done }, { showAll, showActive, showDone, clearDone }) => {
  return <div class="footer">
    <div class="todo-count"><text class="strong">{left + ' '}items left</text> </div>
    <div class="filters">
      <div class='ib' onClick={showAll}>
        <text class={type === 'all' ? 'selected' : ''} >All</text>
      </div>
      <div class='ib' onClick={showActive}>
        <text class={type === 'active' ? 'selected' : ''} >Active</text>
      </div>
      <div class='ib' onClick={showDone}>
        <text class={type === 'done' ? 'selected' : ''} >Done</text>
      </div>
    </div>
    {done > 0 && <button class="clear-completed" onClick={clearDone}>Clear done</button>}
  </div>
}

TodoFooter.store = ({props})=> {
  return {
    showAll() {
      props.onFilter('all')
    },

    showActive() {
      props.onFilter('active')
    },

    showDone() {
      props.onFilter('done')
    },

    clearDone() {
      props.onClear()
    }
  }
}

export default TodoFooter

→ TodoApp 源码

实战贪吃蛇

参考和使用了部分 react-tetris 的样式。

→ 贪吃蛇源码

实战 preact Counter

实战 vue Counter

谁在使用 kbone

告诉我们

注意事项

  • 不要使用 bindtap使用 onClick
  • 图片请使用 cdn 地址或者 base64
  • 如果要兼容 web请用 HTML 和 CSS 标签,比如用 div不用 view不用 rpx 单位等

总览

  • Kbone 支持 Omi、React、Vue 和原生JS多端开发
  • Taro 支持 react 多端开发JSX 书写有约束
  • Alita 支持 React Native 转微信小程序JSX 书写无约束
  • uni-app 支持 vue 多端开发
  • mpvue 支持 vue 多端开发

未完待续..