update cache
nodom是一款基于数据驱动的web mvvm框架。 用于搭建单页应用(SPA),目前发展到3.3版本。 插件也在同步更新中。
详情请点击官网nodom
源码所在目录:./core 示例所在目录:./examples 示例运行方式: clone后在根目录执行 npm install 安装依赖包 再执行 npm run build 即可编译出可运行的nodom.js 使用Live Server启动在./examples目录下的html文件即可
Nodom是一款用于构建用户界面的前端框架,Nodom支持按需、渐进式引入。不论是体验Nodom还是构建复杂的单页应用,Nodom均完全支持。
在项目内可引入的方式如下:
下载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模式框架。内置路由,提供数据管理功能,支持模块化、组件化开发。在不使用第三方工具的情况下可独立开发完整的单页应用。
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基于模块进行应用搭建,一个应用由单个或多个模块组成。
模块定义需要继承Module类。模版类定义方式主要便于各模块独立和重复使用。 定义模块时,为提升模块重用性,通过template()方法返回模板字符串定义模板代码,通过data()方法返回对象指定数据,还可以自定义模块需要的方法。 典型定义如下代码所示:
class Module1 extends Module{ template(){ return ` <div> <span class='name'>Hello,{{ name}}</span> <button e-click='change'>change</button> </div> ` } data(){ return { name:'nodom', } } change(model){ model.name='nodom3'; } }
主要支持开发者参与模块的各个环节。
根模块注册除外,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使用基于HTML语法的模板语法,支持原生的HTML语法。
模板的写法遵循两个基本的原则:
nodom支持原生HTML写法,例如:
<span>hello</span> <div class="cls1 cls2"> <p> Something </p> </div> ......
在其他模块里使用定义好的模块,默认使用该模块定义时使用的类名,比如假设我们已经定义了一个模块名为ModuleA,在其他模块的模板中使用时我们直接使用他的类名当作标签名:
ModuleA
<ModuleA />
如果你使用了registModuleAPI注册模块,并且提供了模块别名,则在模板中可直接使用别名,例如我们使用registModule(ModuleA,'mod-a')的方式注册模块,则我们在模板中可以这样写:
registModule
registModule(ModuleA,'mod-a')
<mod-a />
这两种写法的效果完全一样。
表达式是实现数据驱动的方式之一,它可以在静态的模板中插入动态的值。 考虑这样一个场景我们需要在页面上显示欢迎信息,并且带上名字,我们会这样写:
<span>Hello,Bob!</span>
但是如果换一个人,这样写可能就不太合适,这样写不论是谁都只会显示的“Hello,Bob!”,于是我们可以使用表达式来替换后面的名字:
<span>Hello,{{ name }}!</span> // model.name = 'Joe';
这样nodom就会去当前模块实例的model里去寻找名为name的property的值,并且用它替换{{ name }}。比如这里,我们显示的就是“Hello,Joe!”,于是我们就可以通过改变name的值来做到动态渲染后面的人名。
name
{{ name }}
表达式不仅可以解析model里的属性,而且完整的支持了JavaScript的表达式。例如,如下的表达式写法都是合法的:
{{ count + 1 }} {{ flag ? "flag is true" : "flag is false" }} {{ arr.reverse().join('-') }}
但下面的写法时不会被表达式解析的:
<!-- 控制流不会被解析,使用三元表达式可以代替 --> {{ if(flag) {return 'flag is true'} }} <!-- 这不是合法的JavaScript表达式,这是JavaScript语句 --> {{ let flag = true }}
同时在表达式里面可以执行当前模块实例上的方法:
// getStr是模块实例上的方法 getStr(){ return "Hello,nodom"; }
<span>{{ getStr() }}</span>
上面的span会被渲染成为“Hello,nodom”。
表达式还可以传递动态的属性值:
<div class={{ tigger ? 'cls1' : 'cls2' }}> 动态class </div>
默认属性值需要使用引号包裹(单引号'或者双引号"均可),但如果将表达式作为属性值,可以不写引号。
'
"
关于表达式的详细信息可以阅读本章的表达式章节。
nodom的指令一般以x-开头,指令一般用来控制模板的行为,比如我们可以通过x-show指令来控制一个DOM元素渲染与否。
x-
x-show
<span x-show={{ isShow }}> Hello,nodom!</span>
x-show指令接收true或者false,我们可以使用表达式将值传给该指令,这里如果表达式返回为true,则会渲染该节点,如果为false则不会渲染该节点。
true
false
关于指令的详细信息可以阅读本章的指令与指令元素章节。
nodom的事件命名为e-+原生事件名不写on,例如:
e-
on
<!-- 原生事件onclick 在nodom中的写法为e-click --> <button e-click="confirm">确定</button>
事件以字符串的形式接收一个模块实例上方法的名字,当事件触发时,nodom会调用该方法。
关于事件绑定的详细信息可以阅读本章的事件绑定章节。
在Nodom中,与视图进行数据绑定的最常用形式就是使用**{{}}**(双大括号),灵感追溯至Mustache库,用来与对应模块实例data内的属性值进行替换,比如:
<div>{{msg}},I'am Coming</div>
对应模块内的data为:
data(){ return { msg:'HelloWorld' } }
最终在页面上会变成
HelloWorld,I'am Coming
当然,Nodom对JavaScript表达式实现了支持。所以确保双大括号内传入的是单个JavaScript表达式。也就是需要返回一个计算结果。
<!-- 取值 --> {{student.age}} <!-- 三目运算--> {{num>0?1:0}} <!-- 调取JavaScript内置函数--> {{name.toUpperCase()}}
在表达式内,JavaScript常见的内置对象是可用的,比如:Math、Object、Date等。由于表达式的执行环境是一个沙盒,请勿在内部使用用户定义的全局变量,
一些常见非表达式写法包括:赋值,流程控制。避免使用他们:
{{ let a = 1 }} {{ if (true) { return 'HelloWorld!' } }}
表达式功能强大,在表达式内,可以访问模块实例(Module)与表达式所在节点对应的数据对象(Model),赋予了表达式较高的灵活性,常见的用法包括:
例如模块部分代码定义如下: class Hello extends Module{ constructor(){ this.name='hello'; this.getData=function(){ return ['星期一','星期二','星期三','星期四''星期五'] } } data=()=>{ return { title:'helloWorld' } } } <!-- 表达式语法内,普通的属性名对应模块实例data字段的同名属性值,this指向的即是对应模块实例 --> 访问实例数据:{{title}}//'helloWorld' 调用模块方法:{{this.getData()}} //['星期一','星期二','星期三','星期四''星期五'] 访问模块属性:{{this.name}} //'hello'
在视图模板内,表达式用途广泛,包括:
<div> <!-- 展示数据--> <h1>{{title}}</h1> <!-- 属性、指令赋值 --> <div style={{getCss}} x-if={{flag}}> </div>
如果表达式内的计算结果产生不可预知的错误,默认的,会返回空字符串,确保程序运行时不会出错。
<div> <!-- 如果对应模块数据对象内无该字符段,默认会返回空字符串 --> {{age}} //'' </div>
nodom以e- + 原生事件名(不写on)的形式来监听DOM事件,接收一个字符串形式的方法名字,当事件触发的时候,nodom会依据此名字去当前模块的实例方法中找到该方法并且在当前模块的上下文中执行它。用法为e-click="methodName"。 比如:
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++; } }
如上个例子所示,回调函数的第一个参数是当前模块实例的model,实际上,回调函数接收了更多的参数:
// 事件触发回调。 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支持14个指令:module,model,repeat,recur,class,if,else,elseif,endif,show,field,route,router,slot。
指令具有优先级,按照数字从小到大,数字越小,优先级越高。优先级高的指令优先执行。
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' } } } } W
repeat指令用于给按照绑定的数组数据重复生成多个相同的dom,每个dom由指定数组索引对应的数据对象或数据进行渲染。使用方式为x-repeat=’items|filter’,其中items为json数组,filter为过滤器。
索引数据项为$index,为避免不必要的二次渲染,index需要单独配置。
<div x-repeat={{foods1}}> 编号:{{$index+1}},菜名:{{name}},价格:{{price}} <p>配料列表:</p> <ol> <li x-repeat={{rows}}>食材:{{title}},重量:{{weight}}</li> </ol> </div>
data(){ 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指令用于递归,能够实现一层包一层的结构,在使用时,注意数据中的层次关系即可。
<div x-recur='ritem'> <span class="{{cls}}">{{title}}</span> <recur ref/> </div>
ritem: { title: "第一层", cls: "cls1", ritem: { title: "第二层", cls: "cls2", ritem: { title: "第三层", cls: "cls3", }, } }
if/else指令用于条件渲染,当if指令条件为真时,则渲染该节点。当if指令条件为假时,则进行后续的elseif指令及else指令判断,如果某个节点判断条件为真,则渲染该节点,最后通过endif指令结束上一个if条件判断。
<div> <div>如果discount<0.8,显示价格</div> <div x-if={{discount<0.6}}>价格:{{price}}</div> <div x-elseif={{discount<0.7}}>价格:{{price}}</div> <div x-else={{discount<0.8}}>价格:{{price}}</div> <div x-endif></div> </div> data(){ return { discount: 0.7, price: 200 } }
show指令用于显示或隐藏view,如果指令对应的条件为真,则显示该view,否则隐藏。使用方式为x-show=’condition’。
<div> <button e-click='change'>change</button> <div x-show={{show}}>价格:{{price}}</div> </div> data(){ return{ show:true, price:2000 } }
module指令用于表示该元素为一个模块容器,module指向模块类会作为子模块渲染到该元素内。使用前先创建所需要的moduleA,然后编写完毕后,再用nodom(moduleA,’容器’)即可。
class ModuleA extends Module{ template(){ return ` <div>这是我的moduleA</div>`} nodom(ModuleA,'div')
field指令用于实现form表单下的输入类型元素,如input、combo、select、textarea等输入元素与数据项之间的双向绑定,即二者中一个改变另一个也会改变。
<div> 姓名:<input x-field="name" /> 性别:<input type="radio" x-field="sexy" value="M" />男 <input x-field="sexy" type="radio" value="F" />女 已婚:<input type="checkbox" x-field="married" yes-value="1" no-value="0" /> 学历:<select x-field="edu"> <option x-repeat={{edus}} value="{{eduId}}">{{eduName}}</option> </select> 简介:<textarea x-field='intro'></textarea> </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: "博士研究生" }, ] } }
指令元素和指令的使用效果类似,使用方法有一些不同。
module元素用于表示该元素为一个模块容器,module指向模块类会作为子模块渲染到该元素内。使用前先创建所需要的moduleA,然后把模块名作为标签名进行使用。
<ModuleA xxx='333'> <h3 style='color:gold'> 我自动作为solot节点 </h3> </ModuleA>
recur元素和recur指令的功能一样,都用于递归,能够实现一层包一层的结构,在使用时,注意数据中的层次关系即可。
<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{ 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} ]} } ] } } }
<div> <div>单个if指令</div> <div>如果discount<0.8,显示价格</div> <if cond={{discount < 0.8}}>价格:{{price}}</if> <endif/> </div> <div> <div>完整的if/else指令</div> <div>如果age<18,显示未成年,否则显示成年</div> <if cond={{age<18}}>年龄:{{age}},未成年</if> <else>年龄:{{age}},成年</else> <endif/> </div> <div> <div>if elseif else</div> 根据不同分数显示不同等级,<60不及格,60-69及格,70-79中等,80-89良好,>=90优秀 <if cond={{grade<60}}>不及格</if> <elseif cond={{grade>60 && grade<70}}> 及格 </elseif> <elseif cond={{grade>70 && grade<80}}> 中等 </elseif> <elseif cond={{grade>80 && grade<90}}> 良好 </elseif> <else> 优秀 </else> <endif/> </div> data(){ return { discount: 0.7, price: 200, age: 20, grade: 73, } }
在我们的日常开发中,渲染一个列表是十分常见的应用场景。接下来我们看看,在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的索引名,否则将造成不可预知的错误。
Model
如果你需要访问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响应式数组数据中,支持的js原生方法有很多,例如:
js
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树,进行对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在模块之间提供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 ` <ModuleA name='add'></ModuleA>//展示<h1>add</h1> ` } }
借助模板字符串的加持,可以包含特定语法(${expression})的占位符,很大程度的拓展了模板代码的灵活度。可以插入原生的JavaScript表达式。
${expression}
Nodom数据传递为单向数据流,props可以实现父模块向子模块的数据传递,但是这是被动的传递方式,如果需要将其保存至子模块内的代理数据对象,可以在传递的属性名前,加上’$’后缀,Nodom会将其传入子模块的代理对象内,实现响应式监听。
<!--模块A 功能:父模块主动传值,将其保存至模块A的代理对象Model内--> class ModuleA extends Module{ template(props){ return `<h1>{{name}}<h1>` } } registModule(ModuleA,'User'); <!-- 根模块 --> class Main extends Module{ template(){ return ` <ModuleA $name={{name}}></ModuleA>//展示<h1>Nodom</h1> ` } 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值会加一。
以此方法可以实现子模块向父模块的数据传递功能。
对于跨越多个模块层次的数据传递。
可使用第三方数据发布-订阅库。
在开发大型项目时,可以使用数据管理库帮助我们管理数据,使数据以可预测的方式发生变化,我们推荐使用团队开发的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是一个由Proxy代理的对象,模块的model的数据来源是模块实例的data()函数返回的对象。得益于Proxy,我们可以实现数据劫持和数据监听,来做到数据改变的时候自动更新渲染。
Proxy
data()
关于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的模块列表中只有当前模块,如果你通过$data的方式从父模块传入了一个object给子模块,那么该object也会作为一个model传入子模块的model中,且这个传入的model所持有的模块列表中将会有父模块和子模块。由于是引用传值,所以无论是在父还是在子中改变了object,都会触发两个模块的渲染。
$data
object
大多数情况下你都可以使用操作对象的方式来操作model,但是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接收三个参数
string
handler
Function
dom
src
priority
number
自定义元素需要继承DirectiveElement类,且需要加入在DirectiveElementManager中注册。
DirectiveElement
DirectiveElementManager
// 定义自定义元素 class MYELEMENT extends DirectiveElement{ constructor(node,module){ super(node,module); ...... } } // 注册自定义元素 // add 接收一个自定义类或者自定义类数组 DirectiveElementManager.add(MYELEMENT);
定义自定义元素的构造器接收两个参数:
node
我们使用x-animation指令管理动画和过渡,该指令接收一个存在于model上的对象,其中包括tigger属性和name属性。
x-animation
tigger
过渡分为enter和leave,触发enter还是leave由tigger的值触发
enter
leave
对于enter过渡,需要你提供以-enter-active、-enter-from、-enter-to为后缀的一组类名,当然在传入给animation指令的对象中你只需要提供前面的名字,x-animation在工作中会自动的为你加上这些后缀。这些规则对于leave过渡同理。
-enter-active
-enter-from
-enter-to
animation
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
例如上面这个shape过渡的例子:
shape
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指令的对象不止上述提到的这些,还有一些控制参数,下表是所有可以传入的属性所示:
对于一个元素的过渡/动画 我们可以分开配置不同的效果。这样传入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};
nodom是一款基于数据驱动的web mvvm框架,采用typescript开发,用于构建用户界面的渐进式框架.
©Copyright 2023 CCF 开源发展委员会 Powered by Trustie& IntelliDE 京ICP备13000930号
nodom
nodom是一款基于数据驱动的web mvvm框架。 用于搭建单页应用(SPA),目前发展到3.3版本。 插件也在同步更新中。
详情请点击官网nodom
文档
安装
Nodom是一款用于构建用户界面的前端框架,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基于模块进行应用搭建,一个应用由单个或多个模块组成。
模块定义
模块定义需要继承Module类。模版类定义方式主要便于各模块独立和重复使用。 定义模块时,为提升模块重用性,通过template()方法返回模板字符串定义模板代码,通过data()方法返回对象指定数据,还可以自定义模块需要的方法。 典型定义如下代码所示:
模块生命周期
主要支持开发者参与模块的各个环节。
生命周期图示
模块注册
根模块注册除外,Nodom为子模块提供两种注册方式:
模板语法
nodom使用基于HTML语法的模板语法,支持原生的HTML语法。
基础写法
模板的写法遵循两个基本的原则:
nodom支持原生HTML写法,例如:
模块写法
在其他模块里使用定义好的模块,默认使用该模块定义时使用的类名,比如假设我们已经定义了一个模块名为
ModuleA
,在其他模块的模板中使用时我们直接使用他的类名当作标签名:如果你使用了
registModule
API注册模块,并且提供了模块别名,则在模板中可直接使用别名,例如我们使用registModule(ModuleA,'mod-a')
的方式注册模块,则我们在模板中可以这样写:这两种写法的效果完全一样。
表达式写法
表达式是实现数据驱动的方式之一,它可以在静态的模板中插入动态的值。 考虑这样一个场景我们需要在页面上显示欢迎信息,并且带上名字,我们会这样写:
但是如果换一个人,这样写可能就不太合适,这样写不论是谁都只会显示的“Hello,Bob!”,于是我们可以使用表达式来替换后面的名字:
这样nodom就会去当前模块实例的model里去寻找名为
name
的property的值,并且用它替换{{ name }}
。比如这里,我们显示的就是“Hello,Joe!”,于是我们就可以通过改变name
的值来做到动态渲染后面的人名。表达式不仅可以解析model里的属性,而且完整的支持了JavaScript的表达式。例如,如下的表达式写法都是合法的:
但下面的写法时不会被表达式解析的:
同时在表达式里面可以执行当前模块实例上的方法:
上面的span会被渲染成为“Hello,nodom”。
表达式还可以传递动态的属性值:
关于表达式的详细信息可以阅读本章的表达式章节。
指令写法
nodom的指令一般以
x-
开头,指令一般用来控制模板的行为,比如我们可以通过x-show
指令来控制一个DOM元素渲染与否。x-show
指令接收true
或者false
,我们可以使用表达式将值传给该指令,这里如果表达式返回为true
,则会渲染该节点,如果为false
则不会渲染该节点。关于指令的详细信息可以阅读本章的指令与指令元素章节。
事件写法
nodom的事件命名为
e-
+原生事件名不写on
,例如:事件以字符串的形式接收一个模块实例上方法的名字,当事件触发时,nodom会调用该方法。
关于事件绑定的详细信息可以阅读本章的事件绑定章节。
表达式
在Nodom中,与视图进行数据绑定的最常用形式就是使用**{{}}**(双大括号),灵感追溯至Mustache库,用来与对应模块实例data内的属性值进行替换,比如:
对应模块内的data为:
最终在页面上会变成
当然,Nodom对JavaScript表达式实现了支持。所以确保双大括号内传入的是单个JavaScript表达式。也就是需要返回一个计算结果。
在表达式内,JavaScript常见的内置对象是可用的,比如:Math、Object、Date等。由于表达式的执行环境是一个沙盒,请勿在内部使用用户定义的全局变量,
一些常见非表达式写法包括:赋值,流程控制。避免使用他们:
表达式用法
表达式功能强大,在表达式内,可以访问模块实例(Module)与表达式所在节点对应的数据对象(Model),赋予了表达式较高的灵活性,常见的用法包括:
在视图模板内,表达式用途广泛,包括:
如果表达式内的计算结果产生不可预知的错误,默认的,会返回空字符串,确保程序运行时不会出错。
事件绑定
nodom以
e-
+ 原生事件名(不写on)的形式来监听DOM事件,接收一个字符串形式的方法名字,当事件触发的时候,nodom会依据此名字去当前模块的实例方法中找到该方法并且在当前模块的上下文中执行它。用法为e-click="methodName"
。 比如:回调函数的参数
如上个例子所示,回调函数的第一个参数是当前模块实例的model,实际上,回调函数接收了更多的参数:
事件修饰符
对于事件,可以在接收的字符串中以
:
分隔的形式添加事件修饰符。 事件处理支持三种修饰符:指令(Directive)
指令用于增强元素的表现能力,有两种使用方式,一种是以”x-“开头,作为元素的属性(attribute)使用,另外一种是把指令名用作标签名,例如 目前NoDom支持14个指令:module,model,repeat,recur,class,if,else,elseif,endif,show,field,route,router,slot。
指令优先级
指令具有优先级,按照数字从小到大,数字越小,优先级越高。优先级高的指令优先执行。
model指令
model指令用于给view绑定数据,数据采用层级关系,如:需要使用数据项data1.data2.data3,可以直接使用data1.data2.data3,也可以分2层设置分别设置x-model=’data1’,x-model=’data2’,然后使用数据项data3。下面的例子中描述了x-model的几种用法。 model指令改变了数据层级,则如何用外层的数据呢,NoDom支持从根向下查找数据功能,当需要从根数据向下找数据项时,需要使用”$”
模版代码
js代码
repeat 指令
repeat指令用于给按照绑定的数组数据重复生成多个相同的dom,每个dom由指定数组索引对应的数据对象或数据进行渲染。使用方式为x-repeat=’items|filter’,其中items为json数组,filter为过滤器。
数据索引
索引数据项为$index,为避免不必要的二次渲染,index需要单独配置。
模版代码
js代码
recur 指令
recur指令用于递归,能够实现一层包一层的结构,在使用时,注意数据中的层次关系即可。
模版代码
js代码
if/elseif/else/endif指令
if/else指令用于条件渲染,当if指令条件为真时,则渲染该节点。当if指令条件为假时,则进行后续的elseif指令及else指令判断,如果某个节点判断条件为真,则渲染该节点,最后通过endif指令结束上一个if条件判断。
模板代码
show指令
show指令用于显示或隐藏view,如果指令对应的条件为真,则显示该view,否则隐藏。使用方式为x-show=’condition’。
模板代码
module指令
module指令用于表示该元素为一个模块容器,module指向模块类会作为子模块渲染到该元素内。使用前先创建所需要的moduleA,然后编写完毕后,再用nodom(moduleA,’容器’)即可。
模版代码
field指令
field指令用于实现form表单下的输入类型元素,如input、combo、select、textarea等输入元素与数据项之间的双向绑定,即二者中一个改变另一个也会改变。
配置说明
模板代码
指令元素(Element)
指令元素和指令的使用效果类似,使用方法有一些不同。
module元素
module元素用于表示该元素为一个模块容器,module指向模块类会作为子模块渲染到该元素内。使用前先创建所需要的moduleA,然后把模块名作为标签名进行使用。
模板代码
for元素
recur元素
recur元素和recur指令的功能一样,都用于递归,能够实现一层包一层的结构,在使用时,注意数据中的层次关系即可。
模版代码
js代码
if/elseif/else/endif元素
模板代码
列表
在我们的日常开发中,渲染一个
列表
是十分常见的应用场景。接下来我们看看,在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
响应式数组数据中,支持的js
原生方法有很多,例如: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树,进行对dom操作性能的优化,通过状态的改变和与模板容器dom中做比对来进行对dom树的渲染操作。
tagName属性
key属性
Nodom中虚拟dom的key是唯一的标识,对节点的操作时提供并保证正确的位置,也可以通过key来获取虚拟dom中的值
model属性
AddEvent()方法
添加事件时,可以使用Nodom虚拟dom中的addEvent方法,如果这个事件已经添加,将不再进行添加操作
虚拟dom经过diff找出最小差异,批量进行patch,无需手动操作dom元素,极大的提高了页面性能。同时虚拟dom是JS的对象,有利于进行跨平台操作。
深入
模块传值&Props
为了加强模块之间的联系,Nodom在模块之间提供Props来传递数据。除根组件外,每个模块在进行模板解析时,执行模块实例的template方法时,会将父模块通过dom节点传递的属性以对象的形式作为参数传入,也就是说,子模块可以在自己的template函数内,依据传入的props动态创建模板。
借助模板字符串的加持,可以包含特定语法(
${expression}
)的占位符,很大程度的拓展了模板代码的灵活度。可以插入原生的JavaScript表达式。数据传递
Nodom数据传递为单向数据流,props可以实现父模块向子模块的数据传递,但是这是被动的传递方式,如果需要将其保存至子模块内的代理数据对象,可以在传递的属性名前,加上’$’后缀,Nodom会将其传入子模块的代理对象内,实现响应式监听。
反向传递
由于props的存在,父模块可以暴露外部接口,将其通过props传递给子模块,子模块调用该方法即可实现反向传递的功能。例如:
以此方法可以实现子模块向父模块的数据传递功能。
深层数据传递
对于跨越多个模块层次的数据传递。
可使用第三方数据发布-订阅库。
在开发大型项目时,可以使用数据管理库帮助我们管理数据,使数据以可预测的方式发生变化,我们推荐使用团队开发的kayaks库,或者其他优化的数据管理库均可。
插槽
在实际开发中,插槽功能会较大程度的简化项目难度,插槽作为模板暴露的外部接口,增大了模板的灵活度,更利于模块化开发。Nodom以指令和指令元素的方式实现插槽功能,两者的功能类似。
默认插槽
在模块标签内的模板代码会作为待插入的节点,如果子模块内有默认的插入位置
<slot></slot>
,将会将节点插入该位置。如果没有待插入的内容,子模块内slot
标签将会正常显示。命名插槽
在使用插槽的场景下,很多时候默认插槽不足以完成全部功能。在内置多个插槽的时候,就需要使用命名插槽了。命名插槽就是给插槽定义插槽名,传入的标签需要与插槽名一致才可发生替换。
内部渲染插槽
在某些场景中,可能需要将插槽内容在子模块渲染,也就是相当于传递模板代码,而不在父模块内渲染。对于这种情况,只需要在子模块的插槽定义处,附加
innerRender
属性即可。数据模型model
model
是实现数据驱动的核心,一个model
是一个由Proxy
代理的对象,模块的model
的数据来源是模块实例的data()
函数返回的对象。得益于Proxy
,我们可以实现数据劫持和数据监听,来做到数据改变的时候自动更新渲染。在使用的时候,你可以直接把
model
当作对象来操作:model
对象里的所有的对象都是一个model
,而每个model
保有一个模块列表,当model
内部的数据变化,会引起该model
的模块列表中所有模块的渲染,默认model
的模块列表中只有当前模块,如果你通过$data
的方式从父模块传入了一个object
给子模块,那么该object
也会作为一个model
传入子模块的model
中,且这个传入的model
所持有的模块列表中将会有父模块和子模块。由于是引用传值,所以无论是在父还是在子中改变了object
,都会触发两个模块的渲染。$set()
大多数情况下你都可以使用操作对象的方式来操作
model
,但是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
接收三个参数name
,string
类型,指令的名字,使用指令的时候需要在前面加上x-
;handler
,Function
类型,处理指令逻辑的方法,nodom会给这个方法传三个参数。module
,当前模块的实例;dom
,当前节点本次渲染的虚拟dom节点;src
,当前节点的在originTree中的虚拟dom节点。priority
,number
类型,默认值为10,可以不传,1-10为保留字段,数字越大优先级越低。自定义元素
自定义元素需要继承
DirectiveElement
类,且需要加入在DirectiveElementManager
中注册。定义自定义元素的构造器接收两个参数:
node
,该自定义元素的虚拟dom节点;module
,当前模块的实例。动画与过渡
我们使用
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
为后缀的一组类名,当然在传入给animation
指令的对象中你只需要提供前面的名字,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
触发动画/过渡之后。例如上面这个
shape
过渡的例子:过渡/动画控制参数
传入
x-animation
指令的对象不止上述提到的这些,还有一些控制参数,下表是所有可以传入的属性所示:进入/离开分开配置
对于一个元素的过渡/动画 我们可以分开配置不同的效果。这样传入
x-animation
指令的对象的写法会又所不同。 例如:路由
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