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 的方式引入了 未使用的模块未使用的模块在最后打包代码的时候会被删除

相关文章
相关标签/搜索