指令

指令是一种扩展的HTML(XML)标签属性,NornJ也为JSX提供了指令语法:

<div n-show={true}>
  Test directive
</div>

上例中的n-show即为指令语法。

指令能做什么

指令通常可以用来封装一些实用功能,以实现写更少的代码去做更多的事情为目的。具体来说NornJ的指令主要可以实现以下几种功能:

操作将传入组件的props值

NornJ的指令最主要的功能就是用来设置(或修改)JSX标签的属性值。比如预置指令n-show,它就是用来设置JSX标签的style.display属性:

<input n-show={false} />
/*
 实际渲染:<input style="display:none" />
*/

目前JSX原生的语法可以实现类似指令的效果吗?答案是可以的。通常可以使用JSX延展操作符来模拟出类似指令的效果,比如react-hanger的useInput

const newTodo = useInput('');

<input name="input" {...newTodo.eventBind} />
/*
 实际渲染:<input name="input" value={newTodo.value} onChange={newTodo.onChange} />
*/

但是,上面这种方式也存在以下这些问题:

  • 封装扩展的内部无法获取JSX标签已有的其他属性值,比如上例中的name="input"。这在开发一些功能时会有局限。
  • 写法与常规的JSX属性区别较大,可读性差一些。

然而NornJ的指令语法可以完美解决上述问题。

封装包装组件

设置(或修改)JSX标签的属性值是NornJ的指令最基本的功能。指令还能实现更高级的功能,可以在当前指令所在组件的外层再套自定义逻辑的包装组件。下面我们看一个简单的应用例子(使用ant-design的Tooltip组件)。

ant-design的Tooltip组件常规的写法:

import { Tooltip, Button } from 'antd';

ReactDOM.render(
  <div className="demo">
    <Tooltip placement="topLeft" title={text}>
      <Button>TL</Button>
    </Tooltip>
  </div>
)

然而可以使用NornJ指令的扩展开发方式将上面的Tooltip组件封装在一个包装组件之中,这样就可以像下面这种方式使用:

ReactDOM.render(
  <div className="demo">
    <Button n-tooltip-topLeft={text}>TL</Button>
  </div>
)

如上,使用了指令后组件树结构减少了一层,看起来会更加简洁清晰。上述n-tooltip指令的扩展实现方式,我们将在本章节最后详细阐述。


下面是NornJ已有内置的指令:

n-show

使用n-show可以在JSX中很方便地切换标签的style.display值是否为none,当值为false时不显示:

class TestComponent extends Component {
  render() {
    return <input n-show={this.props.show} />;
  }
}

ReactDOM.render(<TestComponent show={false} />);
/*
 渲染结果:<input style="display:none" />
*/
  • n-show指令<If>标签的区别
语法 特点 建议使用场景
n-show 初始渲染开销大;切换时开销小 在条件频繁切换时使用,性能会更好
<If> 初始渲染开销小;切换时开销大 在条件很少改变时使用,性能会更好

n-style

使用n-style可以在JSX中使用与html语法一致的css写法:

class TestComponent extends Component {
  render() {
    //以下与<input style={{ marginLeft: '10px', padding: 0 }} />效果相同
    return <input n-style="margin-left:10px;padding:0" />;
  }
}

n-style中也可以动态嵌入变量:

const cssProp = 'padding';

class TestComponent extends Component {
  render() {
    return <input n-style={`margin-left:${10};${cssProp}:0`} />;
  }
}

n-debounce

使用n-debounce可以在JSX中为input等表单元素增加防抖效果,以减少用户输入频率而提高性能:

class TestComponent extends Component {
  onChange = e => {
    //每次输入后延迟一定毫秒才触发一次
    console.log(e.target.value);
  };

  render() {
    return (
      <>
        <input n-debounce onChange={this.onChange} defaultValue="test" />
        <input n-debounce={200} onChange={this.onChange} />
      </>
    );
  }
}

如上,n-debounce的触发事件默认为onChange。如果不写n-debounce的值,默认为100毫秒

  • 指定任意事件

