npro
v0.0.4
Published
npro,声明式构建和配置html文档
Downloads
33
Maintainers
Readme
npro
简介
npro是一个返璞归真的html文档构建和处理库。 提供声明式构建、链式配置、组件化、样式隔离、WebComponents集成五大核心功能。 npro不是MVVM框架,无脚手架,无虚拟DOM,无数据响应式,专注于高性能和对DOM节点精细控制的场景。
安装
- 原生脚本引入
<script src="npro.min.js"></script>- npm安装
npm i npro- 模块化导入
import {npro,$} from "npro" //$是npro的别名,以下默认使用$快速上手
<!DOCTYPE html>
<html lang="en">
<head>
<script src="npro.min.js"></script>
</head>
<body>
<div id="app"></div>
<script>
const data = [
{ id: 1, name: "张向明" },
{ id: 2, name: "蒋海艳" }
]
const node = $.table`id="table1" class="list" draggable="true"` (
$.thead(
$.tr(
$.th("学号"),
$.th("姓名")
)
),
$.tbody(
data.map( item => $.tr`id=${item.id}`(
$.td(item.id),
$.td(item.name)
))
)
)
$("#app").render(node)
</script>
</body>
</html>模板
const title = "提示"
const data = [1,2,3]
function clickHandle(event){
console.log(this.data)
}
const input = $.input` //标签:所有的html原生标签和自定义标签都可以从$上获取
id="div1" //attribute字面量
title="${title}" //attribute插值:插值表达式外围一定要加双引号
value=""引号"" //attribute值中的双引号一定要转义
disabled //布尔型attribute
data=${data} //property必须使用插值
style=${{color:"red"}} //style插值
onclick=${clickHandle} //事件赋值
click=${[clickHandle,{once:true}]} //事件监听`
$("#app").render(input)
子节点
const node = $.div`id="div1"`(
"文本内容",
$.ul(
$.li("项目1"),
$.li("项目2"),
[
$.li("项目3"),
$.li("项目4")
], //数组会自动展开
()=>$.li("项目5") //函数会自动运行
)
)
$("#app").render(node)节点代理
【节点代理】是真实DOM节点的代理,主要作用是链式配置真节点的属性。可以通过如下几种方式获得节点代理。
- 【查询获取】:使用npro函数传入查询字符串,获取已经存在的真实DOM节点,并包装成节点代理。如:npro("#div1")
- 【加代理】:使用npro函数传入一个真实DOM节点,得到一个节点代理,我们称这一过程为加代理。如:npro(document.querySelector("#div1"))
- 【解代理】:通过节点代理的下标0可以得到真实节点,我们称这一个过程为解代理。如nodeProxy[0]
- 【标签构建】:通过标签函数构建一个全新的节点。如:div("内容")
在节点代理上可以通过属性函数链式配置节点的属性。
- 【配置标签属性】首字母是_,如:nodeProxy._id("myid")
- 【配置样式属性】以$开始,如:nodeProxy.$color("red")
- 【配置事件】以on开始,值是函数,如:nodeProxy.onclick(clickHandle)
- 【配置对象属性】除以上三种情况之外都是配置对象属性:如:draggable(true)
- 【prop】配置对象属性,如:nodeProxy.prop({disabled:true,readonly:true})
- 【attr】配置标签属性,如:nodeProxy.attr({id:"myid",name:"myname"})
- 【css】配置样式属性,如:nodeProxy.css({color:"red",backgoundColor:"green"})
- 【on】配置事件,如:nodeProxy.on("click",clickHandle)
//查询获取
const nodeProxy = $("#app");
//链式配置属性
nodeProxy
.className("class1 class2") //对象属性
._title("提示") //标签属性
.$backgroundColor("green") //样式
.onclick(event=>console.log(event)) //事件
.prop({draggable:true}) //对象属性
.attr({name:"myname"}) //标签属性
.css({color:"#fff",fontSize:"16px"}) //样式
.on("click",()=>console.log(event)) //添加事件监听
//通过标签函数构建
const child = $.ul(
$.li("项目1"),
$.li("项目2")
)
//挂载子节点
nodeProxy.render(child)
//解代理
console.log(nodeProxy[0])通用属性函数
节点代理上提供了大量可链式调用的函数, 称为【属性函数】。 属性函数用来配置和获取节点的对象属性、标签属性、样式属性、事件。
- 【prop(name,value)】配置对象属性
- 【prop(name)】获取对象属性值
- 【prop(obj)】将obj的键值配置到节点对应的对象属性上
- 【arrt(name,value)】配置标签属性
- 【arrt(name,false)】移除标签属性
- 【arrt(name,fn)】以fn的返回值为value递归调用attr(name,value)
- 【attr(name)】获取标签属性值
- 【attr(obj)】将obj的键值配置到节点对应的标签属性上
- 【css(name,value)】配置样式属性
- 【css(name,fn)】以fn的返回值为value递归调用css(name,value)
- 【css(name)】获取样式属性值
- 【css(obj)】将obj的键值配置到节点对应的样式属性上
- 【class(class1)】添加class1
- 【class([class1,calss2])】添加class1和class2
- 【class(class1,false)】移除class1
- 【class(fn)】将fn的返回值添加到className
- 【class(class1,fn)】如果fn的返回值是false则移除class1,否则添加class1
- 【on(name,fn,option)】配置事件
- 特别注意:attr的第二个参数如何为false,会删除该标签属性,如果要将标签属性设置为false请使用字符串"false"
$("#app") //加代理
.prop("innerHTML","内容") //配置对象属性
.attr("draggable","true") //配置标签属性
.class("class1") //配置class
.class("class2 class3 class4") //添加多个class
.class("class3 class4",false) //移除class
.css("display","block") //配置样式
.on("click",()=>console.log("单击")) //配置事件
.attr("title",function(){ //attr和css两个链函数的第二个参数可以是函数,函数的返回值就是属性值
let title = this.className ; //this指向当前真节点
return title;
})
//当prop、attr、css三个函数只有一个参数时,用来获取属性值
const node = $("#app")
console.log(node.prop("innerHTML"))
console.log(node.attr("draggable"))
console.log(node.css("display"))动态属性函数
可以通过【属性函数】配置节点的对象属性、标签属性、样式属性、事件。 除此之外npro还提供了一套按函数名来配置属性的动态函数, 由于这套函数是完全由你的意图动态生成的, 因此我们称为【动态属性函数】。 动态属性函数的函数名约定如下:
- 以_开始的动态链函数用来配置节点的标签属性;
- 以$开始的动态链函数用来配置节点的样式;
- 以on开始的动态链函数用来配置节点的事件;
- 注意on与动态onxxx的区别,on使用的是addEventListener,而动态的onxxx使用的是属性赋值;
- 其他动态链函数用来配置节点的对象属性。
$("#app")
//配置对象属性,innerHTML既是函数名,也是对象属性名,函数的参数是对象属性值
.innerHTML("当前时间:"+new Date())
//配置标签属性,以下划线开始
._title("提示文字")
//配置样式,美元符号+小驼峰命名
.$padding(20).$backgroundColor("lightgray")
//以属性赋值的方式配置click事件
.onclick(()=>console.log("onclick"))
//以addEventListener方式配置事件
.on(
"click",
()=>console.log("addEventListener"),
{once:true}
)
//标签属性函数和样式属性函数的参数可以是一个函数,函数的返回值就是属性值
._draggable(function(){
let time = new Date().getTime()
return `\${time%2===0}`
});
//当动态属性函数无参时,将返回对应的属性值
const node = $("#app");
console.log(node.innerHTML())
插入html字符串
虽然提倡用npro声明式构建DOM树, 但也难免需要在节点上直接插入一个待解析的html字符串, 特别是在webpack等工程化环境中。 此时可以使用【节点代理】的html方法。
$("#app").html(`
<h3>营利项目</h3>
<ul>
<li>项目1</li>
<li>项目2</li>
<li>项目3</li>
</ul>
`)事件处理
npro的事件处理有两种方式:
1. 将事件当属性配置
nodeProxy
.onmousedown(event=>console.log("鼠标按下"))
.onmouseup(event=>console.log("鼠标松开"))2. 用on(type,fn,option)配置事件
【节点代理】的on函数是原生addEventListener的加强版, 会在内部生成一个新的事件处理函数, 由于该内部事件处理函数对外不可见, 因此无法通过removeEventListener移除。 需要使用AbortController的abort方法来移除该事件处理函数。
const controller = new AbortController()
$("#text").on(
"keydown", //事件类型
function(event){ //事件处理函数
console.log(event.key)
},
{ //事件行为配置,每一项默认值为false
capture: false, //true表示只在捕获节点处理
once: false, //true表示只触发一次
passive: false, //true表示阻止调用event.preventDefault()
signal: controller.signal, //将来调用controller的abort函数时将移除事件
prevent: true, //true表示阻止默认行为
stop: true, //true表示阻止冒泡
self: true, //true表示只在本节点上触发,子节点不触发
letf: true, //true表示只在按下鼠标左键时才触发事件
right: false, //true表示只在按下鼠标右键时才触发事件
ctrl: true, //true表示只在按下了ctrl键不松才触发事件
alt: false, //true表示只在按下了alt键不松才触发事件
shift: false, //true表示只在按下了shift键不松才触发事件
meta: false, //true表示只在按下了meta键不松才触发事件
debounce: 200 //200毫秒防抖
}
)方法代理
节点代理的首字母大写的方法称为【方法代理】, 调用方法代理时,内部会先将首字母小写,然后调用真节点的同名方法。 特别注意【方法代理】的返回值是当前【节点代理】,而不是真节点的方法返回值。
//链式调用真节点的setAttribute方法
$("#app")
.SetAttribute("title","提示文字")
.SetAttribute("draggable",true)动态构建
标签函数的参数可以是一个节点数组, 也可以是一个能返回节点(或节点数组)的函数, 从而实现对子节点的动态构建。 动态构建为组件化提供天然的支持。
//准备数据
const data = ["item1","item2"]
//构建节点
const node = $.div(
$.div(
"当前时间:",
()=>$.label(new Date()) //无参函数会自动运行,并将返回值插入当前节点
),
$.ul(
append=>{ //函数会自动运行,参数append是一个钩子函数,指向上级节点代理的append方法
for( let i=0; i<5; i++ ){
append($.li("项目"+i)) //此处append钩子函数实际调用的是ul节点代理的append方法
}
}
),
$.ul(
data.map(item => $.li(item)), //数组会自动展开
)
)
//挂载节点
$("#app").render(node)
无参函数组件
所谓【函数组件】就是一个有返回值的函数, 命名为函数组件是为了区别于WebComponents组件。 函数组件的返回值通常是节点代理、字符串、属性配置对象, 以及上述类型的数组。
//创建组件
function component() {
return $.ul(
$.li("项目1"),
$.li("项目2"),
$.li("项目3")
)
}
//使用组件
const node = $.div(
$.div("项目列表"),
component //等价与component(),框架会自动调用无参的函数组件
)
//渲染节点
$("#app").render(node)
有参函数组件
无论是无参函数组件还是有参函数组件,其本质都是一个能返回节点代理(或节点数组)的函数,唯一的不同点是有参的函数组件在使用时必须手动传参调用。
//定义组件
function component(data) {
return $.ul(
data.map(item =>
$.li(
$.span(item.id),
$.span(item.name))
)
)
}
//准备数据
const data = [{id:1,name:"项目1"},{id:2,name:"项目2"},{id:3,name:"项目3"}]
//使用组件
const node = $.div(
$.div("项目列表"),
component(data) //调用组件函数并传入参数
)
//渲染节点
$("#app").render(node)
文档碎片
fragment是一个特殊的【标签函数】,其用途是创建一个文档碎片节点代理,其真节点是一个DocumentFragment实例。
//创建文档碎片
const content = $.fragment(
$.li("项目1"),
$.li("项目2"),
$.li("项目3")
)
//使用文档碎片
const node = $.ul(
content
)
//渲染节点
$("#app").render(node)
shadowRoot
npro实现了对WebComponents API的全面支持。 你可以使用customElement这种重型的实现, 也可以仅使用shadowRoot这种轻型实现。 shadowRoot可以做到最完美的样式隔离和样式穿透。 可以使用以下几种方式实现shadowRoot:
- 调用容器节点代理的shadowNode(node)函数
- 调用容器节点代理的shadowHTML(html)函数
- 设置子节点的root=true,声明式启用shadowRoot
shadowNode函数
$("#container").shadowNode(
$.ul(
$.li("项目1"),
$.li("项目2")
)
)shadowHTML函数
$("#container").shadowHTML(`
<ul>
<li>项目1</li>
<li>项目1</li>
</ul>
`)声明式启用shadowRoot
//ul设置了root属性,将先在容器上创建shadowRoot,然后再将ul插入容器。
//注意root属性要配置到容器的子节点上,而不是配置到容器本身
$("#container")(
$.ul`shadow=${true}`(
$.li("项目1"),
$.li("项目2")
)
)样式隔离
<!DOCTYPE html>
<html lang="en">
<head>
<script src="../npro.js"></script>
</head>
<body>
<div>这组件外边的div,样式没有被污染</div>
<div id="app"></div>
<script>
const node = $.template`shadow=${true}`(
$.style(`
li{
color:var(--color); /*使用外部定义的css变量*/
}`),
$.ul(
$.li("项目1"),
$.li("项目2"),
$.li("项目3")
)
)
$("#app").render(node)
</script>
</body>
</html>
样式穿透
shadowRoot将隔离容器外部与内部的样式, 在通常情况下这是优点, 但有时确实需要在容器内部使用外部的样式。 使用浏览器原生支持的css变量,可以深度穿透多层ShadowRoot, 是目前最优的样式穿透方案。
<!DOCTYPE html>
<html lang="en">
<head>
<style>
:root{--color:green;}
</style>
<script src="../npro.js"></script>
</head>
<body>
<div id="app"></div>
<script>
const node = $.fragment`shadow=${true}`(
$.style(`
li{
color:var(--color); /*使用外部定义的css变量*/
}`),
$.ul(
$.li("项目1"),
$.li("项目2"),
$.li("项目3")
)
)
$("#app").render(node)
</script>
</body>
</html>托管脚本
shadowRoot的子节点可以包含script块, 当子节点被插入到容器的shadowRoot时, 这些脚本块将被托管运行, 脚本块中的this指向容器, 脚本块中的root指向容器的shadowRoot。
const node = $.template`shadow=${true}`(
$.ul(
$.li("项目1"),
$.li("项目2"),
$.li("项目3")
),
$.script(`
this.onclick = ()=>{
console.log("this is ",this)
console.log("root is ",root)
}
`)
)
$("#app").render(node)插槽
//创建一个包含插槽的template,并配置shadow为true
const temp = $.template`shadow=${true}`(
$.style(`
::slotted(*){
padding:10px;
border:1px solid lightgray;
}
`),
$.header("页头"),
$.main(
$.slot //没有命名的插槽是一个默认插槽
),
$.footer(
$.slot`name="slot1"`
)
)
$("#app")(temp)( //将template的内容插入shadowRoot
$.div("主体"), //插入默认插槽
$.div`slot=slot1`("页脚") //插入具名插槽
)CustomElement
//定义CustomElement类,请参考w3c官网
//以下示例省略了生命周期函数
//此处完全使用原生代码来创建CustomElement,
//其实完全可以用npro来构建shadowRoot的内容
class MyDiv extends HTMLElement {
constructor() {
super();
this.attachShadow({mode:"open"})
this.shadowRoot.innerHTML = `
<style>
:host{display:block;border:1px solid lightgray;}
*{padding:10px;}
header{border-bottom:1px solid lightgray;font-weight:bolder;s}
</style>
<header>这是一个webcomponent组件</header>
<main>
<slot></slot>
</main>`
}
}
//为CustomElement定义标签
customElements.define("my-div",MyDiv)
//注意my_div的下划线对应是my-div的中划线
const node = $.my_div("这是组件的内容")
//渲染节点
$("#app").render(node)文档处理
npro对常用的html文档处理函数做了轻量封装。
- 【nodeProxy[0]】通过节点代理的下标0可以获取代理的真实节点。
- 【nodeProxy[-1]】通过节点代理的下标-1可以获得真节点的shadowRoot。
- 【nodeProxy.html(value)】设置节点的innerHTML。
- 【nodeProxy.text(value)】设置节点的innerText。
- 【nodeProxy.append(...children)】添加子节点或配置属性。
- 【nodeProxy.prepend(...children)】在节点部的第一个子元素之前插入子节点
- 【nodeProxy.appendTo(container)】将当前节点追加到容器
- 【nodeProxy.prependTo(container)】将当前节点插入到容器的第一个子节点的前面
- 【nodeProxy.render(...chidren)】先清空本节点,在追加子节点
- 【nodeProxy.insert(value,where)】向当前节点插入子节点或html字符串内容
- 【nodeProxy.before(value)】在当前节点之前插入兄弟节点,value为空表示获取当前节点的上一个兄弟节点
- 【nodeProxy.after(value)】在当前节点之后插入兄弟节点,value为空表示获取当前节点的下一个兄弟节点
- 【nodeProxy.wrap(value)】用value来包裹当前节点
- 【nodeProxy.unwrap()】剥离当前节点的外层节点
- 【nodeProxy.up()】如果存在上一个兄弟节点,则与之交换位置
- 【nodeProxy.down()】如果存在下一个兄弟节点,则与之交换位置
- 【nodeProxy.top()】与父节点的第一个子节点交换位置
- 【nodeProxy.bottom()】与父节点的最后一个子节点交换位置
- 【nodeProxy.swap(target)】与父节点的指定子节点交互位置,target可以是下标、查询字符串、节点、节点代理
- 【nodeProxy.replace(target)】用当前节点替换目标节点
svg
$("#app")(
$.svg `width="100" height="100"` (
$.svg_path `d="M0,0 L100,100" stroke="red" stroke-width="1"`
)
)
$("#app").render(node)构建范式
【构建范式】是指你使用npro构建DOM树的习惯,并非强制要求。
//事件处理函数,不建议使用箭头函数,会丢失this
function clickHandle(event){
console.log(this);
console.log(event)
}
//【对象式构建】优点:性能最高,最方便属性共享。缺点:过多的大括号
const ul3 = $.ul.prop({id:"ul1",draggable:true,onclick:clickHandle}).css({color:"#fff",backgroundColor:"green"})(
$.li.prop({id:"li11",title:"项目1"})("项目1"),
$.li.prop({id:"li12",title:"项目2"})("项目2"),
$.li.prop({id:"li13",title:"项目3"})("项目3")
)
//【链式构建】,优点:结构清晰,没有大括号。缺点:需要用_和$区分标签属性和样式,
const ul2 = $.ul._id("ul2").draggable(true).$color("#fff").$backgroundColor("green").onclick(clickHandle)(
$.li.id("li21").title("项目1")("项目1"),
$.li.id("li22").title("项目2")("项目2"),
$.li.id("li23").title("项目3")("项目3")
)
//【模板字符串构建】,优点:最接近原生HTML文档。缺点:性能稍逊于【对象式构建】
const ul1 = $.ul`id="ul3" draggable=${true} onclick=\${clickHandle} style="color:#fff;background-color:green;"` (
$.li`id="li31" title="项目1"`("项目1"),
$.li`id="li32" title="项目2"`("项目2"),
$.li`id="li33" title="项目3"`("项目3")
)
//将节点挂载到app
$("#app")(ul1, ul2, ul3)