vue-loader 學習筆記

問題

  1. 使用 vue-loader 的時候,在 webpack 中如何配置?javascript

  2. vue-loader 如何將一個 .vue 文件 轉化爲 瀏覽器可識別的.js?css

  3. css scoped & 深度做用選擇器html

  4. css modulevue

  5. .vue文件 是怎麼實現熱更新的?java

  6. tree shaking 的反作用node

vue-loader 的使用配置

使用 vue-loader 的以前, 咱們須要安裝一些必要的 loader。。webpack

必需的 loader 包括:vue-loadervue-style-loadervue-template-compilercss-loader。 可能須要的 loader 包含:sass-loaderless-loaderurl-loader 等。web

一個包含 vue-loader 的簡單 webpack配置 以下:json

const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const { VueLoaderPlugin } = require('vue-loader')
const isProduction = process.env.NODE_ENV === 'production'
const extractLoader = {
    loader: MiniCssExtractPlugin.loader,
    options: {
        publicPath: '../',
        hmr: process.env.NODE_ENV === 'development'
    },
}
const cssExtractplugin = new MiniCssExtractPlugin({
    filename: '[name].css',
    chunkFilename: '[id].css',
    ignoreOrder: false
})
const webpackConfig = {
    entry: {...},
    output: {...},
    optimization: {...},
    resolve: {...},
    modules: {
        rules: [{
            test: /\.vue$/,
            loader: 'vue-loader'
        }, {
            test: /\.css$/,
            oneOf: [{
                resourceQuery: /\?vue/,
                use: [isProduction ? extractLoader  : 'vue-style-loader', 'css-loader']
            }, {
                use: [isProduction ? extractLoader  : 'style-loader', 'css-loader']
            }]
        },
        ...
        ]
    },
    plugins: [
        new VueLoaderPlugin(),
        isProduction ? cssExtractplugin : ''
    ]
    
}

複製代碼

注意,當使用的 vue-loader 版本爲 15.x.x 時, 必須使用 vue-loader 提供的 VueLoaderPluginapi

vue-loader 工做原理

經過 vue-loaderwebpack 能夠將 .vue 文件 轉化爲 瀏覽器可識別的javascript

vue-loader 的工做流程, 簡單來講,分爲如下幾個步驟:

  1. 將一個 .vue 文件 切割成 templatescriptstyles 三個部分。

  2. template 部分 經過 compile 生成 renderstaticRenderFns

  3. 獲取 script 部分 返回的配置項對象 scriptExports

  4. styles 部分,會經過 css-loadervue-style-loader, 添加到 head 中, 或者經過 css-loaderMiniCssExtractPlugin 提取到一個 公共的css文件 中。

  5. 使用 vue-loader 提供的 normalizeComponent 方法, 合併 scriptExports、render、staticRenderFns, 返回 構建vue組件須要的配置項對象 - options, 即 {data, props, methods, render, staticRenderFns...}

經過 vue-loader 生成的 js 文件 以下:

// 從 template區域塊 獲取 render、 staticRenderFns 方法
import { render, staticRenderFns } from "./App.vue?vue&type=template&id=7ba5bd90&scoped=true&"
// 從 script區域塊 獲取 組件的配置項對象
import script from "./App.vue?vue&type=script&lang=js&"
export * from "./App.vue?vue&type=script&lang=js&"
// 獲取 styles區域塊的內容
import style0 from "./App.vue?vue&type=style&index=0&lang=css&"
// 獲取 styles(scoped)區域塊的內容
import style1 from "./App.vue?vue&type=style&index=1&id=7ba5bd90&scoped=true&lang=css&"


/* normalize component */
import normalizer from "!../node_modules/_vue-loader@15.7.1@vue-loader/lib/runtime/componentNormalizer.js"
// 返回構建組件須要的配置項對象, 包含 data、props、render、staticRenderFns 等
var component = normalizer(
  script,
  render,
  staticRenderFns,
  false,
  null,
  "7ba5bd90",
  null
  
)

component.options.__file = "src/App.vue"
// 輸出組件完整的配置項
export default component.exports
複製代碼

