背景
腾讯问卷有两种创建问卷的模式,分别是高级编辑与文本编辑。高级编辑提供方便的UI界面,用户通过拖拽形式即可完成问卷创建。而文本编辑则是通过纯文本来生成问卷,适合需要大量创建题目的用户。当前文本编辑模式的解析器基于正则表达式实现,代码逻辑十分分散且复杂,使得想添加新语法会变得非常困难。而且并没有实现文本与问题ID的绑定,每次对文本内容修改时,会重新解析并实时生成新的问题ID。问题ID的变化会使之前设置的DSL(根据ID来处理逻辑)全部失效,同时还会导致以往关联的答案数据因为ID的丢失,而变得无效。
任务
前置工作
在行动前,我们还组织了一次方案评审会议,在这次会议中,也提到了一些没有考虑到的问题,比如:
行动
基于PEG.js改造
动机
由文本生成问卷的结构化对象(SurveyJson),这无异于是在做类似Markdown的事,说白了,就是写一门问卷语言。既然是要创建一门新语言,经过我们的研究,PEG.js是一项比较好的选择。从维基百科我们可以了解到,PEG在实现一门语言的方面,十分有优势。基于上述优势,使用PEG.js来实现解析器还是很有说服力的。
思路
由PEG.js来实现文本到SurveyJson的话,大概会经历这么几个步骤:Parser仅负责输出问卷文本的AST,而Transformer则将AST转换成SurveyJson即可。要转换成SurveyJson,还需要校验用户输入的文本是否正确,即ASTQuestion 是否正确。按照单一职责原则的话,是需要单独遍历一遍AST来校验后,再转换成 SurveyJson的,但是为了性能考虑,我还是选择了在一次遍历中,完成两件事情的操作,即有了如下的流程:按照上述流程,我们很快就完成了基于PEG.js的解析器改造。
解决 ID 问题
要解决每次ID重新生成的问题,我们想到最好的方法是在文本中显式写入ID,即使文本被编辑,也能根据显式写入的ID实现与SurveyJson中已有的问题绑定。Parser解析文本时,大概会生成这样的ASTQuestion结构:type Location = {
start: { offset: number, line: number, column: number }
end: { offset: number, line: number, column: number }
}
type ASTQuestion = {
id: string | null
title: {
content: object[],
location: Location
}
}
const astQuestion = {
id: 'q-1-abcd' || null,
title: {
content: ['a', 'b'],
location: {
start: { offset: 0, line: 1, column: 1 },
end: { offset: 2, line: 1, column: 3 },
}
}
}
而经过Tramsformer转换后,会变成这样的Question:
type Question = {
id: string,
title: string,
}
const question = {
id: 'q-1-abcd',
title: 'ab',
}
我们可以看出,只有在Question中,ID才是一定要存在的,因此,我们可以在Transformer中,实现ID的绑定或重新生成,即astQuestion.id===null为True时,自动生成ID,否则,沿用astQuestion.id。这样也给解决第三个问题打下了非常好的基础。
const refillQid = (text: string, astQuestion, question: Question) {
if (astQuestion.id === null) {
text = insert(text, question.id, astQuestion.title.location)
}
return text
}
这样,我们就可以做到仅向没有ID的题目写入新生成的ID了。
衍生问题
但是也带来了一个新问题:文本中有ID的存在,若是直接显示且允许编辑会造成更大的混乱。幸运的是,文本编辑器基于CodeMirror实现,给这个问题带来了比较好的解决方案。我们可以用CodeMirror提供的markText方法来实现ID的隐藏,然后通过各种输入或删除事件的拦截,实现ID总是在处于正确位置。在上面的输入情况的处理之后,使用CodeMirror隐藏ID选项,即可实现几乎无感知的输入:
迁移
鉴于该功能在业务端较为重要,且具有较高使用量,因此要迁移到新版本,必须要有足够的测试度。我们选择了由文本编辑创建,回收量最多的前1000份问卷作为测试材料,对比新旧两个编辑器解析出的结果,发现能全部通过测试。
总结
经过上述努力,现在已经实现了PEG.js的改造,以及ID丢失问题的解决。在用户输入体验上,基本可以做到无感知地迁移,对于用户来说,可以非常平滑地过渡到新版本中。但是,通过事件拦截来确保ID能处在一个正确的位置是一个比较困难的问题,毕竟有输入输出的形式有很多,不一定能覆盖到所有情况。因此,这是一个需要长期测试,发现BUG并改进的任务。