BUG!从编写 Loader 到窥探大佬 Debug 全过程
原文:BUG!从编写 Loader 到窥探大佬 Debug 全过程
审核:k、nightcat、geeku、L·Lawliet、goodboy
不同于 web 开发的 html + js + css,在原生小程序开发中,我们使用的是 wxml + js + wxss。web 开发中,我们常借助 webpack 的能力进行代码打包,小程序中同理可用。
今天我们的目标是编写一个真实可用的 wxml-loader,这个 loader 主要用于收集 wxml 中的本地资源,比如图片,然后就可以交由 file-loader 来进行文件的处理;以及支持输出压缩后的 wxml 文件,减少文件大小。
一、术语解释
WXML 是小程序的一套标签语言,可类比于 HTML。
AST 是抽象语法树(Abstract Syntax Tree)。
sax 是可以用于 XML 和 HTML 的解析器。
html-minifier 是基于 JavaScript 开发的 HTML 压缩工具
二、目标功能
收集 WXML 中的本地依赖,预期最终输出目录中,包含这些被引用的资源文件;
压缩 WXML 文件内容;
三、实现思路
收集依赖:获取到 WXML 字符串内容后,我们自然而然希望把他转换为 AST 进行分析,此处我们借助第三方工具 sax parser,通过解析后的数据,根据节点类型、属性类型匹配的情况,按需收集对应的本地资源地址。
压缩文件:此处可以直接使用第三方工具 html-minifier 。
四、实战踩坑
以上,我们的 wxml-loader 的核心功能就完成了(不好意思,省略了很多代码)。放入我们的实际开发中项目进行验证测试。
Bug1
实践出真实,遇到了 Parse Error 的报错:
在小程序中,可以用 {{变量名}} 这样的插值表达式来绑定 WXML 文件和对应的 JavaScript 文件中的 data 对象属性。
而对于 html-minifier 而言,这个语法只是普通的字符串内容,在解析到 {{'<'}} 中的 < 时,会被理解为标签的开头,因此报了 Parse Error 的错。
为减少对已有项目的内容改动,选择了以下修复方案:
增加 html-minifier 配置,用于忽略插值表达式片段。:ignoreCustomFragments = [/{{[\s\S]*?}}/] 。
Bug2
增加配置后,webpack 编译成功。但是打开小程序编辑器,体验 dist 目录结果:项目无法运行。排查发现编译结果出现问题。
这个编译问题是在我们加了忽略插值表达式的配置后才出现的。刚才添加的配置,影响了标签属性引号的处理。
我们可以找到 html-minifier 的相关源码进行分析:
由于我们没有特别配置过 quoteCharacter ,根据源码逻辑,他会走入 typeof options.quoteCharacter === 'undefined' 分支。
该分支逻辑是对 attrValue 中包含的单/双引号的个数进行比较:属性值中双引号多,属性引号应当用单引号,反之亦然。举个具体例子:
这个例子中的属性值 attrValue 是 abcd"e,放进前面这段分支逻辑处理,代码逻辑会认为,这个属性值中有一个双引号,零个单引号,因此当前的属性值一定是被单引号括住,即 attrQuote 是单引号。
为什么会有这样的判断逻辑?
我们可以进一步查看 html 的相关规范。对于单引号属性值语法、双引号属性值语法,有规定:
Single-quoted attribute value syntax
The attribute name, followed by zero or more ASCII whitespace, followed by a single U+003D EQUALS SIGN character, followed by zero or more ASCII whitespace, followed by a single U+0027 APOSTROPHE character ('), followed by the attribute value, which, in addition to the requirements given above for attribute values, must not contain any literal U+0027 APOSTROPHE characters ('), and finally followed by a second single U+0027 APOSTROPHE character (').
Double-quoted attribute value syntax
The attribute name, followed by zero or more ASCII whitespace, followed by a single U+003D EQUALS SIGN character, followed by zero or more ASCII whitespace, followed by a single U+0022 QUOTATION MARK character ("), followed by the attribute value, which, in addition to the requirements given above for attribute values, must not contain any literal U+0022 QUOTATION MARK characters ("), and finally followed by a second single U+0022 QUOTATION MARK character (").
基于单引号的语法规范,我们画个图来快速理解下(双引号语法规范类似):
属性名 name
后面可以有零或若干个空格
等号 (EQUALS SIGN character)
后面可以有零或若干个空格
一个单引号 (single U+0027 APOSTROPHE character ('))
属性值 value,值中不可以有单引号
一个单引号 (single U+0027 APOSTROPHE character ('))
我们可以快速测试一下以下 3 个用例(文章后面也会再提及),以下三个用例会以双引号的语法规范进行解析
前两者的 html 会解析成
第三种写字符实体的会解析成
基于对规范的理解,我们再回过头看刚刚的 html-minifier 的实现,可以意识到,这个库是对 html 规范进行了更宽松的处理(允许属性值中含有引号,并帮你按需转译),他对于属性值的单双引号的处理逻辑是:“对 attrValue 中包含的单/双引号的个数进行比较:属性值中双引号多,属性引号用单引号,反之亦然。”。
这么做实际是为了,在没有指定单双引号值配置的前提下,尝试检查属性值中是否含有双引号或单引号,以此来推测,当前属性值是用双引号还是单引号括着的。
假如值内,有且主要是单引号,那外部肯定是用双引号,反之亦然,确定好属性引号后,再将属性值中含有的相关引号转换成字符实体,以免造成用例 1/2 中的不在预期内的解析结果。
由于我们前面设置了 ignoreCustomFragments,将所有插值表达式忽略掉,那么根据逻辑,当前的属性引号就会被认为应该取双引号,导致这个 bug:
而为了解决这个连锁 bug2,我们可以考虑把 preventAttributesEscaping 设为 true,不让 html-minifier 进行属性值引号的处理。
基于以上对 html 规范的理解,假设我们这么做,会引入 bug3,就是用例 1/2 所示的属性值中含有应当被转译的实体字符:
预期:
实际:
因此最正确的选择应该是改业务代码,特殊字符应该用字符实体来代替。
至此,可能有人早就在质疑,为什么不直接用实体字符,为什么要写这种奇葩代码:
因为小程序不支持直接在 wxml 中书写实体字符,实体字符会被当作普通的字符串进行展示。而直接写 < ,小程序开发工具本身也会解析失败,因为会把他当作标签的开头。
因此才会有开发者,曲线的利用插值表达式,将' < '单拎出来,不让 wxml 的 parser 处理。但是其实可以使用小程序提供的 text 组件,该组件支持 decode 参数,decode 可以解析以下实体字符:
五、总结
最后, wxml-loader 的编写其实很简单,难点总是在于兼容各种人写出来的代码。本文用较大的篇幅记录了一次 debug 的过程,在已有项目中使用我们所编写的 wxml-loader 时,可以通过项目实际情况,按需配置 ignoreCustomFragments 和 preventAttributesEscaping 参数规避文中所说的部分问题。
当然,如果团队代码书写规范,更正确的操作应该是迎合小程序规则,使用 text 来解决问题,就不会有这么多衍生的 bug。
同时,在踩坑过程中我们还发现一个“彩蛋”,就是 sax 作为 html parser,一直都没有掺合进来折磨我们,报错的一直都是小程序本身和 html-minifier 库。这也可以说明 html parser 的割裂问题,各自有自己的 htmlParser 的实现。
而现代化的工具一般都统一了解析方式,比如 EStree / PostCSS 等,就是为了统一定个标准出现的。html-minifier 虽然是个成熟的库,但是也比较老了,有兴趣的小伙伴可以了解一下 unifiedjs ,它定义了一个通用语法树结构, 旗下 markdown / html / text / Graphviz 互转很方便。
六、参考链接
https://html.spec.whatwg.org/multipage/syntax.html#attributes-2
版权声明: 本文为 InfoQ 作者【HZFEStudio】的原创文章。
原文链接:【http://xie.infoq.cn/article/b4afe8689fba4c284407293d1】。文章转载请联系作者。
评论