css scoped

.vue 文件 中的 style 標籤scoped 屬性時,它的 css 樣式 只做用於當前 組件 中的 元素

css scoped工做流程 以下:

  1. 使用 vue-loader 處理 .vue 文件, 根據 .vue 文件請求路徑文件內容, 生成 .vue 文件hash 值, 如:7ba5bd90

  2. 若是 .vue 文件某一個 style 標籤scoped 屬性, 爲 .vue 文件 生成一個 scopedIdscopedId 的格式爲 data-v-hash, 如:data-v-7ba5bd90

  3. 使用 vue-loader.vue 文件 中獲取 style區域塊(scoped)樣式內容(字符串);若是使用了 less 或者 sass, 要使用 less-loader 或者 sass-loader 處理 樣式內容,使 樣式內容 變爲 瀏覽器可識別的css樣式; 而後使用 PostCSS 提供的 parser 處理 樣式內容, 爲 樣式內容 中的每個 css選擇器 添加 [data-v-hash]; 再使用 css-loader;最後使用 style-loadercss 樣式 添加到 head 中或者經過 miniCssExtractPlugincss 樣式 提取一個公共的 css 文件中。

  4. 經過 normalizer 方法返回 完整的組件配置項 optionsoptions 中有屬性 _scopeId, 如 _scopedId: data-v-7ba5bd90;

  5. 使用 組件配置項 options 構建組件實例, 給 組件 中每個 dom元素 添加屬性: data-v-hash

經歷上述過程,style(scoped) 中的樣式就變成了 組件的私有樣式

深度做用選擇器

咱們能夠經過 >>> 操做符, 在 組件 中修改 子組件私有樣式

// child component
.hello {...}

// parent component 
<style scoped>
    .parant .hello {...}
    .parent >>> .hello {...}
</style>

// 進過 postCSS 處理之後的 css
.parent .hello[data-v-xxx] {...}  // 沒法影響子組件

.parant[data-v-xxx] .hello {....} // 可影響子組件
複製代碼

有些像 Sass 之類的 預處理器 沒法 正確解析 >>>。這種狀況下咱們可使用 /deep/::v-deep 操做符取而代之,二者都是 >>>別名,一樣能夠正常工做。

深度做用選擇器, 必須在含有 scoped 屬性 的 style 標籤中使用,不然無效。 這是由於 >>>、/deep/、::v-deep 須要被 postCSS 解析才能起做用。 只有 style 標籤 中有 scoped 屬性樣式內容 纔會被 postCSS 解析。

postCSS 解析樣式內容的時候, 會給 >>> 操做符 前面css選擇器 添加 [data-v-hash]

注意: 父組件 中修改 子組件私有樣式 時, 父組件 中的 樣式的優先級 要大於 子組件 中的 樣式的優先級, 不然會致使 父組件中定義的樣式不生效

CSS Modules

咱們也能夠在 .vue 文件style 標籤 上添加 module 屬性, 使得 style 標籤 中的 樣式 變爲 組件私有,具體使用方法詳見 - 官網

css modulescss scoped 均可以使 樣式 變爲 組件私有,可是 原理 不同。

css scoped 的實質是利用 css屬性選擇器 使得 樣式 稱爲 局部樣式,而 css modules 的實質是讓 樣式的類名、id名惟一 使得 樣式 稱爲 局部樣式