n-debounce也可以支持onChange以外的其他事件。比如onInput,则需要在n-debounce后面添加onInput参数:

class TestComponent extends Component {
  onInput = e => {
    console.log(e.target.value);
  };

  render() {
    return <input n-debounce-onInput={200} onInput={this.onInput} />;
  }
}

n-mobxBind

使用n-mobxBind指令可以配合Mobx的可观察变量在<input><textarea>等表单元素上创建双向数据绑定,它会根据控件类型自动选取正确的方法来更新值。

  • 基本使用方法
import { Component } from 'react';
import { observable } from 'mobx';

class TestComponent extends Component {
  @observable inputValue = 'test';

  render() {
    return <input n-mobxBind={this.inputValue} />;
  }
}

如上所示,无需编写<input>标签的onChange事件,inputValue变量已自动和<input>标签建立了双向数据绑定的关系。

  • 实质上,n-mobxBind的实现原理其实就是下面的语法糖形式:
class TestComponent extends Component {
  @observable inputValue = 'test';

  onChange = e => {
    this.inputValue = e.target.value;
  };

  render() {
    return <input value={this.inputValue} onChange={this.onChange} />;
  }
}
  • onChange事件

由于n-mobxBind默认自动设置了组件的onChange事件,但有些情况下我们可能还是需要在onChange中做一些其他的操作:

class TestComponent extends Component {
  @observable inputValue = 'test';

  onChange = e => {
    console.log(e.target.value);
  };

  render() {
    return <input n-mobxBind={this.inputValue} onChange={this.onChange} />;
  }
}

如上所示,onChange事件的行为和标签原生的onChange完全相同,它会在文本框的值变化后执行。

  • 增加防抖效果

可以使用debounce参数为n-mobxBind提供防抖效果:

import { Component } from 'react';
import { observable } from 'mobx';

class TestComponent extends Component {
  @observable inputValue = '';

  onChange = e => {
    console.log(e.target.value);
  };

  render() {
    return (
      <>
        <input n-mobxBind-debounce={this.inputValue} onChange={this.onChange} />
        <input n-mobxBind-debounce$200={this.inputValue} onChange={this.onChange} />
      </>
    );
  }
}

上例中的debounce参数默认值为100毫秒。也支持自定义设置,如例中为debounce加修饰符即可。

  • 使用action更新变量

Mobx开发中如果启动严格模式或者使用mobx-state-tree时,则须要使用action来更新变量。可按下面方式配置使用action:

import { observable, action, configure } from 'mobx';

// don't allow state modifications outside actions
configure({ enforceActions: true });

class TestComponent extends Component {
  @observable inputValue = 'test';

  @action.bound
  setInputValue(value, args) {
    this.inputValue = value;  //value是用户输入的新值
    console.log(args);        //args为控件onChange事件的全部参数,类型为数组
  }

  render() {
    return <input n-mobxBind={this.inputValue} />;
  }
}

如存在camel命名法(set + 首字母大写的observable变量名)定义的action时,n-mobxBind会默认执行它来更新数据。上例中为setInputValue

接下来我们来按控件分类列举下n-mobxBind指令可支持的场景:

绑定原生表单控件

原生表单控件包含文本框复选框单选按钮选择框等,以上都可以直接使用n-mobxBind指令,会自动监听相应控件的onChange事件并正确地更新值。

文本框

单行文本框:

class TestComponent extends Component {
  @observable inputValue = 'test';

  render() {
    return (
      <>
        <input n-mobxBind={this.inputValue} />
        <p>Message is: {this.inputValue}</p>
      </>
    );
  }
}

多行文本框:

class TestComponent extends Component {
  @observable inputValue = 'test';

  render() {
    return (
      <>
        <textarea n-mobxBind={this.inputValue}></textarea>
        <p>Message is: {this.inputValue}</p>
      </>
    );
  }
}

复选框

单个复选框,绑定到布尔值:

class TestComponent extends Component {
  @observable checked = false;

