nodom3
TypeScript icon, indicating that this package has built-in type declarations

0.2.2 • Public • Published

nodom是一款基于数据驱动的web mvvm框架。用于搭建单页应用(SPA)。内置路由,提供数据管理功能,支持模块化开发。在不使用第三方工具的情况下可独立开发完整的单页应用。

开始

源码

  1. gitee: https://gitee.com/weblabsw/nodom3
  2. github: https://github.com/nodomjs/nodom3

npm包

  1. nodom3: https://www.npmjs.com/package/nodom3
  2. nodom3-cli(脚手架):https://www.npmjs.com/package/nodom3-cli

API

更多使用细节参考API

目录结构

  1. 核心库目录./core:核心框架源码。
  2. 扩展目录./extend:预定义指令、自定义element和事件。
  3. 示例目录./examples:示例。
  4. 发布目录./dist:发布包,所有示例从该目录引入编译后的nodom文件。

dist目录文件说明

  1. nodom.esm.js:es module模式的开发包
  2. nodom.esm.min.js: es module模式的生产包

示例

以vscode为例,使用Live Server插件启动./examples目录下的html文件即可,示例目录总入口在index.html文件。

编译

先运行npm i安装依赖,具体依赖包参考package.json文件“devDependencies”配置项,安装依赖包后,执行“npm run build”,编译结果在“/dist”目录中。

调试模式

使用Nodom.debug()启动调试模式,调试模式会对表达式的异常进行输出,启动调试模式示例如下:

import{Nodom} from '/dist/nodom.esm.js'
Nodom.debug();

国际化

使用Nodom.setLang(language)设置语言,默认为中文,Nodom支持语言包括:

设置项 描述
zh 中文
en 英文

设置语言方法示例如下:

import{Nodom} from '/dist/nodom.esm.js'
//设置语言为英文
Nodom.setLang('en');

实例化单例模式

使用Nodom.Use(clazz)以单例模式实例化类,实例化后,可以通过Nodom['$'+类名]方式进行使用,便于用户在代码中当作静态类使用。示例如下:

import{Nodom,Router} from '/dist/nodom.esm.js'
//启用Router功能
const router = Nodom.use(Router);
//以下两种方式使用,foo为Router类的成员方法
router.foo();
//或
Nodom['$Router'].foo();

CDN

下列代码引入nodom.esm.min.js文件,即es module模式的nodom生产环境包。

import{Nodom,Module} from "https://unpkg.com/nodom3"

下载引入

Nodom使用ES Module实现模块化,无需构建工具即可完成模块化开发,引入方式如下:

<script type="module">
	//引入nodom和Module
    import{Nodom,Module} from '/dist/nodom.esm.js'
	//定义模块类
	class Module1 extends Module{
		...
	}
	//启动应用,把Module1渲染到document.body
	Nodom.app(Module1);
</script>

第一个例子

此例子在页面中输出"Hello Nodom"。

假设你已经掌握一定的Html,Css,JavaScript基础,如果没有,那么阅读文档将会有些困难。

<!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>nodom examples - first</title>
	</head>
	<body>
	</body>
	<script type='module'>
		import{Nodom,Module} from '/dist/nodom.esm.min.js'
		class MHello extends Module {
			//模板函数,返回模板串
			template() {
				return `
					<div>
						Hello {{name}}
					</div>
				`;
			}
			data(){
				return{
					name:'Nodom'
				}
			}
		}
		//把MHello模块渲染到document.body下
		Nodom.app(MHello);
	</script>
</html>

后续示例代码主要阐述各类用法,主要对各示例的模块类进行描述,完整的使用需要参考上面代码结构。

核心概念

模块(Module)

Nodom以模块为单位进行应用构建,一个应用由单个或多个模块组成。模块定义需要继承Nodom提供的模块基类Module

class Module1 extends Module{
	//your code
}

模板(Template)

模板是模块必不可少的组成元素,通过template()方法返回字符串形式(建议使用模板字符串)的模板代码,Nodom采用基于HTML的模板语法。

template(){
	return `
		<div>
			Hello,Nodom
		</div>
	`;
}

模型(Model)

模型是模块必不可少的组成元素,通过data()方法返回模块所需的数据对象,类型为object,Nodom对数据对象做响应式处理,响应式处理后的数据对象,Nodom称为Model对象,并存储在模块实例中。

注:如果data方法不存在,则会创建一个空模型

data(){
	return {
		name:'nodom'
	}
}	

为了描述方便,随后的章节中,我们将响应式处理后的对象称为Model。一个Model中还可能包含其它Model对象。Model实际是对原始数据对象进行代理拦截的Proxy对象。

指令(Directive)

为增强dom节点的使用,增加了指令功能,指令用于模板串中,指令以“x-”开头,作为属性放置于标签头中,见下面代码的x-repeat,目前NoDom支持指令:module,model,repeat,class,if,else,show,field,route,router,详情见指令

class M1 extends Module{
	template(props){
		return `
			<div>
				<!-- x-repeat指令 -->
				<div x-repeat={{rows}}>
					{{name}}
				</div>
			</div>
		`
	}
	data(){
		return{
			rows:[
				{name:nodom},
				{name:noomi}
			]
		}
	}
}

表达式(Expression)

表达式用于数据,以{{expr}}表示,其中expr为你自己的表达式串,表达式可以作为元素属性值、文本节点值使用,支持属性运算、JS内置对象操作、模块方法操作及其组合操作,示例如下,详情见表达式

class M1 extends Module{
	template(props){
		return `
			<div>
				<div x-repeat={{getRows()}}>
					<!-- 直接返回name的值 -->
					{{name}}
				</div>
			</div>
		`
	}
	getRows(){
		return [
			{name:nodom},
			{name:noomi}
		]
	}
}

事件(NEvent)

事件和元素的事件相对应,以"e-"开头,覆盖html标准事件类型和nodom自定义事件类型,模板中事件定义不能带参数,NoDom会自动传递约定的参数,共四个,依次为:

序号 参数 类型
1 事件对应dom的model Model
2 事件对应的虚拟dom RenderedDom
3 nodom event对象 NEvent
4 Html Event对象 Html Event

示例如下,更多详情见事件

class M1 extends Module{
	template(props){
		return `
			<div>
				<!-- click 事件 -->
				<button e-click='click'>点击</button>
				...					
			</div>
		`
	}
	//事件方法
	click(model,dom,evObj,event){
		...
	}
}

虚拟Dom、编译及渲染

  1. 模板串经过编译后,形成虚拟dom树,树中节点为虚拟Dom(VirtualDom);
  2. 虚拟dom树经过renderDom方法渲染后,形成渲染树,树中节点为渲染节点(RenderedDom);
  3. 渲染树经过renderToHtml方法渲染后,渲染到html document,渲染方式分为首次渲染和增量渲染。

详细介绍

模块(Module)

定义模块类时,类名必须全局唯一(ModuleA和modulea是两个合法且不同的类名,但在nodom中会当作一个模块类)。

用户在编写模块时,主要用到5个部分,模块声明,模板,模型,方法和事件

模块声明