css modules工做流程 以下:

  1. 使用 vue-loader 處理 .vue 文件, 將 .vue 文件內容 轉化爲 js 代碼。 若是 .vue 文件 中的 style 標籤 中有 module 屬性, 向 js 代碼 中注入一個 injectStyle 方法, 以下:

    import { render, staticRenderFns } from "./App.vue?vue&type=template&id=3512ffa2&scoped=true&"
    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&module=a&lang=css&"
    import style1 from "./App.vue?vue&type=style&index=1&id=3512ffa2&module=true&scoped=true&lang=css&"
    
    // 經過injectStyle方法, 會向vue實例中添加屬性
    function injectStyles (context) {
        // 對應 <style module="a">...</style>
        // 給vue實例添加屬性a, 對應的值爲使用css-loader處理樣式內容之後返回的對象
        this["a"] = (style0.locals || style0)
        // 對應 <style module>...</style>
        // 給vue實例添加屬性$style, 對應的值爲使用css-loader處理樣式內容之後返回的對象
        this["$style"] = (style1.locals || style1)
    }
    /* normalize component */
    import normalizer from "!../node_modules/_vue-loader@15.7.1@vue-loader/lib/runtime/componentNormalizer.js"
    // normalize 會返回一個組件完整配置項對象
    // 在執行過程當中, 會將render方法從新包裝成 renderWithStyleInjection 方法
    // 執行 renderWithStyleInjection 方法時的時候, 先執行 injectStyles 方法, 再執行 原來的render 方法
    var component = normalizer(
      script,
      render,
      staticRenderFns,
      false,
      injectStyles,
      "3512ffa2",
      null
      
    )
    
    export default component.exports" 複製代碼
  2. 使用 css-loader 處理 .vue 文件style 區域塊,會將 style 區域塊 中的樣式內容, 轉化爲 js 代碼, 以下:

    exports = module.exports = require("../node_modules/_css-loader@3.2.0@css-loader/dist/runtime/api.js")(false);
    // Module
    exports.push([module.id, "\n#_3cl756BP8kssTYTEsON-Ao {\n font-family: 'Avenir', Helvetica, Arial, sans-serif;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n text-align: center;\n color: #2c3e50;\n margin-top: 60px;\n}\n._3IbrnaW__7RJMXk4rh9tW- {\n background-color: blue;\n}\n", ""]);
    // Exports
    exports.locals = {
        // app是id名
    	"app": "_3cl756BP8kssTYTEsON-Ao",
    	// class 是 類名
    	"class1": "_3IbrnaW__7RJMXk4rh9tW-"
    }
    複製代碼

    在處理過程當中, css-loader 會將樣式中的 類名id名 等用一個 惟一的命名代替

    在執行 步驟1 的代碼時,會執行上面的代碼, 返回一個 對象, 即 步驟一 中的 style0style1, 格式以下:

    // css樣式內容會經過 style-loader 提供的方法添加到 head 中
    // 或者被 miniCssExtractPlugin 提取到一個 公共的css文件 中
    style0 = [[css模塊 id, css樣式內容字符串, ''], ...]
    style0.locals = {
        "app": "_3cl756BP8kssTYTEsON-Ao",
    	"class1": "_3IbrnaW__7RJMXk4rh9tW-"
    }
    複製代碼
  3. 運行項目執行打包之後的js代碼, 即 步驟1中的代碼, 獲取 renderstaticRenderFnsscriptExprotsstyle0style1, 而後經過 normalizer 方法返回 組件完整配置項 - options。 在執行過程當中,將 render 方法從新包裝成 renderWithStyleInjection 方法。

    構建 vue 實例 時,執行 renderWithStyleInjection 方法, 此時會 執行 injectStyles 方法,給 vue 實例 添加 $stylea 屬性,屬性值爲 stlye0.localsstyle1.locals, 再執行原來的 render 方法。

    這樣, 咱們就能夠經過 vue 實例$style、a 屬性訪問 樣式類名id名。

熱更新

開發模式 下,當使用 vue-loadervue-style-loader 處理 .vue 文件 的時候, 會向 生成的js代碼 中注入與 熱更新 相關的代碼邏輯。 當咱們修改 .vue 文件 時, dev-server 會通知 瀏覽器 進行 熱更新

