表达式扩展语法

NornJ提供了一种在tagged templates语法中写表达式的功能,称为NornJ 表达式。主要用作增强现有的JS表达式,并实现过滤器等内建的新语法。使用方式很简单,即标签名为ntagged templates

ReactDOM.render(
  <>
    <input value={n`'Jack' | lowerFirst`} />
    <input value={n`(1 .. 5).join('-')`} />
  </>
)
/*
  渲染结果:<input value="jack" /><input value="1-2-3-4-5" />
*/

表达式能做什么

简单地说,NornJ表达式中内建了一些常规JS语法不支持的特性:

过滤器

过滤器是什么?是一种在模板引擎(或含有模板引擎的框架,如Vue)中常见的功能,主要用于过滤展示数据。可参考Nunjucks的文档:https://mozilla.github.io/nunjucks/templating.html#filters。

NornJ过滤器的基本用法:

<i>{n`1234.567 | currency(2.05 | toInteger) | isString`}</i>
/*
  例中先执行currency过滤器返回"$1234.57",然后再执行isString过滤器判断是否字符串。
  渲染结果:<i>true</i>
*/

如上,过滤器的语法的常规模板引擎一致使用|符号作为分隔符,并且还支持为过滤器传参数嵌套过滤器。更重要的是,还可以扩展出新的过滤器。

运算符

NornJ表达式中除了支持常规运算符外,还可以支持一些在常规JS语法中不支持的运算符,比如范围运算符..

<i>{n`(1 .. 5).join('-')`}</i>
/*
  例中"1 .. 5"的运算结果是一个含1-5的数组(即"[1, 2, 3, 4, 5]"),然后再执行数组的join方法用"-"连接每项。
  渲染结果:<i>1-2-3-4-5</i>
*/

如上,此类自定义运算符是可以在NornJ表达式中同常规的JS语法一起运作。同过滤器一样,运算符也可以支持扩展出新的。

适配常规 JS 表达式

NornJ表达式对于常规的 JS 语法有非常好的适配性,例如下面列举的这些常用场景都可以支持:

const Test = () => (
  <>
    <i>{n`1 + 2 * 3 - 4 / 5`}</i>
    <i>{n`1 < (2 + 1) && !(5 > 4)`}</i>
    <i>{n`'abc'.trim() + 'def'.substr(1).length`}</i>
  </>
);

使用变量

借助于Babel 插件的转换能力,NornJ表达式内可以直接使用当前作用域内可用的变量:

const Test = props => {
  const str = 'abc';
  const str2 = 'def';

  return (
    <>
      <i>{n`props.a > (2 + 1) && !props.b < 4`}</i>
      <i>{n`str.trim() + str2.substr(1).length`}</i>
    </>
  );
};

除了上述使用方法外,变量也可以通过tagged templates参数方式传入,效果是一样的:

const Test = props => {
  const str = 'abc';
  const str2 = 'def';

  return (
    <>
      <i>{n`${props.a} > (2 + 1) && ${props}.b < 4`}</i>
      <i>{n`${str}.trim() + ${str2}.substr(1).length`}</i>
    </>
  );
};

每个NornJ表达式计算后都会返回正确的类型,所以也可以和外部 JS 语法混合:

const Test = props => (
  <>
    <i>{n`${props.a} > (2 + 1)` && n`${props}.b < 4`}</i>
  </>
);

插入过滤器等自定义语法

NornJ表达式内可以同时使用常规 JS 语法过滤器等自定义语法,这正是它的特色:

const Test = () => {
  const str = 'abc';

  return (
    <>
      <i>{n`str.trim() | upperFirst`}</i>
      <i>{n`12345678 | currency`}</i>
      <i>{n`(1 .. 100).length * 100`}</i>
    </>
  );
};

表达式内目前不支持的语法

NornJ表达式虽然适配性很强,但当前还是有一些不支持的语法,如下:

