了却一年前没解决的问题
上面这一代文字,蓝色部分是动态的,通过接口获取。
这时候如果需要在前端呈现那么一段文案,是非常容易的,这时候原生 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>。
,这种设计显得很不友好。
所以我决定使用解析字符串模版的方式。
[
{
"type": "normal",
"text": "全民制作人大家好,我是练习时长"
},
{
"type": "variable",
"text": "time"
},
{
"type": "normal",
"text": "的个人练习生"
},
{
"type": "variable",
"text": "name"
},
{
"type": "normal",
"text": "。喜欢"
},
{
"type": "variable",
"text": "likes"
},
{
"type": "normal",
"text": "。"
}
]
如果我们能够将字符串解析成上方这样的形式,我们就可以通过简单的模版循环,针对变量类型的节点应用特殊的样式。
变量类型的节点,再进一步结合接口,去取相应的值去显示。这样这个算是拖了一年的需求算是完成了。
前段时间,我学习了编译原理和力扣上刷了状态机的题目,这次便有了用状态机去解析这个字符串模版的思路。
我们不需要处理双括号嵌套的情况,类型也只需要区分普通的文本节点,和变量类型的节点,但是即便如此,状态的转移还是比较复杂的,但是厘清思路后,不算太难。
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;
}
}