首发:mp.weixin.qq.com/s/HOGmlICuH…
不同于 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 的报错:
<view>
500元≤累积业绩{{'<'}}1000元
</view>
在小程序中,可以用 {{变量名}} 这样的插值表达式来绑定 WXML 文件和对应的 JavaScript 文件中的 data 对象属性。
而对于 html-minifier 而言,这个语法只是普通的字符串内容,在解析到 {{'<'}} 中的 < 时,会被理解为标签的开头,因此报了 Parse Error 的错。
为减少对已有项目的内容改动,选择了以下修复方案:
增加 html-minifier 配置,用于忽略插值表达式片段。:ignoreCustomFragments = [/{{[\s\S]*?}}/] 。
Bug2
增加配置后,webpack 编译成功。但是打开小程序编辑器,体验 dist 目录结果:项目无法运行。排查发现编译结果出现问题。
<!-- 能正常运行(输入输出内容一致)-->
<!-- 输入 -->
<div class="{{a?'aa':'bb'}}">1</div>
<!-- 输出 -->
<div class="{{a?'aa':'bb'}}">1</div>
<!-- 不能正常运行(输入输出内容不一致)-->
<!-- 输入 -->
<div class='{{a?"aa":"bb"}}'>1</div>
<!-- 输出 -->
<div class="{{a?"aa":"bb"}}">1</div>
这个编译问题是在我们加了忽略插值表达式的配置后才出现的。刚才添加的配置,影响了标签属性引号的处理。
我们可以找到 html-minifier 的相关源码进行分析:
// 是否禁止属性转译
if (!options.preventAttributesEscaping) {
// 是否有指定过标签属性的引号是什么
if (typeof options.quoteCharacter === 'undefined') {
var apos = (attrValue.match(/'/g) || []).length;
var quot = (attrValue.match(/"/g) || []).length;
attrQuote = apos < quot ? '\'' : '"';
} else {
attrQuote = options.quoteCharacter === '\'' ? '\'' : '"';
}
// 根据属性引号值
// 按需转译属性值内实体字符
if (attrQuote === '"') {
attrValue = attrValue.replace(/"/g, '"');
} else {
attrValue = attrValue.replace(/'/g, ''');
}
}
emittedAttrValue = attrQuote + attrValue + attrQuote;
由于我们没有特别配置过 quoteCharacter ,根据源码逻辑,他会走入 typeof options.quoteCharacter === 'undefined' 分支。
该分支逻辑是对 attrValue 中包含的单/双引号的个数进行比较:属性值中双引号多,属性引号应当用单引号,反之亦然。举个具体例子:
<div class='abcd"e'>1</div>
这个例子中的属性值 attrValue 是 abcd"e,放进前面这段分支逻辑处理,代码逻辑会认为,这个属性值中有一个双引号,零个单引号,因此当前的属性值一定是被单引号括住,即 attrQuote 是单引号。
为什么会有这样的判断逻辑?
我们可以进一步查看 html 的相关规范。对于单引号属性值语法、双引号属性值语法,有规定:
基于单引号的语法规范,我们画个图来快速理解下(双引号语法规范类似):
-
属性名 name
-
后面可以有零或若干个空格
-
等号 (EQUALS SIGN character)
-
后面可以有零或若干个空格
-
一个单引号 (single U+0027 APOSTROPHE character ('))
-
属性值 value,值中不可以有单引号
-
一个单引号 (single U+0027 APOSTROPHE character ('))
我们可以快速测试一下以下3个用例(文章后面也会再提及),以下三个用例会以双引号的语法规范进行解析
// 用例1 (name 是 testsome,value 是 a"aa)
a.innerHTML =
'<div testsome = "a"aa">123</div>'
// 用例2(name 是 testsome,value 是 a\"aa)
a.innerHTML =
'<div testsome = "a\"aa">123</div>'
// 用例3(name 是 testsome,value 是 a"aa)
a.innerHTML =
'<div testsome = "a"aa">123</div>'
前两者的 html 会解析成
第三种写字符实体的会解析成
基于对规范的理解,我们再回过头看刚刚的 html-minifier 的实现,可以意识到,这个库是对 html 规范进行了更宽松的处理(允许属性值中含有引号,并帮你按需转译),他对于属性值的单双引号的处理逻辑是:“对 attrValue 中包含的单/双引号的个数进行比较:属性值中双引号多,属性引号用单引号,反之亦然。”。
这么做实际是为了,在没有指定单双引号值配置的前提下,尝试检查属性值中是否含有双引号或单引号,以此来推测,当前属性值是用双引号还是单引号括着的。
假如值内,有且主要是单引号,那外部肯定是用双引号,反之亦然,确定好属性引号后,再将属性值中含有的相关引号转换成字符实体,以免造成用例1/2中的不在预期内的解析结果。
由于我们前面设置了 ignoreCustomFragments,将所有插值表达式忽略掉,那么根据逻辑,当前的属性引号就会被认为应该取双引号,导致这个bug:
<!-- 输入 -->
<div class='{{a?"aa":"bb"}}'>1</div>
<!-- 插值表达式被忽略 也就是可以被看作 -->
<div class=''>1</div>
<!-- 然后就会被 html-minifier 解析成 -->
<div class="">1</div>
<!-- 输出
属性引号为双引号 表达式内也正好是双引号
小程序运行报错 -->
<div class="{{a?"aa":"bb"}}">1</div>
而为了解决这个连锁 bug2,我们可以考虑把 preventAttributesEscaping 设为 true,不让 html-minifier 进行属性值引号的处理。
基于以上对 html 规范的理解,假设我们这么做,会引入 bug3,就是用例1/2 所示的属性值中含有应当被转译的实体字符:
预期:
实际:
因此最正确的选择应该是改业务代码,特殊字符应该用字符实体来代替。
至此,可能有人早就在质疑,为什么不直接用实体字符,为什么要写这种奇葩代码:
<view> 500元≤累积业绩{{'<'}}1000元</view>
因为小程序不支持直接在 wxml 中书写实体字符,实体字符会被当作普通的字符串进行展示。而直接写 < ,小程序开发工具本身也会解析失败,因为会把他当作标签的开头。
因此才会有开发者,曲线的利用插值表达式,将' < '单拎出来,不让 wxml 的 parser 处理。但是其实可以使用小程序提供的 text 组件,该组件支持 decode 参数,decode 可以解析以下实体字符:
< > & '    
五、总结
<!-- web 能正常显示 < 符号 -->
<div><</div>
<!-- web 能正常显示 < 符号 -->
<div><</div>
<!-- 小程序解析出错、html-minifier解析出错 -->
<div><</div>
<!-- 小程序直接显示 < -->
<div><</div>
<!-- html-minifier解析出错 需要额外加若干配置来解决 -->
<div>{{'<'}}</div>
最后, 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 互转很方便。
六、参考链接
html.spec.whatwg.org/multipage/s…
github.com/kangax/html…
我是彩蛋分割线
Hello,各位老伙计,不知道你们有没有发现,我们的公众号多了两个菜单-> 内推 和 店铺。
上一篇文章中我们提到,希望把天赋带到公众号,捣腾一番。思来想去,觉得大多数人都在开课,而我们作为全职选手,其实没有那么多的精力去做这样的事情。
但是我们却可以帮忙模拟面试!之所以有这样的想法,是因为可能有校招/社招小伙伴们不了解当下的面试趋势和难度,以及多少有点对实战面试有抵触心理和些许恐惧,或者是害怕没有准备好,浪费了面试机会。
那么我们就是想为这类小伙伴提供一个真人模拟面试的机会。那么我们为什么能有底气做这件事呢?首先我们是一群来自不同大厂的小伙伴,从能力上,多少还是有保证的(拍胸脯),其次,我们人多(?),如果一个人持续做模拟面试这件事,那么其实是很耗费时间精力的,毕竟一次面试时间也不短。但是我们有多位靠谱的小伙伴可以持续性提供这样的服务,所以相对而言能够保证把这件事持续做下去。
以及我们也会考虑提供简历修改意见等服务,因为有很多小伙伴苦于做了事情,却不知道怎么去写去表达,那么我们可以一一帮你解决这类似问题。
当然,这个事情还在筹划中,我们的店铺中的相关服务商品,也还没有完善好,但是你们已经可以访问店铺中的商品了。伙计们不妨在评论区或者私信告诉我们有什么建议或者想法,或者是其他希望我们提供的帮助,因为我们目前想要做的就是集思广益,看一下我们能为大家做些什么有意义的事。以上~
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!