zl程序教程

您现在的位置是:首页 >  IT要闻

当前栏目

vue 快速入门 系列 —— vue loader 下

2023-02-18 16:38:10 时间

其他章节请看:

vue 快速入门 系列

vue loader 下

CSS Modules

CSS Modules 是一个流行的,用于模块化和组合 CSS 的系统。vue-loader 提供了与 CSS Modules 的一流集成,可以作为模拟 scoped CSS 的替代方案。

Tip:请看下面的用法来了解 css modules。

用法

将 App.vue 内容修改为:

<template>
  <div>
    <p :class="$style.red">
      This should be red
    </p>
    <p :class="{ [$style.red]: apple.isRed }">
      Am I red?
    </p>
    <p :class="[$style.red, $style.bold]">
      Red and bold
    </p>
  </div>
</template>

<script>
export default {
  created () {
    // -> 类似"red_2hCxILSe"
    console.log(`red=${this.$style.red}`)
  },
  data () {
    return {
      msg: 'Hello world!',
      // 注释掉 apple 也不会报错
      apple:{
        isRed: false
      },
    }
  },
}
</script>

<style module>
.red {
  color: red;
  font-size: 2em;
}
.bold {
  font-weight: bold;
}
</style>

这段代码,在 <style> 上添加 module 特性。这个 module 特性指引 Vue Loader 作为名为 $style 的计算属性,向组件注入 CSS Modules 局部对象。然后就可以在模板中通过一个动态类绑定来使用它了,就像 $style.red,还可以通过 javascript 访问到它。

接着修改配置来开启 modules:

// webpack.config.js -> module.rules
{
  test: /\.css$/i,
  use: [
    "style-loader", 
    {
      loader: 'css-loader',
      options: {
        // 开启 CSS Modules
        modules: {
          // 自定义生成的类名
          localIdentName: '[local]_[hash:base64:8]'
        }
        
      }
    }

  ]
},

重启服务,页面显示三句文案:

// 红色
This should be red

Am I red?

// 红色 + 粗
Red and bold

控制台输出red=red_2hCxILSe

通过浏览器查看生成的代码:

<style>
    .red_2hCxILSe {
        color: red;
        font-size: 2em;
    }

    .bold_2rUIHzbD {
        font-weight: bold;
    }
</style>

<div>
    <p class="red_2hCxILSe">
        This should be red
    </p>
    <p class="">
        Am I red?
    </p>
    <p class="red_2hCxILSe bold_2rUIHzbD">
        Red and bold
    </p>
</div>
可选用法

如果你只想在某些 Vue 组件中使用 CSS Modules,你可以使用 oneOf 规则并在 resourceQuery 字符串中检查 module 字符串。

什么意思?从 oneOf 这个关键字我嗅到上面的用法是否只能匹配一种情况。于是给 App.vue 在增加一个 style 的样式:

<template>
  <!-- 引用定义的样式 -->
  <div class="f-border">
    ...
  </div>
</template>

<style module>
...
</style>

<style>
.f-border{border: 1px solid}
</style>

页面看不到边框(border)效果,普通的 <style> 没有生效。

于是根据文档配置如下:

// webpack.config.js -> module.rules
{
  test: /\.css$/,
  oneOf: [
    // 这里匹配 `<style module>`
    {
        resourceQuery: /module/,
        use: [
          'vue-style-loader',
          {
              loader: 'css-loader',
              options: {
                // 开启 CSS Modules
                modules: {
                    // 自定义生成的类名
                    localIdentName: '[local]_[hash:base64:8]'
                }
              }
          }
        ]
    },
    // 这里匹配普通的 `<style>` 或 `<style scoped>`
    {
        use: [
        'vue-style-loader',
        'css-loader'
        ]
    }
  ]
},

重启服务,页面能看到边框,border 生效,而且 module 的效果也还在。

和预处理器配合使用

CSS Modules 可以与其它预处理器一起使用。

我们尝试给 less 增加 css modules。