//注意,以下为一些目前不支持的语法
const Test = props => (
  <>
    <i>{n`!!12345`}</i>  {/* 只有1个!是支持的;多个!暂不支持 */}
    <i>{n`12345++`}</i>  {/* 暂不支持自增等运算 */}
    <i>{n`+12345`}</i>  {/* 暂不支持一元运算(!除外) */}
    <i>{n`'12345'.map((item, i) => item)`}</i>  {/* 暂不支持定义函数体 */}
  </>
);

//上面不支持的语法可以用原生JS语法弥补,这样改写:
const Test2 = props => (
  <>
    <i>{!n`!12345`}</i>
    <i>{n`12345`++}</i>
    <i>{+n`12345`}</i>
    <i>{n`'12345'.map(${(item, i) => item})`}</i>
  </>
);

不过,日后不排除NornJ会支持上述语法的可能性,已支持的语法已足够支撑常规开发使用。

另外,NornJ表达式的目标定位是为现有 JS 语法做增强,故它的内部或许并不需要实现全部的 JS 表达式。

过滤器

NornJ过滤器提供了一些常用内置功能,且完全可以支持用户扩展。

过滤器的管道与函数形式

NornJ的过滤器除了支持管道形式写法外,还可以支持用函数形式写法,效果是一样的:

const Test = () => {
  const str = 'abc';

  return (
    <>
      {/* 管道形式 */}
      <i>{n`str.trim() | upperFirst`}</i>
      <i>{n`12345678 | currency`}</i>

      {/* 函数形式 */}
      <i>{n`upperFirst(str.trim())`}</i>
      <i>{n`currency(12345678)`}</i>
    </>
  );
};

我们不妨对比一下过滤器的管道形式函数形式写法,可发现管道形式的主要优势在于对于数据过滤看起来更为直观。

下面是NornJ的内置过滤器:

注意,内置过滤器只包含一些基础功能,例如很大一部分是NornJ底层必要使用的工具函数。NornJ的定位并不是一个类似Lodash的工具函数库,它的目标只是为常规JS开发提供过滤器这种新的扩展方式而已。

字符串处理

upperFirst

upperFirst可以实现首字母大写:

const Test = () => (
  <>
    <i>{n`'jack' | upperFirst`}</i>
  </>
);

//输出:<i>Jack</i>

lowerFirst

lowerFirst可以实现首字母小写:

const Test = () => (
  <>
    <i>{n`'Jack' | lowerFirst`}</i>
  </>
);

//输出:<i>jack</i>

camelCase

camelCase可以将kebab-case字符串转换为camel-case

const Test = () => (
  <>
    <i>{n`'margin-left' | camelCase`}</i>
  </>
);

//输出:<i>marginLeft</i>

类型测定

isObject

isObject用于检查类型是否为对象:

const Test = ({ children }) => (
  <>
    <i>{n`children | isObject`}</i>
  </>
);

//输出:<i>true</i>

isNumber

isNumber用于检查类型是否为数字:

const Test = ({ children }) => (
  <>
    <i>{n`children.length | isNumber`}</i>
  </>
);

//输出:<i>true</i>

isString

isString用于检查类型是否为字符串:

const Test = ({ children }) => (
  <>
    <i>{n`children.length | isString`}</i>
  </>
);

//输出:<i>false</i>

isArrayLike

isArrayLike用于检查类型是否为类数组:

const Test = ({ children }) => (
  <>
    <i>{n`children | isArrayLike`}</i>
  </>
);

//输出:<i>true</i>

currency

currency可以将数字转换货币形式:

const Test = () => (
  <>
    <i>{n`98765 | currency`}</i>                 {/* '$98,765.00' */}
    <i>{n`98765.32132 | currency(2)`}</i>        {/* '$98,765.32' */}
    <i>{n`98765.321 | currency(0, '¥')`}</i>    {/* '¥98,765' */}
    <i>{n`'abc' | currency(2, '$', '-')`}</i>    {/* '-' */}
  </>
);
参数 用法 类型 默认值 作用
decimals 98765 | currency(decimals) Int 2 小数位
symbol 98765 | currency(2, symbol) String '$' 钱币符号
placeholder 98765 | currency(2, '¥', placeholder) String '' 如传入非数字,则输出占位符