  render() {
    return (
      <>
        <input type="checkbox" id="checkbox" n-mobxBind={this.checked} />
        <label for="checkbox">{this.checked}</label>
      </>
    );
  }
}

多个复选框,绑定到同一个数组:

class TestComponent extends Component {
  @observable checkedNames = ['Jack', 'Mike'];

  render() {
    return (
      <>
        <input type="checkbox" id="jack" value="Jack" n-mobxBind={this.checkedNames} />
        <label for="jack">Jack</label>
        <input type="checkbox" id="john" value="John" n-mobxBind={this.checkedNames} />
        <label for="john">John</label>
        <input type="checkbox" id="mike" value="Mike" n-mobxBind={this.checkedNames} />
        <label for="mike">Mike</label>
        <br />
        <span>Checked names: {this.checkedNames}</span>
      </>
    );
  }
}

单选按钮

class TestComponent extends Component {
  @observable picked = '';

  render() {
    return (
      <>
        <input type="radio" id="one" value="One" n-mobxBind={this.picked}>
        <label for="one">One</label>
        <br />
        <input type="radio" id="two" value="Two" n-mobxBind={this.picked}>
        <label for="two">Two</label>
        <br />
        <span>Picked: {this.picked}</span>
      </>
    );
  }
}

选择框

单选时:

class TestComponent extends Component {
  @observable selected = '';

  render() {
    return (
      <>
        <select n-mobxBind={this.selected}>
          <option disabled value="">请选择</option>
          <option>A</option>
          <option>B</option>
          <option>C</option>
        </select>
        <span>Selected: {this.selected}</span>
      </>
    );
  }
}

多选时,绑定到一个数组:

class TestComponent extends Component {
  @observable selected = [];

  render() {
    return (
      <>
        <select n-mobxBind={this.selected} multiple n-style="width: 50px;">
          <option>A</option>
          <option>B</option>
          <option>C</option>
        </select>
        <br />
        <span>Selected: {this.selected}</span>
      </>
    );
  }
}

<Each>渲染的动态选项:

class TestComponent extends Component {
  @observable selected = 'A';
  options = [
    { text: 'One', value: 'A' },
    { text: 'Two', value: 'B' },
    { text: 'Three', value: 'C' }
  ];

  render() {
    return (
      <>
        <select n-mobxBind={this.selected}>
          <Each of={this.options}>
            <option value={item.value}>{item.text}</option>
          <Each/>
        </select>
        <span>Selected: {this.selected}</span>
      </>
    );
  }
}

绑定组件

除了上述的原生表单控件外,n-mobxBind指令也可以绑定到任意React组件上。当然,前提是该组件可能需要使用nj.registerComponent进行注册,并且设置一些必要的参数。

例如我们注册一个使用ant-design的Input组件的例子,首先是注册组件:

import nj from 'nornj';
import { Input } from 'antd';

nj.registerComponent(
  'ant-Input',            //组件名(全局唯一),类型为字符串
  Input,                  //组件对象
  {                       //组件配置参数对象
    hasEventObject: true    //为true时使用e.target.value获取值
  }
);

上述代码在全局统一注册一次就可以了。然后便可以正常地使用n-mobxBind指令进行绑定:

import { Component } from 'react';
import { observable } from 'mobx';
import { Input } from 'antd';

class TestComponent extends Component {
  @observable inputValue = 'test';

  render() {
    return <Input n-mobxBind={this.inputValue} />;
  }
}

注册组件

在注册很多组件时按各参数的默认值就可以了,也就是说其实可以不写nj.registerComponent的第三个参数的。但是也有组件需要配置一些参数,例如:

import nj from 'nornj';
import { Cascader } from 'antd';

nj.registerComponent(
  'ant-Cascader',     //组件名(全局唯一),类型为字符串
  Cascader,           //组件对象
  {                   //组件配置参数对象,如果下表中的默认配置都满足要求也可以省略
    needToJS: true      //值被更新到该组件前,需要执行一次Mobx.toJS
  }
);

所有组件参数列表:

参数名 类型 默认值 作用
hasEventObject Boolean false 为true时,更新事件中使用<input onChange={e => e.target.value} />取值。
为false时,更新事件中使用<input onChange={value => value} />取值。
targetPropName String 'value' 如果hasEventObject参数为true,则更新事件中使用<input onChange={e => e.target[targetPropName]} />取值。
不填时默认值是value,也就是使用e.target.value取值。
valuePropName String 'value' 被绑定控件的值属性名,即<input value={...} />中的value属性名称。比如可以依不同组件特性修改为textValuechecked等等。
changeEventName String 'onChange' 被绑定控件的更新事件属性名,即<input onChange={...} />中的onChange属性名称。比如可以依不同组件特性修改为onInputonTextChange等等。
needToJS Boolean false 输入的新值在被更新到组件时,是否需要执行一次Mobx.toJS。例如一些需要绑定到数组值的组件可能需要设置needToJS为true,否则无法正确地更新值到相应的组件中,比如ant-design的Cascader组件
需要进行这一步操作,是由Mobx可观察变量的特性与该组件的内部实现是否有冲突来决定的,这个有时候也无法避免。

已预置注册的组件

目前ant-design组件库已在nornj-react包中预置注册了全部组件。也就是说对于ant-design组件库无需再手工注册了,按下面方式直接引入就可以使用n-mobxBind指令。

首先需要安装babel-plugin-import插件,并在.babelrc增加以下配置:

"plugins": [
   ...
   [
     "import",
     {
       "libraryName": "nornj-react/antd",
       "style": true
     }
   ],
   ...
]

然后这样引入使用各ant-design组件即可:

import {
  Table,
  Input,
  Button,
  Pagination,
  Tabs,
  Tree,
  Select,
  Checkbox,
  Modal,
  message,
  Row,
  Col,
  Form,
  DatePicker,
  Icon,
  Steps,
  Divider
} from 'nornj-react/antd';  //注意,此处由"antd"改为"nornj-react/antd"

...
class TestComponent extends Component {
  @observable inputValue = 'test';

  render() {
    return <Input n-mobxBind={this.inputValue} />;
  }
}

开发新的指令

NornJ的指令都是支持可扩展的,也就是说可以自行封装各种新功能。

开发一个最简单的指令

例如实现一个n-class指令,功能即为与classnames库相同:

<div id="test" n-class={{ foo: true, bar: true }}>Test</div>
/* 以上渲染内容为:
<div id="test" class="foo bar">Test</div>
*/

上面的n-class指令实际上是一个扩展函数,使用nj.registerExtension方法注册:

import nj from 'nornj';
import classNames from 'classnames';

nj.registerExtension(
  'class',     //注意:指令名称需要使用小写开头的camel命名方式
  options => {
    const {
      tagProps,  //指令所在组件的props对象,本例中为{ id: 'test' }
      value      //指令值函数,注意它是个函数需要执行才能取到结果
    } = options;

    //在组件渲染前,使用classNames库来设置className属性的值
    tagProps.className = classNames(
      value()  //此处返回例中的{ foo: true, bar: true }
    );
  }
);

配置.babelrc(该例中此步骤也可以省略):

{
  ...
  "plugins": [
    [
      "nornj-in-jsx",
      {
        "extensionConfig": {
          "class": {
            "isDirective": true
          }
        }
      }
    ]
  ]
}

这样我们就成功开发了一个n-class指令,该实例演示了NornJ指令的操作将传入组件的props值功能。

更复杂的指令

接下来我们来实现一个内部封装了包装组件的指令n-tooltip,它的作用和ant-design的Tooltip组件是一样的:

<div>
  <Button n-tooltip-topLeft={text}>TL</Button>
</div>

首先,我们组要实现一个包装组件WrappedTooltip.jsx

import React from 'react';
import { Tooltip } from 'antd';