.vue 文件各個區域塊(template、script、styles) 對應的 熱更新邏輯 都不同。

  • template & script

    vue-loader 會在 打包代碼 中注入 熱更新 template、script 區域塊 的代碼,以下:

    // 從 template區域塊 獲取 render、 staticRenderFns 方法
    import { render, staticRenderFns } from "./App.vue?vue&type=template&id=7ba5bd90&scoped=true&"
    // 從 script區域塊 獲取 組件的配置項對象
    import script from "./App.vue?vue&type=script&lang=js&"
    export * from "./App.vue?vue&type=script&lang=js&"
    // 獲取 styles區域塊的內容
    import style0 from "./App.vue?vue&type=style&index=0&lang=css&"
    // 獲取 styles(scoped)區域塊的內容
    import style1 from "./App.vue?vue&type=style&index=1&id=7ba5bd90&scoped=true&lang=css&"
    
    
    /* normalize component */
    import normalizer from "!../node_modules/_vue-loader@15.7.1@vue-loader/lib/runtime/componentNormalizer.js"
    // 返回構建組件須要的配置項對象, 包含 data、props、render、staticRenderFns 等
    var component = normalizer(
      script,
      render,
      staticRenderFns,
      false,
      null,
      "7ba5bd90",
      null
      
    )
    
    /* hot reload */
    // .vue 文件的 script 區域塊更改時, 客戶端執行這一段代碼
    if (module.hot) {
      var api = require("D:\\study\\demo\\webpack\\webpack-4-demo\\node_modules\\_vue-hot-reload-api@2.3.3@vue-hot-reload-api\\dist\\index.js")
      api.install(require('vue'))
      if (api.compatible) {
        module.hot.accept()
        if (!api.isRecorded('7ba5bd90')) {
          api.createRecord('7ba5bd90', component.options)
        } else {
          // 執行 reload 方法, 觸發更新
          // 使用 新的 options 替換原來的 options 
          api.reload('7ba5bd90', component.options)
        }
        module.hot.accept("./App.vue?vue&type=template&id=7ba5bd90&scoped=true&", function () {
          // 當 .vue 文件的 template 區域塊更改時, 客戶端執行這一段代碼
          // 使用新的 render、staticRenderFns 更新原來的render、staticRenderFns
          api.rerender('7ba5bd90', {
            render: render,  
            staticRenderFns: staticRenderFns
          })
        })
      }
    }
    
    component.options.__file = "src/App.vue"
    // 輸出組件完整的配置項
    export default component.exports
    
    複製代碼

    若是咱們只修改了 .vue 文件script 部分, 客戶端(即瀏覽器) 會進行 熱更新, 過程以下:

    1. 服務端 經過 websocket 鏈接 通知 客戶端 更新;

    2. 客戶端 經過 動態添加script元素 的方式獲取 更新之後的打包文件

    3. 安裝打包文件,即執行 新的打包文件 中的 js 代碼, 使用 打包文件中的 module 更新瀏覽器緩存的同名 module

    4. 從新安裝組件對應的 module, 即 從新執行組件對應的js代碼, 獲取 renderstaticRenderFns 和 新的 scriptExports, 從新生成 組件 對應的 完整配置項

    5. 執行 api 提供的 reload 方法, 更新組件

      reload 方法中,會經過執行 父組件實例$forceUpdate 方法來 更新組件

      更新組件的時候, 因爲組件配置項(data、props、methods等屬性) 發生變化, 須要爲 組件 生成 新的構造函數 VueComponent, 而後使用 新的構造函數,構建 新的組件實例

      即, 每次修改 .vue 文件script 部分, 都會爲 組件 生成一個 新的實例對象銷燬舊的實例對象

    若是咱們只修改了 .vue 文件template 部分, 客戶端(即瀏覽器) 會進行 熱更新, 過程以下:

    1. 同上服務端 經過 websocket 鏈接 通知 客戶端 更新;

    2. 同上客戶端 經過 動態添加script元素 的方式獲取 更新之後的打包文件

    3. 同上安裝打包文件,即執行 新的打包文件 中的 js 代碼, 使用 打包文件中的 module 更新瀏覽器緩存的同名 module

    4. 觸發經過 module.hot.accept 註冊的 callback

    5. 執行 api 提供的 rerender 方法, 更新組件

      執行 rerender 方法時, 會先獲取 修改之後的template區域塊 對應的 renderstaticRenderFns, 而後 更新原組件的 render、staticRenderFns, 而後執行 組件實例$forceUpdate 方法來更新 組件(更新組件的時候, 會使用新的render方法, 生成新的vnode節點樹)

    若是咱們 同時 修改了 .vue 文件templatescript部分, 會按照上面 第一種狀況 進行 熱更新,而且不會觸發上面代碼中經過 module.hot.accept 註冊的 callback

  • style

    vue-style-loader 會在 打包代碼 中注入 熱更新 style區域塊 的代碼, 以下:

    ...
    
    var add = require("!../node_modules/_vue-style-loader@4.1.2@vue-style-loader/lib/addStylesClient.js").default
    var update = add("05835b6f", content, false, {});
    // Hot Module Replacement
    if(module.hot) {
     // When the styles change, update the <style> tags
     if(!content.locals) {
       module.hot.accept("!!../node_modules/_css-loader@3.1.0@css-loader/dist/cjs.js!../node_modules/_vue-loader@15.7.1@vue-loader/lib/loaders/stylePostLoader.js!../node_modules/_vue-loader@15.7.1@vue-loader/lib/index.js??vue-loader-options!./App.vue?vue&type=style&index=0&lang=css&", function() {
         // 當 .vue 文件的 styles 區域塊更改時, 客戶端執行這一段代碼
         var newContent = require("!!../node_modules/_css-loader@3.1.0@css-loader/dist/cjs.js!../node_modules/_vue-loader@15.7.1@vue-loader/lib/loaders/stylePostLoader.js!../node_modules/_vue-loader@15.7.1@vue-loader/lib/index.js??vue-loader-options!./App.vue?vue&type=style&index=0&lang=css&");
         if(typeof newContent === 'string') newContent = [[module.id, newContent, '']];
         // 執行update方法, 更新styles
         update(newContent);
       });
     }
    }
    ...
    複製代碼

    若是咱們修改了 .vue 文件styles 區域塊客戶端(即瀏覽器) 會進行 熱更新, 過程以下:

    1. 同上,服務端 經過 websocket 鏈接 通知 客戶端 更新;

    2. 同上,客戶端 經過 動態添加script元素 的方式獲取 更新之後的打包文件

    3. 同上,安裝打包文件,即執行 新的打包文件 中的 js 代碼, 使用 打包文件中的 module 更新瀏覽器緩存的同名 module;

    4. 觸發經過 module.hot.accept 註冊的 callback

    5. 執行 update 方法, 更新樣式

      更新樣式 的時候, 會先 移除原來的 style 標籤, 而後 添加新的 style 標籤

    若是 style 標籤 上有 module 屬性,除了 vue-style-loader 會注入 熱更新代碼 外,vue-loader 也會在 打包代碼 中注入 熱更新代碼,以下:

    // 熱更新代碼
        module.hot && module.hot.accept(["./App.vue?vue&type=style&index=1&id=7ba5bd90&module=true&scoped=true&lang=css&"], function () {
          // 當.vue的style區域塊發生變化, 且style標籤有module屬性, 執行這一段邏輯
          var oldLocals = cssModules["$style"]
          if (oldLocals) {
            // 獲取新的惟一類名、id名
            var newLocals = require("./App.vue?vue&type=style&index=1&id=7ba5bd90&module=true&scoped=true&lang=css&")
            if (JSON.stringify(newLocals) !== JSON.stringify(oldLocals)) {
              // 更新vue實例的$style屬性
              cssModules["$style"] = newLocals
              // 執行vue實例的 $forceUpdate 方法,從新執行 render 方法
              require("D:\\study\\demo\\webpack\\webpack-4-demo\\node_modules\\_vue-hot-reload-api@2.3.3@vue-hot-reload-api\\dist\\index.js").rerender("7ba5bd90")
            }
          }
        })
    複製代碼

    執行上述 熱更新代碼, 會 更新 vue實例 的 $style 屬性, 而後觸發 vue 實例$forceUpdate 方法, 從新渲染

    一個 style 區域塊 對應一個 style 標籤。修改某一個 style 區域塊 以後,會更新對應的 style 標籤

    style 區域塊熱更新templatescript 區域塊熱更新 互不影響。

