理解vue-loader

it2022-05-06  29

事情的起源是被人问到,一个以.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、templatestyle-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 = /\r?\n/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(          `\n  [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.\n` +          `\n  See http: //vue-loader.vuejs.org/en/features/postcss.html#using-a-config-file for more details.\n`        )      }      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( '!' ) }

转载于:https://www.cnblogs.com/Sherlock09/p/11023593.html


最新回复(0)