当模块中需要引入其它模块时,需要在该模块中声明,声明方式为:modules=[子模块类1,子模块类2,...]。当然,如果该模块已在其他地方声明或采用Nodom.registModule方法注册,此模块中可以不再声明。示例如下:

引用模块M1(文件名为m1.js)定义如下,需要在class前用export修饰(es module方式)。

import{Module} from '/dist/nodom.esm.js'
export class M1 extends Module{
	...
}

主模块定义如下:

import{Module} from '/dist/nodom.esm.js'
import{M1} from './m1.js'
class Main extends Module{
	//声明子模块,此处需区分大小写
	modules=[M1];
	//模板
	template(props){
		return `
			<div>
				<!--此处直接用类名使用子模块,不区分大小写-->
				<m1/>
			</div>
		`
	}
	...
}

模板(Template)

模板在模块中用template()进行声明,参数为props,props为从父模块(如果该模块为子模块)对应标签传递的属性(attribute),改写上例:

M1模块类定义

export class M1 extends Module{
	template(props){
		//根据不同的type生成不同的模板串
		if(props.type===1){
			return `
				<div>
					type为1的模板
				</div>
			`
		}else{
			return `
				<div>
					type不为1的模板
				</div>
			`
		}
	}
}

主模块类定义

import{Module} from '/dist/nodom.esm.js'
import{M1} from './m1.js'
class Main extends Module{
	modules=[M1];
	template(props){
		return `
			<div>
				<button e-click='changeType'>修改type</button>
				<m1 type={{mytype}}/>
			</div>
		`
	}
	data(){
		return{
			mytype:1
		}
	}
	//点击按钮修改mytype
	changeType(){
		this.model.mytype = this.model.mytype===1?0:1;
	}
}

模板的写法遵循两个基本原则:

  1. 所有的标签都应该闭合,没有内容的标签可以写成自闭合标签;
<!-- 闭合标签 -->
<div>do something</div>
<!-- 自闭合标签 -->
<ModuleA />
  1. 所有模块的模板都应该有一个根节点。
<!-- 外层div作为该模块的根 -->
  <div> 
      <!-- 模板代码 -->
      template code...
  </div>

模型(Model)

模型通过data()方法返回模块所需的数据对象,如果data方法不存在,则会创建一个空模型,在模块方法中,根模型通过this.model访问。
model进行分层提取,子节点自动继承父节点model对象,x-model指令可以修改节点对应的model对象

class Main extends Module{
	template(){
		return `
			<div p1={{data}}>
				<span>{{data}}</span>
			</div>
		`
	}
	data(){
		return{
			data:'nodom'
		}
	}
}

渲染后的结果为:

	<div p1='nodom'>
		<span>nodom</span>
	</div>

可以看到div节点和span节点都使用了根model。

下例通过x-model修改dom节点model对象

class Main extends Module{
	template(){
		return `
			<div>
				<div x-model='date'>{{year}}-{{month}}-{{day}}</div>
				<!-- 等价于 -->
				<div>{{date.year}}-{{date.month}}-{{date.day}}</div>
			</div>
		`
	}
	data(){
		return{
			date:{
				year:2017,
				month:11,
				day:15
			}
		}
	}
}

渲染后的结果为:

<div>
	<div>2017-11-15</div>
	<div>2017-11-15</div>
</div>

通过上例可以看到,x-model指令设置了第一个div节点的model对象为this.model.date,更多详情见指令

方法(Method)

模块类和通常的JavaScript类一致,模块内的方法可以使用在模板中,主要用于事件和表达式,也可以像普通方法那样使用,对于所有方法,this都指向模块实例(与普通JavaScript类一致)。示例如下:

class Module1 extends Module{
	template(){
		return `
			<div>
				<button e-click='change'>change</button>
				<div class={{genClass(type)}}>Hello {{name}}</div>
				<!-- style -->
				<style>
					.class1{
						color:red;
					}
					.class2{
						color:blue;
					}
				</style>
			</div>
		`
	}
	//定义模块需要的数据
	data(){
		return {
			name:'Nodom'
		}
	}	
	//此方法用于事件,参数无法手动传递
	//有以下四个默认参数:Model,虚拟Dom, NEvent对象,HtmlEvent对象
	change(model,dom,nevent,event){
		model.name='Nodom3';
		this.model.type = this.model.type === 1?0:1;
	}
	//此方法用于表达式,参数type可以手动传递,也可以通过this.model获取
	genClass(type){
		if(type === 1){
			return 'class1';	
		}else{
			return 'class2';
		}
	}
}

模块事件(Module Event)

模块事件是在Module不同工作环节被调用的方法,定义方式与普通方法一致,参数为model,当然也可以通过this.model操作。Nodom提供的模块事件如下,注意区分大小写:

事件名 描述 前置事件 后置事件
onInit 初始化后(constructor后,已经有model对象,但是尚未编译,只执行1次) onCompile
onCompile 模板编译后执行事件,如果模板串有改动,则会重新编译,此时已存在VirtualDom树 onInit onBeforeFirstRender 或 onBeforeRender
onBeforeFirstRender 首次渲染前执行(只执行1次) onCompile onRender
onBeforeRender 每次渲染前执行 onBeforeRender或无 onFirstRender或onRender
onFirstRender 首次渲染后执行(只执行1次),此时已有RenderedTree onBeforeRender onRender
onRender 每次渲染后执行,此时已有RenderedTree,如果为增量渲染,尚未执行Diff(新旧渲染树对比)运算 onFirstRender或onBeforeRender onBeforeMount
onBeforeMount 挂载到document前执行 onRender onMount
onMount 挂载到document后执行 onBeforeMount
onBeforeUnMount 从document脱离前执行 onUnMount
onUnMount 从document脱离后执行 onBeforeUnMount
onBeforeUpdate 更新到document前,增量渲染师时有效 onRender onUpdate
onUpdate 更新到document后,增量渲染时有效 onBeforeUpdate

其中 onInit,onBeforeFirstRender,onFirstRender只执行1次;onBeforeRender,onRender每次执行,其它事件则满足条件时执行。

示例代码如下:

class Hello extends Module{
	template(){
		return `
			<div>
				Hello World
			</div>
		`
	}
	//模块在渲染前会在控制台输出 onBeforeRender
	onBeforeRender(model){
		console.log("onBeforeRender");
	}
	//模块在初始化后执行
	onInit(model){
		console.log("oninit");
	}
	onRender(model){

	}
	...
}

模块状态

模块分为三个状态,包括:

状态名 描述
INIT 已初始化
UNMOUNTED 未挂载到document
MOUNTED 挂载到document

模块注册和别名

使用Nodom.registModuleAPI注册模块,注册的同时可提供别名。

// 定义模块A
export class ModuleA extends Module{
   template(){
    	return `
  		<div>
  			<span>This is ModuleA</span>
  		</div>
  	`
  }
}
// 模块A注册并设置别名(别名不区分大小写)
Nodom.registModule(ModuleA,'mod-a');

class Main extends Module{
    template(){
    	return `
  		<div>
  		   <span>This is Main</span>
  			<!-- 使用模块A注册时使用的别名 -->
  			<mod-a />
			<!-- 以下两种写法效果与上面一样 -->
			<modulea />
			<ModuleA />
  		</div>
  	`
  }
}

