字符串插值

了却一年前没解决的问题

发表于 2023-07-04 08:06:46
更新于 2024-04-18 13:33:37
Javascript

需求

上面这一代文字,蓝色部分是动态的,通过接口获取。

这时候如果需要在前端呈现那么一段文案,是非常容易的,这时候原生 JS 可以利用模版字符串,Vue 模版也可以使用文本插值的形式完成需求。

如果要对插值的部分做加粗或者颜色高亮,也是容易的,在插值外包裹标签写上样式即可。

但是如果再加上整个文案的格式也会变化,这时候就会非常难做,因为我们很难在比较动态的文案格式上在哪包裹标签。

现在,文案格式会通过配置中心下发,当前格式为:全民制作人大家好,我是练习时长[[time]]的个人练习生[[name]]。喜欢[[likes]]。

随着需求变更,格式可能会变成:大家好,我是[[name]]。练习时长[[time]],强调一下,时长是[[time]]。

可以看到,用到的插值可能会改变顺序,或减少,或重复使用。(增加的新的插值,可能仍然需要变更代码)。

去年,我遇到的问题是国际化的问题,国际化文本中也有类似的插值允许使用变量,不同语言的翻译使得这个变量出现的顺序和位置可能是不同的,而且 UI 上希望变量有加粗强调的效果。

当时,由于是 B 端项目,向上游反馈了额外处理这种文案的加粗的效果无法实现,UI、产品同时也就妥协了。

最近遇到了类似我上方描述的需求,这次我希望把这个顽疾解决掉了。

PS: 这里我插值用了方括号,这是因为我的博客在 vitepress,它似乎不能在 md 里正确显示双花括号,应该是和 Vue 冲突了,后续示例代码实际处理的是双花括号。

富文本方案?

富文本肯定能够完成这个需求,即使是小程序和 React Native 都分别能用 rich-text 或者三方组件库呈现富文本。

但是,配置此文本的产品不一定具有前端背景,比起简单的插值 大家好,我是[[name]]。,让他额外去配置一些前端标签 大家好,我是<span style="color:blue">[[name]]</span>。,这种设计显得很不友好。

所以我决定使用解析字符串模版的方式。

字符串解析

json
[
    {
        "type": "normal",
        "text": "全民制作人大家好,我是练习时长"
    },
    {
        "type": "variable",
        "text": "time"
    },
    {
        "type": "normal",
        "text": "的个人练习生"
    },
    {
        "type": "variable",
        "text": "name"
    },
    {
        "type": "normal",
        "text": "。喜欢"
    },
    {
        "type": "variable",
        "text": "likes"
    },
    {
        "type": "normal",
        "text": ""
    }
]

如果我们能够将字符串解析成上方这样的形式,我们就可以通过简单的模版循环,针对变量类型的节点应用特殊的样式。

变量类型的节点,再进一步结合接口,去取相应的值去显示。这样这个算是拖了一年的需求算是完成了。

前段时间,我学习了编译原理和力扣上刷了状态机的题目,这次便有了用状态机去解析这个字符串模版的思路。

我们不需要处理双括号嵌套的情况,类型也只需要区分普通的文本节点,和变量类型的节点,但是即便如此,状态的转移还是比较复杂的,但是厘清思路后,不算太难。

ts
enum State {
    Normal = 'Normal',
    MStart = 'Mustache_Start',
    Variable = 'Variable',
    MClose = 'Mustache_Close',
    Error = 'Error',
}

type StringParsed = {
    type: 'normal' | 'variable',
    text: string,
}

export default function parser(str: string) {
    let state = State.Normal;
    let curr = '';

    const data: StringParsed[] = [];

    for (let i=0; i<str.length;) {
        const currStr = str[i];
        const nextStr = str[i+1];

        state = handleStateTransfer(state, currStr, nextStr);
        if (state === State.Error) {
            throw new Error('出错了');
        }
        if (state === State.MStart || state === State.MClose) {
            i += 2;
            if (curr) {
                data.push({
                    type: state === State.MStart ? 'normal' : 'variable',
                    text: curr,
                });
                curr = '';
            }
        } else {
            i += 1;
            curr += currStr;
        }
    }
    if (curr && state === State.Normal) {
        data.push({
            type: 'normal',
            text: curr,
        });
    }
    return data;
}

function handleStateTransfer(state: State, currStr: string, nextStr: string): State {
    if (state === State.Normal && (currStr + nextStr) !== '{{') {
        return State.Normal;
    } else if (state === State.Normal && (currStr + nextStr) === '{{') {
        return State.MStart;
    } else if (state === State.MStart) {
        return State.Variable;
    } else if (state === State.Variable && (currStr + nextStr) !== '}}') {
        return State.Variable;
    } else if (state === State.Variable && (currStr + nextStr) === '}}') {
        return State.MClose;
    } else if (state === State.MClose) {
        return State.Normal;
    } else {
        return State.Error;
    }
}

效果演示