# nodomdev
**Repository Path**: kyleslie/nodomdev
## Basic Information
- **Project Name**: nodomdev
- **Description**: nodom 开发库
- **Primary Language**: Unknown
- **License**: MIT
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 3
- **Created**: 2021-11-30
- **Last Updated**: 2024-06-16
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
nodom
nodom是一款基于数据驱动的web mvvm框架。
用于搭建单页应用(SPA),目前发展到版本3。
插件也在同步更新中。
详情请点击官网[nodom](http://www.nodom.cn/webroute/home)
```js
源码所在目录:./core
示例所在目录:./examples
发布包所在目录:./dist
示例运行方式:
git clone后在根目录执行
npm install
安装依赖包
再执行
npm run build
即可编译出可运行的nodom.js
使用Live Server启动在./examples目录下的html文件即可
```
# 文档
## 安装
Nodom是一款用于构建用户界面的前端`MVVM`模式框架,Nodom支持按需、渐进式引入。不论是体验Nodom还是构建复杂的单页应用,Nodom均完全支持。
在项目内可引入的方式如下:
1. 下载JavaScript文件,以ES Module的形式引入。
2. 在页面以CDN包的方式引入。
### 最新的Nodom
最新的版本可在[GitHub](https://github.com/fieldyang/nodom3)上获取,内有官方发布的重要信息,包括详细的更新日志,及之前的版本。
### 体验Nodom
你可以在[CodePen](https://codepen.io/pen/?template=wvqPeJQ)平台在线体验Nodom。
也可前往GitHub平台下载源码,运行./examples目录内提供的示例代码。
### CDN
对于CDN引入的方式,可以这样引入:
```html
```
以确保使用最新版本。
### 下载引入
在生产环境下,建议引入完整的**nodom.js**文件,Nodom建议使用ES Module实现模块化,无需构建工具即可完成模块化开发,引入方式如下:
```html
```
## 基础
### 起步
Nodom是一款基于数据驱动,用于构建用户界面的前端`MVVM`模式框架。内置路由,提供数据管理功能,支持模块化、组件化开发。在不使用第三方工具的情况下可独立开发完整的单页应用。
一个简单的Hello World例子如下:
```js
```
> @code:base_Start
#### 引入方式
Nodom支持以普通JavaScript文件的形式引入至HTML文件,比如:
```html
```
但是我们建议以ES Module的形式引入script文件,利于模块化开发。与普通的script文件引入不同的是,ES Module的引入在标签内需要配置**type="module"**浏览器才能识别。比如:
```html
```
#### 渲染元素
Nodom支持渐进式开发,框架内部会将传入的容器作为框架处理的入口。所以,传入你的元素选择器作为渲染的容器,将该容器完全交给Nodom托管。
例如有一节点:
```html
{{title}}
```
如果表达式内的计算结果产生不可预知的错误,默认的,会返回空字符串,确保程序运行时不会出错。
```html
{{age}}
```
> @code:RwZOeZq
### 事件绑定
Nodom使用了专门的事件类`NEvent`来处理Dom的事件操作,在模板中以`e-`开头,如:`e-click`、`e-mouseup`等。事件支持所有HTML元素标准事件,接收一个模块实例上的方法名作为事件处理方法,如:`e-click="methodName"`,当事件触发的时,Nodom会执行该方法。具体用法如下:
```js
export class ModuleA extends Module{
template(){
return `
{{ count }}
`;
}
// model
data(){
return {
count:0
}
}
// button onclick事件触发回调。
addCount(model){
model.count++;
}
}
```
> @code:qBXwQBQ
#### 回调函数的参数
与原生事件使用不同,Nodom中不需要指定事件参数,事件方法会自带四个参数。参数如下所示:
| 序号 | 参数名 | 描述 |
|:----:|:------:|:---------------------:|
| 1 | model | dom对应的model |
| 2 | dom | 事件对象对应的虚拟dom |
| 3 | nEvent | Nodom事件对象 |
| 4 | event | html原生事件对象 |
代码如下:
```js
// 事件触发回调。
addCount(model,vdom,nEvnet,event){
......
}
```
#### 事件修饰符
在传入事件处理方法的时,允许以`:`分隔的形式传入指定事件修饰符。
事件处理支持三种修饰符:
| 名字 | 作用 |
|:------:|:----------------:|
| once | 事件只执行一次 |
| nopopo | 禁止冒泡 |
| delg | 事件代理到父对象 |
```html
```
### 指令(Directive)
指令用于增强元素的表现能力,以"x-"开头,以设置元素属性(attribute)的形式来使用。指令具有优先级,按照数字从小到大,数字越小,优先级越高。优先级高的指令优先执行。
含有指令的标签经过编译之后默认为`div标签`,若想使用其它标签包裹,可通过tag属性指定。
```html
//使用tag属性之后,通过repeat指令生成的元素被`span标签`包裹
{{foodName}}
```
目前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 | 路由占位 |
#### Model 指令
model指令用于给view绑定数据,数据采用层级关系,如:需要使用数据项data1.data2.data3,可以直接使用data1.data2.data3,也可以分2层设置分别设置x-model='data1',x-model='data2',然后使用数据项data3。下面的例子中描述了x-model的几种用法。
model指令改变了数据层级,则如何用外层的数据呢,Nodom支持从根向下查找数据功能,当需要从根数据向下找数据项时,需要使用"$$"
模板代码
```html
顾客信息:
姓氏:{{lastName}}
名字:{{firstName}}
```
```javascript
data(){
return{
user: {
name: { firstName: 'Xiaoming', lastName: 'Zhang' }
}
}
}
```
> @code:QWMPJyp
#### Repeat 指令
Repeat指令用于给按照绑定的数组数据生成多个dom节点,每个dom由指定的数据对象进行渲染。使用方式为x-repeat={{item}},其中items为数组对象。
数据索引
索引数据项为$index,为避免不必要的二次渲染,index需要单独配置。
模板代码
```html
编号:{{$index+1}},菜名:{{name}},价格:{{price}}
配料列表:
- 食材:{{title}},重量:{{weight}}
```
```javascript
data(){
return{
foods1:[
{name: '夫妻肺片',price: 25,rows:[
{title:'芹菜',weight:100},
{title:'猪头肉',weight:200}
]},
{name: '京酱肉丝',price: 22,rows:[
{title:'瘦肉',weight:100},
{title:'葱',weight:200}
]},
{name: '糖醋里脊',price: 20,rows:[
{title:'排骨',weight:200}
]}
]}
}
}
```
#### Recur 指令
recur指令生成树形节点,能够实现嵌套结构,在使用时,注意数据中的层次关系即可。recur也可以通过使用recur元素来实现嵌套结构。
```html
{{title}}
id is:{{id}}-{{title}}
```
```javascript
data(){
return{
ritem: {
title: "第一层",
cls: "cls1",
ritem: {
title: "第二层",
cls: "cls2",
ritem: {
title: "第三层",
cls: "cls3",
},
},
},
ritem1: {
cls: "cls1",
items: [{ title: "数据11" }, { title: "数据12" }],
ritem1: {
cls: "cls2",
items: [{ title: "数据21" }, { title: "数据22" }],
ritem1: {
cls: "cls3",
items: [{ title: "数据31" }, { title: "数据32" }, { title: "数据33" }],
},
},
},
}
}
```
#### If/Elseif/Else/Endif 指令
指令用法
- 指令说明:if/else指令用于条件渲染,当if指令条件为true时,则渲染该节点。当if指令条件为false时,则进行后续的elseif指令及else指令判断,如果某个节点判断条件为true,则渲染该节点,最后通过endif指令结束上一个if条件判断。
模板代码
```html
如果discount<0.8,显示价格
价格:{{price}}
价格:{{price}}
价格:{{price}}
```
```javascript
data(){
return {
discount: 0.7,
price: 200
}
}
```
标签用法
- 需要设置cond属性用于添加判断条件。
模板代码
```html
如果discount<0.8,显示价格
价格:{{price}}
如果age<18,显示未成年,否则显示成年
年龄:{{age}},未成年
年龄:{{age}},成年
根据不同分数显示不同等级,<60不及格,60-69及格,70-79中等,80-89良好,>=90优秀
不及格
60 && grade<70}}> 及格
70 && grade<80}}> 中等
80 && grade<90}}> 良好
优秀
```
```javascript
data(){
return {
discount: 0.7,
price: 200,
age: 20,
grade: 73,
}
}
```
#### Show 指令
show指令用于显示或隐藏视图,如果指令对应的条件为true,则显示该视图,否则隐藏。使用方式为x-show='condition'。
模板代码
```html
```
```javascript
data(){
return{
show:true,
price:2000
}
}
```
#### Module 指令
module指令用于表示该元素为一个模块容器,module指令数据对应的模块会被渲染至该元素内。使用方式为x-module='模块类名',Nodom会自动创建实例并将其渲染。
模版代码
```
import Title from './src/dist';
class ModuleA extends Module{
template(){
return `
`
}
```
#### Field 指令
- 指令说明:field指令用于实现输入类型元素,如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绑定数据项即可。
模板代码
```html
姓名:
性别:男
女
已婚:
学历:
```
```javascript
data(){
return{
name: 'nodom',
sexy: 'F',
married: 1,
edu: 2,
birth: '2017-05-11',
edus: [
{ eduId: 1, eduName: "高中" },
{ eduId: 2, eduName: "本科" },
{ eduId: 3, eduName: "硕士研究生" },
{ eduId: 4, eduName: "博士研究生" },
]
}
}
```
### 列表
在日常开发中,渲染一个`列表`是十分常见的应用场景。接下来看看,在`Nodom`中是如何来实现`列表`的渲染的。
#### 基础使用
在`Nodom`中,提供了两种方式来实现`列表`的渲染。
第一种是通过内置指令`x-reapet`的方式。将列表数据直接传递给该指令。
第二种方式通过`Nodom`实现的`
`内置标签。该标签含有一个`cond`属性,用来传入需要渲染的列表数据。
并且都需要通过`$index`属性指定索引名。
```html
菜单:
菜名:{{name}},价格:{{price}}
菜单:
菜名:{{name}},价格:{{price}}
```
**结果**:
#### 访问`Model`中的数据
如果需要访问`model`中的数据,直接访问是不行的。因为每一个`x-repeat`复制出来的`module`拥有独立的`model`,与基础`module`指向的全局`model`不同。这类`module`会指向自己的独立`model`,所以仅能访问`cond`里对象的值。只有在当前`module`中通过`this.model`调用全局`model`来访问`model`中的数据。
```html
访问 Model 中的数据
菜单:
data中的show: {{this.model.show}}
```
#### 索引号的使用(编号从0开始)
`$index`这一变量,是用来获取当前索引的。但在使用之前,需要指定索引的名字。
```html
索引号的使用(编号从0开始)
菜单:
编号:{{idx}},菜名:{{name}},价格:{{price}}
菜单:
编号:{{idx}},菜名:{{name}},价格:{{price}}
```
**结果**:
**注意**:不论是否使用,都建议指定`$index`的索引名,否则将造成不可预知的错误。
#### 自定义过滤数组
如果你只想看到`22`元以上的菜,那么,你可以使用一个自定义函数来为你自己筛选这些菜。
```html
自定义过滤数组
菜单:
菜名:{{name}},价格:{{price}}
菜单:
菜名:{{name}},价格:{{price}}
```
```js
getFood(arr) {
return arr.filter(item => item.price > 22);
}
```
**结果**:
或者,你需要将所有的数据排序展示,那么你可以将`getFood`方法修改如下:
```js
getFood(arr) {
return arr.sort((a,b) => a.price - b.price);
}
```
**结果**:
**注意**:自定义函数中传入的数据已经不是原来`data`中的初始数据了,而是做了响应式处理的响应式数据。`Nodom`响应式数组数据中,支持的`js`原生方法有很多,例如:
- `push()`
- `pop()`
- `unshift()`
- `shift()`
- `splice()`
- `sort()`
- `reverse()`
- `filter()`
- `map()`
#### 嵌套列表
有时候,我们会遇到复杂一点的嵌套列表。
```html
repeat 嵌套
菜单:
编号:{{idx+1}},菜名:{{name}},价格:{{price}}
配料列表:
- 食材:{{title}},重量:{{weight}}
```
**结果**:
#### `x-repeat` 指令与 ``标签
`x-reapt`指令和``标签有什么不同呢?二者并无什么不同,``标签其实就是封装了`x-repeat`指令的一个标签。所以,``标签和`x-repeat`指令可以在任何时候互换。
#### `x-repeat`指令和`x-recur`指令
`x-recur`指令可以和`x-repeat`指令一起使用,更快速的解析`树形结构`的数据。现在有一个数据格式是这样的:
```js
{
ritem2: {
items: [
{
title: "aaa",
id: 1,
items: [
{
id: 1,
title: "aaa1",
items: [
{ title: "aaa12", id: 12 },
{
title: "aaa11",
id: 11,
items: [
{ title: "aaa111", id: 111 },
{ title: "aaa112", id: 112 },
],
},
{ title: "aaa13", id: 13 },
],
},
{
title: "aaa2",
id: 2,
items: [
{
title: "aaa21",
id: 21,
items: [
{
title: "aaa211",
id: 211,
items: [
{ title: "aaa2111", id: 111 },
{ title: "aaa2112", id: 112 },
],
},
{ title: "aaa212", id: 212 },
],
},
{ title: "aaa22", id: 22 },
],
},
],
},
{
title: "bbb",
id: 2,
items: [
{
title: "bbb1",
id: 10,
items: [
{ title: "bbb11", id: 1011 },
{ title: "bbb12", id: 1012 },
],
},
{
title: "bbb2",
id: 20,
items: [
{ title: "bbb21", id: 201 },
{ title: "bbb22", id: 202 },
],
},
],
},
],
},
};
```
如果仅仅使用`x-repeat`指令,很难去生成一个`树形结构`,现在将`x-recur`指令加入进来。
```html
递归带repeat
```
十分简洁的代码就搞定了树形结构。
**结果**:
#### 注意
+ `x-repeat`指令和``标签中均只能使用对象数组作为数据。
+ 不要将``标签和`x-repeat`指令一起使用。
### 虚拟DOM
虚拟dom相较于真实dom很大的提高了开发效率,优化了用户的体验,同时提升了页面渲染的性能。
#### Nodom中的虚拟dom的结构如下:
```typescript
{
/**
* 元素名,例如标签,tagName为div;的tagName为span
*/
public tagName: string;
/**
* Nodom中虚拟DOM的key是唯一的标识,对节点进行操作时提供正确的位置,获取对应的真实dom
*/
public key: string
/**
* 绑定事件模型,在方法中可以传入model参数来获得模型中的值
*/
public model: Model;
/**
* 移除多个指令
* @param directives 待删除的指令类型数组或指令类型
*/
public removeDirectives(directives: string[]) {
}
```
#### 属性
| **属性** | **类型** | **定义** |
| :--------- | --------------------- | :----------------------------------------------------------- |
| tagName | string | 标签名如的他给Name为'div' |
| key | string | 是唯一的标识符,也可以通过key来获取虚拟dom的值 |
| model | Model | 绑定Model |
| directives | Directive[] | 指令集合,是一个数组用来存放各个指令 |
| events | Map | 事件的集合,同时一个事件可以绑定多个事件方法对象 |
| staticNum | number | 静态标识数,初始为0,大于0时每次做比较都减一直到等于0,当小于0时不进行处理 |
| children | Array | 子节点数组,在进行对子节点的操作,如add(),会将子节点加到子节点数组中 |
| ...... | | |
#### 虚拟dom的加速器“Diff”
在虚拟dom的操作中,diff算法起到了关键的作用,而Nodom通过DiffTool中的比较节点方法(**compare**)来得出需要修改的最小单位,再更新视图,减少了dom操作,达到提高性能的目的。
#### compare
在原始的diff算法中需要循环递归遍历节点依次进行比较,虽然比起没有diff算法之前有所优化,依旧效率比较低,**compare**方法对此做了一些改变。
在**compare**方法中有三个参数分别是**src**(待比较节点)、**dst**(被比较节点)、**changeArr**(增删改的节点数组)三个参数。**compare**方法在节点开始比较前根据节点类型的不同有不同的策略。相应方法解决相应类型问题,在子节点的对比中也有子节点对比策略。在进行新旧节点的**compare**操作后,若节点会提前移动,则跳过这个节点从而减少移动操作,然后**sameKey**方法会确定一遍**src**和**dst**是否有相同key来确定是否还能减少不必要的移动次数。当有新增节点或删除节点时则对**children**数组进行相应操作,最后进行真实dom的渲染结束。
#### 总结
虚拟dom通过找出最小差异,达到最小次数操作dom目的。同时虚拟DOM是JS的对象,有利于进行跨平台操作。
## 深入
#### 模块注册
根模块的注册除外,Nodom为其余模块提供两种注册方式:
* 模块modules数组注册
```js
class ModuleA extends Module{
...
}
class ModuleB extends Module{
...
}
class Module extends Module{
...
modules=[ModuleA,ModuleB]//或者在构造函数内指定
...
template(){
return `
`
}
}
```
* *registModule*方法
registModule方法可以给待注册模块设置**别名**,在模板代码中使用模块时,既可以使用模块类名作为标签名引入,也可以使用注册的别名作为标签名引入。
```js
class ModuleA extends Module{
...
}
registModule(ModuleA,'User');
class Main extends Module{
template(){
return `
`
}
}
```
### 模块传值&Props
为了加强模块之间的联系,Nodom在模块之间提供Props来传递数据。除根模块外,每个模块在进行模板代码解析,执行模块实例的template方法时,会将父模块通过dom节点传递的属性以对象的形式作为参数传入,也就是说,子模块可以在自己的template函数内,依据传入的props**动态创建模板**。
```js
class ModuleA extends Module{
template(props){
//在template函数内可以进行模板预处理
if(props.name=='add'){
return `${props.name}`
}else{
return `none
`
}
}
}
registModule(ModuleA,'User');
class Main extends Module{
template(){
return `
`
}
}
```
借助模板字符串的加持,可以使用包含特定语法(`${expression}`)的占位符,很大程度的拓展了模板代码的灵活度。在占位符内可以插入原生的JavaScript表达式。
#### 数据传递
Nodom数据传递为单向数据流,Props可以实现父模块向子模块的数据传递,但是这是被动的传递方式,如果需要将其保存至子模块内的代理数据对象,可以在传递的属性名前,加上`$`前缀,Nodom会将其传入子模块的根Model内,实现响应式监听。
> 注意:以$前缀开头的Props属性,如果对应的是一个Model对象,该Model对象存在于两个模块内,Model内数据的改变会造成两个模块的渲染。
```js
class ModuleA extends Module{
template(props){
return `{{name}}`
}
}
registModule(ModuleA,'User');
class Main extends Module{
template(){
return `
`
}
data(){
return {
name:'Nodom',
}
}
}
```
#### 反向传递
由于Props的存在,父模块可以暴露外部接口,将其通过Props传递给子模块,子模块调用该方法即可实现反向传递的功能。例如:
```js
class ModuleA extends Module{
template(props){
this.parentChange=props.add;
return `