表达式(Expression)

表达式是一段可执行代码,代码以{{}}包裹,并可返回一个结果,如:Math.round(x),x+y*z等。其中变量由model提供,支持标准js运算符、js内置对象如:Math、Object、Date等。

注意事项

  1. 由于表达式的执行环境是一个沙盒,请勿在内部使用用户定义的全局变量。
  2. Nodom表达式并不支持所有的Javascript表达式,对于某些原生函数如Array.prototype.map()等,这些原生函数接收一个callback作为回调函数,Nodom无法处理这些回调函数,因为这些回调函数的参数由内部传入。
  3. 还有一些情况是函数内接收字面量形式的正则表达式时,如String.prototype.replace()等,Nodom会将正则表达式解析为Model内部的变量,导致这些函数执行异常。
  4. 一个可行的解决方案是将这些操作使用模块方法封装,在表达式内部调用封装好的模块方法即可。
  5. 一些常见非表达式写法包括:赋值,流程控制。避免使用它们,如:
{{ let a = 1 }}
{{ if (true) { return 'HelloWorld!' } }}

保留字

表达式提供了两个保留字:this和$model,其中:

  • this: 模块实例,可以通过它访问模块所有方法、属性和模型,如:this.name,this.model.age等。
  • $model: 当前节点对应的model,如:$model.age。

表达式示例

<div>
    {{20*((price+2)*discount)}}
	<div>{{year + '年' + month + '月'}}</div>
	<div>多级数据:{{ac.age.as}}</div>
	<h2>数据计算</h2>
	<div>价格:{{Math.round(price * discount) + 'hello'}}</div>
	<--需在模块中提供cacDis方法-->
	<div>折扣:{{cacDis(price*discount)}}</div>
	<--需在模块中提供addStr方法-->
	<div>描述:{{30 + addStr('hello' + desc) + 20}}</div>
	<div>随机折扣:{{(Math.random()*price).toFixed(1)}}</div>
	<--需在模块中提供genDate方法-->
	<div>当前日期是:{{genDate(date1)}}</div>
	<div>当前日期时间是:{{genDate(date1,1)}}</div>
	<div>当前时间是:{{genDate(date1,2)}}</div>
	<div x-if={{Object.keys(goods).length>0}}>商品列表存在则显示</div>
	<div>路径:{{'/'+'path'+'/'+url}}</div>
	<div>{{!true}}</div>
	<div>转换为小写字母:{{name.toLowerCase()}}</div>
	<div>转换为大写字母:{{name.toUpperCase()}}</div>
	<div>数组求和:{{sum(...arr)}}</div>
	<div>判断数组中有没有‘num’: <span x-if={{new Set(arr).has(num)}}>true</span></div>
	<div>价格求和: {{sum(1,2)+price}}</div>
	<div>{{genDate(new Date().getTime())}} 是否为工作日:<span x-if={{new Date().getDay()<6}}>true</span></div>
	<div>货币:¥{{(price*discount).toFixed(1)}}</div>
	<div x-if={{price>30 && discount !== undefined}}>是低价商品并且还有折扣</div>
	<div>计算:{{cac(1,2)+ (Math.round((price * discount))).toFixed(1) + 1}}</div>
	<div>instanceof用法:{{arr instanceof Array}}</div>
	<div>{{num+1}}</div>
	<div>三目运算:{{num>0?1:0}}</div>
	<div>对象判断:{{{x:1,yyy:2}.constructor.name === 'Object'}}</div>
	<div>数组方法:{{arr.join(',')}}</div>
	<div>使用this:{{desc + ' ' + this.state}}</div>
	<div>扩展运算-数组求和:{{sum(...arr)}}</div> 
	<div>typeof:{{typeof arr}}</div>
</div>

表达式值

表达式都应该有一个返回值,如果表达式内的计算结果产生不可预知的错误,默认会返回空字符串,确保程序运行时不会出错。

如果在调试模式,出现计算异常时,会在控制台输出表达式计算异常相关信息。

事件(NEvent)

可以通过两种方式定义事件:

  1. 在模板中使用e-事件名='事件方法名'在模板中定义;
  2. 在js代码中使用new NEvent(module,eventName,eventString|handler)方法定义。

绝大部分场景,采用第1种方式定义,后续示例采用第1种方式。

示例如下:

class Main extends Module{
	template(){
		return `
			<div>
				<button e-click="add">addNum</button>
				<div e-mouseenter="enter">mouseenter test</div>
			</div>
		`
	}