<style module> 块增加 lang='less',并添加 less 语句:

<style module lang='less'>
...
.bold {
  font-weight: bold;
}

/* less 语法 */
@italic: italic;
p{
  font-style: italic
}
</style>

页面中文字全部变成斜体,但 css module 定义的红色、加粗效果都没了。

修改配置文件:

// webpack.config.js -> module.rules
{
  test: /\.less$/,
  use: [
    'vue-style-loader',
    // +
    {
      loader: 'css-loader',
      options: {
        // 开启 CSS Modules
        modules: {
          localIdentName: '[local]_[hash:base64:8]'
        }
      }
    },
    // postcssLoader 可以参考本文末尾的“核心代码”
    postcssLoader,
    'less-loader'
  ]
},

重启服务器,less 和 css module 都生效了。

自定义的注入名称

在 .vue 中你可以定义不止一个 <style>,为了避免被覆盖,你可以通过设置 module 属性来为它们定义注入后计算属性的名称。就像这样:

<style module="a">
  /* 注入标识符 a */
</style>

<style module="b">
  /* 注入标识符 b */
</style>

将 App.vue 的内容修改为:

<script>
export default {
  created () {
    console.log(this.a)
    console.log(this.$style)
  }
}
</script>

<style module='a' >
.a {}
.c1 {
  color: red;
}
</style>

<style module>
.c1 {
  color: blue;
}
</style>

这段代码定义了一个默认的 module 以及一个名为 a 的 module,浏览器控制台输出:

{a: "a_132IjK4h", c1: "c1_R9Fj2CxU"}
{c1: "c1_R9Fj2CxU"}

热重载

“热重载”不只是当你修改文件的时候简单重新加载页面。启用热重载后,当你修改 .vue 文件时,该组件的所有实例将在不刷新页面的情况下被替换。它甚至保持了应用程序和被替换组件的当前状态!当你调整模版或者修改样式时,这极大地提高了开发体验。

Tip: 与”webpack 快速入门 系列 —— 性能“一文中的热模块差不多意思,所以有关热模块的细节这里就不在复述。

状态保留规则

当编辑一个组件的 <template> 时,这个组件实例将就地重新渲染,并保留当前所有的私有状态。能够做到这一点是因为模板被编译成了新的无副作用的渲染函数。

当编辑一个组件的 <script> 时,这个组件实例将就地销毁并重新创建。(应用中其它组件的状态将会被保留) 是因为 <script> 可能包含带有副作用的生命周期钩子,所以将重新渲染替换为重新加载是必须的,这样做可以确保组件行为的一致性。这也意味着,如果你的组件带有全局副作用,则整个页面将会被重新加载。

<style> 会通过 vue-style-loader 自行热重载,所以它不会影响应用的状态。

:”如果你的组件带有全局副作用,则整个页面将会被重新加载“未测试出来

用法

当使用脚手架工具 vue-cli 时,热重载是开箱即用的。

当手动设置你的工程时,热重载会在你启动 webpack-dev-server --hot 服务时自动开启。

现在我们没有开启热模块,所以修改代码浏览器就会刷新。你可以尝试将:

<template>
    <div>1</div>
</template>

改为:

<template>
    <div>12</div>
</template>

而倘若开启热模块,就像这样:

// webpack.config.js
module.exports = {
    devServer: {
        hot: true,
    }
}

重启服务器,再次修改代码,浏览器则不会在刷新就能响应我们的更改。

关闭热重载

热重载默认是开启的,除非遇到以下情况:

  1. webpack 的 target 的值是 node (服务端渲染)
  2. webpack 会压缩代码
  3. process.env.NODE_ENV === 'production'

测试一下最后一条规则:

// webpack.config.js
const process = require('process');
process.env.NODE_ENV = 'production'

重启服务,修改代码,发现热模块果然失效。

还可以通过 hotReload 显式地关闭热重载:

// webpack.config.js -> module.rules
{
    test: /\.vue$/,
    loader: 'vue-loader',
    options: {
        hotReload: false // 关闭热重载
    }
},

Tip:建议开启热模块替换,方便后续测试。

函数式组件

函数式组件无状态(没有响应式数据),也没有实例(没有 this 上下文)。一个函数式组件就像这样:

Vue.component('my-component', {
  functional: true,
  // Props 是可选的
  props: {
    // ...
  },
  // 为了弥补缺少的实例
  // 提供第二个参数作为上下文
  render: function (createElement, context) {
    // ...
  }
})

在一个 *.vue 文件中以单文件形式定义的函数式组件,现在对于模板编译、scoped CSS 和热重载也有了良好的支持。

要声明一个应该编译为函数式组件的模板,请将 functional 特性添加到模板块中。这样做以后就可以省略 <script> 块中的 functional 选项。请看示例:

首先定义一个函数式组件:

// Box.vue
<template functional>
  <div>
    <!-- props:提供所有 prop 的对象 -->
    {{ props.foo }}
    <!-- parent 对父组件的引用 -->
    <p>{{parent.$data.msg}}</p>
  </div>
</template>

接着在 App.vue 中使用 Box.vue:

// App.vue
<template>
  <div>
    <Box foo='1'/>
  </div>
</template>

<script>
import Box from './Box.vue'
export default {
  data () {
    return {
      msg: '2',
    }
  },
  components: {
    Box
  }
}
</script>

页面显示:

1
2

自定义块

在 .vue 文件中,你可以自定义语言块。应用于一个自定义块的 loader 是基于这个块的 lang 特性、块的标签名以及你的 webpack 配置进行匹配的。

如果指定了一个 lang 特性,则这个自定义块将会作为一个带有该 lang 扩展名的文件进行匹配。

你也可以使用 resourceQuery 来为一个没有 lang 的自定义块匹配一条规则。如果这个自定义块被所有匹配的 loader 处理之后导出一个函数作为最终结果,则这个 *.vue 文件的组件会作为一个参数被这个函数调用。

Example

这里创建一个 <docs> 自定义块。

为了注入自定义块的内容,我们先写一个自定义 loader:

// src/docs-loader.js
module.exports = function (source, map) {
  this.callback(
    null,
    `export default function (Component) {
      Component.options.__docs = ${
        JSON.stringify(source)
      }
    }`,
    map
  )
}

Tip: loader 本质上是导出为函数的 JavaScript 模块。有关自定义 loader 更多介绍请看我的另一篇文章”webpack 快速入门 系列 - 自定义 webpack 上“。

接着我们给 <docs> 自定义块配置上自定义 loader。

// wepback.config.js -> module.rules
{
  resourceQuery: /blockType=docs/,
  loader: require.resolve('./src/docs-loader.js')
},

接下来在 Box.vue 中使用 <docs>

<template>
  <div>Hello</div>
</template>

<docs>
i am docs
</docs>

然后在 App.vue 中引入 Box.vue,然后输出 docs 中的内容:

<template>
  <div>
    <Box/>
    <p>{{ docs }}</p>
  </div>
</template>

<script>
import Box from './Box.vue'
export default {
  data () {
    return {
      docs: Box.__docs
    }
  },
  components: {
    Box
  }
}
</script>

页面输出:

Hello
i am docs

<docs>自定义块中的内容被成功输出。

CSS 提取

Tip:请只在生产环境下使用 CSS 提取;这里针对的是 webpack 4,而非 webpack 3。

先安装依赖,然后修改配置:

npm i -D mini-css-extract-plugin@1
// webpack.config.js

const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
  ...
  module: {
    rules: [
      {
        test: /\.css$/,
        oneOf: [
          ...
          {
            use: [
              process.env.NODE_ENV !== 'production' 
                ? 'vue-style-loader'
                : MiniCssExtractPlugin.loader,
              'css-loader'
            ]
          }
        ]
      },
    ],
  },
  plugins: [
    new MiniCssExtractPlugin(),
    ...
  ],
};

