npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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>