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);
            }
    }

问题:双向绑定后的数据,在修改之后,数据类型自动转为字符类型

如下:

image-20220801093030941

列表

在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类型数据

基于ProxyNodom可以实现数据劫持和数据监听,来做到数据改变时候的响应式更新渲染。

在使用时,可以直接把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为过渡的触发条件

过渡分为enterleave,触发enter还是leavetigger的值确定。

  • tiggertrue,触发enter
  • tiggerfalse,触发leave

对于enter过渡,需要提供以-enter-active-enter-from-enter-to为后缀的一组类名。在传入给x-animation指令的对象中只需要将名字传入给name属性,而不必添加后缀,x-animation在工作时会自动的加上这些后缀。这些规则对于leave过渡同理。

tiggertrue时,指令首先会在元素上添加-enter-from-enter-active的类名,然后再下一帧开始的时候添加-enter-to的类名,同时移除掉-enter-from的类名。

tiggerfalse时,处理流程完全一样,只不过添加的是以-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属性,可以配置过渡/动画执行前后的钩子函数。且这两个函数名字固定,分别为beforeafter

他们触发的时机为:

  • 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插件

提供模板代码高亮功能,以及其他多种辅助功能。