最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • BUG!从编写 Loader 到窥探大佬 Debug 全过程

    正文概述 掘金(Akiq2016)   2021-04-08   543

    首发:mp.weixin.qq.com/s/HOGmlICuH…

    不同于 web 开发的 html + js + css,在原生小程序开发中,我们使用的是 wxml + js + wxss。web 开发中,我们常借助 webpack 的能力进行代码打包,小程序中同理可用。

    今天我们的目标是编写一个真实可用的 wxml-loader,这个 loader 主要用于收集 wxml 中的本地资源,比如图片,然后就可以交由 file-loader 来进行文件的处理;以及支持输出压缩后的 wxml 文件,减少文件大小。

    一、术语解释

    1. WXML 是小程序的一套标签语言,可类比于 HTML。

    2. AST 是抽象语法树(Abstract Syntax Tree)。

    3. sax 是可以用于 XML 和 HTML 的解析器。

    4. html-minifier 是基于 JavaScript 开发的 HTML 压缩工具

    二、目标功能

    • 收集 WXML 中的本地依赖,预期最终输出目录中,包含这些被引用的资源文件;

    • 压缩 WXML 文件内容;

    三、实现思路

    • 收集依赖:获取到 WXML 字符串内容后,我们自然而然希望把他转换为 AST 进行分析,此处我们借助第三方工具 sax parser,通过解析后的数据,根据节点类型、属性类型匹配的情况,按需收集对应的本地资源地址。

    BUG!从编写 Loader 到窥探大佬 Debug 全过程

    • 压缩文件:此处可以直接使用第三方工具 html-minifier 。

    BUG!从编写 Loader 到窥探大佬 Debug 全过程

    四、实战踩坑

    以上,我们的 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, '&#34;');
      } else {
        attrValue = attrValue.replace(/'/g, '&#39;');
      }
    }
    
    emittedAttrValue = attrQuote + attrValue + attrQuote;
    

    由于我们没有特别配置过 quoteCharacter ,根据源码逻辑,他会走入 typeof options.quoteCharacter === 'undefined' 分支。

    该分支逻辑是对 attrValue 中包含的单/双引号的个数进行比较:属性值中双引号多,属性引号应当用单引号,反之亦然。举个具体例子:

    <div class='abcd"e'>1</div>
    

    这个例子中的属性值 attrValue 是 abcd"e,放进前面这段分支逻辑处理,代码逻辑会认为,这个属性值中有一个双引号,零个单引号,因此当前的属性值一定是被单引号括住,即 attrQuote 是单引号。

    为什么会有这样的判断逻辑?

    我们可以进一步查看 html 的相关规范。对于单引号属性值语法、双引号属性值语法,有规定:

    基于单引号的语法规范,我们画个图来快速理解下(双引号语法规范类似):

    BUG!从编写 Loader 到窥探大佬 Debug 全过程

    1. 属性名 name

    2. 后面可以有零或若干个空格

    3. 等号 (EQUALS SIGN character)

    4. 后面可以有零或若干个空格

    5. 一个单引号 (single U+0027 APOSTROPHE character ('))

    6. 属性值 value,值中不可以有单引号

    7. 一个单引号 (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&quot;aa)
    a.innerHTML =
      '<div testsome  = "a&quot;aa">123</div>'
    

    前两者的 html 会解析成

    BUG!从编写 Loader 到窥探大佬 Debug 全过程

    第三种写字符实体的会解析成

    BUG!从编写 Loader 到窥探大佬 Debug 全过程

    基于对规范的理解,我们再回过头看刚刚的 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 所示的属性值中含有应当被转译的实体字符:

    预期:

    BUG!从编写 Loader 到窥探大佬 Debug 全过程

    实际:

    BUG!从编写 Loader 到窥探大佬 Debug 全过程

    因此最正确的选择应该是改业务代码,特殊字符应该用字符实体来代替。

    至此,可能有人早就在质疑,为什么不直接用实体字符,为什么要写这种奇葩代码:

    <view>  500元≤累积业绩{{'<'}}1000元</view>
    

    因为小程序不支持直接在 wxml 中书写实体字符,实体字符会被当作普通的字符串进行展示。而直接写 < ,小程序开发工具本身也会解析失败,因为会把他当作标签的开头。

    因此才会有开发者,曲线的利用插值表达式,将' < '单拎出来,不让 wxml 的 parser 处理。但是其实可以使用小程序提供的 text 组件,该组件支持 decode 参数,decode 可以解析以下实体字符:

    &nbsp; &lt; &gt; &amp; &apos; &ensp; &emsp;
    

    五、总结

    <!-- web 能正常显示 < 符号 -->
    <div><</div>
    
    <!-- web 能正常显示 < 符号 -->
    <div>&lt;</div>
    
    <!-- 小程序解析出错、html-minifier解析出错 -->
    <div><</div>
    
    <!-- 小程序直接显示 &lt; -->
    <div>&lt;</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,各位老伙计,不知道你们有没有发现,我们的公众号多了两个菜单-> 内推 和 店铺。

    上一篇文章中我们提到,希望把天赋带到公众号,捣腾一番。思来想去,觉得大多数人都在开课,而我们作为全职选手,其实没有那么多的精力去做这样的事情。

    但是我们却可以帮忙模拟面试!之所以有这样的想法,是因为可能有校招/社招小伙伴们不了解当下的面试趋势和难度,以及多少有点对实战面试有抵触心理和些许恐惧,或者是害怕没有准备好,浪费了面试机会。

    那么我们就是想为这类小伙伴提供一个真人模拟面试的机会。那么我们为什么能有底气做这件事呢?首先我们是一群来自不同大厂的小伙伴,从能力上,多少还是有保证的(拍胸脯),其次,我们人多(?),如果一个人持续做模拟面试这件事,那么其实是很耗费时间精力的,毕竟一次面试时间也不短。但是我们有多位靠谱的小伙伴可以持续性提供这样的服务,所以相对而言能够保证把这件事持续做下去。

    以及我们也会考虑提供简历修改意见等服务,因为有很多小伙伴苦于做了事情,却不知道怎么去写去表达,那么我们可以一一帮你解决这类似问题。

    当然,这个事情还在筹划中,我们的店铺中的相关服务商品,也还没有完善好,但是你们已经可以访问店铺中的商品了。伙计们不妨在评论区或者私信告诉我们有什么建议或者想法,或者是其他希望我们提供的帮助,因为我们目前想要做的就是集思广益,看一下我们能为大家做些什么有意义的事。以上~

    BUG!从编写 Loader 到窥探大佬 Debug 全过程


    起源地 » BUG!从编写 Loader 到窥探大佬 Debug 全过程

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元