tree shaking 反作用

生產模式 下, webpack 默認啓用 tree shaking。若是此時項目 根目錄 中的 package.json 中的 sideEffects 的值爲 false,且 .vue 文件style 標籤 沒有 module 屬性,使用 vue-loader 處理 .vue 文件 的時候, 會產生 樣式丟失 的狀況,即 styles 區域塊 不會添加到 head 中或者 被提取到公共的css文件中

首先,先看一下 .vue 文件 通過處理之後生成的 js代碼, 以下:

// 從 template區域塊 獲取 render、 staticRenderFns 方法
import { render, staticRenderFns } from "./App.vue?vue&type=template&id=7ba5bd90&scoped=true&"
// 從 script區域塊 獲取 組件的配置項對象
import scriptExports from "./App.vue?vue&type=script&lang=js&"
export * from "./App.vue?vue&type=script&lang=js&"
// 獲取 styles區域塊的內容
import style0 from "./App.vue?vue&type=style&index=0&lang=css&"
// 獲取 styles(scoped)區域塊的內容
import style1 from "./App.vue?vue&type=style&index=1&id=7ba5bd90&scoped=true&lang=css&"


/* normalize component */
import normalizer from "!../node_modules/_vue-loader@15.7.1@vue-loader/lib/runtime/componentNormalizer.js"
// 返回構建組件須要的配置項對象, 包含 data、props、render、staticRenderFns 等
var component = normalizer(
  scriptExports,
  render,
  staticRenderFns,
  false,
  null,
  "7ba5bd90",
  null
  
)

