zoukankan      html  css  js  c++  java
  • 理解vue-loader

    事情的起源是被人问到,一个以.vue结尾的文件,是如何被编译然后运行在浏览器中的?突然发现,对这一块模糊的很,而且看mpvue的文档,甚至小程序之类的都是实现了自己的loader,所以十分必要抽时间去仔细读一读源码,顺便总结一番。

    首先说结论:

        一、vue-loader是什么

        

        简单的说,他就是基于webpack的一个的loader,解析和转换 .vue 文件,提取出其中的逻辑代码 script、样式代码 style、以及 HTML 模版 template,再分别把它们交给对应的 Loader 去处理,核心的作用,就是提取,划重点。


        至于什么是webpack的loader,其实就是用来打包、转译js或者css文件,简单的说就是把你写的代码转换成浏览器能识别的,还有一些打包、压缩的功能等。

        这是一个.vue单文件的demo   

    vue文件式例 折叠源码
    <template>
      <div class="example">{{ msg }}</div>
    </template>
     
    <script>
    export default {
      data () {
        return {
          msg: 'Hello world!'
        }
      }
    }
    </script>
     
    <style>
    .example {
      color: red;
    }
    </style>

    二、 vue-loader 的作用(引用自官网)

    • 允许为 Vue 组件的每个部分使用其它的 webpack loader,例如在 <style> 的部分使用 Sass 和在 <template> 的部分使用 Pug;
    • 允许在一个 .vue 文件中使用自定义块,并对其运用自定义的 loader 链;
    • 使用 webpack loader 将 <style> 和 <template> 中引用的资源当作模块依赖来处理;
    • 为每个组件模拟出 scoped CSS;
    • 在开发过程中使用热重载来保持状态。

    三、vue-loader的实现

        先找到了vue-laoder在node_modules中的目录,由于源码中有很多对代码压缩、热重载之类的代码,我们定一个方向,看看一个.vue文件在运行时,是被vue-loader怎样处理的

        

        既然vue-loader的核心首先是将以为.vue为结尾的组件进行分析、提取和转换,那么首先我们要找到以下几个loader

    •  selector–将.vue文件解析拆分成一个parts对象,其中分别包含style、script、template
    • style-compiler–解析style部分
    • template-compiler 解析template部分
    • babel-loader-- 解析script部分,并转换为浏览器能识别的普通js

        首先在loader.js这个总入口中,我们不关心其他的,先关心这几个加载的loader,从名字判断这事解析css、template的关键

        

        3.1 首先是selector

    selector 折叠源码
    var path = require('path')
    var parse = require('./parser')
    var loaderUtils = require('loader-utils')
     
     
    module.exports = function (content) {
      this.cacheable()
      var query = loaderUtils.getOptions(this) || {}
      var filename = path.basename(this.resourcePath)
      // 将.vue文件解析为对象parts,parts包含style, script, template
      var parts = parse(content, filename, this.sourceMap)
      var part = parts[query.type]
      if (Array.isArray(part)) {
        part = part[query.index]
      }
      this.callback(null, part.content, part.map)
    }

        selector的最主要的功能就是拆分parts,这个parts是一个对象,用来盛放将.vue文件解析出的style、script、template等模块,他调用了方法parse。

        parse.js部分

    parse.js 折叠源码
    var compiler = require('vue-template-compiler')
    var cache = require('lru-cache')(100)
    var hash = require('hash-sum')
    var SourceMapGenerator = require('source-map').SourceMapGenerator
     
     
    var splitRE = / ? /g
    var emptyRE = /^(?://)?s*$/
     
    module.exports = function (content, filename, needMap) {
      // source-map cache busting for hot-reloadded modules
      // 省略部分代码
      var filenameWithHash = filename + '?' + cacheKey
      var output = cache.get(cacheKey)
      if (output) return output
      output = compiler.parseComponent(content, { pad: 'line' })
      if (needMap) {
      }
      cache.set(cacheKey, output)
      return output
    }
     
    function generateSourceMap (filename, source, generated) {
      // 生成sourcemap
      return map.toJSON()
    }

    parse.js其实也没有真正解析.vue文件的代码,只是包含一些热重载以及生成sourceMap的代码,最主要的还是调用了compiler.parseComponent 这个方法,但是compiler并不是vue-loader的方法,而是调用vue框架的parse,这个文件在vue/src/sfc/parser.js中,一层层的揭开面纱终于找到了解析.vue文件的真正处理方法parseComponent。

    vue的parse.js 折叠源码
    /**
     * 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
    }

    但是令人窒息的是parseHTML才是核心的方法,翻了一下文件,parseHTML是调用的vue源码中的compiler/parser/html-parser.js

     折叠源码
    export function parseHTML (html, options) {
      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)
            }
        }
      }
    }

    这个parseHTML的主要组成部分就是解析传入的template标签,同时分离style和script

    3.2 解析了template 接下来再看style样式部分的解析,在源码中调用的是style-compiler这个模块

    style-compiler模块 折叠源码
    var postcss = require('postcss')
    module.exports = function (css, map) {
      var query = loaderUtils.getOptions(this) || {}
      var vueOptions = this.options.__vueOptions__
     
      if (!vueOptions) {
        if (query.hasInlineConfig) {
          this.emitError(
            `   [vue-loader] It seems you are using HappyPack with inline postcss ` +
            `options for vue-loader. This is not supported because loaders running ` +
            `in different threads cannot share non-serializable options. ` +
            `It is recommended to use a postcss config file instead. ` +
            `   See http://vue-loader.vuejs.org/en/features/postcss.html#using-a-config-file for more details. `
          )
        }
        vueOptions = Object.assign({}, this.options.vue, this.vue)
      }
     
      // use the same config loading interface as postcss-loader
      loadPostcssConfig(vueOptions.postcss).then(config => {
        var plugins = [trim].concat(config.plugins)
        var options = Object.assign({
          to: this.resourcePath,
          from: this.resourcePath,
          map: false
        }, config.options)
     
        // add plugin for vue-loader scoped css rewrite
        if (query.scoped) {
          plugins.push(scopeId({ id: query.id }))
        }
     
       // souceMap略
     
        return postcss(plugins)
          .process(css, options)
          .then(function (result) {
            var map = result.map && result.map.toJSON()
            cb(null, result.css, map)
            return null // silence bluebird warning
          })
      }).catch(e => {
        console.log(e)
        cb(e)
      })
    }

    简单的说,这一部分其实是调用了webpack原有的postcss这个loader,不过值得注意的是在vue中style标签scope的实现

    实现的效果,在加了scope的style的文件中,为所设置的样式添加私有属性data,同时css中也加入单独的id,起到不同组件之间css私有的作用

    这里调用了scopeId这个方法,是在postcss的基础上自定义的插件,调用postcss-selector-parser这个插件,在css转译后的选择器上生成特殊的id,从而起到隔离css的作用

    vue-loader针对postcss的拓展 折叠源码
    var postcss = require('postcss')
    // 调用postcss-selector-parser 这个基于postcss的css选择器解析插件
    var selectorParser = require('postcss-selector-parser')
     
     
    module.exports = postcss.plugin('add-id'function (opts) {
      return function (root) {
        root.each(function rewriteSelector (node) {
          if (!node.selector) {
            // handle media queries
            if (node.type === 'atrule' && node.name === 'media') {
              node.each(rewriteSelector)
            }
            return
          }
          node.selector = selectorParser(function (selectors) {
            selectors.each(function (selector) {
              var node = null
              selector.each(function (n) {
                if (n.type !== 'pseudo') node = n
              })
              selector.insertAfter(node, selectorParser.attribute({
                attribute: opts.id
              }))
            })
          }).process(node.selector).result
        })
      }
    })

    同时在对应的组件标签上,添加自定义的data属性,在vue-loader下的loader.js中

    而genId则是生成scopeId的方法,其中调用了基于npm的hash-sum插件,快速生成唯一的哈希值

     折叠源码
    var path = require('path')
    var hash = require('hash-sum'//此处引用了hash-sum插件
    var cache = Object.create(null)
    var sepRE = new RegExp(path.sep.replace('\''\\'), 'g')
     
     
    module.exports = function genId (file, context, key) {
      var contextPath = context.split(path.sep)
      var rootId = contextPath[contextPath.length - 1]
      file = rootId + '/' + path.relative(context, file).replace(sepRE, '/') + (key || '')
      return cache[file] || (cache[file] = hash(file))
    }

    而hash-sum生成唯一hash值的基本函数也比较有意思,通过charCodeAt 以及左移运算符产生新的值,最基本的一个fold函数贴到下边

    hash-sum 折叠源码
    function fold (hash, text) {
      var i;
      var chr;
      var len;
      if (text.length === 0) {
        return hash;
      }
      for (i = 0, len = text.length; i < len; i++) {
        chr = text.charCodeAt(i); // 调用了charCodeAt()这个方法转换为unicode编码
        hash = ((hash << 5) - hash) + chr; // 左移运算符改变hash值
        hash |= 0; // hash = hash | 0;
      }
      return hash < 0 ? hash * -2 : hash;
    }

    hash-sum还通过嵌套多层fold函数,以及pad、foldObject、foldValue等函数进一步混淆保证hash值的唯一不重复,感兴趣的可以翻看下hash-sum的源码。

    3.3 script的处理

        vue-loader对于script的处理则要简单一些,因为相对于自定义的程度,需要学习的v-指令,以及vue css中划分的scope,js反而是最通用的。

        

        

        

        如果script标签有lang的标签,确保解析方式

        

    根据属性lang的内容,加载使用对应的loader

     折叠源码
    function ensureLoader (lang) {
        return lang.split('!').map(function (loader) {
          return loader.replace(/^([w-]+)(?.*)?/, function (_, name, query) {
            return (/-loader$/.test(name) ? name : (name + '-loader')) + (query || '')
          })
        }).join('!')
    }
    吾生有涯 而知也无涯矣
  • 相关阅读:
    vuex
    JS判断浏览器类型和详细区分IE各版本浏览器
    javascript json对象操作(基本增删改查)
    react 使用antd 按需加载
    vue-cli 3.0 豆瓣api接口使用element做分页
    vue-cli 3.0 使用axios配置跨域访问豆瓣接口
    es6之扩展运算符 三个点(...)
    Vue.js——十分钟入门Vuex
    js数组的处理使用
    如何发布自己模块到NPM
  • 原文地址:https://www.cnblogs.com/Sherlock09/p/11023593.html
Copyright © 2011-2022 走看看