• home > webfront > ECMAS > vue >

    vue编译原理(1):vue-loader浅析

    Author:zhoulujun Date:

    vue-loader将 vue 文件变成 bundle js,然后放入浏览器运行。vue-loader整体分析vue-loader 将 basic vue 编译到最终输出的 bundl

    vue-loader将 .vue 文件变成 .bundle.js,然后放入浏览器运行。

    vue-loader整体分析

    vue-loader 将 basic.vue 编译到最终输出的 bundle.js 的过程中,其实调用了四个小的 loader。它们分别是:

    1. selector

    2. style-compiler

    3. template-compiler

    4. babel-loader

    以上四个 loader ,除了 babel-loader 是外部的package,其他三个都存在于 vue-loader 的内部(lib/style-compiler 和 lib/template-compiler 和 lib/selector)。

     vue-loader 将 A.vue 编译成以下内容

    /* script  从做右到左,被先后被 selector 和 babel-loader 处理过了。 selector(参数type=script) */
    import __vue_script__ from "!!babel-loader!../../lib/selector?type=script&index=0&bustCache!./A.vue"
    
    /* template  从左到右,先后被 selector 和 template-compiler 处理过了。 selector (参数type=template) 的处理结果是将 basic.vue 中的 template 抽出来之后交给 template-compiler 处理,最终输出成可用的 HTML。*/
    import __vue_template__ from "!!../../lib/template-compiler/index?{\"id\":\"data-v-793be54c\",\"hasScoped\":false,\"buble\":{\"transforms\":{}}}!../../lib/selector?type=template&index=0&bustCache!./A.vue"
    
    /* styles  
    style 涉及的 loader 较多,一个一个来分析, 从上代码可知,basic.vue 先后要被 selector, style-compiler, css-loader 以及 vue-style-loader 处理。
    selector (参数type=style) 的处理结果是将 basic.vue 中的 css 抽出来之后交给 style-compiler 处理成 css, 然后交给 css-loader 处理生成 module, 最后通过 vue-style-loader 将 css 放在 <style> 里面,然后注入到 HTML 里。
    注意,这里之所以没有用 style-loader 是因为 vue-style-loader 是在 fork 了 style-loader 的基础上,增加了后端绘制 (SSR) 的支持。具体的不同,读者可以查看官方文档,笔者这里不再累述。
    */
    import __vue_styles__ from "!!vue-style-loader!css-loader!../../lib/style-compiler/index?{\"vue\":true,\"id\":\"data-v-793be54c\",\"scoped\":false,\"hasInlineConfig\":false}!../../lib/selector?type=styles&index=0&bustCache!./A.vue"
    var Component = normalizeComponent(
      __vue_script__,
      __vue_template__,
      __vue_template_functional__,
      __vue_styles__,
      __vue_scopeId__,
      __vue_module_identifier__
    )

    在三个 import 语句中,不管它们用了多少个不同的 loader 去加载,loader chain 的源头都是 A.vue。





    vue-loader 源码解析系列之 selector


    const path = require('path')
    const parse = require('./parser')
    const loaderUtils = require('loader-utils')
    
    module.exports = function (content) {
      // 略
      const query = loaderUtils.getOptions(this) || {}
      // 略
      const parts = parse(content, filename, this.sourceMap, sourceRoot, query.bustCache)
      let part = parts[query.type]
      // 略
      this.callback(null, part.content, part.map)
    }

    selector的代码非常简单,

    通过 parser 将 .vue 解析成对象 parts, 里面分别有 style, script, template。可以根据不同的 query, 返回对应的部分。

    很明显那么这个 parser 完成了分析分解 .vue 的工作,那么让我们继续深入 parser

    parser 做了什么

    const compiler = require('vue-template-compiler')
    const cache = require('lru-cache')(100)
    
    module.exports = (content, filename, needMap, sourceRoot, bustCache) => {
      const cacheKey = hash(filename + content)
      // 略
      let output = cache.get(cacheKey)
      if (output) return output
      output = compiler.parseComponent(content, { pad: 'line' })
      if (needMap) {
        // 略去了生成 sourceMap 的代码
      }
      cache.set(cacheKey, output)
      return output
    }

    从上面代码可以看到,.vue 解析的工作其实是交给了 compiler.parseComponent 去完成,那么我们需要继续深入 compiler。

    注意,这里 vue-template-compiler 并不是 vue-loader 的一部分,从 vue-template-compiler 的 npm 主页可以了解到, vue-template-compiler 原来是 vue 本体的一部分

    并不是一个单独的 package。通过查看文档可知,compiler.parseComponent 的逻辑在 vue/src/sfc/parser.js 里。

    parseComponent 做了什么

    /**
     * Parse a single-file component (*.vue) file into an SFC Descriptor Object.
     */
    export function parseComponent (
      content: string,
      options?: Object = {}
     ): SFCDescriptor {
      const sfc: SFCDescriptor = {
        template: null,
        script: null,
        styles: [],
        customBlocks: []
      }
      let depth = 0
      let currentBlock: ?(SFCBlock | SFCCustomBlock) = null
    
      function start (
        tag: string,
        attrs: Array<Attribute>,
        unary: boolean,
        start: number,
        end: number
      ) {
        // 略
      }
    
      function checkAttrs (block: SFCBlock, attrs: Array<Attribute>) {
        // 略
      }
    
      function end (tag: string, start: number, end: number) {
        // 略
      }
    
      function padContent (block: SFCBlock | SFCCustomBlock, pad: true | "line" | "space") {
        // 略
      }
    
      parseHTML(content, {
        start,
        end
      })
    
      return sfc
    }

    parseComponent 里面有以下变量

    • 处理对象 sfc

      把 .vue 里的 css, javaScript, html 抽离出来之后,存放到找个这个对象里面

    • 变量 depth

      当前正在处理的节点的深度,比方说,对于 <template><div><p>foo</p></div></template>来说,处理到 foo 时,当前深度就是 3, 处理到 </div> 时,当前深度就是 2 。

    • currentBlock

      当前正在处理的节点,以及该节点的 attr 和 content 等信息。

    • 函数 start

      遇到 openTag 节点时,对 openTag 的相关处理。逻辑不是很复杂,读者可以直接看源码。有一点值得注意的是,style 是用 array 形式存储的

    • 函数 end

      遇到 closeTag 节点时,对 closeTag 的相关处理。

    • 函数 checkAttrs

      对当前节点的 attrs 的相关处理

    • 函数 parseHTML

      这是和一个外部的函数,传入了 content (其实也就是 .vue 的内容)以及由 start和 end 两个函数组成的对象。看来,这个 parseHTML 之才是分解分析 .vue 的关键

      跟之前一样,我们要继续深入 parseHTML 函数来分析,它到底对 .vue 做了些什么,源码如下

    parseHTML 做了什么

    export function parseHTML (html, options) {
      const stack = []
      const expectHTML = options.expectHTML
      const isUnaryTag = options.isUnaryTag || no
      const canBeLeftOpenTag = options.canBeLeftOpenTag || no
      let index = 0
      let last, lastTag
      while (html) {
        last = html
        if (!lastTag || !isPlainTextElement(lastTag)) {
          // 这里分离了template
        } else {
          // 这里分离了style/script
      }
      // 略
    
      // 前进n个字符
      function advance (n) {
        // 略
      }
    
      // 解析 openTag 比如 <template>
      function parseStartTag () {
        // 略
      }
    
      // 处理 openTag
      function handleStartTag (match) {
        // 略
        if (options.start) {
          options.start(tagName, attrs, unary, match.start, match.end)
        }
      }
    
      // 处理 closeTag
      function parseEndTag (tagName, start, end) {
        // 略
        if (options.start) {
          options.start(tagName, [], false, start, end)
        }
        if (options.end) {
          options.end(tagName, start, end)
        }
      }
    }

    深入到这一步,我想再提醒一下读者,selector的目的是将 .vue 中的 template, javaScript, css 分离出来。带着这个目的意识,我们再来审视这个 parseHTML。

    parseHTML 整个函数的组成是:

    • 一个 while 循环

      在 while 循环中,存在两个大的分支,一个用来分析 template ,一个是用来分析 script 和 style。

    • 函数 advance

      向前跳过文本

    • 函数 parseStartTag

      判断当前的 node 是不是 openTag

    • 函数 handleStartTag

      处理 openTag, 这里就用到了之前提到的 start() 函数

    • 函数 parseEndTag

      判断当前的 node 是不是 closeTag,同时这里也用到了 end() 函数

    通过以上各个函数的组合,在while循环中就将 sfc 分割成了三个不同的部分,读者可以对比我的注释和源码自行解读源码逻辑。

    顺便在这里吐个槽,很明显这里的 parseHTML 是函数名是有问题的,parseHTML 应该叫做 parseSFC 比较合适。



    参考文章:

    vue-loader 源码解析系列之 selector https://segmentfault.com/a/1190000012336392

    vue-loader 源码解析系列之 整体分析 https://nicholaslee119.github.io/2017/11/24/vueLoader源码解析/



    转载本站文章《vue编译原理(1):vue-loader浅析》,
    请注明出处:https://www.zhoulujun.cn/html/webfront/ECMAScript/vue/8445.html