const WrappedTooltip = ({
    TooltipDirectiveTag,      //指令所在组件的组件对象;如果是原生html标签的话就是标签名字符串,如div
    tooltipDirectiveOptions,  //指令扩展函数的options对象
    ...tagProps               //指令所在组件的props对象
  }) => {
  const {
    props,
    value
  } = tooltipDirectiveOptions;

  //获取指令参数
  const args = props && props.arguments;

  return (
    <Tooltip
      placement={
        (args && args[0].name) || 'rightTop'  //指令的第一个参数传递到Tooltip组件的placement属性,即显示位置
      }
      title={value()}  //指令的值传到Tooltip组件的title属性,即显示文本
    >
      <TooltipDirectiveTag  //此处渲染指令所在组件
        {...tagProps}       //传递指令所在组件的props对象
      />
    </Tooltip>
  );
};

export default WrappedTooltip;

然后使用nj.registerExtension方法注册扩展函数:

import nj from 'nornj';
import WrappedTooltip from './WrappedTooltip.jsx';

nj.registerExtension(
  'tooltip',     //注意:指令名称需要使用小写开头的camel命名方式
  options => {
    const {
      tagName,    //指令所在组件对象
      tagProps,   //指令所在组件的props对象
      setTagName  //运行此函数,可以修改当前即将渲染的组件对象
    } = options;

    setTagName(WrappedTooltip);  //将当前渲染的组件修改为包装组件
    tagProps.TooltipDirectiveTag = tagName;      //传递指令所在组件对象到包装组件中
    tagProps.tooltipDirectiveOptions = options;  //传递指令的options到包装组件中
  }
);

上例有个需要注意的地方,就是TooltipDirectiveTagtooltipDirectiveOptions参数的命名应当特例化而避免和其他指令的重复。因为这样才能适应同时存在多个含有包装组件的指令的场景,比如<div n-directive1 n-directive2>

这样n-tooltip指令就开发完成了,还可以变更参数控制显示方向:

<div>
  <Button n-tooltip-topLeft="test1">TL</Button>
  <Button n-tooltip-topRight="test2">TL</Button>
</div>

数据绑定指令

数据绑定指令一般用来将传入的值与表单控件建立双向绑定关系n-mobxBind就是一个数据绑定指令,这种特殊的指令同样也可以支持扩展。下面我们先来实现一个用于React Hooks Api的n-bind指令,用法如下:

function TestBind() {
  const $count = useState(100),  //useState的返回值是一个数组,需要将它传到n-bind指令中
    [count] = $count;            //如有需要,可以再从$count解构出count和setCount

  return (
    <div>
      <input
        n-bind={$count} //与count建立双向数据绑定关系
        onChange={e => console.log(e.target.value)}
      />
      input: {count}
    </div>
  );
}

编写n-bind的扩展函数:

nj.registerExtension(
  'bind',
  options => {
    const {
      tagProps,
      value
    } = options;

    const [state, setState] = value();  //按useState的返回值结构来解构变量
    tagProps.value = state;             //设置当前组件的value对象

    const _onChange = tagProps.onChange;  //暂存当前组件的onChange事件函数
    tagProps.onChange = function (e) {    //重新设置onChange事件
      setState(e.target.value);         //更新变量值
      _onChange.apply(null, arguments)  //执行组件的onChange事件,并传递参数
    };
  }
);

用如上的方式我们就成功实现了一个简单的数据绑定指令n-bind。但是它目前只支持文本框,下面我们再让它支持单选按钮,用法如下:

function TestBind() {
  const $count = useState(100),
    [count] = $count;
  const $num = useState(''),
    [num] = $num;

  return (
    <div>
      <input
        n-bind={$count}
        onChange={e => console.log(e.target.value)}
      />
      input: {count}

      <input
        type="radio"
        value="first"
        n-bind={$num}
      />
      <input
        type="radio"
        value="second"
        n-bind={$num}
      />
      radio: {num}
    </div>
  );
}

修改n-bind的扩展函数:

nj.registerExtension(
  'bind',
  options => {
    const {
      tagProps,
      value
    } = options;

    const [state, setState] = value();  //按useState的返回值结构来解构变量

    if(tagProps.type == 'radio') {  //单选按钮
      tagProps.checked = tagProps.value === state;  //判断当前单选按钮组件是否为选中状态
    }
    else {  //文本框
      tagProps.value = state;  //设置当前文本框组件的value对象
    }

    const _onChange = tagProps.onChange;  //暂存当前组件的onChange事件函数
    tagProps.onChange = function (e) {    //重新设置onChange事件
      setState(e.target.value);         //更新变量值
      _onChange.apply(null, arguments)  //执行组件的onChange事件,并传递参数
    };
  }
);

如上,我们就实现了一个同时支持文本框和单选按钮的n-bind指令。而判断控件类型的逻辑,则是利用了NornJ指令能取到标签的所有其他props的特性。

接下来还有一种更复杂的场景,比如我们需要实现一个支持React Class组件的n-stateBind指令,用法如下:

class TestStateBind extends Component {
  state = {
    count: 100,
    foo: {
      count: 100
    },
    bar: {
      baz: {
        count: 100
      }
    }
  };

  render() {
    return (
      <div>
        <input
          n-stateBind={this.state.foo.count}
          onChange={e => console.log(e.target.value)}
        />
        input: {this.state.foo.count}
      </div>
    );
  }
}

首先需要修改.babelrc配置:

{
  ...
  "plugins": [
    [
      "nornj-in-jsx",
      {
        "extensionConfig": {
          "stateBind": {
            "isDirective": true,
            "isBindable": true   //设置isBindable为true,在取指令的值时会返回特殊的格式
          }
        }
      }
    ]
  ]
}

然后编写n-stateBind的扩展函数:

nj.registerExtension(
  'stateBind',
  options => {
    const {
      tagProps,
      value,
      context: {
        $this  //对于Class组件,可以这样取出当前组件的实例对象"$this"变量,也就是组件的实例引用"this"
      }
    } = options;

    const _value = value();         //注意,这里的value返回值是个特殊的对象结构,各属性如示例下面的表格所示
    tagProps.value = _value.value;  //设置当前组件的value对象

    const _onChange = tagProps.onChange;  //暂存当前组件的onChange事件函数
    tagProps.onChange = function (e) {    //重新设置onChange事件
      $this.setState(  //使用组件实例上的setState函数更新值
        putStateValue(_value, e.target.value),   //用自定义的putStateValue函数创建出setState所需的参数结构,下面有putStateValue的详细实现
        () => _onChange.apply($this, arguments)  //执行组件的onChange事件,并传递参数
      );
    };
  }
);

上面扩展函数代码中的_value对象的各属性为:

属性名 类型 作用
value Any 指令值,例:<input n-stateBind={this.state.foo.bar} />中的this.state.foo.bar值。
prop String 指令值的属性名,例:<input n-stateBind={this.state.foo.bar} />中的'bar'
source Object 指令值的当前层级所属对象引用,例:<input n-stateBind={this.state.foo.bar} />中的this.state.foo
parent Object 指令值的当前层级所属对象的父级对象引用,例:<input n-stateBind={this.state.foo.bar} />中的this.state
但是parent对象也是一个包含source属性的对象,所以可以向上级递归取出所有层级的对象引用。

最后是putStateValue函数的实现:

function putStateValue(value, ret) {
  return value.prop == 'state' ?
    ret :
    putStateValue(value.parent, { [value.prop]: ret });
}

putStateValue函数的实现逻辑其实很简单,就是递归获取this.state.foo.count当前层级值的parent属性,然后按相应格式构造出setState函数所需的参数结构即可。

如上,我们就实现了一个更复杂的数据绑定指令n-stateBind。其实n-mobxBind指令的实现方式也与本例中的n-stateBind类似。

为什么NornJ中只内置实现了支持Mobx的数据绑定指令?答案其实很简单:因为Mobx可观察变量的特性与操作方式,更适合此种指令方案的语法结构等各方面,可以更好地呈现双向数据绑定的优势而提高开发效率。

results matching ""

    No results matching ""