dominative
为 NativeScript 提供的最小可行 DOM 文档实现。
npm i --save dominative

DOMiNATIVE

NPM

下一代,为性能而构建的最小可行 DOM 文档实现,适用于 NativeScript。


免责声明

缺乏维护并不意味着项目已过时。相反,这表明代码质量非常高,无需持续维护。

警告

由于该项目专注于性能,其标准实现不包括对 innerHTML 的支持。在浏览器中,innerHTML 允许通过原生代码高效解析 XML 并生成 DOM 树,这要快得多。然而,在 JavaScript 模拟的 DOM 中,使用 innerHTML 成为一个昂贵的操作。它涉及在 JavaScript 中解析 XML 字符串并创建相关标签,导致在 JavaScript 中递归创建原生视图的实际实例。与直接使用 createElement 相比,这个过程要慢得多。

对于那些需要出于特定原因使用 innerHTML 的人,有关 "innerHTML" 的更多信息可在 "innerHTML" 部分找到。

我建议框架开发者考虑采用不依赖于 innerHTMLcloneNode 的节点创建方法。这个建议特别相关,因为相同的 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 })

准备全局环境

自动全局注册 documentwindow 和相关变量

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 设置 keytype 的辅助函数,可以是 key:typemulti.level.key:type

事件

无。

KeyProp

Proptype 已设置为 key

ArrayProp

Proptype 已设置为 array

ItemTemplate

* Template 已重命名为 ItemTemplate 以避免与 HTML template 标签冲突。

ItemTemplate 元素包含稍后要复制的模板,或者可以程序化创建视图。

属性

主要从 Prop 中共享。以下列出差异

key: String: RWProp 形式相同,也用作 KeyedTemplate 的键名。默认为 itemTemplate

type: 'single': R 不应该能够设置 ItemTemplate 上的 type

value: Function: RcreateView 相同。

content: <T extends ViewBase>: RW 该节点的单个子节点。除非你知道自己在做什么,否则不要触碰。

patch: Function(PatchOptions): R 修补现有克隆的方法。

createView: Function: R 从此模板创建视图的函数。

事件

itemLoading: 当修补且模板没有内容时触发。将 event.view 设置为更改此项目的视图。事件 event 上的额外属性:viewindexitemdata。此事件的回调参数不扩展自 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: truecaptures: 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()

FramePageContentView 默认已注册。


注意事项

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