理解vue-loader

事情的起源是被人問到,一個以.vue結尾的文件,是如何被編譯而後運行在瀏覽器中的?忽然發現,對這一塊模糊的很,並且看mpvue的文檔,甚至小程序之類的都是實現了本身的loader,因此十分必要抽時間去仔細讀一讀源碼,順便總結一番。css

首先說結論:html

    1、vue-loader是什麼vue

    

    簡單的說,他就是基於webpack的一個的loader,解析和轉換 .vue 文件,提取出其中的邏輯代碼 script、樣式代碼 style、以及 HTML 模版 template,再分別把它們交給對應的 Loader 去處理,核心的做用,就是提取,劃重點。java


    至於什麼是webpack的loader,其實就是用來打包、轉譯js或者css文件,簡單的說就是把你寫的代碼轉換成瀏覽器能識別的,還有一些打包、壓縮的功能等。node

    這是一個.vue單文件的demo   webpack

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

2、 vue-loader 的做用(引用自官網)web

  • 容許爲 Vue 組件的每一個部分使用其它的 webpack loader,例如在 <style> 的部分使用 Sass 和在 <template> 的部分使用 Pug;
  • 容許在一個 .vue 文件中使用自定義塊,並對其運用自定義的 loader 鏈;
  • 使用 webpack loader 將 <style> 和 <template> 中引用的資源看成模塊依賴來處理;
  • 爲每一個組件模擬出 scoped CSS;
  • 在開發過程當中使用熱重載來保持狀態。

3、vue-loader的實現npm

    先找到了vue-laoder在node_modules中的目錄,因爲源碼中有不少對代碼壓縮、熱重載之類的代碼,咱們定一個方向,看看一個.vue文件在運行時,是被vue-loader怎樣處理的小程序

    

    既然vue-loader的核心首先是將覺得.vue爲結尾的組件進行分析、提取和轉換,那麼首先咱們要找到如下幾個loaderapi

  •  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 = /\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( '!' )
}
相關文章
相關標籤/搜索