Nodom学习
安装后启动方法
在文件夹中,使用cli工具初始化
npm run build
打包文件,输出结果在/dist目录中
之后安装live-server
cmd中输入
live-server
即可查看example中对应的示例文件
引入方式
以ES Module的形式引入script文件
<script type='module'>
import{nodom,Module} from '../dist/nodom.esm.js'
</script>
渲染dom需要在html中添加标签,将此标签标记为渲染的容器。
此标签作为渲染的根节点,nodom传入的模块类都将插入此根节点。
<div id='app'></div>
<script>
import{nodom,Module} from '../dist/nodom.esm.js'
class Module1 extends Module{
template(){
return `
<div>
{{msg}}
</div>
`
}
data(){
return {
msg:'Hello World'
}
}
}
nodom(Module1,'div');
</script>
模块
模块定义
模块的定义需要继承Nodom提供的模块基类Module.
模板
同时可以通过template()方法添加模板代码,描述模板视图。
<script>
class ModuleA extends Module{
// template(props) {
// return `
// <div>
// <span>This is ModuleA</span>
// </div>`
// }
}
</script>
还可以通过data方法在模块中添加响应式的数据。
响应式数据
data(){
return {
msg:'Hello World'
}
}
我们将响应式处理后的对象称之为model,model为原始数据对象进行代理拦截的Proxy对象。
方法
在模块中定义方法,用于逻辑处理。
应用于事件的方法。
模板中的事件定义,通常以模块方法名传入(参考event),默认参数依次为Model,时间触发的虚拟Dom,Nodom封装事件NEvent,原生事件对象Event。
this在方法指向模块实例对象。
<script type="module">
import {nodom,Module} from "../dist/nodom.esm.js";
class Module1 extends Module{
//Nodom会将模板代码编译成虚拟Dom树,再渲染至真实Dom上
template() {
return `
<div>
<span>{{msg}}</span>
<button e-click="change">修改数据</button>
</div>
`
}
//定义模块需要的数据
data() {
return {
msg:'Hello World'
}
}
//自定义模块方法
change(model,Vdom,Nevent,event){
model.msg='nodom3';
}
}
nodom(Module1,'#app');
</script>
生命周期钩子函数
事件名 | 描述 | 参数 | this指向 |
---|---|---|---|
onBeforeRender | 渲染前执行事件 | 模块model | 当前模块实例 |
onBeforeFirstRender | 首次渲染前执行事件(只执行1次) | 模块model | 当前模块实例 |
onFirstRender | 执行首次渲染后事件(只执行1次) | 模块model | 当前模块实例 |
onRender | 执行每次渲染后事件 | 模块model | 当前模块实例 |
onMount | 挂载到html dom后执行事件 | 模块model | 当前模块实例 |
unUnmount | 从html卸载后执行事件 | 模块model | 当前模块实例 |
onCompile | 编译后执行事件 | 模块model | 当前模块实例 |
注意:
所有的模板都需要一个<div>
根节点
模块嵌套
在一个模块中使用另一个模块
<script type="module">
import {nodom,Module} from "../dist/nodom.esm.js";
class Module1 extends Module{
//Nodom会将模板代码编译成虚拟Dom树,再渲染至真实Dom上
template() {
return `
<div>
<span>{{msg}}</span>
<button e-click="change">修改数据</button>
<Module2/>
</div>
`
}
//定义模块需要的数据
data() {
return {
msg:'Hello World'
}
}
//自定义模块方法
change(model,Vdom,Nevent,event){
model.msg='nodom3';
}
modules = [Module2];
}
class Module2 extends Module{
template() {
return `
<div>
<span>这是另一个模块</span>
</div>
`
}
}
nodom(Module1,'#app');
</script>
模块别名
使用registModule
方法修改模块使用时的别名。
class ModuleA extends Module{
template(){
return `
<div>
<span>This is ModuleA</span>
</div>
`
}
}
// 模块A注册
registModule(ModuleA,'mod-a');
指令写法
Nodom的指令以x-
开头。如x-show
指令,用于控制一个元素是否渲染
事件写法
Nodom的事件命名为e-
+原生事件名
如:e-click
表达式(响应式数据)
使用{{}}
进行数据绑定
Nodom的表达式对原生JS表达式
实现了支持。确保双括号内传入的是单个JS表达式,即可获得返回计算结果。
import {nodom,Module} from "../dist/nodom.esm.js";
class Module1 extends Module{
//Nodom会将模板代码编译成虚拟Dom树,再渲染至真实Dom上
template() {
return `
<div>
<span>{{msg}}</span>
<button e-click="change">修改数据</button>
<button e-click="switchShow">开关</button>
<Module2 x-show={{isShow}}/>
<p>userName is:{{user.userName}}</p>
<p>Is the age older than 15 years old? {{user.age>15?true:false}}</p>
<p>userName 大写:{{user.userName.toUpperCase()}}</p>
</div>
`
}
//定义模块需要的数据
data() {
return {
msg:'Hello World',
isShow:true,
user:{
userName:'jack',
age:18
},
}
}
//自定义模块方法
change(model,Vdom,Nevent,event){
model.msg='nodom3';
}
//指令
switchShow(model){
model.isShow =! model.isShow;
}
modules = [Module2];
}
在表达式内,JS常见的内置对象是可用的,比如:Math、Object、Date等。由于表达式的执行环境是一个沙盒,不能在内部使用用户定义的全局变量。
如正则表达式、原生callback作为回调函数,在内部传参时。无法处理。
一个可行的解决方案是,将这些操作使用函数封装,在表达式内部调用封装好的函数。
class Module3 extends Module{
template() {
return `
<div>
<div x-repeat={{getRows(rows)}}>
name is :{{name}}
</div>
<span>{{handleStr(str)}}</span>
</div>
`
}
getRows(rows){
//map会返回一个新数组
return rows.map((item)=>{
item.name = 'name' + item.id;
return item;
});
}
handleStr(str){
return str.replace(/str/,'nodom');
}
data(){
return{
str:'this is str',
rows:[
{
id:1,
},{
id:2,
}
]
}
}
}
在表达式内,可以访问模块实例与表达式所在节点对应的model。包括(获取实例数据,访问模块属性,调用模块方法)
当表达式内的计算结果产生不可预知的错误,默认的,会返回空字符串,确保程序运行时不会出错。
事件绑定
Nodom中有专门的事件类NEvent来处理Dom的事件操作。如:e-click、e-mouseup等。支持所有HTML元素标准事件。
回调函数的参数:
事件自带4个参数
序号 | 参数名 | 描述 |
---|---|---|
1 | model | dom对应的model |
2 | vdom | 事件对象对应的虚拟dom |
3 | nEvent | Nodom事件对象 |
4 | event | html原生事件对象 |
如:
addCount(model,vdom,nEvent,event){
......
}
事件修饰符
在传入事件处理方法时,可以用:
分隔的形式传入指定时间修饰符。支持三种修饰符:
名字 | 作用 |
---|---|
once | 事件只执行一次 |
nopopo | 禁止冒泡 |
delg | 事件代理到父对象 |
指令
指令用于增强元素的表现能力。以设置元素属性(attribute)的形式来使用。指令具有优先级,按照数字从小到大,数字越小,优先级越高。优先级高的指令有限执行。
指令名 | 指令优先级 | 指令描述 |
---|---|---|
model | 1 | 绑定数据 |
repeat | 2 | 按照绑定的数组数据生成多个相同节点 |
recur | 2 | 递归结构 |
if | 5 | 条件判断 |
else | 5 | 条件判断 |
elseif | 5 | 条件判断 |
endif | 5 | 结束判断 |
show | 5 | 显示视图 |
slot | 5 | 插槽 |
module | 8 | 加载模块 |
field | 10 | 双向数据绑定 |
route | 10 | 路由跳转 |
router | 10 | 路由占位 |
Model 指令
model指令用于给view双向绑定数据,数据采用层级关系。
使用x-module指令加模块名,Nodom会自动创建实例并将其渲染。
Filed指令
filed指令用于实现输入类型元素,如input、select、textarea等输入元素与数据项之间的双向绑定。
配置说明:
- 绑定单选框radio:多个radio 的x-field值必须设置为同一个数据项,同时需要设置value属性,该属性与数据项可能选值保持一致。
- 绑定复选框checkbox:除了设置x-field绑定数据项外,还需要设置yes-value和no-value两个属性,分别对应选中和未选中时所绑定数据项的值。
- 绑定select:多个option选项可以使用x-repeat指令生成,同时使用x-field给select绑定初始数据即可。
- 绑定textarea:直接使用x-field绑定数据项即可。
class Module5 extends Module{
template(){
return `
<div>
<!-- 绑定name数据项-->
姓名:<input x-field="name" e-click="printInput"/>
<!-- 绑定sexy数据-->
性别:<input type="radio" x-field="sexy" value="M" e-click="printSexy"/>男
<input type="radio" x-field="sexy" value="F" e-click="printSexy"/>女
<!-- 绑定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-5-11',
edus:[
{eduId:1,eduName:'高中'},
{eduId:2,eduName:'本科'},
{eduId:3,eduName:'硕士研究生'},
{eduId:4,eduName:'博士研究生'},
]
}
}
printSexy(model)
{
console.log(model.sexy);
}
printInput(model)
{
console.log(model);
}
}
问题:双向绑定后的数据,在修改之后,数据类型自动转为字符类型
如下:
列表
在Nodom中,提供了两种方式来实现列表的渲染
1、通过内置指令x-repeat的方式。将列表数据直接传递给该指令。
2、通过Nodom实现的<for>
内置标签。该标签含有一个cond属性,用来传入需要渲染的列表数据。
class Module5 extends Module{
template(){
return `
<div>
<div class="code">
菜单:
<div x-repeat={{foods}}>
<span>菜名:{{name}},价格:{{price}}</span>
</div>
</div>
<div class="code">
菜单:
<for cond={{foods}}>
<span>菜名:{{name}},价格:{{price}}</span>
</for>
</div>
</div>
`
}
data(){
return {
foods:[
{
name:'青椒肉丝',
price:15,
},
{
name:'回锅肉',
price:20,
},
{
name:'宫爆鸡丁',
price:15,
},
{
name:'红烧肉',
price:25,
},
{
name:'红烧猪蹄',
price:30,
},
]
}
}
}
在x-repeat
指令会为生成出来的节点绑定对应model,如果需要访问外层model中的数据,可以在表达式中使用this.model
。这样表达式就会从模块的根model开始解析数据。
$index
这一变量,用来获取当前索引。使用之前需要指定索引的名称。
自定义过滤数组
class Main extends Module{
template(){
return `
<div>
<!-- 使用函数筛选列表 -->
<div x-repeat={{getFood(foods)}} >
菜名:{{name}},价格:{{price}}
</div>
<for cond={{getFood(foods)}} >
菜名:{{name}},价格:{{price}}
</for>
</div>
`
}
getFood(arr) {
return arr.filter(item => item.price > 22);
}
data(){
return {
foods:[
{
name:'青椒肉丝',
price:15,
},
{
name:'回锅肉',
price:20,
},
{
name:'宫爆鸡丁',
price:15,
},
{
name:'红烧肉',
price:25,
},
{
name:'红烧猪蹄',
price:30,
},
]
}
}
}
nodom(Main,"#app");
在自定义函数中传入的数据已经不是原来data中的初始数据了,而是做了响应式处理的响应式数据。
x-recur
指令可以用来处理复杂的嵌套列表
虚拟DOM
{
/**
* 元素名,例如<div></div>标签,tagName为div;<span></span>的tagName为span
*/
tagName: string;
/**
* Nodom中虚拟DOM的key是唯一的标识,对节点进行操作时提供正确的位置,获取对应的真实dom
*/
key: string
/**
* 绑定事件模型,在方法中可以传入model参数来获得模型中的值
*/
model: Model;
/**
* 存放虚拟dom的属性
*/
props:Object
/**
* 当前虚拟dom节点的父虚拟dom节点
*/
parent:IRenderedDom
/**
* 当前虚拟dom节点对应的VirtualDom
*/
vdom:VirtualDom
}
属性
属性 | 类型 | 定义 |
---|---|---|
tagName | string | 标签名如的他给Name为’div’ |
key | string | 是唯一的标识符,也可以通过key来获取虚拟dom的值 |
model | Model | 绑定Model |
props | Object | 存放当前节点的属性 |
parent | IRenderedDom | 当前节点的父虚拟dom节点 |
vdom | VirtualDom | 当前节点对应的VirtualDom,管理该节点的指令,事件等 |
数据传递
props传值
class Main extends Module{
template() {
return `
<div>
<!-- <User name="ad"></User>-->
<User $name={{name}}></User>
</div>
`
}
data(){
return {
name:'Nodom',
}
}
}
//模块传值,父模块传递给子模块
// class ModuleA extends Module{
// template(props) {
// let str;
// if(props.name=='add'){
// str = `<h1>add</h1>`;
// }else{
// str = `<h2>none</h2>`;
// }
// return `<div> ${str}</div>`
// }
// }
// registModule(ModuleA,'User')
Nodom数据传递为单项数据流,Props可以实现父模块向子模块的数据传递,但是这是被动的传递方式。如果需要将其保存至子模块内的代理数据对象,可以在传递的属性名前,加上$前缀,Nodom会将其传入子模块的根Model内,实现响应式监听。
import {Module, nodom, registModule} from "../dist/nodom.esm.js";
class Main extends Module{
template() {
return `
<div>
<!-- <User name="ad"></User>-->
<User $name={{name}}></User>
</div>
`
}
data(){
return {
name:'Nodom',
}
}
}
//数据传递
class ModuleB extends Module{
template(props) {
return `<div>
<h1>{{name}}</h1>
</div>`
}
}
//必须使用registModule的形式才能实现,响应式监听
registModule(ModuleB,'User');
反向传递
子模块向父模块传递值
import {Module, nodom, registModule} from "../dist/nodom.esm.js";
class Main extends Module{
template() {
return `
<div>
<!--<User name="ad"></User>-->
<!--<User $name={{name}}></User>-->
count={{sum}}
<User add={{this.add}}></User>
</div>
`
}
data(){
return {
name:'Nodom',
sum:0,
}
}
add=(num)=>{
this.model.sum++
}
}
class ModuleC extends Module{
template(props) {
this.parentChange=props.add;
return `
<div>
<button e-click="change">父模块+1</button>
</div>
`
}
change(){
this.parentChange(1)
}
onRender(){
console.log(this.parentChange);
}
}
registModule(ModuleC,'User');
深层数据传递
可以跨越多个模块层次的数据传递
Nodom提供一个GlobalCache
来管理共享数据。
GlobalCache
内置get、set、remove、subscribe
方法
import {Module, nodom, registModule,GlobalCache} from "../dist/nodom.esm.js";
class Main extends Module{
modules=[ModuleD];
template() {
return `
<div>
<!--<User name="ad"></User>-->
<!--<User $name={{name}}></User>-->
<!--<User add={{this.add}}></User>-->
{{msg}}
<ModuleD/>
</div>
`
}
data(){
return {
name:'Nodom',
sum:0,
msg:0,
}
}
onBeforeFirstRender(model){
let data = GlobalCache.get("globalData");
console.log(data);
// model.msg=data.msg;
//做出响应式修改。
GlobalCache.subscribe(this,"globalData",(val)=>{
model.msg=val.msg;
});
}
add=(num)=>{
this.model.sum++
}
}
//深层数据传递,对于跨越多个模块层次的数据传递
GlobalCache.set("globalData",{
msg:0,
});
class ModuleD extends Module{
template() {
return `
<div>
<button e-click="change">change</button>
</div>
`
}
change(){
let data = GlobalCache.get('globalData');
data.msg++;
GlobalCache.set("globalData",data);
}
}
也可以使用第三发数据发布-订阅库
在开发大型项目时,可以使用数据管理库帮助我们管理数据,使数据以可预测的方式发生变化,推荐使用Nodom团队开发的kayaks库,或者其他优秀的数据管理库。
Props中的template
若子模块中的模板生成依赖父模块中的某些字符串,可以使用以下方式传递:
插槽
插槽作为模板暴露的外部接口,增大了模板的灵活度,有利于模块化开发。Nodom以指令和自定义元素的方式实现插槽功能。
<!--自定义元素的方式使用插槽-->
<slot>
<h1>
title
</h1>
</slot>
<!--指令的形式使用插槽-->
<div x-slot='title'></div>
默认插槽
在模块标签内的模板代码会作为待插入的节点,如果子模块内有默认的插入位置<slot></slot>
,将会将节点插入该位置。如果没有待插入的内容,子模块内slot
标签将会正常显示。
import {Module,nodom,registModule} from "../dist/nodom.esm.js";
class Main extends Module{
template() {
return `
<div>
<User>
<button>我是父模块</button>
</User>
</div>
`
}
}
class ModuleA extends Module{
template(props) {
return `
<div>
<slot>
我是默认内容
</slot>
</div>
`
}
}
registModule(ModuleA,'User');
nodom(Main,'#app')
命名插槽
命名插槽就是给插槽定义插槽名,传入的标签需要与插槽名一致才可以发生替换。
class Main extends Module{
template() {
return `
<div>
<User>
<slot name="title">
<button>我是父模块title</button>
</slot>
<slot name="footer">
<button>我是父模块footer</button>
</slot>
</User>
</div>
`
}
}
//命名插槽
class ModuleB extends Module{
template(props) {
return `
<div>
<slot name="title">
我是title
</slot>
<slot name="footer">
我是footer
</slot>
</div>
`
}
}
registModule(ModuleB,'User');
内部渲染插槽
插槽内容在子模块渲染。就是相当于传递模板代码,而不再父模块内渲染。对于这种情况,只需要在子模块的插槽定义出,附加innerRender属性即可。
数据模型(Model)
Model
作为模块数据的提供这,绑定到模块的数据模型都由Model
管理。
Model
是一个有Proxy
代理的对象,Model
的数据来源有两个:
- 模块实例的
data()
函数返回的对象; - 父模块通过
$data
方式传入的值。
每个模块都有独立的Model
,但可以通过在使用子模块时传入属性useDomModel
的方式与子模块共享Model
,示例:
import {nodom,Module,registModule} from "../dist/nodom.esm.js";
class Main extends Module{
template() {
return `
<div>
<ModuleA useDomModel></ModuleA>
</div>
`
}
data(){
return {
name:'Nodom',
}
}
}
class ModuleA extends Module{
template(props) {
return `
<div>
{{name}}
</div>
`;
}
}
registModule(ModuleA,'User');
nodom(Main,'#app');
Model
会深层代理内部的object
类型数据
基于Proxy
,Nodom
可以实现数据劫持和数据监听,来做到数据改变时候的响应式更新渲染。
在使用时,可以直接把Model
当做对象来操作
class Main extends Module{
template() {
return `
<div>
{{count}}
<!-- <ModuleA useDomModel></ModuleA>-->
<button e-click="changeCount">click</button>
</div>
`
}
data(){
return {
title:'Hello',
name:'Nodom',
count:0,
obj:{
arr:[1,2,3]
}
}
}
changeCount(model){
model.count++;
}
}
Model与模块渲染
每个Model
存有一个模块列表,当Model
内部的数据变化时,会引起该Model
模块列表中所有模块的渲染。一个Model
的模块列表中默认只有初始化该Model
的模块,如果需要该Model
触发多个模块的渲染,则要将需要触发渲染的模块添加到该Model
对应的模块列表中(Model
与模块的绑定请查看API ModelManager.bindToModule
)
$set()方法
Model
中有一个$set
方法,可以应对一些特殊情况。例如,需要往Model
上设置一个深层次的对象。
change(model){
//会报错,因为data1为undefined
model.data1.data2.data3={a:'a'};
//使用$set方法可以避免该问题,如果不存在这么深层次的对象$set会帮忙创建
model.$set("data1.data2.data3",{a:'a'});
}
$watch()
Nodom在Model
里提供了$watch()
方法来见识Model
中的数据变化,当数据变化时执行指定的操作。
参数说明
参数名 | 类型 | 参数说明 |
---|---|---|
key | string或string[] | 监听属性 |
operate | Function | 监听触发方法,默认参数为(model,key,oldValue,newValue) |
module | Module | 监听模块,如果设置,则触发时,只针对该模块进行操作,否则如果model绑定了多个模块,则每个模块都会触发operate方法 |
deep | boolean | 如果设置为true,当key对应项为对象时,对象的所有属性、子孙对象所有属性都会watch,慎重使用该参数,避免watch过多造成性能缺陷。 |
取消watch
$watch
方法会返回一个函数,当不再需要watch
时,执行该函数即可取消watch
class Main extends Module{
template() {
return `
<div>
{{count}}
<!-- <ModuleA useDomModel></ModuleA>-->
<button e-click="changeCount">change</button>
<button e-click="watch">watch</button>
</div>
`
}
data(){
return {
title:'Hello',
name:'Nodom',
count:0,
obj:{
arr:[1,2,3]
}
}
}
changeCount(model){
model.count++;
}
//watch方法的使用
watch(model){
model.$watch('count',(model,key,oldVal,newVal)=>{
console.log('检测到数据变化');
console.log(model);
console.log(key);
console.log('oldVal:',oldVal);
console.log('newVal:',newVal);
})
}
}
渲染
Nodom的渲染是基于数据驱动的,也就是说只有Model内的数据发生了变化,当前模块才会进行重新渲染的操作。渲染时,Nodom将新旧两次渲染产生的虚拟Dom树进行对比,找到变化的节点,实现最小操作真实Dom的目的。
class Main extends Module{
template() {
return `
<div>
父模块:{{title}}
<!-- 点击按钮后,由于改变了响应式数据,触发了根模块的渲染-->
<button e-click="change">改变title</button>
<User title={{title}}></User>
</div>
`
}
data(){
return {
title:'parent',
name:'Nodom',
count:0,
obj:{
arr:[1,2,3]
}
}
}
change(model){
model.title='none';
}
}
//渲染 由于父模块传入的Props未发生改变,那么父模块的更新不会影响子模块。
class ModuleA extends Module{
template(props) {
return `
<div>
${props.title}{{title}}
</div>
`;
}
data(){
return {
title:'child'
}
}
}
registModule(ModuleA,'User');
Props的副作用
在使用props的场景下,如果我们传递的属性值发生改变,那么子模块会先触发编译模板的过程在进行渲染操作,也就是模块重新激活。
特殊的,在Props中,对于传递Object类型的数据,每次渲染,Nodom会将该模块默认为数据改变。
class Main extends Module{
template() {
return `
<div>
<button e-click="change">改变title</button>
<User title={{title}}></User>
</div>
`;
}
data(){
return{
title:'parent',
}
}
change(model){
model.title='none';
}
}
//模块A 由于父模块传入的Props数据发生了改变,ModuleA重新激活,触发template函数进行编译,再进行渲染
class ModuleA extends Module{
template(props) {
return `
<div>
${props.title}{{title}}
</div>
`
}
data(){
return {
title:'child'
}
}
}
registModule(ModuleA,'User');
单词渲染模块
如果想要摒弃Props带来的渲染副作用,Nodom提供单词渲染模块。单词渲染模块只有在首次渲染时才会结构Props,随后无论Props如何变化,都不会影响到模块本身。使用方式为在模块标签内附加renderOnce
属性
class Main extends Module{
template(){
return `
<div>
<button e-click='change'>改变title</button>
<!-- 添加renderOnce属性使其只渲染一次-->
<user renderOnce title={{title}}></user>
</div>
`
}
data(){
return {
title:'parent'
}
}
change(model){
model.title='none';
}
}
nodom(Main,"#app");
CSS支持
Nodom对CSS提供支持
在模板代码中添加<style></style>
标签中直接写入CSS样式,示例
import {nodom,Module,registModule} from "../dist/nodom.esm.js";
class Main extends Module{
template(props) {
return `
<div>
<h1 class="test">Hello nodom!</h1>
<!-- style标签创建css样式-->
<style>
.test{
color:red;
text-align: center;
}
</style>
</div>
`
}
}
nodom(Main,'#app');
在模板代码中的<style></style>
标签中通过表达式调用函数返回CSS样式代码串,示例代码如下:
import {nodom,Module,registModule} from "../dist/nodom.esm.js";
class Main extends Module{
template(props) {
return `
<div>
<h1 class="test">Hello nodom!</h1>
<style>
{{css()}}
</style>
</div>
`
}
css(){
return `
.test{
color:red;
}
`;
}
}
nodom(Main,'#app');
在模板代码中的<style></style>
标签汇总通过@import url('CSS url路径')
引入对应CSS样式文件,示例代码:
template() {
return `
<div>
<h1 class="test">Hello nodom!</h1>
<style>
@import url('./style.css')
</style>
</div>
`;
}
对模板代码中需要样式的节点直接写行内样式,示例代码如下:
class Module1 extends Module {
template() {
return `
<div>
<h1 style="color: red;" class="test">Hello nodom!</h1>
</div>
`;
}
}
nodom(Module1,"#app");
scope属性
在style中添加该属性后,Nodom会自动在CSS 选择器前加前置名。使CSS样式的作用域限定在当前模块内,不会影响到其他模块。
template() {
return `
<div>
<h1 class="test">Hello nodom!</h1>
<style scope="this">
@import url('./style.css')
</style>
</div>
`;
}
Cache
Nodom提供了缓存功能,缓存空间是一个Object,以key-value的形式存储在内存中;
key 的类型是string,支持多级数据分割,例如:China.capital;
value支持任意类型的数据。
用户可以自行选择将常用的内容存储在缓存空间,如下:
GlobalCache.set('China.capital','北京')
根据键名从缓存中读取数据,例子如下:
GlobalCache.get("China.captial")
根据键名从缓存中移除,例子如下:
GlobalCache.remove("China.captial")
另外,还提供对以下对象在内存中进行存储、获取和移除等操作。
- 指令实例
- 指令参数
- 表达式实例
- 事件实例
- 事件参数
- 虚拟dom
- html节点
- dom参数
动画与过渡
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
管理过渡
import {nodom,Module,GlobalCache} from "../dist/nodom.esm.js";
class Main extends Module{
template(props) {
return `
<div>
<button e-click="tiggerTransition">tiggerTransition</button>
<div x-animation={{transition}} style="width:100px;height:100px;background-color:cornflowerblue">
</div>
<style>
.myfade-enter-active,
.myfade-leave-active {
transition: all 1s ease;
}
.myfade-enter-from,
.myfade-leave-to {
opacity: 0;
}
.myfade-enter-to,
.myfade-leave-from {
opacity: 1;
}
</style>
</div>
`
}
data() {
return {
transition:{
name:'myfade',
tigger:true,
}
}
}
tiggerTransition(model){
model.transition.tigger = !model.transition.tigger
}
}
nodom(Main,'#app');
</script>
x-animation
管理动画
class Module1 extends Module{
template(props) {
return `
<div>
<button e-click="tiggerAnimation">tiggerAnimation</button>
<div x-animation={{animation}} style="width:100px;height:100px;background-color:blue"></div>
<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>
</div>
`;
}
data(){
return {
animation:{
name:'myfade',
tigger:true,
//type默认值为transition,如果是动画则需要指明
type:'animation'
}
};
}
tiggerAnimation(model){
model.animation.tigger = !model.animation.tigger
}
}
nodom(Module1,'#app');
对于部分常用的过渡效果,我们已经将其封装进入了nodomui.css文件,你只需要全局引入该css文件即可。
提供的过渡效果见下表:
name | 效果 |
---|---|
fade | 渐入渐出 |
scale-fixtop | 固定上面缩放 |
scale-fixleft | 固定左边缩放 |
scale-fixbottom | 固定底边缩放 |
scale-fixright | 固定右边缩放 |
scale-fixcenterX | 固定以X轴为对称轴往中间缩放 |
scale-fixcenterY | 固定以Y轴为对称轴往中间缩放 |
fold-height | 折叠高度 |
fold-width | 折叠宽度 |
进入/离开动画
在传入x-animation
指令的对象属性中设置isAppear
(默认值为true
)属性,可以配置当前的过渡/动画是否进入离开过渡/动画。
- 若未true,则表示在离开动画播放完成之后会隐藏该元素(display:none);
- 若未false,则表示在离开动画播放完成之后不会隐藏该元素。
钩子函数
在传入x-animation
指令的对象中设置hooks
属性,可以配置过渡/动画执行前后的钩子函数。且这两个函数名字固定,分别为before
和after
。
他们触发的时机为:
before
触发动画/过渡之前after
触发动画/过渡之后
class Module2 extends Module{
template(props) {
return `
<div>
<button e-click="tiggerTransition">tiggerTransition</button>
<div x-animation={{transition}} style="width:100px;height:100px;background-color:cornflowerblue"></div>
<style>
@import url('./nodomui.css')
</style>
</div>
`
}
data(){
return {
transition:{
name:'fade',
tigger:true,
hooks:{
//钩子函数的this指向model,第一个参数为module
//过渡执行前钩子函数
before(module){
console.log(module)
},
//过渡执行后钩子函数
after(module){
console.log(module)
}
}
},
};
}
tiggerTransition(model){
model.transition.tigger = !model.transition.tigger
}
}
nodom(Module2,'#app');
过渡/动画控制参数
传入x-animation
指令的对象不止上述提到的这些,还有一些控制参数,下表是所有可以传入的属性所示:
name | 作用 | 可选值 | 默认值 | 必填 |
---|---|---|---|---|
tigger | 触发动画 | true/false | true | 是 |
name | 过渡/动画名(不包含-enter-active等后缀) | - | 无 | 是 |
isAppear | 是否是进入离开过渡/动画 | true/false | true | 否 |
type | 是过渡还是动画 | ‘aniamtion’/‘transition’ | ‘transition’ | 否 |
duration | 过渡/动画的执行时间 | 同css的duration的可选值 | ‘’ | 否 |
delay | 过渡/动画的延时时间 | 同css的delay的可选值 | ‘0s’ | 否 |
timingFunction | 过渡/动画的时间函数 | 同css的timingFunction的可选值 | ‘ease’ | 否 |
hooks | 过渡/动画执行前后钩子函数 | before/after函数或者enter/leave对象 | 无 | 否 |
分别配置enter/leave
对于一个元素的过渡/动画可以分开配置不同的效果。
import {nodom,Module} from "./nodomui.js";
//分别配置enter/leave
class Module3 extends Module{
template(props) {
return `
<div>
<button e-click="tiggerTransition">tiggerTransition</button>
<div x-animation={{transition}} style="width:100px;height:100px;background-color:cornflowerblue"></div>
<style type="text/css">
@import url("nodomui.css")
</style>
</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);
},
},
}
}
}
}
tiggerTransition(model){
model.transition.tigger = !model.transition.tigger
}
}
nodom(Module3,'#app');
路由
Nodom内置了路由功能,可以配合构建单页应用,用于模块间的切换。需要做的是将模块映射到路由。并决定最终在哪里渲染他们。
创建路由
Nodom提供createRoute
方法,用于注册路由。以Object
配置的形式指定路由的路径、对应的模块、子路由等。
路由跳转
借助x-route指令,用户无需手动控制路由跳转。但可以使用Nodom提供的接口进行手动控制路由跳转:
Router.go
Router.redirect
示例:
首先创建modules
文件夹,在modules
文件夹下创建route
文件夹,再创建routemain.js
文件。作为路由渲染的主文件。
import {Module,Router,Util} from "../../../dist/nodom.esm.js";
export class RouteMain extends Module{
template() {
return `
<div>
<a x-route="/router" class={{page1?'colorimp':''}} acctive:'page1'>page1</a>
<div x-router></div>
</div>
`
}
data(){
return {
page1:true
}
}
onBeforeFirstRender(){
let hash = location.hash;
if(hash){
Router.go(hash.substr(1));
}else{
Router.go('/router/route1/home');
}
}
}
再创建routeconfig.js
文件,再其中添加路由配置
import {createRoute, Router} from "../../../dist/nodom.esm.js";
import {RouteDir} from "./routedir.js";
export function initRoute(){
Router.basePath = '/webroute';
createRoute([{
path:'/router',
module: RouteDir,
}])
}
再创建routedir.js
文件
import {Module} from "../../../dist/nodom.esm.js";
export class RouteDir extends Module {
template(){
return `
<div>
<p class='title'>路由用法例子</p>
<div style='background:#f0f0f0'>
<div style='border-bottom: 1px solid #999' test='router'>
<a x-repeat={{routes}} x-route={{path}} class={{active?'colorimp':''}} active='active' style='margin:10px'>{{title}}</a>
</div>
<div x-router test='routerview'></div>
</div>
</div>
`;
}
data(){
return{
routes: [{
title: '路由用法1-基本用法',
path: '/router/route1',
active: true
},
{
title: '路由用法2-路由参数用法',
path: '/router/route2',
active: false
},
{
title: '路由用法3-路由嵌套用法',
path: '/router/route3',
active: false
}
]
}
}
}
最终创建html文件引入路由配置和主路由文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>route</title>
<style>
.colorimp {
color: red;
}
</style>
</head>
<body>
<div></div>
</body>
<script type='module'>
import {nodom,Module} from '../dist/nodom.esm.js'
// import {MdlRouteMain} from "../examples/modules/route/mdlroutemain.js";
import {RouteMain} from "./modules/route/routemain.js";
import {initRoute} from "./modules/route/routeconfig.js";
nodom(RouteMain,'div');
initRoute();
</script>
</html>
初步路由配置基本完成
路由嵌套
创建pmod1.js、pmod2.js、pmod3.js
文件分别如下
pmod1.js
import {Module,Router} from "../../../dist/nodom.esm.js";
export class PMod1 extends Module{
template(props) {
return `
<div class="result code1">
<div style="border-bottom: 1px solid #999">
<a x-route="/route/route1/home" class={{home?'colorimp':''}} active='home'>首页</a>
<a x-route="/route/route1/list" class={{list?'colorimp':''}} active='list'>列表</a>
<a x-route="/route/route1/data" class={{data?'colorimp':''}} active='data'>数据</a>
</div>
<a x-route={{route2}}>to router2</a>
<button e-click='redirect'>to router3</button>
<div x-router test='1'></div>
</div>
`
}
data(){
return {
home:true,
list:false,
data:false,
}
}
onBeforeFirstRender(model){
setTimeout(()=>{
model.route2 = '/router/route2/rparam/home/1';
},0)
}
redirect(){
Router.redirect(
'/router/route3/r1/r2'
)
}
}
pmod2.js
import {Module} from "../../../dist/nodom.esm.js";
export class PMod2 extends Module{
template(props) {
return `
<div class="result code1">
<div style='border-bottom: 1px solid #999' test='route1'>
<a x-repeat={{routes}} x-route={{path}} class={{active?'colorimp':''}} active='active' style='margin:10px'>{{title}}</a>
</div>
<div x-router test='2'></div>
</div>
`
}
data(){
return {
routes:[{
title:'首页2',
path:'/router/route2/rparam/home/1',
useParentPath:true,
active:false
},{
title:'列表2',
path: '/router/route2/rparam/list/2',
useParentPath: true,
active: false
},{
title:'数据2',
path: '/router/route2/rparam/data/3',
useParentPath: true,
active: false
}]
}
}
}
pmod3.js
import {Module} from '../../../dist/nodom.esm.js'
export class PMod3 extends Module {
template(){
return `
<div class='result code1'>
<div style='border-bottom: 1px solid #999'>
<a x-route='/router/route3/r1/r2'>加载子路由r2</a>
</div>
<div x-router test='3'></div>
</div>
`
}
onRender(model){
console.log('render',model)
}
}
在routeconfig.js
文件中添加对应的路由二级跳转
export function initRoute(){
Router.basePath = '/webroute';
createRoute([{
path:'/router',
module: RouteDir,
routes:[
{
path:'/route1',
module:PMod1,
},
{
path: '/route2',
module: PMod2,
},
{
path: '/route2',
module: PMod3,
},
]
}])
}
完善路由功能
使用路由传值、路由事件等功能
分别创建mod1、mod2、mod3、mod4、mod5、mod6、mod7、mod8.js
文件
mod1.js
文件
import {Module} from "../../../dist/nodom.esm.js";
export class Mod1 extends Module{
template(props) {
console.log('template');
return `
<div>
这是首页,路径是{{$route.path}}
</div>
`;
}
onCompile(){
console.log("compile");
}
}
mod2.js
文件
import {Module} from "../../../dist/nodom.esm.js";
export class Mod2 extends Module{
template(props) {
return `
<div>
这是商品列表页,路径是{{$route.path}}
</div>
`;
}
}
mod3.js
文件
import {Module} from "../../../dist/nodom.esm.js";
export class Mod3 extends Module{
template(props) {
return `
<div>
这是数据页,路径是{{$route.path}}
</div>
`;
}
}
mod4.js
文件
import {Module} from "../../../dist/nodom.esm.js";
export class Mod4 extends Module{
template(props) {
return `
<div test="1">
这是{{$route.data.page}}页,编号是{{$route.data.id}}
</div>
`
}
onBeforeFirstRender(model){
console.log(model);
}
}
mod5.js
文件
import {Module} from "../../../dist/nodom.esm.js";
export class Mod5 extends Module{
template(props) {
return `
<div class="code1">
路由r1加载的模块
<div x-router></div>
</div>
`;
}
}
mod6.js
文件
request
发起请求取得data1.json
中的文件
import {Module,request} from "../../../dist/nodom.esm.js";
class Module61 extends Module{
template(props) {
return `
<div>
<p style="background-color: #f0f0f0">{{name}},{{price}}</p>
</div>
`
}
}
export class Mod6 extends Module{
modules=[Module61];
template(props) {
console.log('mod5');
return `
<div>
vip is:{{d1.vip}}
<Module61 x-repeat={{d1.foods}} useDomModel></Module61>
<ul>
<li x-repeat={{d1.foods}}>{{name}},价格:{{price}}</li>
</ul>
<input x-field='title'/>
<div>{{title}}</div>
</div>
`
}
onBeforeFirstRender(model){
request({
url:'/nodom3/examples/data/data1.json',
type:'json'
}).then(r=>{
model.d1=r;
console.log(r);
})
}
}
mod7.js
文件
import {Module} from '../../../dist/nodom.esm.js'
/**
* 路由主模块
*/
export class Mod7 extends Module {
template(){
return '<span>这是商品详情页</span>';
}
}
mod8.js文件
import {Module} from '../../../dist/nodom.esm.js'
/**
* 路由主模块
*/
export class Mod8 extends Module {
template(){
return '<span>这是商品描述页</span>'
}
}
最后在routeconfig中配置路由项
import {createRoute, Router} from "../../../dist/nodom.esm.js";
import {RouteDir} from "./routedir.js";
import {PMod1} from "./pmod1.js";
import {PMod2} from "./pmod2.js";
import {PMod3} from "./pmod3.js";
import {Mod4} from "./mod4.js";
import {Mod7} from "./mod7.js";
import {Mod8} from "./mod8.js";
import {Mod5} from "./mod5.js";
import {Mod6} from "./mod6.js";
export function initRoute(){
Router.basePath = '/webroute';
createRoute([{
path:'/router',
module: RouteDir,
routes:[
{
path:'/route1',
module:PMod1,
routes:[{
path:'/home',
module:()=>import('/nodom3/try/modules/route/mod1.js'),
},{
path:'/list',
module:()=>import('/nodom3/try/modules/route/mod2.js'),
},{
path: '/data',
module:()=>import('/nodom3/try/modules/route/mod3.js'),
}],
onLeave:function (model) {
}
},
{
path: '/route2',
module: PMod2,
onEnter:function (){
},
routes: [{
path: '/rparam/:page/:id',
module:Mod4,
routes:[{
path:'/decs',
module:Mod7,
},{
path: '/comment',
module: Mod8,
}]
}]
},
{
path: '/route3',
module: PMod3,
routes: [{
path: 'r1',
module:Mod5,
routes: [{
path: '/r2',
module: Mod6
}]
}]
},
]
}])
}
在/route2
中的嵌套路由使用到路由传值功能:page/:id
其中路由配置项中的onLeave:function(){}、onEnter:function(){}
为单路由事件
默认路由
浏览器刷新时,会从服务器请求资源,nodom路由在服务器没有匹配的资源,则会返回404。通常的做法是: 在服务器拦截资源请求,如果确认为路由,则做特殊处理。
假设主应用所在页面是/web/index.html,当前路由对应路径为/webroute/member/center。刷新时会自动跳转到/member/center路由。相应浏览器和服务器代码如下:
浏览器代码
import {Router,Module} from './nodom.esm.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};
生态
NodomUI
Kayaks
数据管理库,用于开发大型项目。
Nodom VsCode插件
提供模板代码高亮功能,以及其他多种辅助功能。
- 本文链接:https://archer-lan.github.io/2023/11/20/Nodom%E5%AD%A6%E4%B9%A0/
- 版权声明:本博客所有文章除特别声明外,均默认采用 许可协议。