component.options.__file = "src/App.vue"
// 輸出組件完整的配置項
export default component.exports
複製代碼

在上面的代碼中,template 區域塊 返回的 renderstaticRenderFnsscript 區域塊 返回的 scriptExports, 都有被 normalizer 方法使用, 而 styles 區域塊 返回的 style0style1 則沒有被使用。 在 打包代碼 的時候, tree shaking 就會自動移除 styles 區域塊 對應的代碼,致使 樣式丟失

解決方法:

  1. 修改 package.json 文件中的 sideEffects 屬性, 告訴 webpack .vue 文件在使用 tree shaking 的時候會有 反作用, 以下:

    "sideEffects": [
        "*.vue"
     ]
    複製代碼

    有了上述配置, webpack 在處理 .vue 文件的時候, 不會使用 tree shaking不會出現樣式丟失的問題

    可是這種解決方法有一個問題, 若是 script 區域塊 中經過 import 的方式引入了 未使用的模塊未使用的模塊在最後打包代碼的時候不會被刪除

  2. 經過 rule.sideEffects 指定 具體的模塊 在使用 tree shaking 的時候會有 反作用, 以下:

    // webpackConfig:
        {
            test: /\.css$/,
            oneOf: [{
                resourceQuery: /\?vue/,
                // 指定.vue文件的 style區域塊 使用 tree shaking 時會有反作用
                sideEffects: true,
                use: [isProduction ? MiniCssExtractPlugin.loader  : 'vue-style-loader', 'css-loader']
            }, {
                use: [isProduction ? MiniCssExtractPlugin.loader  : 'style-loader', 'css-loader']
            }]
        },
        {
            test: /\.scss$/,
            oneOf: [{
                resourceQuery: /\?vue/,
                // 指定.vue文件的 style(lang=scss)區域塊 使用 tree shaking 時會有反作用
                sideEffects: true,
                use: [isProduction ? MiniCssExtractPlugin.loader  : 'vue-style-loader', 'css-loader', 'sass-loader']
            }, {
                use: [isProduction ? MiniCssExtractPlugin.loader  : 'style-loader', 'css-loader', 'sass-loader']
            }]
        }
        
        // package.json
        {
            sideEffects: false
        }
    複製代碼

    上述配置, 明確說明了 .vue 文件style 區域塊 在使用 tree shaking 的時候, 會有 反作用在打包的時候不會刪除

    這樣的話,樣式不會丟失, 而且若是 script 區域塊 中經過 import 的方式引入了 未使用的模塊未使用的模塊在最後打包代碼的時候會被刪除

相關文章
相關標籤/搜索