Vue-loader 的巧妙玩法

t174in22 7年前
   <p>声明:可以这么玩,不代表应该这么玩</p>    <h2>一、Vue-loader 干了啥?</h2>    <p>Vue-loader 是一个 webpack 加载器,它可以把形如:</p>    <pre>  <code class="language-javascript"><template>      ...  </template>  <script>      ...  </script>  <style>      ...  </style></code></pre>    <p>的 Vue 组件转化为 Js 模块,这其中最最值得关注的是,它生成了 render function code ,在前之前的一篇文章 <a href="/misc/goto?guid=4959749636554561903" rel="nofollow,noindex">详解 render function code</a> 中,已经对它进行了细致的介绍:</p>    <p>render function code 是从模板编译而来(可以并且应该预编译)的组件核心渲染方法,在每一次组件的 Render 过程中,通过注入的数据执行可生成虚拟 Dom</p>    <p>既然 Vue-loader 预编译生成了 render function code ,那么我们就可以通过改造 Vue-loader 来改写 render function code 的生成结果,从而全局的影响组件的每一次渲染结果</p>    <h2>二、如何改造?</h2>    <h3>找到目标代码</h3>    <p>Vue loader 并不普通,需要通过语法树分析的方式最终生成 render function code (并且不限于此),如果通篇阅读如此复杂的代码可能会让你——沮丧。幸运的是,要完成改写 render function code 的小目标,我们并不需要读得太多,找到生成结果那一小段代码,在返回之前改写即可。那么新的问题又来了,这小段代码又如何定位?</p>    <p>一是靠猜:打开 Vue-loader 的目录结构,见名知义,显然 template-compiler 模板编译的意思更为接近</p>    <p><img src="https://simg.open-open.com/show/5aabadadcf9105fe6dba29c0d24bbb03.png"></p>    <p>二是搜索:在 template-compiler 目录中搜索 render , 有没有发现有一段代码很有嫌疑</p>    <pre>  <code class="language-javascript">var code    if (compiled.errors && compiled.errors.length) {      this.emitError(        `\n  Error compiling template:\n${pad(html)}\n` +        compiled.errors.map(e => `  - ${e}`).join('\n') + '\n'      )      code = 'module.exports={render:function(){},staticRenderFns:[]}'    } else {      var bubleOptions = options.buble      // 这段代码太可疑了      code = transpile('module.exports={' +        'render:' + toFunction(compiled.render) + ',' +        'staticRenderFns: [' + compiled.staticRenderFns.map(toFunction).join(',') + ']' +      '}', bubleOptions)        // mark with stripped (this enables Vue to use correct runtime proxy detection)      if (!isProduction && (        !bubleOptions ||        !bubleOptions.transforms ||        bubleOptions.transforms.stripWith !== false      )) {        code += `\nmodule.exports.render._withStripped = true`      }    }</code></pre>    <p>三是调试确认:打印 toFunction(compiled.render) , 查看输出结果,如果跟 render function code 的表现一致的话,那就是它了</p>    <pre>  <code class="language-javascript">with(this) {      return _c('div', {          attrs: {              "id": "app"          },          staticStyle: {            "width": "100px"          },          style: styleObj      },      [_c('p', [_v("普通属性:" + _s(message))]), _v(" "), _c('p', [_v(_s(msg()))]), _v(" "), _c('p', [_v(_s(ct))]), _v(" "), _c('input', {          directives: [{              name: "model",              rawName: "v-model",              value: (message),              expression: "message"          }],          domProps: {              "value": (message)          },          on: {              "input": function($event) {                  if ($event.target.composing) return;                  message = $event.target.value              }          }      }), _v(" "), _l((items),      function(item) {          return _c('div', [_v("\n\t\t  " + _s(item.text) + "\n\t   ")])      }), _v(" "), _c('button', {          on: {              "click": bindClick          }      },      [_v("点我出奇迹抓同伟")])], 2)  }</code></pre>    <h3>如何改造?</h3>    <p>假如我们想把所有组件的所有静态样式(staticStyle)的像素值乘二(虽然有点搞破坏),那么我们需要对上述 toFunction(compiled.render) 进行正则替换</p>    <pre>  <code class="language-javascript">var renderCode = toFunction(compiled.render)  renderCode = renderCode.replace(/(staticStyle:)(\s*{)([^}]*)(})/g, function (m, n1, n2, n3, n4) {      n3 = n3.replace(/(".*")(\s*:\s*")(\d+px)(")/g, function (m, n31, n32, n33, n34) {        return n31 + n32 + parseInt(n33)*2 + 'px' + n34      })      return n1 + n2 + n3 + n4    })</code></pre>    <p>如果是改造动态样式(style),由于在 render function code 中,动态 style 以变量的形式出现,我们不能直接替换模板,那么我们可以通过传入一个方法,在运行时执行转换。不要企图写一个普通的方法,通过方法名的引用在 render function code 中执行,因为 render function code 执行时的作用域,不是在 Vue-loader 阶段可以确认的,所以我们需要写一个立即执行函数:</p>    <pre>  <code class="language-javascript">var convertCode = "(function(styleObj){styleObj = (...此处省略N行代码)})(##style##)"</code></pre>    <p>立即执行函数的入参用 ##style## 占位,替换的过程中用具体的变量代替,上述 render function code 替换结果为:</p>    <pre>  <code class="language-javascript">with(this) {      return _c('div', {          attrs: {              "id": "app"          },          staticStyle: {            "width": "100px"          },          // 重点在这里 在这里          style: (function(styleObj){styleObj = (...此处省略N行代码)})(styleObj)      },      ...      )  }</code></pre>    <h2>三、有何使用场景?</h2>    <p>例如,当你一开始使用了 px 作为布局单位,却需要改造为 rem 布局单位的时候(业务代码很多很繁杂,不方便一个个去改,并且由于动态样式的存在,难以全局替换)</p>    <p>对于插入立即执行函数去处理动态变量的方式,每一次 Re-render 都会执行一遍转换函数,显然,这对渲染性能有影响</p>    <p>所以,虽然可以这么玩,但是不代表应该这么玩,还需三思而行</p>    <p>其它的使用场景暂时也还没想到,即便如此,这种瞎折腾也不是没有意义的,没准在还无法预见的场景,这是一种绝佳的解决方案呢?如果你刚好遇到了,也同步给我吧~~</p>    <p>更多精彩,期待您的关注</p>    <p> </p>    <p>来自:https://juejin.im/post/5935803a0ce46300572e1160</p>    <p> </p>