App.vue:

// App.vue
<template>
    <p>i am red?</p>
</template>

<style>
  p{color:red}
</style>

重启服务器,通过浏览器查看,样式在 main.css 中。

:别忘了将 process.env.NODE_ENV = 'production' 开启,否则不会提取 css,跟 mode: 'development' 没有关系。

代码校验 (Linting)

ESLint

引入 javascript 语法校验,配置如下:
Tip: 参考”webpack 快速入门 系列 —— 实战一->js 语法检查“

// eslint-loader废弃了,故使用 eslint-webpack-plugin
> npm i -D eslint@7 eslint-webpack-plugin@2 eslint-config-airbnb-base@14 
// webpack.config.js
const ESLintPlugin = require('eslint-webpack-plugin');

module.exports = {
  plugins: [
    new ESLintPlugin({
      // 将启用ESLint自动修复功能。此选项将更改源文件
      fix: true
    })
  ],
};

eslint 的配置文件:

// .eslintrc.js
module.exports = {
    "extends": "airbnb-base",
    "rules": {
        "no-console": "off"
    },
    "env": {
      "browser": true
    }
}

重启服务,终端报错:

ERROR in 
test-vue-loader\src\index.js
  1:1  error  'vue' should be listed in the project's dependencies, not devDependencies  import/no-extraneous-dependencies
  5:1  error  Do not use 'new' for side effects                                          no-new

✖ 2 problems (2 errors, 0 warnings)

修复错误1,通过给 .eslintrc.js 增加一条 rule

// .eslintrc.js module.exports -> rules
"import/no-extraneous-dependencies": ["error", {"devDependencies": true}]

修复错误2,在使用 new 的句子上添加/* eslint-disable no-new */注释来绕开语法检查

// index.js

/* eslint-disable no-new */
new Vue({
  ...
});

重启服务,终端不在抛出错误。

给 index.js 增加如下 js 代码用于检验:

// index.js

new Vue({
 ...
});
// 很多连续空格
function sum(a,     b) {
  return a +     b;
}
console.log(sum(1,     10))

不到三秒,连续的空格就会被合并。效果如下:

...

function sum(a, b) {
  return a + b;
}
// 除了空格被合并,末尾还自动加上了分号
console.log(sum(1, 10));

对 App.vue 进行相同的测试,却没有触发校验,即没有自动合并连续空格。

猜测应该是 eslint 只配置了 js,需要将 vue 也配置上:

// webpack.config.js

new ESLintPlugin({
  // 默认是 js,再增加 vue
  extensions: ['js', 'vue'],
  fix: true
})

重启服务,终端报错:

ERROR in
App.vue
  1:1  error  Parsing error: Unexpected token <

✖ 1 problem (1 error, 0 warnings)

于是决定尝试使用 eslint-plugin-vue(Vue.js 的官方 ESLint 插件)来解决此问题。

安装依赖包:

> npm i -D eslint-plugin-vue@7

修改 .eslintrc.js 的 extends 值:

module.exports = {
    "extends": [
        "airbnb-base",
        "plugin:vue/essential"
      ],
    // "extends": "airbnb-base",
    ...
}

重启服务,再次修改 App.vue 中的 js,则也会自动校验(例如合并连续空格)。

stylelint

尝试给样式增加多个空格:

// App.vue
<style>
/* 多个空格 */
.example        {
  color: red;
  font-size: 2em;
}
</style>

发现样式中的空格没有自动合并,应该需要进行 stylelint 的配置。

Tip:这里就不展开,请自行研究哈。

单文件组件规范

简介

.vue 文件是一个自定义的文件类型,用类 HTML 语法描述一个 Vue 组件。每个 .vue 文件包含三种类型的顶级语言块 <template><script><style>,还允许添加可选的自定义块。

<template>
  <div class="example">{{ msg }}</div>
</template>

<script>
export default {
  data () {
    return {
      msg: 'Hello world!'
    }
  }
}
</script>