	add(model,dom,nevent,event){
		...
	}
	enter(model,dom,nevent,event){
		...
	}

事件参数

在模板配置事件时,只需要事件名,而不能携带参数,Nodom会传递给事件方法4个参数,见上例中click和enter方法,参数如下:

序号 说明 类型
1 事件对象对应虚拟dom的model Model
2 事件对象对应虚拟dom RenderedDom
3 nodom event对象 NEvent
4 HtmlEvent对象 Html Event

事件修饰符

在传入事件处理方法的时,允许以:分隔的形式传入指定事件修饰符,多个修饰符可混合使用。 事件处理支持4种修饰符:

名字 作用
once 事件只执行一次
nopopo 禁止冒泡
delg 事件代理到父对象
capture 使用useCapture模式

示例如下:

class Main extends Module{
	template(){
		return `
			<div>
				<h3>只触发一次</h3>
				<button e-click="tiggerOnce:once">addNum</button>
				<div> num is:{{num}} </div>
				<h3>禁止冒泡</h3>
				<!--点击内部框时,outer不会执行 -->
				<div e-click="outer" 
					style="width:200px;height:200px;background-color: #777777;">
					<div 
						e-click="inner:nopopo" 
						style="width:100px;height:100px;background-color: #cccccc;">
					</div>
				</div>
				<h3>代理事件到父对象</h3>
				<p>代理到ul元素</p>
				<ul>
					<li x-repeat={{rows}} e-click="check:delg">{{name}}</li>
				</ul>
			</div>
		`
	}
	data(){
		return {
			num:1,
			rows:[
				{name:"name1"},
				{name:"name2"},
				{name:"name3"},
			]
		}
	}
	tiggerOnce(model){
		model.num++;
	}
	outer(model){
		console.log("outer");
	}
	inner(model){
		console.log("inner");
	}
	check(model,dom,NEvent,e){
		console.log(model,dom,NEvent,e);
	}
}

指令(Directive)

指令用于增强元素的表现能力,以"x-"开头,以设置元素属性(attribute)的形式来使用。指令具有优先级,数字越小,优先级越高。优先级高的指令优先执行。

指令简写方式

Nodom提供了指令简写方式,可以通过自定义标签方式简写指令。将在后续每个指令单独讲解。
自定义标签经过编译之后默认为div标签,若想使用其它标签,可通过tag属性指定,下面是repeat指令简写的一个示例:

<!-- 未指定tag,默认为div -->
<for cond={{rows}}>
	<span>{{name}}</span>
</for>
<!-- 等价于 -->
<div x-repeat={{rows}}>
	<span>{{name}}</span>
</div>
<!-- 指定tagName为p -->
<for cond={{rows}} tag="p">
	<span>{{name}}</span>
</for>
<!--等价于 -->
<p x-repeat={{rows}}>
	<span>{{name}}</span>
</p>

指令列表

目前NoDom支持以下几个指令:

指令名 指令优先级 指令描述
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 路由容器

自定义指令

除了Nodom自带的指令,用户可以通过Nodom.createDirective()方法创建指令,参数如下:

序号 说明 类型 备注
1 指令名 string
2 指令执行方法 Function 执行方法默认传递两个参数:1 module(dom所属模块), 2 dom(所属渲染节点,类型RenderedDom)。方法中的this指向指令
3 优先级 Number 1-10,如果设置优先级<5,需慎重

指令执行方法返回true/false,当返回false时,不再进行当前节点的后续渲染,包括子节点渲染,同时该dom节点不加入到渲染树中,也就是说,不会渲染到document中,更多详情参考源码 /extend/directiveinit.ts。

Nodom.createDirective(
	'directive name',
	function (module: Module, dom: RenderedDom){
		//your code
	},
	10
)

model 指令

model指令用于给view绑定数据,数据采用层级关系,如:需要使用数据项data1.data2.data3,可以直接使用data1.data2.data3,也可以分2层设置分别设置x-model='data1',x-model='data2',然后使用数据项data3。下面的例子中描述了x-model的几种用法。

class Main extends Module{
	template(){
		return `
			<div>
				<!-- 设置div节点的model为this.model.user -->
				<div x-model="user">
					<p>{{name.firstName}} {{name.lastName}}</p>
					<!-- 设置div节点的model为this.model.user.name -->
					<div x-model="name">
					 	<p>{{firstName}} {{lastName}}</p>
					</div>
				</div>
			</div>
		`
	}
	data(){
		return {
			user: { 
				name: { firstName: 'Nodom', lastName: 'Yang' } 
			} 
		}
	}
}

repeat 指令

repeat指令为循环指令,用于渲染数组数据。

可通过index属性设置索引名,以便在渲染时使用索引,如index='idx',模板中可直接用idx。
如果数组元素不是object类型,则用$model放在表达式中渲染数据,此时index属性无效。

class Main extends Module{
	template(){
		return `
			<div>
				<h3>常规用法</h3>
				<div x-repeat={{rows}}>
					name:{{name}},age:{{age}}
				</div>
				<h3>使用index属性</h3>
				<div x-repeat={{rows}} index="idx">
					index:{{idx}},name:{{name}},age:{{age}}
				</div>
				<h3>数组元素不为object时的用法-使用$model作为表达式</h3>
				<div x-repeat={{rows1}} index='idx'>
					name:{{$model}}
				</div>
			</div>	
		`
	}
	
	data(){
		return {
			rows:[
				{name:"Nodom",age:6},
				{name:"Noomi",age:4},
				{name:"Relaen",age:3},
				{name:"React",age:12},
				{name:"Vue",age:12}
			],
			rows1:['Nodom','Noomi','Relaen','React','Vue']
		}
	}
}

简写方式
repeat指令可以用for标签进行简写,指令值用cond属性进行配置,改写上面的模板如下:

class Main extends Module{
	template(){
		return `
			<div>
				<for cond={{rows}}>
					name:{{name}},age:{{age}}
				</for>
			</div>	
		`
	}
	...
}

recur 指令

recur指令为递归指令,用于渲染递归格式的数据类型,如树形结构,菜单结构等,模板中递归由两部分组成:

  1. 递归定义,定义递归节点内容,见下例第一个带x-recur属性的div,定义时可以通过name属性设置名称,在引用时指定,默认为default
  2. 递归引用,引用必须包含ref属性,如果定义时为匿名,则ref的值为空,否则应与定义中的name属性保持一致,见下例第二个带x-recur属性的div。
class Main extends Module{
	template(){
		return `
			<div>
				<h3>匿名递归</h3>
				<!--定义recur,通过x-recur指令设置递归数据属性名,与data中数据项保持一致-->
				<div x-recur='ritem'>
					<div e-click='itemClick'>
						<span class={{cls}}>{{title}}</span>
					</div>
					<!-- 引用default -->
					<div x-recur ref/>
				</div>

				<h3>命名递归-增加name属性</h3>
				<div x-recur='ritem' name='r1'>
					<p e-click='itemClick'>
						<span class={{cls}}>{{title}}</span>
					</p>
					<!-- 引用r1 -->
					<div x-recur ref='r1'/>
				</div>

				<style>
					.cls1{
						background-color:red;
					}
					.cls2{
						background-color:green;
					}
					.cls3{
						background-color:blue;
					}
				</style>
			</div>
		`
	}
	data(){
		return {
			ritem:{
				title:"第一层",
				cls:'cls1',
				ritem:{
					title:"第二层",
					cls:"cls2",
					ritem:{
						title:"第三层",
						cls:"cls3"
					}
				}
			}
		};
	}
}

在实际使用中,通常数据项由数组构成,如树、菜单等,下面是数据项为数组的结构示例:

class Main extends Module{
	template(){
		return `
			<div>
				<!--定义recur,并设置了name属性-->
				<recur cond='items' name='r1' class='secondct'>
					<for cond={{items}} >
						<div class='second' e-click='itemClick'>id is:{{id}}-{{title}}</div>
						<!--ref指向了recur定义的name-->
						<recur ref='r1' />
					</for>
				</recur>
				<style>
					.secondct{
						background:#ff9900;
						padding:5px 20px;
						margin:5px 0;
						border:1px solid;
					}
					.second{
						padding:5px;
						background-color:beige;
					}
				</style>
			</div>
		`
	}
	data(){
		return{
			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
					}]
				}
			]
		}
	}
}

简写方式

recur指令可以用recur标签进行简写,指令值用cond属性进行配置,从上面的例子可以看到recur标签的用法。

if/elseif/else/endif 指令

与javascript的if/else/else if逻辑一致,当if指令条件为true时,则渲染该节点。当if指令条件为false时,则进行后续的elseif指令或else指令判断,如果某个节点判断条件为true,则渲染该节点,最后通过endif指令结束上一个if条件判断。示例如下:

class Main extends Module{
	template(){
		return `
			<div>
				<button e-click='change'>修改分数为90</button>
				<p x-if={{score<60}}>不及格,分数为:{{score}}</p>
				<p x-elseif={{score<70}}>及格,分数为:{{score}}</p>
				<p x-elseif={{score<80}}>中等,分数为:{{score}}</p>
				<p x-elseif={{score<90}}>良好,分数为:{{score}}</p>
				<p x-else>优秀,分数为:{{score}}</p>
				<p x-endif />
			</div>
		`
	}
	data(){
		return {
			score:75
		}
	}
	change(){
		this.model.score=90;
	}
}

简写方式

使用对应名称的标签即可,改写上例的模板如下:

class Main extends Module{
	template(){
		return `
			<div>
				<if cond={{score<60}}>不及格,分数为:{{score}}</if>
				<elseif cond={{score<70}}>及格,分数为:{{score}}</elseif>
				<elseif cond={{score<80}}>中等,分数为:{{score}}</elseif>
				<elseif cond={{score<90}}>良好,分数为:{{score}}</elseif>
				<else>优秀,分数为:{{score}}</else>
				<endif/>
			</div>
		`
	}
	...
}

