7

JS 标签模板(Tagged templates)什么时候使用?

 2 years ago
source link: https://www.zhangxinxu.com/wordpress/2021/12/js-tagged-templates/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

JS 标签模板(Tagged templates)什么时候使用?

这篇文章发布于 2021年12月29日,星期三,23:17,归类于 JS API。 阅读 662 次, 今日 349 次 2 条评论

by zhangxinxu from https://www.zhangxinxu.com/wordpress/?p=10251 鑫空间-鑫生活
本文欢迎分享与聚合,全文转载就不必了,尊重版权,圈子就这么大,若急用可以联系授权。

圣诞星空图

一、先了解下什么是标签模板

标签模板是模板字符串使用的一种高级形式,用代码示意就是——

这是我们常见的模板字符串:

const author = 'zhangxinxu';
console.log(`write by ${author}`);

这个就是标签模板,是模板字符串使用的高级形式:

const author = 'zhangxinxu';
function tag (arr, exp) {
    return `${arr[0]}${exp}`;
}
console.log(tag`write by ${author}`);

两段内容返回的结果是一样的。

仔细看 tag`write by ${author}` 这段代码,其中 tag 就是标签模板中的“标签”,`write by ${author}` 就是标签模板中的“模板”。

因此,“标签”实际上并不是指的标签,而是类似于标签性质的函数,“模板”则是这个函数的参数。

标签模板由标签函数和模板字符串两部分组成,其中标签函数的名称大家可以根据项目规范随意命名,模板字符串往往是是需要处理的数据内容。

其中,理解的难点在标签函数上。

参数是这样的(假设函数名是 tag):

tag(arrStrings, exp1, exp2, exp3, ...)

其中,arrStrings 指的是被 ${...} 这种表达式分隔的字符串,exp1, exp2, ... 分别表示第1个 ${...} 占位符中表达式的值,第2个 ${...} 表达式的值…

tag`write by ${author}, welcome to share!`

此时,arrStrings 就是 ['write by ', ', welcome to share!'],由于只有一个 ${...} 占位符,因此,exp1 就是 author 对应的变量值,exp2 没有对应的值,因此是 undefined

我们可以测试下:

const author = 'zhangxinxu';
function tag (arr, exp1, exp2) {
    console.log(arr[0], arr[1], exp1, exp2);
}
tag`write by ${author}, welcome to share!`

输出的结果是:

// write by
// , welcome to share!
// zhangxinxu
// undefined
2021-12-27_183900.png

好,现在我们已经知道标签模板是个什么东西了,关键问题是,这个看起来有些厉害的用法有什么用呢?

从上面的案例来看,直接模板字符串一把梭不更好。

关于这个问题,我是这么认为的。

//zxx: 如果你看到这段文字,说明你现在访问是不是原文站点,更好的阅读体验在这里:https://www.zhangxinxu.com/wordpress/?p=10251(作者张鑫旭)

二、标签模板什么时候使用?

讲讲我粗浅的看法,如有不对,欢迎指正。

我认为标签模板就是变体版本的 replace() 替换函数。

举个例子,请看下面这段代码:

'write by ${author}, welcome to share!'
  .replace(/([\w\W]+)\$\{(\w+)\}([\w\W]+)/, function (matches, $1, $2, $3) {
    console.log([matches, $1, $2, $3].join('\n'));
});
write by ${author}, welcome to share!
write by
author
, welcome to share!

截图示意运行效果:

replace 运行效果

此时,我们就可以借助 $1, $2, $3 等参数进行匹配的内容进行处理和返回,从而得到最终的替换后的结果。

const author = 'zhangxinxu';
function tag (str) {
    return str.replace(/([\w\W]+)\$\{(\w+)\}([\w\W]+)/, function (matches, $1, $2, $3) {
        return $1 + new Function('return ' + $2)() + $3;
    });
}
tag('write by ${author}, welcome to share!');
// 结果是 write by zhangxinxu, welcome to share!

是不是和标签模板的内核很相似?

实际上,很多传统的模板匹配引擎其底层就是类似的字符匹配处理。

下面问题来了,既然 repalce() 替换也能实现类似标签模板的效果,那为什么还需要标签模板呢?

原因有二:

  1. 使用成本比较高的,写正则这种事情,那不是一天两天可以学会的;
  2. 只能处理字符串类型的参数,限制了其使用范围。

repalce() 替换语法的唯一优势就是原始的替换内容是动态的,不确定的,则非常适合,具有不可替代性。

于是回到了问题本身,什么时候适合使用标签模板?

回答:

适合结构已知的,需要对变量进行动态处理的场景。

注意这里一个关键点,需要对变量动态处理,如果变量只是单纯显示(或者只是简单的表达式逻辑),直接使用模板字符串就好了,可以完全驾驭,详见“ES6模板字符串在HTML模板渲染中的应用”这篇文章。

以及,如果变量是数值、函数或者纯对象,需要基于类型做动态处理,则也非常适合使用标签模板。

例如下面一段邀请函模板:

诚挚邀请 xxx 先生(女士)作为选手(裁判/记分员/摄影师)于 xxxx年xx月xx日参加上海张江杯垂钓竞技大赛。主办方:上海市浦东钓鱼协会

其中,动态内容有邀请人名字,性别,角色以及日期。

然而,后端返回的数据是这样的:

{
    "name": "邓铁",
    "sex": 0,
    "role": 1,
    "time": 1640678098887
}

像性别和角色返回的是ID标识,无法直接填进去,需要动态处理下,而且处理起来并不是简单的表达式就能搞定的(或者表达式会比较长),此时,我们就可以在标签函数中处理内容转换的问题,相比在外部处理,逻辑会更加干净,代码会更加简洁易读。

