twin-node
v1.0.31
Published
twin-node,声明式构建和配置html文档
Readme
twin-node.js
简介
twin-node是一个返璞归真的html文档构建和处理库。提供声明式构建、链式配置、WebComponents集成、动态代理四大核心功能。无脚手架,无虚拟DOM,专注于高性能场景。
安装
- 原生脚本引入
<script src="twin-node.js"></script>- npm安装
npm i twin-node- 模块化导入
//注意twin-node只支持这一种导入方式,不支持命名导入
import "twin-node"快速上手
twin-node提供javascript偏好和html偏好两种风格的页面构建方式,请根据您的偏好选择一种或两种风格混合使用。
- javascript偏好
<!DOCTYPE html>
<html lang="en">
<head>
<title>快速上手</title>
<script src="twin-node.js"></script>
</head>
<body>
<div id="app"></div>
<script>
//第一步:用以下标准语法解构出标签函数,注意函数名必须与标签名完全一致
const {table,thead,tbody,tr,th,td} = $$.tags
//第二步:声明式构建文档
const node = table(
thead(
tr(
th("学号"),th("姓名"),th("性别")
)
),
tbody(
tr(
td(1),td("张向明"),td("男"),
),
tr(
td(2),td("蒋海艳"),td("女"),
)
)
)
//第三步:挂载
node.render("#app")
</script>
</body>
</html>- html偏好
<!DOCTYPE html>
<html lang="en">
<head>
<title>快速上手</title>
<script src="twin-node.js"></script>
</head>
<body>
<!-- 页面骨架 -->
<header></header>
<main></main>
<footer></footer>
<!-- 页头组件 -->
<template id="tempalte1">
<!-- 以下使用了一个极端的css选择器,但不会污染外部样式 -->
<style>
*{background-color:red;}
</style>
<div>这是页头</div>
</template>
<!-- 主体组件 -->
<template id="tempalte2">
<div>这是网页主体</div>
</template>
<!-- 页脚组件 -->
<template id="tempalte3">
<div>这是页脚</div>
</template>
<script>
//shadowTemplate函数会为当前节点创建一个ShadowRoot,然后将template的内容复制到ShadowRoot中
$$.query("header").shadowTemplate("#template1");
$$.query("main").shadowTemplate("#template2");
$$.query("footer").shadowTemplate("#template3");
</script>
</body>
</html>帮助文档
1. 概念
1.1 孪生节点
孪生节点是在Node实例(以下统一称为真节点)上构建的一层代理,其实现原理是一个Proxy。孪生节点的目的是为了使用精简指令集来操作真节点的对象属性、标签属性、样式、事件,并提供链式写法。请看如下示例:
$$("div") //创建了一个div真节点,并返回其孪生节点
.porp("id","div1") //用prop配置对象属性
.attr("title","div1") //用attr配置标签属性
.class("my-div") //用class配置类名
.css("color","red") //用css配置样式
.on("click",event=>console.log(event)) //用on配置事件1.2 动态代理
动态代理是指使用特殊前缀的函数来为真节点配置对象属性、标签属性、样式属性、事件。这些函数都是动态创建的,因此能够向后兼容所有的节点属性。
$$("div")
.id("div1") //小驼峰命名的动态代理用来配置对象属性
._title("div1") //下划线开始的动态代理用来配置标签属性
.$color("red") //$符号开始的动态代理用来配置样式
.Click(event=>console.log(event)) //大驼峰命名的动态代理用来配置事件1.3 TwinNode函数
TwinNode是一个用来构建孪生节点的函数,其简写形式为$$,以下全部用简写形式。此外TwinNode有一个使用非常频繁的静态方法TwinNode.query(selector),其作用是在document下搜索DOM节点,并包装为孪生节点。
let twinNode1 = $$("div") //创建了一个div真节点,并返回其孪生节点
let twinNode2 = $$(document.querySelector("div")) //将已经存在的真节点包装为孪生节点
let twinNode3 = $$.query("div") //等效于$$(document.querySelector("div"))1.4 标签函数
标签函数是用来声明式构建孪生节点的函数,可以使用以下两种方式来获得标签函数:
//1: 通过对象解构语法获得,要求标签函数名与要构建的DOM节点标签名完全一致,
// 请不要担心$$.tags中没有你想要的标签函数,底层是一个Proxy,你想要就一定有
const {div,span} = $$.tags
//2: 通过TwinNode.defineTag获取,可自定义函数名
const myDiv = $$.defineTag("div")声明式构建孪生节点示例
const {table,tr,td} = $$.tags
let twinNode = table(
tr(
td("单元格11"),
td("单元格12"),
),
tr(
td("单元格21"),
td("单元格22"),
)
)1.5 解孪生
解孪生是指从孪生节点上获取其真节点的过程,通过孪生节点的下标0就可以完成解孪生
const div = $$.tags.div //创建标签函数
const twinNode = div("内容") //创建孪生节点
const realNode = twinNode[0] //解孪生1.6 无参函数组件
所谓函数组件就是一个能够返回孪生节点或真节点的函数,命名为函数组件是为了区别于WebComponents组件,以下是创建函数组件和复用函数组件的过程
//定义函数既是创建组件
function component() {
//返回一个由标签函数构建的孪生节点
return tbody(
tr(
td("内容"), td("内容"), td("内容")
),
tr(
td("内容"), td("内容"), td("内容")
)
)
}
//复用函数组件
const node = table(
thead(
tr(
th("字段1"), th("字段2"), th("字段3")
)
),
component //等价与component(),框架会自动调用无参的函数组件
)
//挂载节点
node.render("#app")1.7 有参函数组件
无论是无参函数组件还是有参函数组件,其本质都是一个能返回孪生节点或真节点的函数,唯一的不同点是有参的函数组件在使用时必须手动传参调用。示例如下:
//定义函数组件
function component(data) {
return tbody(
data.map(item =>
tr(
td(item.id),
td(item.name),
td(item.sex === 1 ? "男" : "女")
)
)
)
}
//准备数据
const myFamily = [
{ id: 1, name: "张向明", sex: 1 },
{ id: 2, name: "蒋海艳", sex: 2 },
{ id: 3, name: "张泽珩", sex: 1 }
]
//使用函数组件
const node = table(
thead(
tr(
th("字段1"), th("字段2"), th("字段3")
)
),
component(myFamily) //调用组件函数并传入参数
)
//挂载节点
node.render("#app")1.8 文档碎片
下滑线_是一个特殊的标签函数,其用途是创建一个文档碎片孪生节点,其真节点是一个DocumentFragment实例。使用方式如下:
//创建文档碎片组件
function component() {
const {_} = $$.tags //解构出_函数
return _(
li("项目1"),
li("项目2"),
li("项目3")
)
}
//复用组件
const node = ul(
component
)
//挂载节点
node.render("#app")
2. 标签函数参数详解
标签函数接收多个不同类型的参数,每种类型的参数,在构建过程中都有自己特定的含义,这些参数的表义与顺序无关,但文档结构与顺序有关。标签函数可无限嵌套调用,以实现复杂的文档构建。请看如下示例:
const node = ul(
//以下空值将被忽略,不参与构建
null,undefined,"",
//如果参数是一个孪生节点,则会将其真节点追加到ul中
li("项目1"),
//如果参数是一个数组,将被自动展开,并追加到ul中
[li("项目2"),li("项目3")],
//如参数是一个函数,将会自动运行并将结果递归追加到当前节点
()=>li("项目4"),
//如果参数是一个正则表达式,则用来为ul配置id和class和其他属性
//#开始的片段将被解析成id,多个id取最后一个
//.开始的片段将被解析成className,多个className可叠加
/#ul5.class1.class2/,
//如果参数是一个object,则用来配置本节点的attribute、样式、事件
{
title: "标题",
style: { color: "red", fontSize: "20px" },
click: event => console.log(event.target)
},
//无限嵌套
li(
{id:"li1",class:"list-item"},
span("内容"),
span("内容")
),
//如参数是一个真Node,这个参数将被追加到ul中
//你几乎永远不会这样做,除非你正在编写复杂的通用库
document.getElementByid("node1")
)3. 配置后置
第2节的文档构建过程太过复杂,twin-node提供了类似于jquery的链式调用来扁平化构建过程。请看如下示例:
const node = ul(
li("我有id颜色").attr("id", "project16").css("color", "red"),
li("class1").class("class1"),
li("点我").on("click",event=>alert(event.target))
)4. 与SVG集成
普通的标签函数从$$.tags中解构,而svg标签函数需要从svgTags中解构
const { svg, path } = $$.svgTags;
const node = svg(
{ width: 100, height: 100 },
path(
{ d: "M0,0 L100,100", stroke: "red", "stroke-width": 1 }
)
)5. TwinNode静态函数
|方法 |功能 | |--------------------------|-----------------------------| |$$.query(selector) |等效于document.querySelector(selector) | |$$.queryAll(selector) |等效于document.querySelectorAll(selector)| |$$.on(type,fn,option) |等效于window.addEventListener(type,fn,option) | |$$.of(type,fn,option) |等效于window.removeEventListener(type,fn,option) | |$$.defineTag(tagName) |定义一个标签函数,极少使用 | |$$.defineSvgTag(tagName) |定义一个SVG标签函数,极少使用|
6. TwinNode静态属性
|属性 |功能 | |-------------------|-----------------------------| |$$.tags |标签函数大全 | |$$.svgTags |SVG标签函数大全 |
7. TwinNode实例属性
|属性名 |功能 | |----------------------|-------------------- | |twinNode[0] |获取真节点| |twinNode[-1] |获取真节点的shadowRoot的孪生节点| |twinNode.isTwinNode |当前对象是否是一个孪生节点|
8. TwinNode实例函数
8.1 检索
|函数 |功能 | |--------------------|--------------------| |query(selector) |用查询表达式查询子节点,返回子节点的孪生节点| |queryAll(selector) |替代querySelectorAll| |children() |获取孪生节点的直接子节点| |children(index) |获取孪生节点的第index个直接子节点| |parent() |获取父节点的孪生节点| |parent(selector) |获取满足selector的祖先节点|
8.2 文档处理
|函数 |功能 | |--------------------|--------------------| |next() |获取下一个孪生节点| |next(node) |在本节点之后插入一个节点| |previous() |获取上一个孪生节点| |previous(node) |在本节点之前插入一个节点| |up() |向上移动| |down() |向下移动| |append(...child) |追加子节点| |prepend(...child) |在首位插入子节点| |appendTo(container) |追加到容器| |prependTo(container)|插入到容器的首位| |insert(node,where) |在指定的位置插入子节点| |render(container) |先清空容器,再追加到容器| |ref(name,target) |使target的name属性指向本孪生节点| |ref(name) |使windowt的name属性指向本孪生节点|
8.3 对象属性(property)
|函数 |功能 | |-----------------------|--------------------| |prop(name) |获取真节点的property| |prop(name,value) |设置真节点的property| |prop(name,fn) |用fn的返回值设置真节点的property| |prop(obj) |以对象形式配置真节点的property| |html(value) |获取或设置真节点的innerHTML| |html(fn) |用fn的返回值设置真节点的innerHTML| |text(value) |获取或设置真节点的innerText| |text(fn) |用fn的返回值设置真节点的innerText| |rect() |getBoundingClientRect()的结果| |offset() |以对象的形式返回真节点的offsetXXX| |do(name,...ages) |调用真节点的函数,并返回本孪生节点| |xxx() |动态代理,获取真节点的xxx对象属性,小驼峰命名| |xxx(value) |设置真节点的xxx对象属性的值| |xxx(fn) |用fn的返回值设置真节点的xxx对象属性值|
8.4 标签属性(attribute)
|函数 |功能 | |--------------------|--------------------| |attr(name) |获取真节点的标签属性| |attr(name,value) |设置真节点的标签属性| |attr(name,false) |移除真节点的标签属性| |attr(name,fn) |用fn的返回值设置真节点的标签属性| |attr(obj) |以对象形式配置真节点的标签属性| |hasAttr(name) |判定真节点是否有标签属性| |class(name,value) |为真节点添加类名| |class(name,false) |移除真节点的类名| |class(name,fn) |用fn的返回值添加类名| |hasClass(name) |判定真节点是否有类名| |_xxx() |动态代理,获取真节点的xxx标签属性值,以下划线开始| |_xxx(value) |设置真节点的标签属性值| |_xxx(fn) |用fn的返回值设置真节点的标签属性值|
8.5 样式(style)
|函数 |功能 | |--------------------|--------------------| |css(name) |调用getComputedStyle获取样式值| |css(name,null,false)|不调用getComputedStyle获取样式值| |css(name,value) |设置真节点的样式| |css(name,fn) |用fn的返回值设置真节点的样式| |css(obj) |以对象形式设置真节点的样式| |var(name,value) |为真节点配置css变量| |hide() |隐藏真节点| |show() |显示真节点| |$xxx() |动态代理,获取真节点的xxx样式属性值,以$开始| |$xxx(value) |设置真节点的xxx样式属性值| |$xxx(fn) |用fn的返回值设置真节点的样式属性值|
8.6 事件(event)
|函数 |功能 | |------------------------|--------------------| |on(type,...args) |为真节点配置事件| |off(type,...args) |卸载真节点的事件| |dispatchEvent(...types) |触发新的事件| |onEventPath(event) |真节点是否在event的传播路径上| |Xxx(...args) |动态代理,首字母大小,表示配置事件|
9. 动态代理
除以上实例函数外的其他函数都是遵循如下规则的动态代理函数
|规则 |功能 |示例| |----------------|-----------------------|----| |第一个字母小写 |为真节点配置对象属性 |id("id1")| |第一个字母为_ |为真节点配置attribute |_title("标题")| |第一个字母为$ |为真节点配置样式 |$display("block")| |第一个字母大写 |为真节点配置事件 |Click(event=>console.log(1))|
注意对象属性函数与标签属性函数的区别,如:
twinNode.checked(true),底层的逻辑是node.checked = true;
twinNode._checked(true),底层的逻辑是node.setAttribute("checked","")
10. 与Web Components集成
Web Components是现代浏览器支持的一套原生组件化API。主要包括自定义组件(Custom Element)、影子节点(ShadowRoot)、模板(template)、插槽(slot)四部分。twin-node实现了与Web Components API的完美集成。tion callback(root)
10.1 使用template填充ShadowRoot
shadowTemplate函数会为当前节点创建一个ShadowRoot,然后用指定的template的副本来填充ShadowRoot。
<template id="template">
<!-- 这里使用了一个极端的css选择器,但他不会污染组件外部的样式 -->
<style>
*{color:green}
</style>
<div>内容</div>
<script root>
//script被标记为root,当template的副本挂载到shadowRoot后,将运行该脚本
console.log("这个脚本块被标记为root,由shadowTemplate函数托管,将注入this和root参数");
console.log("this is ",this);
console.log("root is ",root);
console.log("host is ",root.host);
console.log("this===host",this===root.host);
</script>
<script>
console.log("这个脚本块没有被标记为root,会先于标记为root的脚本块执行");
</script>
</template>
<div id="host"></div>
<script>
$$.query("#host").shadowTemplate("#template")
</script>10.2 插槽
可以在template中定义好插槽占位符,在host中定义插槽的实例,插槽实例的slot属性与插槽占位符的name属性一一对应。host中第一没有slot标记的节点会自动插入到template中的无名(默认)插槽。
<template id="template">
<header>
<!-- 这个一个具名插槽 -->
<slot name="slot1"></slot>
</header>
<main>
<!-- 这个一个默认插槽 -->
<slot></slot>
</main>
<footer>
<!-- 这个一个具名插槽 -->
<slot name="slot2"></slot>
</footer>
</template>
<div id="host">
<div slot="slot1">页头:插入slot1</div>
<div>主体:插入到默认插槽</div>
<div slot="slot2">页脚:插入slot2</div>
</div>
<script>
$$.query("#host").shadowTemplate("#template")
</script>10.3 callback
<template id="template">
<div>内容</div>
</template>
<div id="host"></div>
<script>
const host = $$.query("#host")
const template = $$.query("#template")
host.shadowTemplate(
template,
/**
* 创建ShadowDOM后的回调函数
* @param {ShadowRoot} root
*/
function(root){
console.log("this is ",this)
console.log("root is ",root)
console.log("host is ",root.host)
console.log("this===root.host",this===root.host)
root.querySelector("div").style.color="red"
}
)
</script>10.4 与CustomElement的集成
<div id="container"></div>
<script>
//定义webcomponent组件
class MyDiv extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
this.shadowRoot.innerHTML = "内容";
}
}
//定义与组件对应的标签
customElements.define("my-div",MyDiv)
//解构标签函数
const { my_div } = $$.tags //函数名的下划线对应是标签的中划线
//构建文档
const node = div(
my_div("这是组件的内容")
)
//渲染
node.render("#container")
</script>10.5 调用远程页面
- include.html
<style>
div{
color:green;
}
</style>
<div>
被嵌入的页面
</div>- index.html
<div id="host"></div>
<script>
const host = $$.query("#host")
host.fetchHTML(
"include.html",
{method:"get"},
function(shadowRoot){
console.log("this is ",this)
console.log("root is ",shadowRoot)
console.log("host is ",shadowRoot.host)
}
)
</script>10.6 编译template
twin-node提供一套完全符合html和javascript规范的模板语法。模板语法的规则非常简单:
- 脚本片段:DOM节点上的script属性可插入一段符合javascript语法的脚本,编译时该脚本会包裹在DOM节点的外围。
- 空大括号:脚本片段是用来包裹其所在的DOM节点的,应该包含一对空大括号(这是符合javascript语法的),形象的表示将DOM节点插到此处。如果脚本片段只有一句,则可以省略空大括号。
- 插值语法:DOM节点的属性值和内容可以用${}来插入动态内容,其语法完全符合javascript的模板字符串。
- 数据:运行时引擎会注入data参数,在脚本片段和插值语法中都可以使用data参数。
- 无响应式:twin-node专注于轻量化和高性能,鼓励您控制DOM节点的每一个细节,因此并没有提供类似于vue或react的数据响应式机制。数据变化时,要么重新渲染,要么用原生手段改变DOM。因此提倡使用细粒度的template,以免大面积渲染。
<template id="template">
<table>
<tbody>
<!-- tr上的脚本片段只有一句,可以省略空大括号,运行时会预演绎为"for(let item of data){} -->
<tr script="for(let item of data)" id="${item.id}">
<td>${item.id}</td>
<td>${item.name}</td>
<td script="if(item.sex===0)">女</td>
<td script="else">男</td>
<!-- 最后两个td,由于if else的排他性,事实上只有一个td,可用如下插值语法优化
<td>${item.sex===0?"女":"男"}</td>
-->
</tr>
</tbody>
</table>
<div style="display: flex;gap: 20px;">
<!--span上的脚本片段不只一句,你需要手动加一对空大括号,以确保在运行时能在您预想的位置插入span节点 -->
<span script="for(let i=0;i<data.length;i++){let item=data[i];{}}">${item.name}</span>
</div>
</template>
<div id="host"></div>
<script>
let data = [
{id:1,name:"张向明",sex:1},
{id:2,name:"蒋海艳",sex:0}
]
let host= $$.query("#host")
let template = $$.query("#template")
$$.query("#host").compileTemplate(template,data,function(root){
root.querySelector("table").style.backgroundColor = "lightgray";
//注意:
// 由于ShadowRoot的存在,您无法通过document.querySelector来获取host内部的DOM节点
// 你可以在回调函数中使用root.querySelector来获取host的内部DOM节点
// 在回调函数之外,您只能通过document.querySelector("#host").shadowRoot.querySelector("table")来获取内部DOM节点
})
</script>