<style>
.example {
  color: red;
}
</style>

<custom1>
  This could be e.g. documentation for the component.
</custom1>

vue-loader 会解析文件,提取每个语言块,如有必要会通过其它 loader 处理,最后将他们组装成一个 ES Module,它的默认导出是一个 Vue.js 组件选项的对象。

vue-loader 支持使用非默认语言,比如 CSS 预处理器,预编译的 HTML 模版语言,通过设置语言块的 lang 属性。例如,你可以像下面这样使用 Sass 语法编写样式:

<style lang="sass">
  /* write Sass! */
</style>

语言块

模板
  • 每个 .vue 文件最多包含一个 <template> 块。
  • 内容将被提取并传递给 vue-template-compiler 为字符串,预处理为 JavaScript 渲染函数,并最终注入到从 <script> 导出的组件中

如果在一个 vue 文件中包含多个 <template> 块会怎么样?

给 App.vue 添加两个模板:

<template>
  <div class="example">第一个 {{ msg }}</div>
</template>
<template>
  <div class="example">第二个 {{ msg }}</div>
</template>
...

浏览器页面显示“第二个 Hello world!”。终端和浏览器控制台没有报错。

脚本
  • 每个 .vue 文件最多包含一个 <script> 块。
  • 这个脚本会作为一个 ES Module 来执行。
  • 它的默认导出应该是一个 Vue.js 的组件选项对象。也可以导出由 Vue.extend() 创建的扩展对象,但是普通对象是更好的选择。
  • 任何匹配 .js 文件 (或通过它的 lang 特性指定的扩展名) 的 webpack 规则都将会运用到这个 <script> 块的内容中。

我们逐一分析上述规则。

如果一个 vue 文件包含多个 <script> 块会怎么样?

给 App.vue 写入2个 script 块:

<template>
  <div class="example">{{ msg }}</div>
</template>

<script>
export default {
  data () {
    return {
      msg: 'Hello world!'
    }
  }
}
</script>
<script>
console.log('第二个 script');
</script>
...

页面空白,只有控制台输出“第二个 script”。控制台和终端也没有报错。将 script 块调换,浏览器页面输出“Hello world!”

“它的默认导出应该是一个 Vue.js 的组件选项对象”什么意思?

在 vue 官网学习时,定义一个名为 button-counter 的新组件会这么写:

Vue.component('button-counter', {
  data: function () {
    return {
      count: 0
    }
  },
  template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>'
})

在单页面组件中得这么写:

<template>
    <button v-on:click="count++">You clicked me {{ count }} times.</button>
</template>

<script>
export default {
  data: function () {
    return {
      count: 0
    }
  },
};
</script>

首先将 Vue.component() 的第二个参数作为默认导出,然后把 template 的值(<button ...)放到 template 块中。

样式
  • 默认匹配:/.css$/。
  • 一个 .vue 文件可以包含多个 <style> 标签。
  • <style> 标签可以有 scoped 或者 module 属性 (查看 scoped CSS和 CSS Modules) 以帮助你将样式封装到当组件。具有不同封装模式的多个 <style> 标签可以在同一个组件中混合使用。
  • 任何匹配 .css 文件 (或通过它的 lang 特性指定的扩展名) 的 webpack 规则都将会运用到这个 <style> 块的容中
自定义块

可以在 .vue 文件中添加额外的自定义块来实现项目的特定需求,例如 <docs> 块。vue-loader 将会使用标签名来查找对应的 webpack loader 来应用在对应的块上。webpack loader 需要在 vue-loader 的选项 loaders 中指定。

Src导入

如果喜欢把 .vue 文件分隔到多个文件中,你可以通过 src 属性导入外部文件。

例如将 App.vue 改造成 src 导入模式:

// App.vue
<template src='./AppComponent/template.html'></template>
<script src='./AppComponent/script.js'></script>
...
// AppComponent/script.js
export default {
    data () {
        return {
        msg: 'Hello world! ph'
        }
    }
}
// AppComponent/template.html
<div class="example">{{ msg }}</div>