show 指令

show指令用于显示或隐藏dom节点,如果指令对应的表达式返回为true,则显示该视图,否则隐藏(display='none'),示例如下:

class Main extends Module{
	template(){
		return `
			<div>
				<button e-click='toggle'>{{show?'隐藏':'显示'}}</button>
				<div x-show={{show}}>价格:{{price}}</div>
			</div>
		`
	}
	data(){
		return {
			show:true,
			price:200
		}
	}
	toggle(){
		this.model.show = !this.model.show;
	}
}

简写方式

使用show标签即可,改写上例的模板如下:

class Main extends Module{
	template(){
		return `
			<div>
				<show cond={{show}}>价格:{{price}}</show>
			</div>
		`
	}
	...
}

module 指令

module指令用于表示该元素是一个子模块,module指令对应的模块会被渲染至该元素所在位置。使用方式为x-module='模块类名',子模块需要通过父模块的modules属性进行声明。示例如下:

modulea.js文件

//需使用export
export class ModuleA extends Module{
	...
}

main.js 文件

import {ModuleA} from './modulea.js' 
class Main extends Module{
	//声明 MmoduleA
	modules=[ModuleA]
	template(){
		<div>
			<div x-module='modulea' />
			...
		</div>
	}
	...
}

简写方式

使用module标签或module类名两种方式进行简写,改写上例的模板如下:

class Main extends Module{
	<!--声明 MmoduleA-->
	modules=[ModuleA]
	template(){
		<div>
			<!--方式1,用name指定module类名,名字不区分大小写一-->
			<module name='modulea' />
			<!--方式2,直接使用模块类名,名字不区分大小写-->
			<modulea/>
			...
		</div>
	}
	...
}

field 指令

field指令用于实现input、select、textarea等输入元素与数据项之间的双向绑定。

配置说明

  • 单选框radio:多个radio的x-field值必须设置为同一个model属性名,同时需要设置value属性,选中值为value属性对应的值。
  • 复选框checkbox:除了设置x-field绑定数据项外,还需要设置yes-value和no-value两个属性,分别对应选中和未选中的值。

示例如下:

class Main extends Module{
	template(){
		return `
			<div>
				<!-- 绑定name数据项 -->
				姓名:<input x-field="name" />
				<!-- radio,绑定sexy数据项 -->
				性别:<input type="radio" x-field="sexy" value="M" />男
					<input type="radio" x-field="sexy" value="F" />女
				<!-- checkbox,绑定married数据项 -->
				已婚:<input type="checkbox" x-field="married" yes-value="1" no-value="0" />
				<!-- select,绑定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,
			//下拉列表option数据
			edus: [
				{ eduId: 1, eduName: "高中" },
				{ eduId: 2, eduName: "本科" },
				{ eduId: 3, eduName: "硕士研究生" },
				{ eduId: 4, eduName: "博士研究生" },
			]
		}
	} 
}

slot 指令

slot指令为插槽指令,表示该dom节点是一个插槽,插槽作为模板暴露的外部接口,增大了模板的灵活度,更利于模块化开发。详细使用见插槽

route 指令

route将当前dom设定路有节点,点击dom将跳进行路由跳转。使用方式如下:

<a x-route='path'>跳转到path</a>

可使用route标签进行替代,route指令的值由path代替。默认标签为a,如果修改,则设置tag属性。 改写上例代码如下:

<route path='path'>跳转到path</route>
<!-- dom设置为button标签 -->
<route path='path' tag='button'>跳转到path</route>

path值为定义的路由路径,更多详情参考路由

router 指令

router指令用于定义路由模块的容器,如果使用了route指令,必须在模版中使用router指令,示例如下:

<a x-route='path'>跳转到path</a>
...
<div x-router />

同样,可以用router标签代替,修改如下:

<route path='path'>跳转到path</route>
...
<router/>

ajax请求

通过Nodom.request方法进行ajax请求,请求参数为object或string,如果为string,则直接以get方式获取参数指定的url资源,我们建议传递object,object 各项配置如下:

参数名 类型 默认值 必填 可选值 描述
url string 请求url
method string GET GET,POST,HEAD 请求类型
params Object/FormData {} 参数,json格式
async bool true true,false 是否异步
timeout number 0 请求超时时间
type string text json,text
withCredentials bool false true,false 同源策略,跨域时cookie保存
header Object request header 对象
user string 需要认证的请求对应的用户名
pwd string 需要认证的请求对应的密码
rand bool 请求随机数,设置则浏览器缓存失效

为避免重复请求,可以通过Nodom.setRejectTime(time)方法设置重复请求拒绝的间隔时间,单位为ms。

如果需要使用其它的ajax库,需重写Nodom.request方法,且返回类型为Promise。

深入

本章节建议先阅读完前面内容。

模块

模块注册

Nodom为模块提供两种注册方式:

  1. 模块modules数组注册
// 待注册模块A
export class ModuleA extends Module{
    template(){
    	return `
			<div>this is ModuleA</div>
 		`
 	}
}
// 待注册模块B
export class ModuleB extends Module{
    template(){
		return `
			<div>this is ModuleB</div>
			`
		}
}
// 注册使用模块A,B
class Main extends Module{
    modules=[ModuleA,ModuleB]
	template(){
		return `
		<div>
			<!-- 使用模块A-->
			<ModuleA />
			<!-- 使用模块B-->
			<ModuleB />
		</div>
	 `
	 }
}
  1. Nodom.registModule方法注册 Nodom.registModule方法可以给待注册模块设置别名,在模板代码中使用模块时,既可以使用模块类名作为标签名引入,也可以使用注册的别名作为标签名引入。
<!--待注册模块A -->
export class ModuleA extends Module{
    template(){
    return `
		<div>this is ModuleA</div>
 		`
 	}
}
//注册ModuleA并设置别名为 user
Nodom.registModule(ModuleA,'user');
export class Main extends Module{
    template(){
        return `
		<div>
			<user />
		</div>
		`
    }
}

属性传递

为了加强模块之间的联系,Nodom在父子(如果为插槽,则是模板所在模块和内部模块,下同)模块之间提供props来传递数据。除根模块外,每个模块在执行template方法时,会将子模块对应的节点属性以对象的形式作为参数传入,也就是说,子模块可以在自己的template函数内,依据传入的props来动态创建模板

//模块A 
class ModuleA extends Module{
    template(props){
       	let str;
	   	//根据传递的name属性生成不同模板串
		if(props.name=='add'){
			return `<h1>add<h1>`
		}else{
			return `<h2>none</h2>` 
		}
    }
}
Nodom.registModule(ModuleA,'user');
// 根模块 
class Main extends Module{
    template(){
        return `
			<div>
			<!-- 传递name属性给user模块 -->
				<user name='add' />
			</div>
		`
    }
}

模块传值

props实现了属性传递,也可以实现父模块向子模块的数据传递,但是这是被动的传递方式,需要手动进行转换,如果需要将值传递至子模块的model,可以在传递的属性名前,加上$前缀,Nodom会将其传入子模块的根model内,实现响应式监听。

注意:如果传值是一个对象,则该对象存在于两个模块内,对象内数据的改变会造成两个模块的渲染,建议传值时,尽量使用非对象数据。

//模块A
class ModuleA extends Module{
    template(props){
        return `<div><h1>{{name}}<h1></div>`
    }
}
//根模块
class Main extends Module{
	modules = [ModuleA]
    template(){
        return `
      	 <div>
		 	<!-- name项将直接存放于ModuleA的model中 -->
        	<ModuleA $name={{name}} />
		</div>
    `
    }
    data(){
        return {
            name:'Nodom',
        }
    }
}

反向传递

由于Props的存在,父模块可以暴露外部接口,将其通过Props传递给子模块,子模块调用该方法即可实现反向传递的功能。例如:

//模块A
class ModuleA extends Module{
    template(props){
        this.parentChange=props.add;
        return `
			 <div>
			 	<button e-click='change'>父模块+1</button>
			 </div>
		`
    }
    change(){
        this.parentChange(1);
    }
}
Nodom.registModule(ModuleA,'user');
//根模块 
class Main extends Module{
    template(){
        return `
		<div>
			count={{sum}}
			<user add={{this.add}} />
		</div>
        `
    }
    data(){
        return {
           sum:0,
        }
    }
    //这里需要使用箭头函数,来使该函数的this始终指向根模块,或者使用bind函数绑定this指向
    add=(num)=>{
        this.model.sum++;
    }
}

多模块数据共享

上述的值或属性传递,只能存在于父子之间,不能解决兄弟节点或不同父模块之间的传递问题,Nodom提供了GlobalCache来管理共享数据,实现多个模块的数据共享。

GlobalCache内置get(获取)set(设置)remove(移除)subscribe(订阅)方法以便操作数据。

import{Nodom,Module,GlobalCache} from '/dist/nodom.esm.js'
//无论数据是否存在,都可以订阅
GlobalCache.set("globalData", {
	msg: 0,
});

class ModuleA extends Module {
	template() {
		return ` 
			<div>
				<button e-click="change">change</button>
			</div>
		`;
	}
	change() {
		let data = GlobalCache.get("globalData");
		if(!data){
			data = {msg:0}
		}else{
			data.msg++;
		}
		GlobalCache.set("globalData",data);
	}
}

class ModuleB extends Module {
	template() {
		return ` 
			<div>
				moduleb global data is:{{msg}}
			</div>
		`;
	}
	onInit(model) {
		//订阅数据
		GlobalCache.subscribe(this, "globalData", (val) => {
			model.msg = val.msg;
		});
	}
}

class Main extends Module {
	modules = [ModuleA,ModuleB];
	template() {
		return `
			<div>
				main global data is:{{msg}}
				<ModuleA />
				<ModuleB />
			</div>
		`;
	}
	onInit(model) {
		//订阅数据
		GlobalCache.subscribe(this, "globalData", (val) => {
			model.msg = val.msg;
		});
	}
}

也可使用第三方数据发布-订阅库。 在开发大型项目时,可以使用数据管理库帮助我们管理数据,使数据以可预测的方式发生变化,我们推荐使用Nodom团队开发的kayaks库,或者其他优秀的数据管理库均可。

插槽

在实际开发中,插槽功能会较大程度的降低应用开发难度,插槽作为模板暴露的外部接口,增大了模板的灵活度,更利于模块化开发。Nodom以指令和自定义元素的方式实现插槽功能。

<!--自定义元素的方式使用插槽,命名插槽 -->
<slot name='title'>
    ...
</slot>
<!-- 指令的形式使用插槽,命名插槽-->
<div x-slot='title'>
	...
</div>
<!-- 匿名插槽-->
<slot>
	...
</slot>

innerRender

插槽内的节点渲染时的默认数据来源于所属模板的模块的model,而某些时候,需要用子模块内部的数据进行渲染,Nodom提供innerRender属性支持。

注意:添加innerRender后,插槽内元素表达式依赖的数据项、方法,定义的事件方法都来源于子模块,否则都来源于模板所在模块。

下面的例子中,渲染数据name来源于模块Main。

class ModuleA extends Module{
    template(props){
		 return `
			<div>
				<slot/>
			</div>	
		`
    }
}
class Main extends Module{
	template(props){
		return `
			<div>
				<modulea>
					<!--name来源于Main-->
					<span>my name is : {{name}}</span>
				</modulea>
			</div>	
		`
    }
}	

下面的例子中,渲染数据name来源于模块ModuleA。

class ModuleA extends Module{
    template(props){
		 return `
			<div>
				<!--增加innerRender设置-->
				<slot innerRender/>
			</div>	
		`
    }
}
class Main extends Module{
	template(props){
		return `
			<div>
				<modulea>
					<!--name来源于ModuleA-->
					<span>my name is : {{name}}</span>
				</modulea>
			</div>	
		`
    }
}	

匿名插槽

如果子模块内slot标签无name属性,则模块(如下面的modulea)标签内的元素会替换子模块的slot标签。

//模块A 
class ModuleA extends Module{
      template(props){
		 return `
			<div>
				<!--slot标签会被Main模块modulea标签内的内容代替-->
				<slot>
					我是默认内容
				</slot>
			</div>	
		`
    }
}
<!-- 根模块  User标签内的所有内容作为待插入的内容-->
class Main extends Module{
	modules=[ModuleA];
    template(){
        return `
			<div>
  				<modulea>
					<!--下面的p和button标签会替换ModuleA的slot标签-->
					<p>我是父模块的P标签</p>
					<button>我是父模块</button>
				 </modulea>
			</div>
		`
    }
}

命名插槽

在实际使用中,可能需要多个插槽,就需要使用命名插槽,通过插槽的name属性设置插槽名字。命名插槽就是给插槽定义插槽名,传入的标签需要与插槽名一致才可发生替换。

//模块A
class ModuleA extends Module{
    template(props){
		return `
			<div>
				<slot name='title'>
					我是title
				</slot>
				<slot name='footer'>
					我是footer
				</slot>
			</div>
		`
    }
}
// 根模块  modulea标签内的slot标签内容作为待插入的内容
class Main extends Module{
	modules=[ModuleA];
    template(){
        return `
		<div>
 		 	<modulea>
				<slot name='title'>
					<!--替换ModuleA<slot name='title'>标签-->
					<button>我是父模块的title</button>
				<slot>
				<slot name='footer'>
					<!--替换ModuleA<slot name='name'>标签-->
					<button>我是父模块的footer</button>
				<slot>
 			</modulea>
		</div>	
`
    }
}

详细使用见examples/slot.html

模型(Model)

Model作为模块数据的提供者,绑定到模块的数据模型都由Model管理。Model是一个由Proxy代理的对象,Model的数据来源有两个:

  • 模块实例的data()函数返回的对象;
  • 父模块通过$data方式传入的值。

每一个模块都有独立的Model,使用方式如下:

class ModuleA extends Module{
    template(props){
        return `<div>{{name}}</div>`;
    }
}
//根模块
class Main extends Module{
	modules=[ModuleA];
    template(){
        return `
		<div>
        	<ModuleA $name={{name}}/>
		</div>`
		;
    }
    data(){
        return {
            name:'Nodom'
        }
    }
}

Model会深层代理内部的object类型数据。Model分层结构与所代理的数据对象结构一样,即父Model和子孙Model的关系。

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

关于Proxy的详细信息请参照Proxy-MDN

在使用的时,可以直接把Model当作对象来操作:

class Main extends Module{
	template(){
		return `
		<div>
			{{count}}
			<button e-click="changeCount">click</button>
		</div>
		`
	}
	// 模块的数据来源
	data(){
		return {
			title:'Hello',
			count:0
		}
	}
	changeCount(model){
		model.count++;
	}
}

保留属性

Model提供了4个保留属性,用户在定义数据项时应避免。

数据项 说明 类型 备注
__source 源数据对象 object 通过此属性可以获取被代理的数据对象
__key model key(全局唯一) number -
__module 所属模块 Module -
__parent 父Model Model 可通过此属性获取祖先model
__name 在父模型中的属性名 string -

Model与模块渲染

每个Model存有一个模块列表,当Model内部的数据变化时,会引起该Model的模块列表中所有模块的渲染。一个Model的模块列表中默认只有初始化该Model的模块,当存在slot或模块传值为对象时,将会导致Model绑定到多个模块,当然也可以通过ModelManager的bindToModule方法绑定。

set方法

在module中提供了一个set()方法,该方法可以往model上设置一个深层次的对象或值。当model缺省,则表示模块根model。

参数说明
序号 说明 类型
1 模型 Model
2 属性名 string
3 属性值 any

如果第一个参数为属性名,则第二个参数为属性值,默认model为根模型

data(){
	return {
		data:{
			a:1,
			b:'b'
		}
	}
}
change(model){
	// 会报错,因为data1为undefined
	model.data1.data2.data3 = { a:'a' };
	// 使用$set可以避免该问题,如果不存在这么深层次的对象$set会帮你创建。
	this.set("data1.data2.data3",{a:'a'});
}

get方法

Module中提供了一个get()方法,可以从Model上获取一个深层次的对象值,当不知道对象具体层次时有效。

参数说明

model: Model, key: string, value:any

序号 说明 类型
1 model Model
2 属性名 string

如果第一个参数为属性名,则默认model为根模型

data(){
	return {
		data:{
			a:1,
			b:'b'
		}
	}
}

getValue(){
	// 等同于 this.model.data.a
	console.log(this.get("data.a"));
}

watch方法

module的watch方法用来检测Model里的数据变化,当数据变化时执行配置的钩子函数。

参数说明

model: Model, key: string|string[], operate: Function,module?:Module,deep?:boolean

参数名 类型 参数说明
model Model 监听对象,如果省略,则表示module的根model
key string或string[] 监听属性
operate Function 监听触发方法,默认参数为(model,key,oldValue,newValue),其中model为被监听的model,key为监听的键,oldValue为旧值,newValue为新值
deep boolean 如果设置为true,当key对应项为对象时,对象的所有属性、子孙对象所有属性都会watch,慎重使用该参数,避免watch过多造成性能损失。
取消watch

watch 方法会返回一个函数,当不需要watch时,执行该函数即可取消watch。

示例

详细使用请参考 examples/model.html。

class Main extends Module{
	template(){
		return `
			<div>
				<button e-click='change'>change</button>
				<button e-click='watch'>watch</button>
				<button e-click='cancelWatch'>cancel watch</button>
				<div>{{count}}</div>
			</div>
		`
	}
	data(){
		return {
			count:1,
			user:{
				name:{
					first:'nodom',
					last:'noomi'
				}
			},
			hobbies:[{name:'健身'},{name:'游戏'}]
		}
	}
	//激活watch,通常情况下,我们把watch放置在onBeforeFirstRender事件中
	watch(model){
		//当被监听的model为根model时,可以省略
		this.watcher = this.watch('count',(model,key,oldVal,newVal)=>{
			console.log('检测到数据变化');
			console.log('oldVal:',oldVal);
			console.log('newVal:',newVal);
		})
		//等价于
		// this.watcher = this.watch(this.model,'count',(m,key,oldVal,newVal)=>{
		// 	console.log('检测到数据变化');
		// 	console.log('oldVal:',oldVal);
		// 	console.log('newVal:',newVal);
		// })
		//watch多个,并设置deep为true
		this.watch(['user.name','hobbies'],(model,key,oldVal,newVal)=>{
			console.log(model,key,oldVal,newVal);
		},true);
	}
	//修改数据
	change(){
		this.model.count++;
		this.model.hobbies[1].name='旅游';
		this.model.user.name.last = 'relaen';
	}
	//取消监听
	cancelWatch(){
		//cancel count数据项的watch
		this.watcher();
	}
}

编译

当首次渲染或tempate()返回的模板串发生改变时,会触发模板重新编译,所以在构造模板串时,尽量避免用可变的props值或model项来构造,而是采用指令、表达式或插槽的方式来保持渲染的动态性。

下面的模板是不建议的

//子模块 ModuleA
template(props){
	return `
		<div>
			<div class='${props.type===1?'clsa':'clsb'}>
				hello world
			</div>
		</div>
	`
}

//父模块 Main
template(props){
	return `
		<div>
			<modulea type={{type}}/>
		</div>
	`
}

当模块Main的数据type发生改变时,会导致ModuleA重新编译,改进方式如下:

//子模块 ModuleA
template(props){
	return `
		<div>
			<!--通过表达式获取-->
			<div class={{genClass(type)}}>
				hello world
			</div>
		</div>
	`
}

genClass(type){
	return type===1?'clsa':'clsb';
}

//父模块 Main
template(props){
	return `
		<div>
			<!--通过数据传递-->
			<modulea $type={{type}}/>
		</div>
	`
}

当模块Main的type发生改变时,ModuleA会渲染,但不会重新编译。

渲染

渲染时机

Nodom的渲染是基于数据驱动的,也就是说只有Model内的数据发生了改变,当前模块才会进行重新渲染的操作。
子模块渲染依赖:

  1. Model数据改变;
  2. 父模块传属性(props)发生改变;
  3. 父模块传值发生改变。

手动触发

如果需要手动渲染,则需调用module.active()进行触发。

CSS支持

​ Nodom对CSS提供额外的支持。在模板中使用<style></style> 标签中直接写入CSS样式,示例代码如下:

class Module1 extends Module {
    template() {
        return `
			<div>
				<h1 class="test">Hello nodom!</h1>
				<style>
					.test {
						color: red;
					}
				</style>
			</div>`;
       }
}

在模板代码中的 <style></style> 标签中通过表达式调用函数返回CSS样式代码串,示例代码如下:

class Module1 extends Module {
     template() {
         return `
			<div>
                <h1 class="test">Hello nodom!</h1>
                <style>{{css()}}</style>
            </div>`;
     }
     css() {
         return `
			.test {
				color: red;
			}`;
     }
}

在模板代码中的 <style></style> 标签中通过@import url('CSS url路径')引入CSS样式文件,示例代码如下:

template() {
	return `
		<div>
			<h1 class="test">Hello nodom!</h1>
			<style>
				@import url('./style.css')
			</style>
		</div>
	`;
}

对模板代码中需要样式的节点直接写行内样式,示例代码如下:

template() {
	return `
		<div>
			<h1 style="color: red;" class="test">Hello nodom!</h1>
		</div>
	`;
}

scope属性

​ 给节点添加该属性后,Nodom会自动在CSS选择器前加前置名。使CSS样式的作用域限定在当前模块及其子模块,不会污染其它模块。

​ 示例代码如下:

 template() {
	return  `
		<div>
			<h1 class="test">Hello nodom!</h1>
			<style scope="this">
				.test {
					color: red;
				}`;
			</style>
		</div>
	`;
 }

此例中, .test css class只对当前模块及其子模块有效。

自定义元素

自定义元素需要继承DefineElement类,且需要在DefineElementManager中注册。

// 定义自定义元素
class MYELEMENT extends DefineElement{
	/**
	 * @param node 		VirtualDom
	 * @param module	所属模块
	 */ 
	constructor(node,module){
        super(node,module);
        
		......
    }
}
	
// 注册自定义元素
DefineElementManager.add(MYELEMENT);

更多使用参考/extend/elementinit.ts文件。

路由

Nodom内置了路由功能,可以配合构建单页应用,用于模块间的切换。

路由初始话

如果需要使用路由,则需要在创建路由前引入路由模块,引入方式使用Nodom.use()方法。引入路由初始化参数如下:

序号 说明 类型 备注
1 路由基础路径 String 可选,如果配置此项,则浏览器显示的路径以此路径开始
2 路由进入方法 Function 可选,每个路由进入时都将执行此方法,传递参数为 1:module,2:进入时路径
3 路由离开时方法 Function 可选,每个路由离开时都将执行此方法,传递参数同上
初始化示例如下:
//启用路由
import {Nodom,Router} from '/dist/nodom.esm.js'
Nodom.use(Router,['/router',function(mdl,path){
    console.log('enter',mdl,path)
},function(mdl,path){
    console.log('leave',mdl,path)
}]);

初始化后,可以在任意模块中使用 Nodom['$Router']访问路由对象。

创建路由

Nodom提供Nodom.createRoute方法,用于注册路由。以Object配置的形式指定路由的路径、对应的模块、子路由等。 以下是一个简单的路由示例:

  1. 主模块
class Main extends Module{
	template(){
		return `
			<div>
				<!-- 点击触发路由跳转-->
				<div x-route='/hello'>hello</div>
				<!-- 指定路由模块渲染的位置-->
				<div x-router />
			</div>
		`
	}
}
  1. 创建路由
import {Nodom} from '/dist/nodom.esm.js';
//这里默认Hello为一个完整的模块
import Hello from'./route/hello.js';
//创建路由
Nodom.createRoute({
    path:'/hello',
    //指定路由对应的模块
    module:Hello
});

当点击hello时,浏览器路径会跳转到 /hello,router指令处会显示为Hello模块的内容。

上述方式会导致模块提前加载,nodom提供了通过模块路径实现懒加载,修改上例代码如下:

Nodom.createRoute({
    path:'/hello',
    // 此处设置模块路径,当执行路由时再加载Hello模块
    module:'./route/hello.js'
});

注意事项

  1. 一个模板中,只能有一个节点带router指令。
  2. 实现多级路由,需要在不同模块的模板中配置router指令。

嵌套路由

在实际应用中,通常由多层嵌套的模块组合而成。配置对象内routes属性,以数组的方式注册子路由。例如:

import {Nodom} from '/dist/nodom.esm.js';
import {Main} from './route/main.js';
Nodom.createRoute({
    path:'/main',
    //指定路由对应的模块
    module:Main,
    routes:[
    {
     	path:'/m1',
    	//指定路由对应的模块
    	module:'./route/m1.js' 
	},{
     	path:'/m2',
    	//指定路由对应的模块
    	module:'./route/m2.js'
	}]
});

当访问/main/r1时,先加载Main模块,再加载M1模块。

路由跳转

借助x-route指令,用户无需手动控制路由跳转。但在一些情况下,需要手动控制路由跳转,跳转方式为: js //path为需要跳转的路径 Nodom['$Router'].go(path);

路由传值

如果想要实现路由传值,只需在路径内以:params配置。例如:

import {createRoute} from './nodom.esm.js';
//这里默认Hello为一个完整的模块
import Hello from'./route/hello.js';
//创建路由
createRoute({
    path:'/main/:id',
    //指定路由对应的模块
    module:Hello
});

Nodom将通过路由传的值放入模块根Model的$route中。

路由模块中可以通过$route.data获取path传入的值。

<!--跳转模块 -->
<div>
<div x-route='/main/1'>跳转至模块Hello</div>
    <div x-router></div>
</div>
<!-- 路由模块Hello-->
<div>
    <!-- 值为1-->
   {{$route.data.id}} 
</div>

路由事件

单路由事件

每个路由可设置:

  • onEnter事件,在路由进入时执行
  • onLeave事件,在路由离开时执行

执行时传入参数:

  • module(路由绑定的模块)
  • 当前路径

如:从/r1/r2/r3 切换到 /r1/r4/r5。 则onLeave响应顺序为r3 onLeave、r2 onLeaveonEnter事件则从上往下执行执行顺序为 r4 onEnter、 r5 onEnter

例如:

import {Nodom} from '/dist/nodom.esm.js';
//这里默认Hello为一个完整的模块
import Hello from'./route/hello.js';
//创建路由
createRoute({
    path:'/main',
    module:Hello,
    onLeave:function(module,path){
        console.log('我执行了onleave函数');
    },
    onEnter:function(module,path){
         console.log('我执行了onEnter函数');
    }
});
全局路由事件

通过路由初始化时设置,见路由初始化,全局事件针对所有路由有效。

浏览器刷新

浏览器刷新时,会从服务器请求资源,nodom路由在服务器没有匹配的资源,则会返回404。通常的做法是: 在服务器拦截资源请求,如果确认为路由,则做特殊处理。 假设主应用所在页面是/web/index.html,当前路由对应路径为/webroute/member/center。刷新时会自动跳转到/member/center路由。相应浏览器和服务器代码如下:

浏览器代码
import {Nodom,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 ||= '/home';
       	Nodom['$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/')){
			//去掉/webrouter
            response.redirect('/web/index.html#' + path.substr(9));
            return false;
        }
        return true;
    }
}
export{RouteFilter};

页面路由初始化代码如下:

//设置路由基础路径为`/webroute`,此处的onEnber和onLeave可选填
Nodom.use(Router,['/webroute',onEnter,onLeave]);

更多示例参考/examples/route.html,/exampls/modules/route目录

生态

NodomUI

nodomui npm库,快速搭建应用,http://www.npmjs.com/package/nodomui。

Kayaks

数据管理库,用于开发大型项目。

Nodom VsCode插件

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

Package Sidebar

Install

npm i nodom3

Weekly Downloads

40

Version

0.2.2

License

MIT

Unpacked Size

3.35 MB

Total Files

88

Last publish

Collaborators

  • fieldyang