另外,symbolplaceholder还可以进行全局配置:

import nj from 'nornj';

nj.filterConfig.currency.symbol = '¥';
nj.filterConfig.currency.placeholder = '-';

toJS

toJS即为MobxtoJS方法:

const Test = () => {
  const view = useLocalStore(() => ({
    texts: ['abc', 'def']
  }));

  return (
    <>
      <i>{JSON.stringify(n`view | toJS`)}</i>
    </>
  );
};

使用 Lodash

NornJ的过滤器可以使用Lodash库的全部工具函数。它是一个过滤器的扩展,需要先这样全局引入一次:

import 'nornj/lib/filter/lodash';

然后就可以在NornJ表达式中使用了:

const Test = () => (
  <>
    <i>{n`'-abc-' | repeat(3)`}</i>        {/* '-abc--abc--abc-' */}
    <i>{n`'-abc-' | endsWith('bc-')`}</i>  {/* true */}
    <i>{n`'Foo Bar' | snakeCase`}</i>      {/* 'foo_bar' */}
  </>
);

更多Lodash过滤器的使用方法请查看Lodash 文档

开发新的过滤器

NornJ的过滤器都是支持可扩展的,可以自行封装各种新功能。

  • 每个过滤器实际上是一个扩展函数,使用nj.registerFilter方法注册:
import nj from 'nornj';

nj.registerFilter('2x', num => num * 2);

//支持一次注册过个过滤器
nj.registerFilter({
  '3x': num => num * 3,
  '4x': num => num * 4
});

console.log(n`100 | 2x | 3x | 4x`);
//输出:2400
  • 过滤器可以支持参数:
import nj from 'nornj';

//从扩展函数的第二个参数开始定义过滤器的每个参数
nj.registerFilter('times', (num, times) => num * times);

console.log(n`100 | times(10)`);
//输出:1000
  • 过滤器还可以支持嵌套:
console.log(n`100 | times((10 * 2) | 3x)`);
//输出:6000

上例中(10 * 2) | 3x的乘法运算需要加括号,是因为过滤器的运算优先级是最高的,并从左到右按顺序执行。

运算符

NornJ表达式中除了可以使用常规 JS 运算符外,还内置支持一些自定义的运算符,如%%..等,并且还支持扩展出新的运算符。

可选链

NornJ表达式中的链式调用语法,默认全部都是可选链

const Test = props => (
  <>
    <i>{n`props.abc.length > 1`}</i>  {/* 不会报错 */}
    <i>{props.abc.length > 1}</i>     {/* 报 props.abc 为 undefined 错误 */}
    <i>{props?.abc?.length > 1}</i>   {/* 不会报错(使用 Babel 转换可选链语法) */}
  </>
);

NornJ的可选链运算符在以下各种场景都可以正常使用,不会报错:

const Test = props => (
  <>
    <i>{n`props[abc].length > 1`}</i>
    <i>{n`props.abc.substr(1) > 1`}</i>
    <i>{n`props[abc]().length > 1`}</i>
  </>
);
  • 与 Babel 转换的可选链语法(?.)相比,NornJ的可选链运算符由于不须要写?,所以使用和阅读都会很便捷。

  • 但由于NornJ表达式内部目前不能支持定义函数体,所以有函数的场景可以这样使用:

const Test = props => (
  <>
    {/* 将函数体作为外部参数传入 NornJ 表达式 */}
    {n`props.data.map(${(item, i) => {
      return <i>{item}</i>
    }})`}
  </>
);

范围运算符

范围运算符可以快速生成数组:

const Test = () => {
  const a = 10,
    b = 20;

  return (
    <>
      <i>{n`1 .. 10`}</i>                         {/* [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] */}
      <i>{n`(1 .. 10).length`}</i>                {/* 10 */}
      <i>{n`(a .. b).length | includes(15)`}</i>  {/* true */}
    </>
  );
};

半闭区间范围运算符

..是闭区间的,NornJ还提供了它的半闭区间版本:

const Test = () => {
  const a = 10,
    b = 20;

  return (
    <>
      <i>{n`1 ..< 10`}</i>                         {/* [1, 2, 3, 4, 5, 6, 7, 8, 9] */}
      <i>{n`(1 ..< 10).length`}</i>                {/* 9 */}
      <i>{n`(a ..< b).length | includes(20)`}</i>  {/* false */}
    </>
  );
};

飞船运算符

飞船运算符是一种比较运算,它按照大于小于等于三种结果,分别返回1-10三种值:

const Test = () => (
  <>
    <i>{n`1 <=> 10`}</i>  {/* -1 */}
    <i>{n`1 <=> 1`}</i>   {/* 0 */}
    <i>{n`10 <=> 1`}</i>  {/* 1 */}
  </>
);

向下取整运算符

NornJ内置了一种可实现向下取整的除法运算符,它的底层实现是Math.floor(value1 / value2)

const Test = () => (
  <>
    <i>{n`100 %% 30`}</i>    {/* 3 */}
    <i>{n`56.78 %% 5`}</i>   {/* 11 */}
    <i>{n`-56.78 %% 5`}</i>  {/* -12 */}
  </>
);

开发新的运算符

NornJ的运算符也都是支持可扩展的,可以自行封装各种新功能。

NornJ中的运算符本质上其实是过滤器的一种语法糖形式,目前可以做到自定义扩展出各种二元运算符

  • 每个运算符实际上是一个扩展函数,和过滤器一样使用nj.registerFilter方法注册:
import nj from 'nornj';

//注册***运算符,功能为先进行乘法运算后,再乘上3倍
nj.registerFilter('***', (num, times) => num * times * 3);

//支持一次注册过个运算符,功能和***类似
nj.registerFilter({
  '****': (num, times) => num * times * 4,
  '*****': (num, times) => num * times * 5
});

然后需要配置一下.babelrc

{
  ...
  "plugins": [
    [
      "nornj-in-jsx",
      {
        "filterConfig": {
          "***": {
            isOperator: true
          },
          "****": {
            isOperator: true
          },
          "*****": {
            isOperator: true
          }
        }
      }
    ]
  ]
}

接着就可以在NornJ表达式中使用了:

console.log(n`100***10`);               //输出:100 * 10 * 3 = 3000
console.log(n`100***10****10`);         //输出:100 * 10 * 3 * 10 * 4 = 120000
console.log(n`100***10****10*****10`);  //输出:100 * 10 * 3 * 10 * 4 * 10 * 5 = 6000000
  • 运算符的命名目前是有限制的,如包含下列字符则需要做特殊处理:
[
  '_',
  '#',
  '.',
  '>',
  '<',
  '|'
]

如运算符名称中包名含上述字符,则需要这样注册:

import nj from 'nornj';

nj.registerFilter(
  '*.*',
  (num, times) => num * times * 3,
  {
    alias: '3x'  //添加别名
  }
);

nj.registerFilter({
  '*..*': {
    filter: (num, times) => num * times * 4,
    options: {
      alias: '4x'
    }
  },
  '*...*': {
    filter: (num, times) => num * times * 5,
    options: {
      alias: '5x'
    }
  }
});

然后需要配置一下.babelrc

{
  ...
  "plugins": [
    [
      "nornj-in-jsx",
      {
        "filterConfig": {
          "*.*": {
            isOperator: true,
            alias: '3x'
          },
          "*..*": {
            isOperator: true,
            alias: '4x'
          },
          "*...*": {
            isOperator: true,
            alias: '5x'
          }
        }
      }
    ]
  ]
}

接着就可以在NornJ表达式中使用了:

console.log(n`100*.*10`);               //输出:100 * 10 * 3 = 3000
console.log(n`100*.*10*..*10`);         //输出:100 * 10 * 3 * 10 * 4 = 120000
console.log(n`100*.*10*..*10*...*10`);  //输出:100 * 10 * 3 * 10 * 4 * 10 * 5 = 6000000

results matching ""

    No results matching ""