运行后,与改造之前的效果一样。

注释

在语言块中使用该语言块对应的注释语法 (HTML、CSS、JavaScript、Jade 等)。顶层注释使用 HTML 注释语法:<!-- comment contents here -->。请看示例:

<template>
  <div class="example">
    <!-- html 注释 -->
    {{ msg }}
  </div>
</template>

<!-- 顶层注释使用 HTML 注释 -->
<!--
<template>
  <div>
    第二个 template
  </div>
</template>
-->

<script>
export default {
    data () {
        return {
          msg: 'Hello world! ph'
        }
    }
}
// js 单行注释
/* 块注释 */
console.log('11');
</script>

<style>
.example {
  color: red;
  /* 字号:2em */
  font-size: 2em;
}
</style>

总结

至此,通过本篇文章,我们学会了编写一个简单的,用于单文件组件开发的脚手架。包括:

  • 单文件组件的格式编写 vue 组件
  • 图片
  • 预处理器:sass、less、stylus、postcss、babel、typescript、pug
  • scoped和css module
  • 热重载
  • 函数式组件
  • 自定义块
  • css 提取
  • 代码校验

核心代码

附上项目最终核心文件,方便学习和解惑。

webpack.config.js

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin')
const {
    VueLoaderPlugin
} = require('vue-loader');

const process = require('process');
process.env.NODE_ENV = 'production'
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const ESLintPlugin = require('eslint-webpack-plugin');
const postcssLoader = { 
    loader: 'postcss-loader', 
    options: {
      // postcss 只是个平台,具体功能需要使用插件
      postcssOptions:{
        plugins:[
          [
            "postcss-preset-env",
            {
              browsers: 'ie >= 8, chrome > 10',
            },
          ],
        ]
      }
    } 
  }
