原文地址javascript
vue-loader
是一個webpack
的loader
,它容許你以一種名爲單文件組件的格式撰寫Vue
組件。
npm install vue-loader vue-template-compiler --save-dev
webapck
// webpack.config.js const VueLoaderPlugin = require('vue-loader/lib/plugin') module.exports = { mode: 'development', module: { rules: [ { test: /\.vue$/, loader: 'vue-loader' }, // 它會應用到普通的 `.js` 文件 // 以及 `.vue` 文件中的 `<script>` 塊 { test: /\.js$/, loader: 'babel-loader' }, // 它會應用到普通的 `.css` 文件 // 以及 `.vue` 文件中的 `<style>` 塊 { test: /\.css$/, use: [ 'vue-style-loader', 'css-loader' ] } ] }, plugins: [ // 請確保引入這個插件來施展魔法 new VueLoaderPlugin() ] }
Vue
組件一個標準的 Vue
組件能夠分爲三部分:css
<template> <div id="app"> <div class="title">{{msg}}</div> </div> </template> <script> export default { name: 'app', data() { return { msg: 'Hello world', }; }, } </script> <style lang="scss"> #app { text-align: center; color: #2c3e50; margin-top: 60px; } .title { color: red; } </style>
打包完以後,這個 Vue
組件就會被解析到頁面上:html
<head> <style type="text/css"> #app { text-align: center; color: #2c3e50; margin-top: 60px; } .title { color: red; } </style> </head> <body> <div id="app"> <div class="title">Hello world</div> </div> <script type="text/javascript" src="/app.js"></script> </body>
上面 Vue
組件裏的 <template>
部分解析到 <body>
下,css
部分解析成 <style>
標籤,<script>
部分則解析到 js
文件裏。vue
簡單來講 vue-loader
的工做就是處理 Vue
組件,正確地解析各個部分。java
vue-loader
的源碼較長,咱們分幾個部分來解析。node
咱們先從入口看起,從上往下看:webpack
module.exports = function (source) {}
vue-loader
接收一個 source
字符串,值是 vue
文件的內容。git
const stringifyRequest = r => loaderUtils.stringifyRequest(loaderContext, r)
loaderUtils.stringifyRequest
做用是將絕對路徑轉換成相對路徑。github
接下來有一大串的聲明語句,咱們暫且先不看,咱們先看最簡單的狀況。web
const { parse } = require('@vue/component-compiler-utils') const descriptor = parse({ source, compiler: options.compiler || loadTemplateCompiler(loaderContext), filename, sourceRoot, needMap: sourceMap })
parse
方法是來自於 component-compiler-utils
,代碼簡略一下是這樣:
// component-compiler-utils parse function parse(options) { const { source, filename = '', compiler, compilerParseOptions = { pad: 'line' }, sourceRoot = '', needMap = true } = options; // ... output = compiler.parseComponent(source, compilerParseOptions); // ... return output; }
能夠看到,這裏還不是真正 parse
的地方,其實是調用了 compiler.parseComponent
方法,默認狀況下 compiler
指的是 vue-template-compiler
。
// vue-template-compiler parseComponent function parseComponent ( content, options ) { var sfc = { template: null, script: null, styles: [], customBlocks: [], errors: [] }; // ... function start() {} function end() {} parseHTML(content, { warn: warn, start: start, end: end, outputSourceRange: options.outputSourceRange }); return sfc; }
這裏能夠看到,parseComponent
應該是調用了 parseHTML
方法,而且傳入了兩個方法: start
和 end
,最終返回 sfc
。
這一塊的源碼咱們很少說,咱們能夠猜想 start
和 end
這兩個方法應該是會根據不一樣的規則去修改 sfc
,咱們看一下 sfc
即 vue-loader
中 descriptor
是怎麼樣的:
// vue-loader descriptor { customBlocks: [], errors: [], template: { attrs: {}, content: "\n<div id="app">\n <div class="title">{{msg}}</div>\n</div>\n", type: "template" }, script: { attrs: {}, content: "... export default {} ...", type: "script" }, style: [{ attrs: { lang: "scss" }, content: "... #app {} ...", type: "style", lang: "scss" }], }
vue
文件裏的內容已經分別解析到對應的 type
去了,接下來是否是隻要分別處理各個部分便可。
parseHTML
這個命名是否是有點問題。。。
vue-loader
如何處理不一樣 type
大家能夠先思考五分鐘,這裏的分別處理是如何處理的?好比,樣式內容須要經過 style-loader
才能將其放到 DOM
裏。
好了,就看成聰明的你已經有思路了。咱們繼續往下看。
// template let templateImport = `var render, staticRenderFns` let templateRequest if (descriptor.template) { const src = descriptor.template.src || resourcePath const idQuery = `&id=${id}` const scopedQuery = hasScoped ? `&scoped=true` : `` const attrsQuery = attrsToQuery(descriptor.template.attrs) const query = `?vue&type=template${idQuery}${scopedQuery}${attrsQuery}${inheritQuery}` const request = templateRequest = stringifyRequest(src + query) templateImport = `import { render, staticRenderFns } from ${request}` } // script let scriptImport = `var script = {}` if (descriptor.script) { const src = descriptor.script.src || resourcePath const attrsQuery = attrsToQuery(descriptor.script.attrs, 'js') const query = `?vue&type=script${attrsQuery}${inheritQuery}` const request = stringifyRequest(src + query) scriptImport = ( `import script from ${request}\n` + `export * from ${request}` // support named exports ) } // styles let stylesCode = `` if (descriptor.styles.length) { stylesCode = genStylesCode( loaderContext, descriptor.styles, id, resourcePath, stringifyRequest, needsHotReload, isServer || isShadow // needs explicit injection? ) }
這三段代碼的結構很像,最終做用是針對不一樣的 type
分別構造一個 import
字符串:
templateImport = "import { render, staticRenderFns } from './App.vue?vue&type=template&id=7ba5bd90&'"; scriptImport = "import script from './App.vue?vue&type=script&lang=js&' export * from './App.vue?vue&type=script&lang=js&'"; stylesCode = "import style0 from './App.vue?vue&type=style&index=0&lang=scss&'";
這三個 import
語句有什麼用呢, vue-loader
是這樣作的:
let code = ` ${templateImport} ${scriptImport} ${stylesCode}`.trim() + `\n` code += `\nexport default component.exports` return code
此時, code
是這樣的:
code = " import { render, staticRenderFns } from './App.vue?vue&type=template&id=7ba5bd90&' import script from './App.vue?vue&type=script&lang=js&' export * from './App.vue?vue&type=script&lang=js&' import style0 from './App.vue?vue&type=style&index=0&lang=scss&' // 省略 ... export default component.exports"
咱們知道 loader
會導出一個可執行的 node
模塊,也就是說上面提到的 code
是會被 webpack
識別到而後執行的。
咱們看到 code
裏有三次的 import
,import
的文件都是 App.vue
,至關於又加載了一次觸發此次 vue-loader
的那個 vue
文件。不一樣的是,此次加載是帶參的,分別對應着 template
/ script
/ style
三種 type
的處理。
大家能夠先思考五分鐘,這裏的 分別處理是如何處理的?
這個問題的答案就是,webpack
在加載 vue
文件時,會調用 vue-loader
來處理 vue
文件,以後 return
一段可執行的 js
代碼,其中會根據不一樣 type
分別 import
一次當前 vue
文件,而且將參數傳遞進去,這裏的屢次 import
也會被 vue-loader
攔截,而後在 vue-loader
內部根據不一樣參數進行處理(好比調用 style-loader
)。
後續還有 vue-loader
的第二篇文章,講解 VueLoaderPlugin
的代碼以及如何處理不一樣 type
。