const data = {
    "name": "邓铁",
    "sex": 0,
    "role": 1,
    "time": 1640678098887
}
const invite = function (arrs, nameExp, sexExp, roleExp, timeExp) {
    let strName = nameExp;
    // 性别处理
    let strSex = ['先生', '女士'][sexExp];
    // 角色处理
    const role = {
        "1": "选手",
        "2": "裁判",
        "3": "记分员",
        "4": "摄影师"
    };
    let strRole = role[roleExp];
    // 日期处理
    let strTime = new Date(timeExp).toLocaleDateString(undefined, {
        year: 'numeric',
        month: 'long',
        day: 'numeric'
    });

    // 输出内容
    let output = [arrs[0]];

    [strName, strSex, strRole, strTime].forEach((str, index) => {
        output.push(str, arrs[index + 1] || '');
    });

    return output.join('');
};

let content = invite`诚挚邀请${data.name}${data.sex}作为${data.role}于${data.time}参加上海张江杯垂钓竞技大赛。
主办方:上海市浦东钓鱼协会`;

console.log(content);

模板只负责填充数据,至于最终数据的返回,统一在标签函数中处理,最后的执行结果如下:

诚挚邀请邓铁先生作为选手于2021年12月28日参加上海张江杯垂钓竞技大赛。
主办方:上海市浦东钓鱼协会

字符输出最终结果

更复杂的案例

上面的案例的数据处理还是一对一的枚举处理,复用性并不高。

下面这个例子就要更复杂,要更抽象一点。

实现的效果是,一段 HTML 标签模板,如果有设置类似 onClick 这样的 Function 类型占位,则渲染出来的 DOM 元素自动绑定该事件。

`<button onClick=${() => addTodo()}>添加任务列表</button>`

不仅渲染按钮元素,还会给这个元素绑定 'click' 事件。

有点类似于 JSX 的实现。

完成代码示意如下,HTML部分:

<div id="app"></div>

JS 代码部分:

<script>
const render = function (data, container) {
    container.innerHTML = '';
    container.append(element(data));
};
const html = function (arr, ...keys) {
    let result = [arr[0]];
    keys.forEach(function(key, i) {
      if (typeof key == 'function') {
        result.push(i, arr[i + 1]);
      } else {
        result.push(key, arr[i + 1]);
      }      
    });
    // 创建 template 元素
    let template = document.createElement('template');
    template.innerHTML = result.join('');
    // 遍历与事件添加
    template.content.querySelectorAll('*').forEach(node => {
        let attrs = node.attributes;
        for (let i = attrs.length - 1; i >= 0; i--) {
            let attr = attrs[i].name;
            let value = attrs[i].value;
            if (/^on[a-z]+$/i.test(attr) && !isNaN(parseFloat(value))) {           
                node.removeAttribute(attr);
                node.addEventListener(attr.replace(/^on/, ''), keys[Number(value)]);
            }
        }
    });

    return template.content;
};

let todos = ['吃饭', '睡觉', '打豆豆'];

function addTodo () {
    todos.push(task.value);
    render(todos, app);
};

let element = function (todos) {
    return html`<h3>任务列表(${todos.length})</h3>
        <ul>
        ${todos.map(
            todo => `<li>${todo}</li>`
        ).join('')}
        </ul>
        <form onSubmit="${e => { e.preventDefault(); }}">
            <input id="task" required>
            <button onClick=${() => addTodo()}>添加任务列表</button>
        </form>
    `;
};
    
render(todos, app);
</script>

实现的原理如下,如果模板参数是个 Function 类型,则把这个 Function 替换为对应的索引值,然后使用 <template> 元素构造 DOM 结构的时候,匹配到符合规则的 HTML 属性,重新找回该 Function,使用 addEventListener 添加事件。

并返回完整的文档片段。

最终实现的效果如下,默认进入页面可以看到渲染了 3 个列表:

页面列表渲染示意

然后添加一个选项,并点击按钮,会看到新的选项 append 到列表中了,如下 GIF 动图所示:

TODO内容添加截图

//zxx: 这个例子主要示意渲染,实际上,真实开发需要通过 DOM 比对进行内容更新,而不是像这样全部替换。

眼见为实,您可以狠狠地点击体验效果:JS标签模板HTML渲染demo

这里的案例来自微博用户 编程加 的反馈:

大部分场景是实现 DSL,比如文中的 html,styled-components 里的 css,还有各种 sql、graphql……比如可以写一个比较优雅的、用来处理 URL 转义的 tag function,使用者不需要关心 URL 转义的细节:

URL 处理

总结一下,JS 标签模板并不是一个非使用不可的特性。

当模板结构固定,同时数据处理比较复杂的时候,会比较合适。

或者,你希望隐藏对不同数据处理的细节,让代码变得更干净。

或者,你希望在团队代码里露两手,都是可以使用的。

OK,以上就是本文的全部内容啦,写得匆匆忙忙的,有错误在所难免,欢迎指正,也欢迎点击这里给你的小伙伴们。

么么哒,元旦快乐。

对了,前两天编辑和我讲,我的新书《CSS新世界》获得年度畅销新书奖,哈哈,有些意外,毕竟 8 月中旬才上架,听到这个消息还挺开心的,目前微博上有这个签名版书的抽奖,大家可以试试转发下,周五开奖,说不定就是你了。

年度作者和新书奖

(本篇完)1f44d.svg 是不是学到了很多?可以分享到微信
1f44a.svg 有话要说?点击这里

本文为原创文章,欢迎分享,勿全文转载,如果实在喜欢,可收藏,永不过期,且会及时更新知识点及修正错误,阅读体验也更好。
本文地址:https://www.zhangxinxu.com/wordpress/?p=10251


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK