- 版本:0.1.3
- GitHub:https://github.com/SudoMaker/dominative
- NPM:https://npmjs.net.cn/package/dominative
- 下载
- 前一天:4
- 上周:78
- 上个月:147
DOMiNATIVE
下一代,为性能而构建的最小可行 DOM 文档实现,适用于 NativeScript。
免责声明
缺乏维护并不意味着项目已过时。相反,这表明代码质量非常高,无需持续维护。
警告
由于该项目专注于性能,其标准实现不包括对 innerHTML
的支持。在浏览器中,innerHTML
允许通过原生代码高效解析 XML 并生成 DOM 树,这要快得多。然而,在 JavaScript 模拟的 DOM 中,使用 innerHTML
成为一个昂贵的操作。它涉及在 JavaScript 中解析 XML 字符串并创建相关标签,导致在 JavaScript 中递归创建原生视图的实际实例。与直接使用 createElement
相比,这个过程要慢得多。
对于那些需要出于特定原因使用 innerHTML
的人,有关 "innerHTML" 的更多信息可在 "innerHTML" 部分找到。
我建议框架开发者考虑采用不依赖于 innerHTML
或 cloneNode
的节点创建方法。这个建议特别相关,因为相同的 DOM 实现 (undom-ng) 可能会在不同的环境中使用,例如资源有限的嵌入式设备——特别是那些可用内存少于 8MB 且 CPU 频率低于 500MHz 的设备。在这种情况下,集成 XML 解析器可能不是最有效的方法。
安装
通过 npm
npm install dominative undom-ng
注意: undom-ng
是一个同级依赖项,您必须手动安装。
注意: 包名是 dominative
,而不是 nativescript-dom-ng
。请安装 dominative
。
用法
纯
app.js
import { Application } from '@nativescript/core'
import { document } from 'dominative'
const page = document.body
const actionBar = document.createElement('ActionBar')
actionBar.title = 'Hello World!'
page.appendChild(actionBar)
Application.run({
create: () => document
})
使用 ef.js
App.eft
>ActionBar
#title = Hello World!
>ActionBarItem
#text = Button
>StackLayout
>Label
.Welcome to the wonderland of ef.native!
app.js
import { Application } from '@nativescript/core'
import { domImpl, document } from 'dominative'
import { setDOMImpl } from 'ef-core'
import App from 'App.eft'
setDOMImpl(domImpl)
const app = new App()
app.$mount({target: document.body})
Application.run({
create: () => document
})
使用 SingUI
app.js
import { Application } from '@nativescript/core'
import { document } from 'dominative'
import { browser, prop, setGlobalCtx, useTags, useElement, build } from 'singui'
setGlobalCtx(browser(document))
const tags = useTags(false)
const app = (target) =>
build(({attach}) => {
const { ActionBar, NavigationButton, ActionItem, StackLayout, Label, Button } = tags
ActionBar(() => {
prop.title = 'Hello World!'
})
StackLayout(() => {
let count = 0
const {ret: updateText} = Label(() => {
return text().$textContent(
() => `You have tapped ${count} time${count === 1 ? '' : 's'}`
)
})
Button(() => {
prop.text = 'Tap me!'
on('tap', () => {
count += 1
updateText()
})
})
updateText()
})
attach(target)
})
app(document.body)
Application.run({
create: () => {
return document
},
})
使用 React + react-dom
游乐场 - 由 Ammar Ahmed
注意: 如果需要,此演示可能存在一些与 Chrome 相关的问题。使用 Firefox。
使用 Vue 3 + runtime-dom + DOMiNATIVE-Vue
app.js
import { Application } from '@nativescript/core'
import { createApp } from '@dominative/vue'
import App from './App.vue'
const app = createApp(App)
app.$run()
使用 SolidJS + DOMiNATIVE-Solid
app.jsx
import { Application } from "@nativescript/core"
import { render } from "@dominative/solid"
import { createSignal } from "solid-js"
document.body.actionbarHidden = false
const App = () => {
const [count, setCount] = createSignal(0)
const increment = () => {
setCount(c => c + 1)
}
return <>
<actionbar title="Hello, SolidJS!"></actionbar>
<stacklayout>
<label>You have taapped {count()} time(s)</label>
<button class="-primary" on:tap={increment}>Tap me!</button>
</stacklayout>
</>
}
render(App, document.body)
const create = () => document
Application.run({ create })
准备全局环境
自动全局注册 document
、window
和相关变量
import { globalRegister } from 'dominative'
globalRegister(global)
注册元素
import { RadSideDrawer } from 'nativescript-ui-sidedrawer'
import { RadListView } from 'nativescript-ui-listview'
import { registerElement, makeListView } from 'dominative'
// If you cannot determin what the component is based on, you can register it directly.
registerElement('RadSideDrawer', RadSideDrawer)
// Register with a specific type by using a pre-defined maker. Usually we check for inheritance, but with force we can make magic happen
registerElement('RadListView', makeListView(RadListView, {force: true}))
虚拟元素
虚拟元素不是真实元素,但它们在组织组合时表现为 DOM 元素。
属性
通过键将子/子元素放入其父节点属性的帮助程序
属性
key: String
:在父节点上设置的属性名。(可读写)
type: <'array'|'key'>
:属性类型,可以是数组属性或单个对象属性。设置后,此属性无法更改。(可读写)
value: any
:要设置的父节点值。通常是当前节点的子节点。除非您知道自己在做什么,否则不要触摸。
parent: Element
: R 该节点的父节点。
class: String
: RW 设置 key
和 type
的辅助函数,可以是 key:type
或 multi.level.key:type
事件
无。
KeyProp
Prop
但 type
已设置为 key
。
ArrayProp
Prop
但 type
已设置为 array
。
ItemTemplate
* Template
已重命名为 ItemTemplate
以避免与 HTML template
标签冲突。
ItemTemplate
元素包含稍后要复制的模板,或者可以程序化创建视图。
属性
主要从 Prop
中共享。以下列出差异
key: String
: RW 与 Prop
形式相同,也用作 KeyedTemplate
的键名。默认为 itemTemplate
。
type: 'single'
: R 不应该能够设置 ItemTemplate
上的 type
。
value: Function
: R 与 createView
相同。
content: <T extends ViewBase>
: RW 该节点的单个子节点。除非你知道自己在做什么,否则不要触碰。
patch: Function
: R 修补现有克隆的方法。
createView: Function
: R 从此模板创建视图的函数。
事件
itemLoading
: 当修补且模板没有内容时触发。将 event.view
设置为更改此项目的视图。事件 event
上的额外属性:view
、index
、item
、data
。此事件的回调参数不扩展自 NativeScript 的数据对象。
createView
: 当从模板创建视图且模板没有内容时触发。将创建的视图设置为 event.view
。如果没有设置,视图将通过克隆模板创建。此事件的回调参数不扩展自 NativeScript 的数据对象。
注意
ItemTemplate
元素只能有一个子元素。如果您想在模板中拥有多个子元素,只需使用不同类型的视图或布局作为唯一的子元素,并将其他内容插入其中。
键值模板
通过简单地将 ItemTemplate
放入一个数组 Prop
,我们可以设置键值模板。
示例
<ListView itemTemplateSelector="$item % 2 ? 'odd' : 'even'">
<Prop key="itemTemplates" type="array">
<ItemTemplate key="odd">
<Label text="odd"/>
</ItemTemplate>
<ItemTemplate key="even">
<Label text="even"/>
</ItemTemplate>
</Prop>
</ListView>
自定义组件的模板处理
有一个特殊的标记函数 makeTemplateReceiver
,您可以在 NativeScript 组件接受模板时使用它。
示例
import { RadListView } from 'nativescript-ui-listview'
import { registerElement, makeTemplateReceiver } from 'dominative'
registerElement('RadListView', makeTemplateReceiver(RadListView, {
templateProps: ['itemTemplate'],
loadingEvents: ['itemLoading']
}))
templateProps: Array
: 接受模板的属性。不要写键值模板属性。
loadingEvents: Array
: 当项目加载时将在组件上触发的事件。
itemEvents: Array
: 组件上引用项时将触发的自定义事件。
调整
使用 registerElement
添加的所有元素都自动扩展了调整能力。
Tweakable.defineEventOption(eventName: string, option: EventOption)
定义事件应如何初始化。如果事件使用 bubbles: true
或 captures: true
定义,它们将自动在元素创建时注册到本地。
事件选项
{
bubbles: boolean // should this event bubble, default false
captures: boolean // should this event have capture phase, default false
cancelable: boolean // should this event be cancelable, defalut true
}
用法
const ButtonElement = document.defaultView.Button
ButtonElement.defineEventOption('tap', {
bubbles: true,
captures: true
})
Tweakable.mapEvent(fromEvent: string, toEvent: string)
见 以下
Tweakable.mapProp(fromProp: string, toProp: string)
见 以下
摇树
默认情况下摇树是关闭的,但如果您想有一个更小的包大小,您可以通过在项目的 webpack 配置中将全局变量 __UI_USE_EXTERNAL_RENDERER__
设置为 true 来手动启用它。例如
const { merge } = require('webpack-merge');
module.exports = (env) => {
webpack.init(env);
webpack.chainWebpack((config) => {
config.plugin('DefinePlugin').tap((args) => {
args[0] = merge(args[0], {
__UI_USE_EXTERNAL_RENDERER__: true, // Set true to enable tree shaking
__UI_USE_XML_PARSER__: false, // Usually XML parser isn't needed as well, so disabling it as well
});
return args;
});
});
return webpack.resolveConfig();
};
但是,请注意,启用摇树后,您需要手动注册 {N} 核心组件,否则它们将无法作为元素使用。例如
import { AbsoluteLayout, StackLayout, Label, Button, registerElement } from 'dominative'
registerElement('AbsoluteLayout', AbsoluteLayout)
registerElement('StackLayout', StackLayout)
registerElement('Label', Label)
registerElement('Button', Button)
或者您也可以使用 registerAllElements
将它们全部注册,尽管在摇树启用时这没有意义
import { registerAllElements } from 'dominative'
registerAllElements()
Frame
、Page
和 ContentView
默认已注册。
注意事项
innerHTML
如初始警告中所述,在JavaScript创建的DOM上使用innerHTML
非常耗费资源。然而,您仍然可以在自己的项目中实现这个功能。为了实现这一点,请确保在注册此元素之前,您的基类包含一个名为innerHTML
的setter方法。
例如
import { StackLayout, registerElement } from 'dominative'
const StackLayoutWithInnerHTML = class extends StackLayout {
// only setter is needed
set innerHTML(val) {
if (val === '') {
// clear all children
}
const parsed = parseXML(val)
createElementRecursively(this, parsed)
}
}
registerElement('StackLayout', StackLayoutWithInnerHTML)
cloneNode
虽然这个方法在大多数情况下都能按预期工作,但请注意,深度克隆可能会导致丢失未通过属性设置的属性。因此,这种方法通常不推荐使用,类似于关于使用innerHTML
的建议。
框架中的硬编码
框架是很有用的工具,但当处理硬编码的元素时,它们的有效性会降低。重要的是要制定策略来减轻这种硬编码方面的负面影响。
请避免硬编码,因为它可能是有害的。
始终使用小写标签名
有时框架过于考虑周全,它们会将所有的标签名转换为小写,而且无法改变这种行为,这意味着您的camelCase或PascalCase标签名将无法按预期工作。
如果您喜欢,我们可以将我们的标签名别名设置为小写
import { aliasTagName } from 'dominative'
const tagNameConverter = (PascalCaseName) => {
// ...whatever your transformation code here
// This is useful when your framework/renderer doesn't support document.createElement with uppercase letters.
const transformedName = PascalCaseName.toLowerCase()
return transformedName
}
// Convert all built-in tag names
aliasTagName(tagNameConverter)
硬编码的事件和属性
一些框架通过提供大量的“即用即得”功能,就像魔法一样工作,您甚至不需要思考背后发生了什么。实际上,它们通过高度假设您在特定的平台上(浏览器)进行操作,并将这些功能硬编码为特定于浏览器的功能来做到这一点。
我们必须模仿它们硬编码的事件和属性,以便让这些框架满意
import { document } from 'dominative'
const TextFieldElement = document.defaultView.TextField
const ButtonElement = document.defaultView.Button
TextFieldElement.mapEvent('input', 'inputChange') // This is redirecting event handler registering for 'input' to 'inputChange'
TextFieldElement.mapProp('value', 'text') // This is redirecting access from 'value' prop to 'text' prop
ButtonElement.mapEvent('click', 'tap') // Redirect 'click' event to 'tap'
const input = document.createElement('TextField')
input.addEventListener('input', (event) => { // This is actually registered to `inputChange`
console.log(event.target.value) // This is actually accessing `event.target.text`
})
然后以下代码可以工作
<TextField v-model="userInput"/>
<!-- 'v-model' hardcoded with `input` or `change` event and `value` or `checked` prop, so we have to provide it with a emulated `input` event and `value` prop -->
<button onClick="onTapHandler"></button> // 'onTapHandler' is actually registered to 'tap', since some frameworks hardcoded "possible" event names so they can know it's an event handler
许可证
MIT