webpack loader 從上手到理解系列:vue-loader(一)

原文地址javascript

0 前言

webpack loaders 從上手到理解系列 還有這些:css

1 什麼是 vue-loader

vue-loader 是一個 webpackloader,它容許你以一種名爲單文件組件的格式撰寫 Vue 組件。html

2 如何使用 vue-loader

2.1 安裝

npm install vue-loader vue-template-compiler --save-dev
複製代碼

2.2 配置 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()
  ]
}
複製代碼

2.3 建立一個 Vue 組件

一個標準的 Vue 組件能夠分爲三部分:vue

  • template: 模板
  • script: 腳本
  • stype: 樣式
<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>
複製代碼

2.4 見證奇蹟的時刻

打包完以後,這個 Vue 組件就會被解析到頁面上:java

<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 文件裏。node

簡單來講 vue-loader 的工做就是處理 Vue 組件,正確地解析各個部分。webpack

vue-loader 的源碼較長,咱們分幾個部分來解析。git

3. 源碼解析之總體分析

咱們先從入口看起,從上往下看:github

module.exports = function (source) {}
複製代碼

vue-loader 接收一個 source 字符串,值是 vue 文件的內容。web

const stringifyRequest = r => loaderUtils.stringifyRequest(loaderContext, r)
複製代碼

loaderUtils.stringifyRequest 做用是將絕對路徑轉換成相對路徑。

接下來有一大串的聲明語句,咱們暫且先不看,咱們先看最簡單的狀況。

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 方法,而且傳入了兩個方法: startend,最終返回 sfc

這一塊的源碼咱們很少說,咱們能夠猜想 startend 這兩個方法應該是會根據不一樣的規則去修改 sfc,咱們看一下 sfcvue-loaderdescriptor 是怎麼樣的:

// 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 這個命名是否是有點問題。。。

3.1 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 裏有三次的 importimport 的文件都是 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)。

4. 待續

後續還有 vue-loader 的第二篇文章,講解 VueLoaderPlugin 的代碼以及如何處理不一樣 type

相關文章
相關標籤/搜索