修改readme文本描述,表格居中对齐
nodom
nodom是一款基于数据驱动的web mvvm框架。 用于搭建单页应用(SPA),目前发展到3.3版本。 插件也在同步更新中。
详情请点击官网nodom
源码所在目录:./core 示例所在目录:./examples 示例运行方式: clone后在根目录执行 npm install 安装依赖包 再执行 npm run build 即可编译出可运行的nodom.js 使用Live Server启动在./examples目录下的html文件即可
Nodom是一款用于构建用户界面的前端MVVM模式框架,Nodom支持按需、渐进式引入。不论是体验Nodom还是构建复杂的单页应用,Nodom均完全支持。
MVVM
在项目内可引入的方式如下:
下载JavaScript文件,以ES Module的形式引入。
在页面以CDN包的方式引入。
最新的版本可在GitHub上获取,内有官方发布的重要信息,包括详细的更新日志,及之前的版本。
你可以在CodePen平台在线体验Nodom。 也可前往GitHub平台下载源码,运行./examples目录内提供的示例代码。
对于CDN引入的方式,可以这样引入:
<script src="https://unpkg.com/nodomjs"></script>
以确保使用最新版本。
在生产环境下,建议引入完整的nodom.js文件,Nodom建议使用ES Module实现模块化,无需构建工具即可完成模块化开发,引入方式如下:
<script type="module"> import{nodom,Module} from '../dist/nodom.js' class Module1 extends Module{ template(){ return ` <div> {{msg}} </div> ` } data(){ return { msg:'Hello World' } } } nodom(Module1,'div'); </script>
Nodom是一款基于数据驱动,用于构建用户界面的前端MVVM模式框架。内置路由,提供数据管理功能,支持模块化、组件化开发。在不使用第三方工具的情况下可独立开发完整的单页应用。
一个简单的Hello World例子如下:
<script type='module'> import{nodom,Module} from '../dist/nodom.js' class HelloWorld extends Module { template() { return ` <div> helloWorld! </div>`; } } nodom(HelloWorld, "div"); </script>
Nodom支持以普通JavaScript文件的形式引入至HTML文件,比如:
<script src="./nodom.global.js"></script>
但是我们建议以ES Module的形式引入script文件,利于模块化开发。与普通的script文件引入不同的是,ES Module的引入在标签内需要配置**type=”module”**浏览器才能识别。比如:
<script type='module'> import{nodom,Module} from '../dist/nodom.js' </script>
Nodom支持渐进式开发,框架内部会将传入的容器作为框架处理的入口。所以,传入你的元素选择器作为渲染的容器,将该容器完全交给Nodom托管。
例如有一节点:
<div id="app"> </div>
我们将其称为根节点,如果需要将一个Nodom模块渲染到根节点,只需要编写元素选择器,依序传入Nodom方法内,第一个参数为定义的模块类,第二个参数为Dom选择器。
nodom(HelloWorld, "#app");
Nodom会将传入模块渲染至传入的选择器。
Nodom以模块为单位进行应用构建,一个应用由单个或多个模块组成。
模块定义需要继承Nodom提供的模块基类Module。
Module
class Module1 extends Nodom.Module
定义模块时,为提升模块重用性,通过template()方法返回字符串形式(建议使用模板字符串)的模板代码,作为模块的视图描述。
template()
template(){ return `<div>Hello,Nodom </div>` }
通过data()方法返回模块所需的数据对象,Nodom再对其做响应式处理,响应式处理后的数据对象,Nodom称为Model对象,并存储在模块实例中。
data()
Model
data(){ return { name:'nodom', } }
为了描述方便,随后的章节中,我们将响应式处理后的对象称为Model。一个Model中还可能包含其它Model对象。Model实际是对原始数据对象进行代理拦截的Proxy对象。
Proxy
还可以自定义模块方法,经过Nodom事件处理机制的模块方法,其this指向当前模块实例。
方法参数由Nodom自动传入,依次为:Model,事件触发的虚拟Dom,Nodom封装事件NEvent,原生事件对象Event。
事件触发的虚拟Dom
Nodom封装事件NEvent
原生事件对象Event
change(model,Vdom,Nevent,event){ Mmodel.name='nodom3'; }
不由Nodom事件触发的模块方法则不会受影响。
示例代码如下:
class Module1 extends Nodom.Module{ //Nodom会将模板代码编译成虚拟 DOM树 ,再渲染至真实DOM上 template(){ return ` <div> <span class='name'>Hello,{{ name}}</span> <button e-click='change'>change</button> </div> ` } //定义模块需要的数据 data(){ return { name:'nodom', } } //自定义模块方法,有以下四个参数:Model,虚拟Dom,Nodom封装事件,原生事件 change(model,Vdom,Nevent,event){ model.name='nodom3'; } }
组件从创建到卸载它会经历一些特定的阶段,Nodom模块中包含一系列生命周期钩子函数,方便开发者在模块生命周期的各个阶段做特定的工作。
开发者可以在模块定义时提供一些特殊的方法(这些方法Nodom在执行时会传入一个参数:模块实例的根Model。this指向为模块实例),模块在特定的时刻Nodom就会去执行这些方法,下表包含所有的生命周期钩子函数:
具体用法如下:
<script type="module"> import{nodom,Module} from '../dist/nodom.js' class Hello extends Module{ template(){ return ` <div> Hello World </div> ` } //模块在渲染前会在控制台输出 onBeforeRender onBeforeRender(model){ console.log("onBeforeRender"); } //模块在首次渲染前会在控制台输出 onBeforeFirstRender onBeforeFirstRender(model){ console.log("onBeforeFirstRender"); } } nodom(Hello,'div'); </script>
Nodom采用基于HTML的模板语法。
模板的写法遵循两个基本的原则:
<!-- 闭合标签 --> <div>Something</div> <!-- 自闭合标签 --> <ModuleA />
<!-- 外层div作为该模块的根 --> <div> <!-- 模板代码 --> template code... </div>
Nodom支持原生的HTML语法,如:
<span>hello</span> <div class="cls1 cls2"> <p> Something </p> </div> ......
在原生HTML语法的基础上,Nodom扩展了模块,表达式,事件,指令等语法。
在模板里使用之前已经定义好的模块是一个常见的需求,在模板中有两种方式使用已经定义好的模块:
import {ModuleA} from "moduleA.js" class Main extends Module{ template(){ return ` <div> <!-- 直接使用类名 --> <ModuleA /> </div> ` } ...... // 使用modules注册子模块 modules = [ModuleA]; }
registModule
import {registModule} from "nodom.js" import {ModuleA} from "modulea.js" // 给ModuleA起了一个别名mod-a registModule(ModuleA,'mod-a'); class ModuleA extends Module{ template(){ return ` <div> <!-- 使用别名 --> <mod-a /> </div> ` } }
表达式是实现数据绑定的方式之一。
比如在构建用户欢迎界面的时候:
...... <h1>Hello,Bob!</h1> ......
页面会显示用户Bob的的欢迎信息Hello,Bob!,当切换用户时,页面还是显示用户Bob的欢迎信息,这显然是不合理的。如果希望根据用户名来显示不同的欢迎信息,需要将用户名userName绑定到模板中,实现动态渲染用户名:
Hello,Bob!
userName
<h1>Hello,{{ userName }}!</h1> // model.userName = 'Joe';
这样Nodom就会去当前模块实例的Model里去寻找为userName的值,并且用它替换{{ userName }}。这样就能够通过操作userName的值来显示不同用户的欢迎信息。
{{ userName }}
默认标签的属性值需要使用引号包裹(单引号'或者双引号"均可),但如果将表达式作为属性值,可以不写引号。 如:<div class="cls1 cls2" name={{userName}}></div>
'
"
<div class="cls1 cls2" name={{userName}}></div>
关于表达式的详细信息可以阅读本章的表达式章节。
Nodom的指令以x-开头,指令用来增强模板的功能,比如,x-show指令用于控制一个元素是否渲染。
x-
x-show
<span x-show={{ isShow }}> Hello,nodom!</span>
x-show指令接收true或者false,可以使用表达式为其传值,如果表达式的值为true,则会渲染该元素,如果为false则不会渲染该元素。
true
false
关于指令的详细信息可以阅读本章的指令与自定义元素章节。
Nodom的事件命名为e-+原生事件名,例如:
e-
原生事件名
<!-- click事件 在nodom中的写法为e-click --> <button e-click="confirm">确定</button>
事件接收一个模块实例上的方法名,当事件触发时,Nodom会执行该方法。
关于事件绑定的详细信息可以阅读本章的事件绑定章节。
为描述方便,接下来将模块实例中对data函数返回的数据对象做响应式处理后的对象,称为Model,也就是说data函数返回的数据会存在于Model内。
在Nodom中,与视图进行数据绑定的最常用形式就是使用双大括号。Nodom将其称为表达式,灵感追溯至Mustache库,用来与对应模块实例的Model内的属性值进行替换。比如:
<div>{{msg}},I'm Coming</div>
模块实例中对应的data函数为:
data(){ return { msg:'HelloWorld' } }
最终在页面上会变为:
HelloWorld,I'm Coming
当然,Nodom对原生的JavaScript表达式实现了支持。所以确保双大括号内传入的是单个JavaScript表达式。也就是其需要返回一个计算结果。
<!-- 取值 --> {{student.age}} <!-- 三目运算--> {{num>0?1:0}} <!-- 调取JavaScript内置函数--> {{name.toUpperCase()}}
在表达式内,JavaScript常见的内置对象是可用的,比如:Math、Object、Date等。由于表达式的执行环境是一个沙盒,请勿在内部使用用户定义的全局变量。
一些常见非表达式写法包括:赋值,流程控制。避免使用他们:
{{ let a = 1 }} {{ if (true) { return 'HelloWorld!' } }}
表达式功能强大,在表达式内,可以访问模块实例与表达式所在节点对应的Model,赋予了表达式较高的灵活性,常见的用法包括:
例如模块部分代码定义如下: class Hello extends Module{ constructor(){ this.name='hello'; this.getData=function(){ return ['星期一','星期二','星期三','星期四''星期五'] } } data=()=>{ return { title:'helloWorld' } } } <!-- 表达式语法内,普通的属性名对应当前节点对应的Model对象内的同名属性值,this指向的即是对应模块实例 --> 获取模块实例数据:{{title}}//'helloWorld' 调用模块方法:{{this.getData()}} //['星期一','星期二','星期三','星期四''星期五'] 访问模块属性:{{this.name}} //'hello'
在视图模板内,表达式用途广泛,包括:
<div> <!-- 展示数据--> <h1>{{title}}</h1> <!-- 属性、指令赋值 --> <div style={{getCss}} x-if={{flag}}> </div>
如果表达式内的计算结果产生不可预知的错误,默认的,会返回空字符串,确保程序运行时不会出错。
<div> <!-- 如果对应Model内无该字段,默认会返回空字符串 --> <!-- 页面显示''--> {{age}} </div>
Nodom使用了专门的事件类NEvent来处理Dom的事件操作,在模板中以e-开头,如:e-click、e-mouseup等。事件支持所有HTML元素标准事件,接收一个模块实例上的方法名作为事件处理方法,如:e-click="methodName",当事件触发的时,Nodom会执行该方法。具体用法如下:
NEvent
e-click
e-mouseup
e-click="methodName"
export class ModuleA extends Module{ template(){ return ` <div> <button e-click="addCount">addCount</button> <span> {{ count }} </span> </div> `; } // model data(){ return { count:0 } } // button onclick事件触发回调。 addCount(model){ model.count++; } }
与原生事件使用不同,Nodom中不需要指定事件参数,事件方法会自带四个参数。参数如下所示:
代码如下:
// 事件触发回调。 addCount(model,vdom,nEvnet,event){ ...... }
在传入事件处理方法的时,允许以:分隔的形式传入指定事件修饰符。 事件处理支持三种修饰符:
:
<!-- 事件只执行一次 --> <button e-click="tiggerClick:once">do something</button> <!--禁止冒泡--> <div e-click="outer"> <div e-click='inner:nopopo'></div> </div> <!-- 事件代理到ul --> <ul> <li x-repeat={{rows}} e-click="check:delg">...</li> </ul>
指令用于增强元素的表现能力,以”x-“开头,以设置元素属性(attribute)的形式来使用。指令具有优先级,按照数字从小到大,数字越小,优先级越高。优先级高的指令优先执行。
目前NoDom支持以下几个指令:
model指令用于给view绑定数据,数据采用层级关系,如:需要使用数据项data1.data2.data3,可以直接使用data1.data2.data3,也可以分2层设置分别设置x-model=’data1’,x-model=’data2’,然后使用数据项data3。下面的例子中描述了x-model的几种用法。 model指令改变了数据层级,则如何用外层的数据呢,NoDom支持从根向下查找数据功能,当需要从根数据向下找数据项时,需要使用”$”
模板代码
<div x-model="user"> <!-- 绑定数据 --!> 顾客信息: <div x-model="name"> <div>姓氏:{{lastName}}</div> <div>名字:{{firstName}}</div> </div> </div>
data(){ return{ user: { name: { firstName: 'Xiaoming', lastName: 'Zhang' } } } }
Repeat指令用于给按照绑定的数组数据生成多个dom节点,每个dom由指定的数据对象进行渲染。使用方式为x-repeat={{item}},其中items为数组对象。
数据索引
索引数据项为$index,为避免不必要的二次渲染,index需要单独配置。
<!-- 绑定数组数据 --!> <div x-repeat={{foods1}}> 编号:{{$index+1}},菜名:{{name}},价格:{{price}} <p>配料列表:</p> <ol> <li x-repeat={{rows}}>食材:{{title}},重量:{{weight}}</li> </ol> </div>
data(){ return{ foods1:[ {name: '夫妻肺片',price: 25,rows:[ {title:'芹菜',weight:100}, {title:'猪头肉',weight:200} ]}, {name: '京酱肉丝',price: 22,rows:[ {title:'瘦肉',weight:100}, {title:'葱',weight:200} ]}, {name: '糖醋里脊',price: 20,rows:[ {title:'排骨',weight:200} ]} ]} } }
recur指令生成树形节点,能够实现嵌套结构,在使用时,注意数据中的层次关系即可。recur也可以通过使用recur元素来实现嵌套结构。
<!-- 绑定数组数据 --!> <div x-recur='ritem'> <span class="{{cls}}">{{title}}</span> <recur ref/> </div> <recur cond='items' name='r1' class='secondct'> <for cond={{items}} > <div class='second'>id is:{{id}}-{{title}}</div> <recur ref='r1' /> </for> </recur>
data(){ return{ ritem: { title: "第一层", cls: "cls1", ritem: { title: "第二层", cls: "cls2", ritem: { title: "第三层", cls: "cls3", }, }, }, ritem1: { cls: "cls1", items: [{ title: "数据11" }, { title: "数据12" }], ritem1: { cls: "cls2", items: [{ title: "数据21" }, { title: "数据22" }], ritem1: { cls: "cls3", items: [{ title: "数据31" }, { title: "数据32" }, { title: "数据33" }], }, }, }, } }
指令用法
<div> <!-- --!> <div>如果discount<0.8,显示价格</div> <!-- 使用if指令判断discount是否小于0.6 --!> <div x-if={{discount<0.6}}>价格:{{price}}</div> <!-- if指令条件为false,进行elseif指令判断 --!> <div x-elseif={{discount<0.7}}>价格:{{price}}</div> <!-- elseif指令为false,进行else判断 --!> <div x-else={{discount<0.8}}>价格:{{price}}</div> <div x-endif></div> </div>
data(){ return { discount: 0.7, price: 200 } }
标签用法
<div> <!-- 单个if指令 --!> <div>如果discount<0.8,显示价格</div> <!-- 判断discount是否小于0.8 --!> <if cond={{discount < 0.8}}>价格:{{price}}</if> <endif/> </div> <div> <!-- 完整的if/else指令 --!> <div>如果age<18,显示未成年,否则显示成年</div> <!-- 判断age是否小于18 --!> <if cond={{age<18}}>年龄:{{age}},未成年</if> <!-- if条件为false,进入else判断 --!> <else>年龄:{{age}},成年</else> <endif/> </div> <div> <!-- if elseif else --!> 根据不同分数显示不同等级,<60不及格,60-69及格,70-79中等,80-89良好,>=90优秀 <!-- 判断grade是否小于60 --!> <if cond={{grade<60}}>不及格</if> <!-- if条件为false,进入elseif判断 --!> <elseif cond={{grade>60 && grade<70}}> 及格 </elseif> <!-- 上一个elseif条件为false,进入该elseif判断 --!> <elseif cond={{grade>70 && grade<80}}> 中等 </elseif> <!-- 上一个elseif条件为true,渲染该节点,结束判断 --!> <elseif cond={{grade>80 && grade<90}}> 良好 </elseif> <else> 优秀 </else> <endif/> </div>
data(){ return { discount: 0.7, price: 200, age: 20, grade: 73, } }
show指令用于显示或隐藏视图,如果指令对应的条件为true,则显示该视图,否则隐藏。使用方式为x-show=’condition’。
<div> <div x-show={{show}}>价格:{{price}}</div> </div>
data(){ return{ show:true, price:2000 } }
module指令用于表示该元素为一个模块容器,module指令数据对应的模块会被渲染至该元素内。使用方式为x-module=’模块类名’,Nodom会自动创建实例并将其渲染。
模版代码
<!-- 这里的Title为一个完整的Nodom模块--> import Title from './src/dist'; class ModuleA extendsModule{ template(){ return ` <!-- 将Title模块渲染至当前div--> <div x-module='Title'></div> ` }
配置说明
<div> <!-- 绑定name数据项 --!> 姓名:<input x-field="name" /> <!-- 绑定sexy数据项 --!> 性别:<input type="radio" x-field="sexy" value="M" />男 <input x-field="sexy" type="radio" value="F" />女 <!-- 绑定married数据项 --!> 已婚:<input type="checkbox" x-field="married" yes-value="1" no-value="0" /> <!-- 绑定edu数据项,并使用x-field指令生成多个option --!> 学历:<select x-field="edu"> <option x-repeat={{edus}} value="{{eduId}}">{{eduName}}</option> </select> </div>
data(){ return{ name: 'nodom', sexy: 'F', married: 1, edu: 2, birth: '2017-05-11', edus: [ { eduId: 1, eduName: "高中" }, { eduId: 2, eduName: "本科" }, { eduId: 3, eduName: "硕士研究生" }, { eduId: 4, eduName: "博士研究生" }, ] } }
在我们的日常开发中,渲染一个列表是十分常见的应用场景。接下来我们看看,在Nodom中是如何来实现列表的渲染的。
列表
Nodom
在Nodom中,我们提供了两种方式来实现列表的渲染。
第一种是通过内置指令x-reapet的方式。
x-reapet
第二种方式通过Nodom实现的<for>标签。回想一下,当你想要渲染一个按钮组件的时候,你会毫不犹豫的想到用<button>标签。那么在你想要渲染列表组件的时候,Nodom也完全支持你使用<for>标签。<for>标签是Nodom的内置的指令元素,含有一个cond属性,用来传入需要渲染的列表数据。
<for>
<button>
cond
Nodom会自动帮你将所有数据转换为响应式数据。
<!-- x-repeat 指令 --> <div class="code"> 菜单: <div x-repeat={{foods}} $index='idx'> <span>菜名:{{name}},价格:{{price}}</span> </div> </div> <!-- <for>标签 --> <div class="code"> 菜单: <for cond="{{foods}}" $index='idx'}}> <span>菜名:{{name}},价格:{{price}}</span> </for> </div>
结果:
目前,有一个这样的需求,你需要为你的列表模块添加一个编号。所以我们需要知道当前元素的索引、那么我们如何去获取当前索引呢?Nodom自动为你当前的model注入了$index这一变量,用来获取当前索引。但在使用之前,我们需要你指定索引的名字
model
$index
<div class=tip>索引号的使用(编号从0开始)</div> <!-- x-repeat 指令 --> <div class=code> 菜单: <div x-repeat={{foods}} $index='idx'> 编号:{{idx}},菜名:{{name}},价格:{{price}} </div> </div> <!-- <for>标签 --> <div class=code> 菜单: <for cond={{foods}} $index='idx'> 编号:{{idx}},菜名:{{name}},价格:{{price}} </for> </div>
结果:
注意:不论你是否使用,我们都建议你指定$index的索引名,否则将造成不可预知的错误。
如果你需要访问data中的数据,那么直接访问是不行的。因为每一个x-repeat复制出来的module是独立的model,他们与基础的module的指向的全局model不同,这类module会指向自己的独立model,你仅能访问cond里对象的属性。那我们如何来访问呢?只有在当前module中通过this.model调用全局model来访问data中的数据。
data
x-repeat
module
this.model
<div class=tip>访问 data 中的数据</div> <div class="code"> 菜单: <for cond={{foods}} $index='idx'> data中的show: {{this.model.show}} </for> </div>
现在,你只想看到22元以上的菜,那么,你可以使用一个自定义函数来为你自己筛选这些菜。
22
<div class=tip>自定义过滤数组</div> <!-- x-repeat 指令 --> <div class="code"> 菜单: <div x-repeat={{getFood(foods)}} $index='idx'> 菜名:{{name}},价格:{{price}} </div> </div> <!-- <for>标签 --> <div class="code"> 菜单: <for cond={{getFood(foods)}} $index='idx'> 菜名:{{name}},价格:{{price}} </for> </div>
getFood(arr) { return arr.filter(item => item.price > 22); }
或者,你需要将所有的数据排序展示,那么你可以将getFood方法修改如下:
getFood
getFood(arr) { return arr.sort((a,b) => a.price - b.price); }
注意:自定义函数中传入的数据已经不是原来data中的初始数据了,而是做了响应式处理的响应式数据。针对会引起响应式数据改变的数组方法,Nodom都提供了支持。例如:
push()
pop()
unshift()
shift()
splice()
sort()
reverse()
filter()
map()
有时候,我们会遇到复杂一点的嵌套列表。Nodom也能出色的完成这项任务。
<div class=tip>repeat 嵌套</div> <div class=code> 菜单: <div x-repeat={{foods1}} $index='idx'> 编号:{{idx+1}},菜名:{{name}},价格:{{price}} <p>配料列表:</p> <ol> <li x-repeat={{rows}} $index='idx'>食材:{{title}},重量:{{weight}}</li> </ol> </div> </div>
或许你会有一个疑问,x-reapt指令和<for>标签有什么不同呢?其实,二者并无什么不同的地方,<for>标签其实就是封装了x-repeat指令的一个标签。所以,<for>标签和x-repeat指令在任何时候都可以互换,这全凭你的喜好。
x-reapt
x-recur
x-recur指令可以和x-repeat一起使用,更快速的解析树形结构的数据。例如,现在你需要写一个树形组件,他的数据格式是这样的
树形结构
{ ritem2: { items: [ { title: "aaa", id: 1, items: [ { id: 1, title: "aaa1", items: [ { title: "aaa12", id: 12 }, { title: "aaa11", id: 11, items: [ { title: "aaa111", id: 111 }, { title: "aaa112", id: 112 }, ], }, { title: "aaa13", id: 13 }, ], }, { title: "aaa2", id: 2, items: [ { title: "aaa21", id: 21, items: [ { title: "aaa211", id: 211, items: [ { title: "aaa2111", id: 111 }, { title: "aaa2112", id: 112 }, ], }, { title: "aaa212", id: 212 }, ], }, { title: "aaa22", id: 22 }, ], }, ], }, { title: "bbb", id: 2, items: [ { title: "bbb1", id: 10, items: [ { title: "bbb11", id: 1011 }, { title: "bbb12", id: 1012 }, ], }, { title: "bbb2", id: 20, items: [ { title: "bbb21", id: 201 }, { title: "bbb22", id: 202 }, ], }, ], }, ], }, };
你如果仅仅使用x-repeat指令,那么很难去生成一个树形结构。还好,Nodom帮你做了处理,现在将x-recur加入进来。
<h3>递归带repeat</h3> <div x-model='ritem2'> <recur cond='items' name='r1' class='secondct'> <for cond={{items}} $index='idx'> <div class='second'>id is:{{id}}-{{title}}</div> <recur ref='r1' /> </for> </recur> </div>
漂亮,十分简洁的代码就搞定了树形结构。
x-for
<div>
Nodom通过js对象的方式实现对真实Dom的映射,通过虚拟Dom树的比对更新,达到最小操作真实Dom的目的。
/** * 元素名,如div */ public tagName: string;
Nodom中虚拟dom的key是唯一的标识,对节点的操作时提供并保证正确的位置,也可以通过key来获取虚拟dom中的值
/** * key,整颗虚拟dom树唯一 */ public key: string;
/** * 绑定模型 */ public model: Model;
public static renderDom(module:Module,src:VirtualDom,model:Model,parent?:VirtualDom,key?:string):VirtualDom{ //节点自带model优先级高 model = src.model?src.model:model; let dst:VirtualDom = new VirtualDom(src.tagName,key?src.key+'_'+key:src.key);
添加事件时,可以使用Nodom虚拟dom中的addEvent方法,如果这个事件已经添加,将不再进行添加操作
public addEvent(event: NEvent) { if(!this.events){ this.events = new Map(); } if(!this.events.has(event.name)){ this.events.set(event.name, [event.id]); }else{ let arr = this.events.get(event.name); //已添加的事件,不再添加 if(arr.indexOf(event.id) === -1){ arr.push(event.id); } } }
虚拟dom经过diff找出最小差异,批量进行patch,无需手动操作dom元素,极大的提高了页面性能。同时虚拟dom是JS的对象,有利于进行跨平台操作。
根模块的注册除外,Nodom为其余模块提供两种注册方式:
<!--待注册模块A --> class ModuleA extends Module{ ... } <!--待注册模块B --> class ModuleB extends Module{ ... } <!--注册使用模块A,B --> class Module extends Module{ ... modules=[ModuleA,ModuleB]//或者在构造函数内指定 ... template(){ return ` <!-- 使用模块A--> <ModuleA></ModuleA> <!-- 使用模块B--> <ModuleB></ModuleB> ` } }
<!--待注册模块A --> class ModuleA extends Module{ ... } registModule(ModuleA,'User'); class Main extends Module{ template(){ return ` <!--两种方式均可--> <ModuleA></ModuleA> <User></User> ` } }
为了加强模块之间的联系,Nodom在模块之间提供Props来传递数据。除根模块外,每个模块在进行模板代码解析,执行模块实例的template方法时,会将父模块通过dom节点传递的属性以对象的形式作为参数传入,也就是说,子模块可以在自己的template函数内,依据传入的props动态创建模板。
<!--模块A 功能:根据父模块依据标签传入props的值展示不同的视图代码--> class ModuleA extends Module{ template(props){ //在template函数内可以进行模板预处理 if(props.name=='add'){ return `<h1>${props.name}<h1>` }else{ return `<h1>none</h1>` } } } registModule(ModuleA,'User'); <!-- 根模块 --> class Main extends Module{ template(){ return ` <!-- 展示<h1>add</h1>--> <ModuleA name='add'></ModuleA> ` } }
借助模板字符串的加持,可以使用包含特定语法(${expression})的占位符,很大程度的拓展了模板代码的灵活度。在占位符内可以插入原生的JavaScript表达式。
${expression}
Nodom数据传递为单向数据流,Props可以实现父模块向子模块的数据传递,但是这是被动的传递方式,如果需要将其保存至子模块内的代理数据对象,可以在传递的属性名前,加上$前缀,Nodom会将其传入子模块的根Model内,实现响应式监听。
$
注意:以$前缀开头的Props属性,如果对应的是一个Model对象,该Model对象存在于两个模块内,Model内数据的改变会造成两个模块的渲染。
<!--模块A 功能:父模块主动传值,将其保存至模块A的代理对象Model内--> class ModuleA extends Module{ template(props){ return `<h1>{{name}}<h1>` } } registModule(ModuleA,'User'); <!-- 根模块 --> class Main extends Module{ template(){ return ` <!-- 展示<h1>Nodom</h1>--> <ModuleA $name={{name}}></ModuleA> ` } data(){ return { name:'Nodom', } } }
由于Props的存在,父模块可以暴露外部接口,将其通过Props传递给子模块,子模块调用该方法即可实现反向传递的功能。例如:
<!--模块A 功能:点击按钮使父模块的数据改变--> class ModuleA extends Module{ template(props){ this.parentChange=props.add; return `<button e-click='change'>点击改变父模块的数据<button>` } change(){ this.parentChange(1); } } registModule(ModuleA,'User'); <!-- 根模块 --> class Main extends Module{ template(){ return ` count={{sum}} <User add={{this.add}}></User> ` } data(){ return { sum:0, } } //这里需要使用箭头函数,来使该函数的this始终指向根模块,或者使用bind函数绑定this指向 add=(num)=>{ this.model.sum+=num; } } //当点击ModuleA内的按钮,根模块Model对象内的sum值会加一。
以此方法可以实现子模块向父模块的数据传递功能。
对于跨越多个模块层次的数据传递。
可使用第三方数据发布-订阅库。
在开发大型项目时,可以使用数据管理库帮助我们管理数据,使数据以可预测的方式发生变化,我们推荐使用Nodom团队开发的kayaks库,或者其他优秀的数据管理库均可。
在实际开发中,插槽功能会较大程度的降低应用开发难度,插槽作为模板暴露的外部接口,增大了模板的灵活度,更利于模块化开发。Nodom以指令和自定义元素的方式实现插槽功能,两者的功能类似。
<!--自定义元素的方式使用插槽 --> <slot> <h1> title </h1> </slot> <!-- 指令的形式使用插槽--> <div x-slot='title'></div>
在模块标签内的模板代码会作为待插入的节点,如果子模块内有默认的插入位置<slot></slot>,将会将节点插入该位置。如果没有待插入的内容,子模块内slot标签将会正常显示。
<slot></slot>
slot
<!--模块A 父模块待插入的内容,与slot标签进行替换 最终的模板代码为`<button>我是父模块</button>`--> class ModuleA extends Module{ template(props){ return ` <slot> 我是默认内容 </slot>` } } registModule(ModuleA,'User'); <!-- 根模块 User标签内的所有内容作为待插入的内容--> class Main extends Module{ template(){ return ` <User> <button>我是父模块</button> </User> ` } }
在使用插槽的场景下,很多时候默认插槽不足以完成全部功能。在内置多个插槽的时候,就需要使用命名插槽了。命名插槽就是给插槽定义插槽名,传入的标签需要与插槽名一致才可发生替换。
<!--模块A 父模块待插入的内容,依据name属性与与slot标签进行替换 最终的模板代码为: `<button>我是父模块的title</button> <button>我是父模块的footer</button>` --> class ModuleA extends Module{ template(props){ return ` <div> <slot name='title'> 我是title </slot> <slot name='footer'> 我是footer </slot> </div>` } } registModule(ModuleA,'User'); <!-- 根模块 User标签内的slot标签内容作为待插入的内容--> class Main extends Module{ template(){ return ` <User> <slot name='title'> <button>我是父模块的title</button> <slot> <slot name='footer'> <button>我是父模块的footer</button> <slot> </User> ` } }
在某些场景中,可能需要将插槽内容在子模块渲染,也就是相当于传递模板代码,而不在父模块内渲染。对于这种情况,只需要在子模块的插槽定义处,附加innerRender属性即可。
innerRender
<!--模块A 由于子模块插槽具有innerRender属性,父模块待替换的模板区域不会在父模块内进行渲染,在本模块渲染 --> class ModuleA extends Module{ template(props){ return ` <!--最终页面会显示'child' --> <slot innerRender> 我是默认内容 </slot>` } data(){ return { title:'child' } } } registModule(ModuleA,'User'); <!-- 根模块 User标签内的所有内容作为待插入的内容--> class Main extends Module{ template(){ return ` <User> {{title}} </User> ` } data(){ return { title:'parent' } } }
Model作为模块数据的提供者,绑定到模块的数据模型都由Model管理。Model是一个由Proxy代理的对象,Model的数据来源有两个:
$data
Model会深层代理内部的object类型数据。
object
基于Proxy,Nodom可以实现数据劫持和数据监听,来做到数据改变时候的响应式更新渲染。
关于Proxy的详细信息请参照Proxy-MDN。
在使用的时,可以直接把Model当作对象来操作:
// 模块的数据来源 data(){ return { title:'Hello', count:0, obj:{ arr:[1,2,3] } } } // 在其他地方使用model changeTitle(model){ model.count++; }
Model在管理数据的时候会新增部分以$开头的数据项和方法,所以在定义方法和数据时,尽量避免使用$开头的数据项和方法名。
每个Model存有一个模块列表,当Model内部的数据变化时,会引起该Model的模块列表中所有模块的渲染。一个Model的模块列表中默认只有初始化该Model的模块,如果需要该Model触发多个模块的渲染,则要将需要触发渲染的模块添加到该Model对应的模块列表中(Model与模块的绑定请查看API ModelManager.bindToModule)。
需要触发渲染的模块
Nodom在Model上提供了一个$set()方法,来应对一些特殊情况。例如,需要往Model上设置一个深层次的对象。
$set()
data(){ return { data:{ a:1, b:'b' } } } change(model){ // 会报错,因为data1为undefined model.data1.data2.data3 = { a:'a' }; // 使用$set可以避免该问题,如果不存在这么深层次的对象$set会帮你创建。 model.$set("data1.data2.data3",{a:'a'}); }
Nodom在Model里提供了$watch方法来监视Model里的数据变化,当数据变化时执行指定的操作。
$watch
data(){ return { obj:{ arr:[1,2,3]; } } } watch(model){ model.$watch('obj.arr',(oldVal,newVal)=>{ console.log('检测到数据变化'); console.log('oldVal:',oldVal); console.log('newVal:',newVal); }) } changArr(model){ model.obj.arr = [3,2,1]; // 执行完成之后会看到打印值。 } unwatch(model){ // 第三个参数为true表示取消监视,取消监视可以将第二个参数设置为undefined model.$watch('obj.arr',undefined,true); }
Nodom的渲染是基于数据驱动的,也就是说只有Model内的数据发生了改变,当前模块才会进行重新渲染的操作。渲染时,Nodom将新旧两次渲染产生的虚拟Dom树进行对比,找到变化的节点,实现最小操作真实Dom的目的。
<!--模块A 由于父模块传入的Props未发生改变,那么父模块的更新不会影响子模块--> class ModuleA extends Module{ template(props){ return ` <!--最终页面会显示'child' --> ${props.title}{{title}}` } data(){ return { title:'child' } } } registModule(ModuleA,'User'); <!-- 根模块 点击按钮后,由于改变了响应式数据,触发了根模块的渲染--> class Main extends Module{ template(){ return ` <button e-click='change'>改变title</button> <User title={{title}}></User> ` } data(){ return { title:'parent' } } change(model){ model.title='none'; } }
在使用props的场景下,如果我们传递的属性值发生改变,那么子模块会先触发编译模板的过程,再进行渲染操作,也就是模块重新激活。
特殊的,在Props中,对于传递Object类型的数据,每次渲染,Nodom会将该模块默认为数据改变。
Object
<!--模块A 由于父模块传入的Props数据发生了改变,ModuleA重新激活,触发template函数进行编译,再进行渲染--> class ModuleA extends Module{ template(props){ return ` <!--父模块按钮点击后,最终页面会显示’nonechild' --> ${props.title}{{title}}` } data(){ return { title:'child' } } } registModule(ModuleA,'User'); <!-- 根模块 点击按钮后,由于改变了响应式数据,触发了根模块的渲染--> class Main extends Module{ template(){ return ` <button e-click='change'>改变title</button> <User title={{title}}></User>` } data(){ return { title:'parent' } } change(model){ model.title='none'; } }
如果想要摒弃Props带来的渲染副作用,Nodom提供单次渲染模块。单次渲染模块只有在首次渲染时才会接收Props,随后无论Props如何变化,都不会影响到模块本身。使用方式为在模块标签内附加renderOnce属性。
renderOnce
<!--模块A 由于renderOnce属性,Props的改变不会影响到模块A本身--> class ModuleA extends Module{ template(props){ return ` <!--父模块按钮点击后,最终页面会显示’nonechild' --> ${props.title}{{title}}` } data(){ return { title:'child' } } } registModule(ModuleA,'User'); <!-- 根模块 点击按钮后,由于改变了响应式数据,触发了根模块的渲染--> class Main extends Module{ template(){ return ` <button e-click='change'>改变title</button> <User renderOnce title={{title}}></User>` } data(){ return { title:'parent' } } change(model){ model.title='none'; } }
Nodom对Css提供额外的支持。
<style></style>
class Module1 extends Module { template() { return plate = ` <div> <h1 class="test">Hello nodom!</h1> <style> .test { color: red; } </style> </div>`; } }
class Module1 extends Module { template() { let plate = ` <div> <h1 class="test">Hello nodom!</h1> <style>{{css()}}</style> </div>`; } css() { return ` .test { color: red; }`; } }
class Module1 extends Module { template() { let plate = ` <div> <h1 class="test">Hello nodom!</h1> <style> @import url('./style.css') </style> </div> `; } }
class Module1 extends Module { template() { let plate = ` <div> <h1 style="color: red;" class="test">Hello nodom!</h1> </div> `; } }
scope属性
给节点添加该属性后,Nodom会自动在Css选择器前加前置名。使Css样式的作用域限定在当前模块内,不会污染其它模块。
示例代码如下:
class Module1 extends Module { template() { let plate = ` <div> <h1 class="test">Hello nodom!</h1> <style scope="this"> @import url('./style.css') </style> </div> `; } }
Nodom提供了缓存功能,缓存空间是一个Object,以key-value的形式存储在内存中;
用户可以自行选择将常用的内容存储在缓存空间,例子如下:
GlobalCache.set("China.captial","北京")
根据键名从缓存中读取数据,例子如下:
GlobalCache.get("China.captial")
根据键名从缓存中移除,例子如下:
GlobalCache.remove("China.captial")
另外,还提供将指令实例,指令参数,表达式实例,事件实例,事件参数,渲染树虚拟dom,html节点,dom参数进行操作。具体使用参考API文档。
对渲染树虚拟dom的操作如下所示。
将渲染树虚拟dom存储在内存中:
// 引入模块 import { ObjectManager } from '../dist/nodom.js' let om = new ObjectManager(module)
om.saveElement(dom)
根据提供的键名获取内存中对应的渲染树虚拟dom:
om.getElement(key)
根据提供的键名将对应的渲染树虚拟dom从内存中移除:
om.removeElement(key)
Nodom提供createDirective接口来自定义指令。
createDirective
createDirective( 'directiveName', function(module, dom, src){ ...... }, 11 )
createDirective接收的参数列表如下:
handler
handler函数接收的参数列表如下:
自定义元素需要继承DefineElement类,且需要在DefineElementManager中注册。
DefineElement
DefineElementManager
// 定义自定义元素 class MYELEMENT extends DefineElement{ constructor(node,module){ super(node,module); ...... } } // 注册自定义元素 // add 接收一个自定义类或者自定义类数组 DefineElementManager.add(MYELEMENT);
定义自定义元素的构造器接收的参数列表如下:
Nodom使用x-animation指令管理动画和过渡,该指令接收一个存在于Model上的对象,其中包括tigger属性和name属性。
x-animation
tigger
name
过渡分为enter和leave,触发enter还是leave由tigger的值决定
enter
leave
对于enter过渡,需要提供以-enter-active、-enter-from、-enter-to为后缀的一组类名。在传入给x-animation指令的对象中只需要将名字传入给name属性,而不必添加后缀,x-animation在工作时会自动的加上这些后缀。这些规则对于leave过渡同理。
-enter-active
-enter-from
-enter-to
tigger为true时,指令首先会在元素上添加-enter-from和-enter-active的类名,然后再下一帧开始的时候添加-enter-to的类名,同时移除掉-enter-from的类名。
tigger为false时,处理流程完全一样,只不过添加的是以-leave-from、-leave-active、-leave-to为后缀的类名。
-leave-from
-leave-active
-leave-to
下面是一个过渡的例子和一个动画的例子:
<style> .shape-enter-active, .shape-leave-active { transition: all 1s ease; } .shape-enter-from, .shape-leave-to { height: 100px; width: 100px; } .shape-enter-to, .shape-leave-from { height: 200px; width: 200px; } </style>
class Module1 extends Module { template() { return ` <div> <button e-click="tiggerTransition">tiggerTransition</button> <div x-aniamtion={{transition}}> ...... </div> </div> `; } data() { return { transition: { name: "shape", tigger: true, }, }; } }
对于动画,后缀为-from和-to的类名没有那么重要,如果对元素在执行动画前后的状态没有要求,那么可以不用提供以这两个后缀结尾的类名,尽管如此,x-animation指令还是会添加这些后缀结尾的类名,以防止其他因素触发了模块的更新导致动画异常触发的情况。(x-animation检测这些类名来判断该元素动画或者过渡的执行状态)
-from
-to
<style> .myfade-enter-active { animation-name: myfade; animation-duration: 1s; } .myfade-leave-active { animation-name: myfade; animation-duration: 1s; animation-direction: reverse; } @keyframes myfade { 0% { opacity: 0; } 100% { opacity: 1; } } </style>
class Module1 extends Module { template() { return ` <div> <button e-click="tiggerAnimation">tiggerAnimation</button> <div x-aniamtion={{animation}}> ...... </div> </div> `; } data() { return { animation: { name: "myfade", tigger: true, // type的值默认为'transition',如果是动画则需要指明 type:'aniamtion', }, }; } }
对于部分常用的过渡效果,我们已经将其封装进入了nodomui.css文件,你只需要全局引入该css文件即可。 提供的过渡效果见下表:
在传入x-aniamtion指令的对象属性中设置isAppear(默认值为true)属性,可以配置当前的过渡/动画是否是进入离开过渡/动画。
x-aniamtion
isAppear
在传入x-aniamtion指令的对象里中设置hooks属性,可以配置过渡/动画执行前后的钩子函数。且这两个函数名字固定,分别为before和after。 他们的触发时机为:
hooks
before
after
class Module1 extends Module { template() { return ` <div> <button e-click="tiggerTransition">tiggerTransition</button> <div x-aniamtion={{transition}}> ...... </div> </div> `; } data() { return { transition: { name: "shape", tigger: true, hooks:{ // 钩子函数的this指向model,第一个参数为module // 过渡执行前钩子函数 before(module){ console.log(module) }, // 过渡执行后钩子函数 after(module){ console.log(module) } } }, }; } }
传入x-animation指令的对象不止上述提到的这些,还有一些控制参数,下表是所有可以传入的属性所示:
对于一个元素的过渡/动画可以分开配置不同的效果。 例如:
class Module1 extends Module { template() { return ` <div> <button e-click="tiggerTransition">tiggerTransition</button> <div x-aniamtion={{transition}}> ...... </div> </div> `; } data() { return { transition: { tigger: true, // 必填 name: { enter: "scale-fixtop", leave: "scale-fixleft", }, duration: { enter: "0.5s", leave: "0.5s", }, delay: { enter: "0.5s", leave: "0.5s", }, timingFunction: { enter: "ease-in-out", leave: "cubic-bezier(0.55, 0, 0.1, 1)", }, hooks: { enter: { before(module) { console.log("scale-fixtop前", module); }, after(module) { console.log("scale-fixtop后", module); }, }, leave: { before(module) { console.log("scale-fixleft前", module); }, after(module) { console.log("scale-fixleft后", module); }, }, }, }, }; } }
Nodom内置了路由功能,可以配合构建单页应用,用于模块间的切换。你需要做的是将模块映射到路由 。并指定最终在哪里渲染它们。
Nodom提供createRoute方法,用于注册路由。以Object配置的形式指定路由的路径、对应的模块、子路由等。
createRoute
以下是一个简单的路由示例:
<!-- 点击触发路由跳转--> <div x-route='/main'>page1</div> <!-- 指定路由模块渲染的位置--> <div x-router>
import {createRoute} from './nodom.js'; //这里默认Hello为一个完整的模块 import Hello from'./route/hello.js'; //创建路由 createRoute({ path:'/main', //指定路由对应的模块 module:Hello });
这样就可以实现简单的路由功能了。
在实际应用中,通常由多层嵌套的模块组合而成。配置对象内routes属性,以数组的方式注册子路由。例如:
routes
import {createRoute} from './nodom.js'; //这里默认Hello为一个完整的模块 import Main from'./route/hello.js'; import MTop from'./route/top.js'; import MBottom from'./route/bottom.js'; createRoute({ path:'/main', //指定路由对应的模块 module:Module, routes:[ { path:'/top', //指定路由对应的模块 module:MTop, },{ path:'/bottom', //指定路由对应的模块 module:MBottom, } ] });
可以发现,每个配置对象内均可设置子路由,那么就可以实现嵌套多层路由了。
借助x-route指令,用户无需手动控制路由跳转。但在一些情况下,需要手动控制路由跳转,Nodom提供两种方式手动跳转:
x-route
Router.go
Router.redirect
用来切换路由,实现路由的跳转。
如果想要实现路由传值,只需在路径内以:params配置。例如:
:params
import {createRoute} from './nodom.js'; //这里默认Hello为一个完整的模块 import Hello from'./route/hello.js'; //创建路由 createRoute({ path:'/main:id', //指定路由对应的模块 module:Hello });
Nodom将通过路由传的值放入模块根Model的$route中。
$route
路由模块中可以通过$route.data获取path传入的值。
$route.data
<!--跳转模块 --> <div> <div x-route='/main/1'>跳转至模块Hello</div> <div x-router></div> </div> <!-- 路由模块Hello--> <div> <!-- 值为1--> {{$route.data.id}} </div>
每个路由可设置:
onEnter事件 ,onEnter事件在路由进入时执行,
onEnter
onLeave事件,onLeave事件在路由离开时执行。
onLeave
执行时传入第一个参数:当前模块的根Model。
如:从/r1/r2/r3 切换到 /r1/r4/r5。 则onLeave响应顺序为r3 onLeave、r2 onLeave。 onEnter事件则从上往下执行执行顺序为 r4 onEnter、 r5 onEnter。
例如:
import {createRoute} from './nodom.js'; //这里默认Hello为一个完整的模块 import Hello from'./route/hello.js'; //创建路由 createRoute({ path:'/main', module:Hello, onLeave:function(model){ console.log('我执行了onleave函数'); }, onEnter:function(model){ console.log('我执行了onEnter函数'); } });
通过设置 Router.onDefaultEnter 和Router.onDefaultLeave 事件作为全局路由事件,执行方式与单个路由事件执行方式相同,只是会作用于每个路由。
Router.onDefaultEnter
Router.onDefaultLeave
浏览器刷新时,会从服务器请求资源,nodom路由在服务器没有匹配的资源,则会返回404。通常的做法是: 在服务器拦截资源请求,如果确认为路由,则做特殊处理。 假设主应用所在页面是/web/index.html,当前路由对应路径为/webroute/member/center。刷新时会自动跳转到/member/center路由。相应浏览器和服务器代码如下:
import {Router,Module} from './nodom.js'; class Main extends Module{ ... //在根模块中增加onFirstRender事件代码 onFirstRender:function(module){ let path; if(location.hash){ path = location.hash.substr(1); } //默认home ,如果存在hash值,则把hash值作为路由进行跳转,否则跳转到默认路由 path = path || '/home'; Router.go(path); } ... }
服务器代码为noomi框架示例代码,其它如java、express做法相似。 如果Nodom路由以’/webroute’开头,服务器拦截到请求后,分析资源路径开始地址是否以’/webroute/‘开头,如果是,则表示是nodom路由,直接执行重定向到应用首页,hash值设定为路由路径(去掉‘/webroute’)。
@Instance({ name:'routeFilter' }) class RouteFilter{ @WebFilter('/*',2) do(request:HttpRequest,response:HttpResponse){ const url = require("url"); let path = url.parse(request.url).pathname; //拦截资源 if(path.startsWith('/webroute/')){ response.redirect('/web/index.html#' + path.substr(9)); return false; } return true; } } export{RouteFilter};
数据管理库,用于开发大型项目。
提供模板代码高亮功能,以及其他多种辅助功能。
一款基于数据驱动的web mvvm框架
©Copyright 2023 CCF 开源发展委员会 Powered by Trustie& IntelliDE 京ICP备13000930号
nodom
nodom是一款基于数据驱动的web mvvm框架。 用于搭建单页应用(SPA),目前发展到3.3版本。 插件也在同步更新中。
详情请点击官网nodom
文档
安装
Nodom是一款用于构建用户界面的前端
MVVM
模式框架,Nodom支持按需、渐进式引入。不论是体验Nodom还是构建复杂的单页应用,Nodom均完全支持。在项目内可引入的方式如下:
下载JavaScript文件,以ES Module的形式引入。
在页面以CDN包的方式引入。
最新的Nodom
最新的版本可在GitHub上获取,内有官方发布的重要信息,包括详细的更新日志,及之前的版本。
体验Nodom
你可以在CodePen平台在线体验Nodom。 也可前往GitHub平台下载源码,运行./examples目录内提供的示例代码。
CDN
对于CDN引入的方式,可以这样引入:
以确保使用最新版本。
下载引入
在生产环境下,建议引入完整的nodom.js文件,Nodom建议使用ES Module实现模块化,无需构建工具即可完成模块化开发,引入方式如下:
基础
起步
Nodom是一款基于数据驱动,用于构建用户界面的前端
MVVM
模式框架。内置路由,提供数据管理功能,支持模块化、组件化开发。在不使用第三方工具的情况下可独立开发完整的单页应用。一个简单的Hello World例子如下:
引入方式
Nodom支持以普通JavaScript文件的形式引入至HTML文件,比如:
但是我们建议以ES Module的形式引入script文件,利于模块化开发。与普通的script文件引入不同的是,ES Module的引入在标签内需要配置**type=”module”**浏览器才能识别。比如:
渲染元素
Nodom支持渐进式开发,框架内部会将传入的容器作为框架处理的入口。所以,传入你的元素选择器作为渲染的容器,将该容器完全交给Nodom托管。
例如有一节点:
我们将其称为根节点,如果需要将一个Nodom模块渲染到根节点,只需要编写元素选择器,依序传入Nodom方法内,第一个参数为定义的模块类,第二个参数为Dom选择器。
Nodom会将传入模块渲染至传入的选择器。
模块基础
Nodom以模块为单位进行应用构建,一个应用由单个或多个模块组成。
模块定义
模块定义需要继承Nodom提供的模块基类
Module
。定义模块时,为提升模块重用性,通过
template()
方法返回字符串形式(建议使用模板字符串)的模板代码,作为模块的视图描述。通过
data()
方法返回模块所需的数据对象,Nodom再对其做响应式处理,响应式处理后的数据对象,Nodom称为Model
对象,并存储在模块实例中。还可以自定义模块方法,经过Nodom事件处理机制的模块方法,其this指向当前模块实例。
方法参数由Nodom自动传入,依次为:
Model
,事件触发的虚拟Dom
,Nodom封装事件NEvent
,原生事件对象Event
。不由Nodom事件触发的模块方法则不会受影响。
示例代码如下:
模块生命周期
组件从创建到卸载它会经历一些特定的阶段,Nodom模块中包含一系列生命周期钩子函数,方便开发者在模块生命周期的各个阶段做特定的工作。
开发者可以在模块定义时提供一些特殊的方法(这些方法Nodom在执行时会传入一个参数:模块实例的根
Model
。this指向为模块实例),模块在特定的时刻Nodom就会去执行这些方法,下表包含所有的生命周期钩子函数:具体用法如下:
生命周期图示
模板语法
Nodom采用基于HTML的模板语法。
基础写法
模板的写法遵循两个基本的原则:
Nodom支持原生的HTML语法,如:
在原生HTML语法的基础上,Nodom扩展了模块,表达式,事件,指令等语法。
模块写法
在模板里使用之前已经定义好的模块是一个常见的需求,在模板中有两种方式使用已经定义好的模块:
registModule
API注册模块,并且使用registModule
注册模块时的提供的别名。 两种写法的效果完全一样。表达式写法
表达式是实现数据绑定的方式之一。
比如在构建用户欢迎界面的时候:
页面会显示用户Bob的的欢迎信息
Hello,Bob!
,当切换用户时,页面还是显示用户Bob的欢迎信息,这显然是不合理的。如果希望根据用户名来显示不同的欢迎信息,需要将用户名userName
绑定到模板中,实现动态渲染用户名:这样Nodom就会去当前模块实例的
Model
里去寻找为userName
的值,并且用它替换{{ userName }}
。这样就能够通过操作userName
的值来显示不同用户的欢迎信息。关于表达式的详细信息可以阅读本章的表达式章节。
指令写法
Nodom的指令以
x-
开头,指令用来增强模板的功能,比如,x-show
指令用于控制一个元素是否渲染。x-show
指令接收true
或者false
,可以使用表达式为其传值,如果表达式的值为true
,则会渲染该元素,如果为false
则不会渲染该元素。关于指令的详细信息可以阅读本章的指令与自定义元素章节。
事件写法
Nodom的事件命名为
e-
+原生事件名
,例如:事件接收一个模块实例上的方法名,当事件触发时,Nodom会执行该方法。
关于事件绑定的详细信息可以阅读本章的事件绑定章节。
表达式
在Nodom中,与视图进行数据绑定的最常用形式就是使用双大括号。Nodom将其称为表达式,灵感追溯至Mustache库,用来与对应模块实例的Model内的属性值进行替换。比如:
模块实例中对应的data函数为:
最终在页面上会变为:
当然,Nodom对原生的JavaScript表达式实现了支持。所以确保双大括号内传入的是单个JavaScript表达式。也就是其需要返回一个计算结果。
在表达式内,JavaScript常见的内置对象是可用的,比如:Math、Object、Date等。由于表达式的执行环境是一个沙盒,请勿在内部使用用户定义的全局变量。
一些常见非表达式写法包括:赋值,流程控制。避免使用他们:
表达式用法
表达式功能强大,在表达式内,可以访问模块实例与表达式所在节点对应的Model,赋予了表达式较高的灵活性,常见的用法包括:
在视图模板内,表达式用途广泛,包括:
如果表达式内的计算结果产生不可预知的错误,默认的,会返回空字符串,确保程序运行时不会出错。
事件绑定
Nodom使用了专门的事件类
NEvent
来处理Dom的事件操作,在模板中以e-
开头,如:e-click
、e-mouseup
等。事件支持所有HTML元素标准事件,接收一个模块实例上的方法名作为事件处理方法,如:e-click="methodName"
,当事件触发的时,Nodom会执行该方法。具体用法如下:回调函数的参数
与原生事件使用不同,Nodom中不需要指定事件参数,事件方法会自带四个参数。参数如下所示:
代码如下:
事件修饰符
在传入事件处理方法的时,允许以
:
分隔的形式传入指定事件修饰符。 事件处理支持三种修饰符:指令(Directive)
指令用于增强元素的表现能力,以”x-“开头,以设置元素属性(attribute)的形式来使用。指令具有优先级,按照数字从小到大,数字越小,优先级越高。优先级高的指令优先执行。
目前NoDom支持以下几个指令:
Model 指令
model指令用于给view绑定数据,数据采用层级关系,如:需要使用数据项data1.data2.data3,可以直接使用data1.data2.data3,也可以分2层设置分别设置x-model=’data1’,x-model=’data2’,然后使用数据项data3。下面的例子中描述了x-model的几种用法。 model指令改变了数据层级,则如何用外层的数据呢,NoDom支持从根向下查找数据功能,当需要从根数据向下找数据项时,需要使用”$”
模板代码
Repeat 指令
Repeat指令用于给按照绑定的数组数据生成多个dom节点,每个dom由指定的数据对象进行渲染。使用方式为x-repeat={{item}},其中items为数组对象。
数据索引
索引数据项为$index,为避免不必要的二次渲染,index需要单独配置。
模板代码
Recur 指令
recur指令生成树形节点,能够实现嵌套结构,在使用时,注意数据中的层次关系即可。recur也可以通过使用recur元素来实现嵌套结构。
If/Elseif/Else/Endif 指令
指令用法
模板代码
标签用法
模板代码
Show 指令
show指令用于显示或隐藏视图,如果指令对应的条件为true,则显示该视图,否则隐藏。使用方式为x-show=’condition’。
模板代码
Module 指令
module指令用于表示该元素为一个模块容器,module指令数据对应的模块会被渲染至该元素内。使用方式为x-module=’模块类名’,Nodom会自动创建实例并将其渲染。
模版代码
Field 指令
配置说明
模板代码
列表
在我们的日常开发中,渲染一个
列表
是十分常见的应用场景。接下来我们看看,在Nodom
中是如何来实现列表
的渲染的。基础使用
在
Nodom
中,我们提供了两种方式来实现列表
的渲染。第一种是通过内置指令
x-reapet
的方式。第二种方式通过
Nodom
实现的<for>
标签。回想一下,当你想要渲染一个按钮组件的时候,你会毫不犹豫的想到用<button>
标签。那么在你想要渲染列表
组件的时候,Nodom
也完全支持你使用<for>
标签。<for>
标签是Nodom
的内置的指令元素,含有一个cond
属性,用来传入需要渲染的列表数据。Nodom
会自动帮你将所有数据转换为响应式数据。结果:
索引号的使用(编号从0开始)
目前,有一个这样的需求,你需要为你的列表模块添加一个编号。所以我们需要知道当前元素的索引、那么我们如何去获取当前索引呢?
Nodom
自动为你当前的model
注入了$index
这一变量,用来获取当前索引。但在使用之前,我们需要你指定索引的名字结果:
注意:不论你是否使用,我们都建议你指定
$index
的索引名,否则将造成不可预知的错误。访问
Model
中的数据如果你需要访问
data
中的数据,那么直接访问是不行的。因为每一个x-repeat
复制出来的module
是独立的model
,他们与基础的module
的指向的全局model
不同,这类module
会指向自己的独立model
,你仅能访问cond
里对象的属性。那我们如何来访问呢?只有在当前module
中通过this.model
调用全局model
来访问data
中的数据。自定义过滤数组
现在,你只想看到
22
元以上的菜,那么,你可以使用一个自定义函数来为你自己筛选这些菜。结果:
或者,你需要将所有的数据排序展示,那么你可以将
getFood
方法修改如下:结果:
注意:自定义函数中传入的数据已经不是原来
data
中的初始数据了,而是做了响应式处理的响应式数据。针对会引起响应式数据改变的数组方法,Nodom都提供了支持。例如:push()
pop()
unshift()
shift()
splice()
sort()
reverse()
filter()
map()
嵌套列表
有时候,我们会遇到复杂一点的嵌套列表。
Nodom
也能出色的完成这项任务。结果:
x-repeat
指令与<for>
标签或许你会有一个疑问,
x-reapt
指令和<for>
标签有什么不同呢?其实,二者并无什么不同的地方,<for>
标签其实就是封装了x-repeat
指令的一个标签。所以,<for>
标签和x-repeat
指令在任何时候都可以互换,这全凭你的喜好。x-repeat
指令和x-recur
指令x-recur
指令可以和x-repeat
一起使用,更快速的解析树形结构
的数据。例如,现在你需要写一个树形组件,他的数据格式是这样的你如果仅仅使用
x-repeat
指令,那么很难去生成一个树形结构
。还好,Nodom
帮你做了处理,现在将x-recur
加入进来。漂亮,十分简洁的代码就搞定了树形结构。
结果:
注意
x-for
指令和<for>
标签中均只能使用对象数组作为数据。<for>
标签最终渲染的标签为<div>
<for>
标签和x-repeat
指令一起使用。虚拟Dom
Nodom通过js对象的方式实现对真实Dom的映射,通过虚拟Dom树的比对更新,达到最小操作真实Dom的目的。
tagName属性
key属性
Nodom中虚拟dom的key是唯一的标识,对节点的操作时提供并保证正确的位置,也可以通过key来获取虚拟dom中的值
model属性
AddEvent()方法
添加事件时,可以使用Nodom虚拟dom中的addEvent方法,如果这个事件已经添加,将不再进行添加操作
虚拟dom经过diff找出最小差异,批量进行patch,无需手动操作dom元素,极大的提高了页面性能。同时虚拟dom是JS的对象,有利于进行跨平台操作。
深入
模块注册
根模块的注册除外,Nodom为其余模块提供两种注册方式:
registModule方法可以给待注册模块设置别名,在模板代码中使用模块时,既可以使用模块类名作为标签名引入,也可以使用注册的别名作为标签名引入。
模块传值&Props
为了加强模块之间的联系,Nodom在模块之间提供Props来传递数据。除根模块外,每个模块在进行模板代码解析,执行模块实例的template方法时,会将父模块通过dom节点传递的属性以对象的形式作为参数传入,也就是说,子模块可以在自己的template函数内,依据传入的props动态创建模板。
借助模板字符串的加持,可以使用包含特定语法(
${expression}
)的占位符,很大程度的拓展了模板代码的灵活度。在占位符内可以插入原生的JavaScript表达式。数据传递
Nodom数据传递为单向数据流,Props可以实现父模块向子模块的数据传递,但是这是被动的传递方式,如果需要将其保存至子模块内的代理数据对象,可以在传递的属性名前,加上
$
前缀,Nodom会将其传入子模块的根Model内,实现响应式监听。反向传递
由于Props的存在,父模块可以暴露外部接口,将其通过Props传递给子模块,子模块调用该方法即可实现反向传递的功能。例如:
以此方法可以实现子模块向父模块的数据传递功能。
深层数据传递
对于跨越多个模块层次的数据传递。
可使用第三方数据发布-订阅库。
在开发大型项目时,可以使用数据管理库帮助我们管理数据,使数据以可预测的方式发生变化,我们推荐使用Nodom团队开发的kayaks库,或者其他优秀的数据管理库均可。
插槽
在实际开发中,插槽功能会较大程度的降低应用开发难度,插槽作为模板暴露的外部接口,增大了模板的灵活度,更利于模块化开发。Nodom以指令和自定义元素的方式实现插槽功能,两者的功能类似。
默认插槽
在模块标签内的模板代码会作为待插入的节点,如果子模块内有默认的插入位置
<slot></slot>
,将会将节点插入该位置。如果没有待插入的内容,子模块内slot
标签将会正常显示。命名插槽
在使用插槽的场景下,很多时候默认插槽不足以完成全部功能。在内置多个插槽的时候,就需要使用命名插槽了。命名插槽就是给插槽定义插槽名,传入的标签需要与插槽名一致才可发生替换。
内部渲染插槽
在某些场景中,可能需要将插槽内容在子模块渲染,也就是相当于传递模板代码,而不在父模块内渲染。对于这种情况,只需要在子模块的插槽定义处,附加
innerRender
属性即可。数据模型(Model)
Model
作为模块数据的提供者,绑定到模块的数据模型都由Model
管理。Model
是一个由Proxy
代理的对象,Model
的数据来源有两个:data()
函数返回的对象;$data
方式传入的值。Model
会深层代理内部的object
类型数据。基于
Proxy
,Nodom可以实现数据劫持和数据监听,来做到数据改变时候的响应式更新渲染。在使用的时,可以直接把
Model
当作对象来操作:Model与模块渲染
每个
Model
存有一个模块列表,当Model
内部的数据变化时,会引起该Model
的模块列表中所有模块的渲染。一个Model
的模块列表中默认只有初始化该Model
的模块,如果需要该Model
触发多个模块的渲染,则要将需要触发渲染的模块
添加到该Model
对应的模块列表中(Model
与模块的绑定请查看API ModelManager.bindToModule)。$set()
Nodom在
Model
上提供了一个$set()
方法,来应对一些特殊情况。例如,需要往Model
上设置一个深层次的对象。$watch()
Nodom在
Model
里提供了$watch
方法来监视Model
里的数据变化,当数据变化时执行指定的操作。渲染
Nodom的渲染是基于数据驱动的,也就是说只有Model内的数据发生了改变,当前模块才会进行重新渲染的操作。渲染时,Nodom将新旧两次渲染产生的虚拟Dom树进行对比,找到变化的节点,实现最小操作真实Dom的目的。
Props的副作用
在使用props的场景下,如果我们传递的属性值发生改变,那么子模块会先触发编译模板的过程,再进行渲染操作,也就是模块重新激活。
特殊的,在Props中,对于传递
Object
类型的数据,每次渲染,Nodom会将该模块默认为数据改变。单次渲染模块
如果想要摒弃Props带来的渲染副作用,Nodom提供单次渲染模块。单次渲染模块只有在首次渲染时才会接收Props,随后无论Props如何变化,都不会影响到模块本身。使用方式为在模块标签内附加
renderOnce
属性。Css支持
Nodom对Css提供额外的支持。
<style></style>
标签中直接写入Css样式,示例代码如下:<style></style>
标签中的通过表达式调用函数返回Css样式代码串,示例代码如下:<style></style>
标签中通过@import url(‘css url路径’)引入Css样式文件,示例代码如下:scope属性
给节点添加该属性后,Nodom会自动在Css选择器前加前置名。使Css样式的作用域限定在当前模块内,不会污染其它模块。
示例代码如下:
Cache
Nodom提供了缓存功能,缓存空间是一个Object,以key-value的形式存储在内存中;
用户可以自行选择将常用的内容存储在缓存空间,例子如下:
根据键名从缓存中读取数据,例子如下:
根据键名从缓存中移除,例子如下:
另外,还提供将指令实例,指令参数,表达式实例,事件实例,事件参数,渲染树虚拟dom,html节点,dom参数进行操作。具体使用参考API文档。
对渲染树虚拟dom的操作如下所示。
将渲染树虚拟dom存储在内存中:
根据提供的键名获取内存中对应的渲染树虚拟dom:
根据提供的键名将对应的渲染树虚拟dom从内存中移除:
自定义
自定义指令
Nodom提供
createDirective
接口来自定义指令。createDirective
接收的参数列表如下:x-
handler
参数列表handler
函数接收的参数列表如下:自定义元素
自定义元素需要继承
DefineElement
类,且需要在DefineElementManager
中注册。定义自定义元素的构造器接收的参数列表如下:
动画与过渡
Nodom使用
x-animation
指令管理动画和过渡,该指令接收一个存在于Model
上的对象,其中包括tigger
属性和name
属性。name
属性的值就是过渡或者动画的类名;tigger
为过渡的触发条件。过渡分为
enter
和leave
,触发enter
还是leave
由tigger
的值决定tigger
为true
,触发enter
;tigger
为false
,触发leave
。对于
enter
过渡,需要提供以-enter-active
、-enter-from
、-enter-to
为后缀的一组类名。在传入给x-animation
指令的对象中只需要将名字传入给name
属性,而不必添加后缀,x-animation
在工作时会自动的加上这些后缀。这些规则对于leave
过渡同理。tigger
为true
时,指令首先会在元素上添加-enter-from
和-enter-active
的类名,然后再下一帧开始的时候添加-enter-to
的类名,同时移除掉-enter-from
的类名。tigger
为false
时,处理流程完全一样,只不过添加的是以-leave-from
、-leave-active
、-leave-to
为后缀的类名。下面是一个过渡的例子和一个动画的例子:
x-animation
管理过渡x-animation
管理动画对于部分常用的过渡效果,我们已经将其封装进入了nodomui.css文件,你只需要全局引入该css文件即可。 提供的过渡效果见下表:
进入/离开动画
在传入
x-aniamtion
指令的对象属性中设置isAppear
(默认值为true
)属性,可以配置当前的过渡/动画是否是进入离开过渡/动画。true
,则表示在离开动画播放完成之后会隐藏该元素(dispaly:none);false
,则表示在离开动画播放完成之后不会隐藏该元素。钩子函数
在传入
x-aniamtion
指令的对象里中设置hooks
属性,可以配置过渡/动画执行前后的钩子函数。且这两个函数名字固定,分别为before
和after
。 他们的触发时机为:before
触发动画/过渡之前。after
触发动画/过渡之后。过渡/动画控制参数
传入
x-animation
指令的对象不止上述提到的这些,还有一些控制参数,下表是所有可以传入的属性所示:分别配置
enter
/leave
对于一个元素的过渡/动画可以分开配置不同的效果。 例如:
路由
Nodom内置了路由功能,可以配合构建单页应用,用于模块间的切换。你需要做的是将模块映射到路由 。并指定最终在哪里渲染它们。
创建路由
Nodom提供
createRoute
方法,用于注册路由。以Object
配置的形式指定路由的路径、对应的模块、子路由等。以下是一个简单的路由示例:
这样就可以实现简单的路由功能了。
嵌套路由
在实际应用中,通常由多层嵌套的模块组合而成。配置对象内
routes
属性,以数组的方式注册子路由。例如:可以发现,每个配置对象内均可设置子路由,那么就可以实现嵌套多层路由了。
路由跳转
借助
x-route
指令,用户无需手动控制路由跳转。但在一些情况下,需要手动控制路由跳转,Nodom提供两种方式手动跳转:Router.go
Router.redirect
用来切换路由,实现路由的跳转。
路由传值
如果想要实现路由传值,只需在路径内以
:params
配置。例如:Nodom将通过路由传的值放入模块根Model的
$route
中。路由模块中可以通过
$route.data
获取path传入的值。路由事件
单路由事件
每个路由可设置:
onEnter
事件 ,onEnter
事件在路由进入时执行,onLeave
事件,onLeave
事件在路由离开时执行。执行时传入第一个参数:当前模块的根Model。
如:从/r1/r2/r3 切换到 /r1/r4/r5。 则
onLeave
响应顺序为r3onLeave
、r2onLeave
。onEnter
事件则从上往下执行执行顺序为 r4onEnter
、 r5onEnter
。例如:
全局路由事件
通过设置
Router.onDefaultEnter
和Router.onDefaultLeave
事件作为全局路由事件,执行方式与单个路由事件执行方式相同,只是会作用于每个路由。默认路由
浏览器刷新时,会从服务器请求资源,nodom路由在服务器没有匹配的资源,则会返回404。通常的做法是: 在服务器拦截资源请求,如果确认为路由,则做特殊处理。 假设主应用所在页面是/web/index.html,当前路由对应路径为/webroute/member/center。刷新时会自动跳转到/member/center路由。相应浏览器和服务器代码如下:
浏览器代码
服务器代码
服务器代码为noomi框架示例代码,其它如java、express做法相似。 如果Nodom路由以’/webroute’开头,服务器拦截到请求后,分析资源路径开始地址是否以’/webroute/‘开头,如果是,则表示是nodom路由,直接执行重定向到应用首页,hash值设定为路由路径(去掉‘/webroute’)。
生态
NodomUI
Kayaks
数据管理库,用于开发大型项目。
Nodom VsCode插件
提供模板代码高亮功能,以及其他多种辅助功能。