module.exports = {
    entry: './src/index.js',
    output: {
        filename: 'main.js',
        path: path.resolve(__dirname, 'dist')
    },
    module: {
        rules: [
            /*
            {
                test: /\.css$/i,
                use: ["style-loader", "css-loader"]
            },
            */

            /*
            {
                test: /\.css$/i,
                use: [
                  "style-loader", 
                  {
                    loader: 'css-loader',
                    options: {
                      // 开启 CSS Modules
                      modules: {
                        // 自定义生成的类名
                        localIdentName: '[local]_[hash:base64:8]'
                      }
                      
                    }
                  }
              
                ]
            },
            */
            {
                test: /\.css$/,
                oneOf: [
                    // 这里匹配 `<style module>`
                    {
                        resourceQuery: /module/,
                        use: [
                        'vue-style-loader',
                        {
                            loader: 'css-loader',
                            options: {
                            // 开启 CSS Modules
                            modules: {
                                // 自定义生成的类名
                                localIdentName: '[local]_[hash:base64:8]'
                            }
                            
                            }
                        }
                        ]
                    },
                    // 这里匹配普通的 `<style>` 或 `<style scoped>`
                    /*
                    {
                        use: [
                        'vue-style-loader',
                        'css-loader'
                        ]
                        
                    }
                    */
                    {
                        use: [
                        process.env.NODE_ENV !== 'production' 
                            ? 'vue-style-loader'
                            : MiniCssExtractPlugin.loader,
                        'css-loader'
                        ]
                    },
                ]
            },
            {
                test: /\.vue$/,
                loader: 'vue-loader',
                options: {
                    hotReload: true // 关闭热重载
                }
            },
            {
                test: /\.(png|jpg|gif)$/i,
                use: [{
                    loader: 'url-loader',
                    options: {
                        // 调整的比 6.68 要小,这样图片就不会打包成 base64 
                        limit: 1024 * 6,
                        esModule: false,
                    },
                }, ],
            },
            {
                test: /\.scss$/,
                use: [
                  'vue-style-loader',
                  'css-loader',
                  'sass-loader'
                ]
            },
            {
                test: /\.sass$/,
                use: [
                    'vue-style-loader',
                    'css-loader',
                    {
                        loader: 'sass-loader',
                        options: {
                            // sass-loader version >= 8
                            sassOptions: {
                                indentedSyntax: true
                            },
                            additionalData: `$size: 3em;`,
                        }
                    }
                ]
            },
            /*
            {
                test: /\.less$/,
                use: [
                  'vue-style-loader',
                  'css-loader',
                  postcssLoader,
                  'less-loader'
                ]
            },
            */

            {
                test: /\.less$/,
                use: [
                  'vue-style-loader',
                  // +
                  {
                    loader: 'css-loader',
                    options: {
                      // 开启 CSS Modules
                      modules: {
                        localIdentName: '[local]_[hash:base64:8]'
                      }
                      
                    }
                  },
                  postcssLoader,
                  'less-loader'
                ]
            },
            {
                test: /\.styl(us)?$/,
                use: [
                  'vue-style-loader',
                  'css-loader',
                  'stylus-loader'
                ]
            },
            {
                test: /\.js$/,
                // exclude: /node_modules/,
                exclude: file => (
                    /node_modules/.test(file) &&
                    !/\.vue\.js/.test(file)
                ),
                use: {
                  loader: 'babel-loader',
                  options: {
                    presets: [
                      ['@babel/preset-env']
                    ]
                  }
                }
            },
            {
                test: /\.ts$/,
                loader: 'ts-loader',
                options: { appendTsSuffixTo: [/\.vue$/] }
            },
            {
                test: /\.pug$/,
                loader: 'pug-plain-loader'
            },
            {
                resourceQuery: /blockType=docs/,
                loader: require.resolve('./src/docs-loader.js')
            },
        ]
    },
    plugins: [
        new HtmlWebpackPlugin({
            template: 'src/index.html'
        }),
        new VueLoaderPlugin(),
        new MiniCssExtractPlugin(),
        new ESLintPlugin({
          extensions: ['js', 'vue'],
          // 将启用ESLint自动修复功能。此选项将更改源文件
          fix: true
        })
    ],
    mode: 'development',
    devServer: {
        hot: true,
        // open: true,
        contentBase: path.join(__dirname, 'dist'),
        compress: true,
        port: 9000,
    },
    resolve: {
        alias: {
            '@': path.resolve(__dirname, 'src/'),
        },
        extensions: ['.ts', '.js'],
    },
};

package.json

{
  "name": "test-vue-loader",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack",
    "dev": "webpack-dev-server"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/preset-env": "^7.14.7",
    "babel-loader": "^8.2.2",
    "css-loader": "^5.2.4",
    "eslint": "^7.30.0",
    "eslint-config-airbnb-base": "^14.2.1",
    "eslint-plugin-vue": "^7.12.1",
    "eslint-webpack-plugin": "^2.5.4",
    "file-loader": "^6.2.0",
    "html-webpack-plugin": "^4.5.2",
    "less": "^4.1.1",
    "less-loader": "^7.3.0",
    "mini-css-extract-plugin": "^1.6.2",
    "node-sass": "^6.0.1",
    "postcss-loader": "^4.3.0",
    "postcss-preset-env": "^6.7.0",
    "pug": "^3.0.2",
    "pug-plain-loader": "^1.1.0",
    "sass-loader": "^10.2.0",
    "style-loader": "^2.0.0",
    "stylus": "^0.54.8",
    "stylus-loader": "^4.3.3",
    "ts-loader": "^7.0.5",
    "typescript": "^4.3.5",
    "url-loader": "^4.1.1",
    "vue": "^2.6.14",
    "vue-loader": "^15.9.7",
    "vue-template-compiler": "^2.6.14",
    "webpack": "^4.46.0",
    "webpack-cli": "^3.3.12",
    "webpack-dev-server": "^3.11.2"
  }
}

其他章节请看:

vue 快速入门 系列