前言

若文章有误,欢迎读者留言反馈

💻Installation

1
git clone https://github.com/coding327/learn_webpack5.git

webpack-vue篇

前面我们已经对于js代码进行打包了,其实我们编写的vue代码也是属于js代码
webpack中,我们是通过模块去安装vue来使用它,这和CDN引入或者说把vue下载到本地,通过script再引入使用有点不同
因为在webpack我们是通过模块的方式来使用vue,所以这里我们安装一下vue核心代码包

1
2
3
# vue3已经是默认版本了,所以这里没指定版本号,另外-S可以省略是因为npm5.0+开始默认会加入到生产环境
# 这里解释一下生产环境,最终打包部署到静态服务器,用户下载的时候还是需要vue相关的核心代码的
npm install vue

接着进入src/main.js文件中,去使用vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 在webpack中文件后缀可以省略,它会自动帮我们加上去
import { sum } from "./js/math"

// 从vue中引入createApp
import { createApp } from "vue"

const {priceFormat} = require('./js/format')

// 使用import引入文件,和main.js产生依赖关系
import "./js/element"

console.log(sum(20, 30))
console.log(priceFormat())

// 由于已经拿到createApp,这里我们就可以使用了
// 编写vue代码
const app = createApp({
template: `<h2>Hello World</h2>`,
data() {
return {
message: "Hello World"
}
}
})
// 由于index.html中已经有咱们的挂载容器了,直接放选择器在里面
app.mount("#app")

我们写的这一系列vue代码是写在js文件【先不说vue文件】中的,它本质也是js代码,所以应该是可以打包的
来打包测试一下,运行咱们打包文件夹里的index.html,并没有把我们模板里的Hello World给渲染出来,但是打包没有报错,也就是说渲染显示有问题
然后打开浏览器控制台再来看一下,发现有两条信息,
报错信息如下:
10179

这里直接说一下第一个并不是主要原因,先看第二个有warn的警告信息
大概意思就是:组件提供了模板选项,但是runtime compilation不支持打包的vue,然后需要配置
个人解释:我们编写的代码里面有个templateVue源代码会对其进行解析,其实Vue源代码给我们提供了特别多的这个版本,所有的版本又把它分为了以下两类:

  • Vue版本一:runtime+compiler
  • Vue版本二:runtime-only

这个版本一中的compiler它的功能就是对template来做编译的,但是它默认用的是版本二,这个runtime-only不包含对template的编译

关于它Vue的这个源代码打包后,它可不止两个版本,这个我们可以去node_modules/vue下找到这个打包的dist文件夹,里面就有很多不同的版本

Vue源代码打包后不同版本解析

  • vue(.runtime).global(.prod).js :
    • 通过浏览器中的<script src= ".”>直接使用【vue.global.js】;
    • 我们之前通过CDN引入和下载的Vue版本就是这个版本【vue.global.js】;
    • 会暴露一个全局的Vue来使用;
    • 关于(.runtime)vue.runtime.global.js(.runtime)可有可无,本来的包是vue.global.js,但是如果你只想用runtime的版本不包含compiler【不需要对template做编译】,打包的时候它就会更小一点,到时候引入【vue.runtime.global.js】;
    • 关于(.prod)vue.global.prod.js(.prod)可有可无,prod表示的是production版本,它是做过压缩的 ;
  • vue(.runtime).esm-browser(.prod).js :
    • 用于通过原生ES模块导入使用(在浏览器中通过<script type="module">来使用) ;
  • vue(.runtime).esm-bundler.js :
    • 用于webpackrollupparcel等构建工具 ;
    • 构建工具中默认是vue.runtime.esm-bundler.js ;
    • 如果我们需要解析模板template,那么需要手动指定vue.esm-bundler.js ;
  • vue.cjs(.prod).js :
    • 服务器端渲染使用 ;
    • 通过require()Node.js中使用 ;

这里我们就去指定版本,回到main.js文件中

1
2
// 从vue中引入createApp,这里指定一下版本
import { createApp } from "vue/dist/vue.esm-bundler"

这时我们再重新打包运行一下,发现咱们的模板就显示出来了

运行时+编译器 vs 仅运行时

  • Vue的开发过程中我们有三种方式来编写DOM元素︰
    • 方式一: template模板的方式(之前经常使用的方式);
    • 方式二: render函数的方式,使用h函数来编写渲染的内容;
    • 方式三: 通过.vue文件中的template来编写模板;
  • 它们的模板分别是如何处理的呢?
    • 方式二中的h函数可以直接返回一个虚拟节点,也就是Vnode节点;
    • 方式一和方式三的template都需要有特定的代码来对其进行解析∶
      • 方式三.vue文件中的template可以通过在vue-loader对其进行编译和处理
      • 方式一中的template我们必须要通过源码中一部分代码来进行编译;
  • 所以,Vue在让我们选择版本的时候分为运行时+编译器 vs 仅运行时
    • 运行时+编译器包含了对template模板的编译代码,更加完整,但是也更大一些;
    • 仅运行时没有包含对template版本的编译代码,相对更小一些;

真实开发中,我们是不可能在配置项里的template中写很多代码的,既没有代码高亮,太多堆积在配置项中也不好

所以这里我们先尝试把这个template中的这么多模板代码转移到public/index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!DOCTYPE html>
<html lang="">

<head>
<meta charset="utf-8">
<meta http-equiv="X一UA-Compatible" content="TE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>

<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<template id="my-app">
<h2>Hello World</h2>
<h2>{{message}}</h2>
</template>
<!-- built files will be auto injected-->
</body>

</html>

接着我们还要做一个绑定,回到main.js文件中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
...
// 由于已经拿到createApp,这里我们就可以使用了
// 编写vue代码
const app = createApp({
// template: `<h2>Hello World</h2>`,
template: "#my-app", // 把上面模板代码抽到index.html中,同时做绑定
data() {
return {
message: "Hello World"
}
}
})
// 由于index.html中已经有咱们的挂载容器了,直接放选择器在里面
app.mount("#app")

接着进行打包,打包成功,浏览器运行也没有什么问题
但是它还是有弊端,一个是在html文件里面,一个是在main.js文件里面,这个源代码相当于分开的,当我们在数据仓库中添加数据时,还要跑到另外一个文件中去编写,并且当我们组件比较多时,还要在html文件中写一大堆template,结构就会太混乱了

之前也说过createApp里的配置项就是个组件【并且它还是个根组件】,我们想把这个根组件里的模板,加上逻辑,以及样式,把这三个整合到一个文件里面,也就是.vue文件,这个文件也称为SFC文件【single-file-components(单文件组件)】

VSCode对SFC文件的支持

  • 在前面我们提到过,真实开发中多数情况下我们都是使用SFC ( single-file components(单文件组件) )
  • 我们先说一下VSCodeSFC的支持:
    • 插件一: Vetur,从Vue2开发就一直在使用的VSCode支持Vue的插件 ;
    • 插件二: Vue Language Features(Volar),官方推荐的插件(后续会基于Vue Language Features(Volar)开发官方的VSCode插件);

补充几个插件使用注意事项:
VeturVuterVue Language Features(Volar)插件
vue2我们使用的插件是veturvutervue3使用的是Vue Language Features(Volar)这个插件,注意使用哪个vue版本就使用哪个插件

我们在src下创建一个vue文件夹,接着创建一个App.vue文件,在这里面可以编写模板(template)、逻辑(script)、样式(style)
把代码转移到这个vue文件中:

  1. 把我们index.html之前写的模板中的内容移到App.vue文件中;
  2. main.js文件中data配置项移到App.vue文件中;

App.vue代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<template>
<h2>Hello World</h2>
<h2>{{ message }}</h2>
</template>

<script>
export default {
data() {
return {
message: "Hello World"
}
},
methods: {

}
}
</script>

<style>
h2 {
color: red;
}
</style>

  1. main.js导入根组件并作为配置项传入createApp中;
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 在webpack中文件后缀可以省略,它会自动帮我们加上去
    import { sum } from "./js/math"

    // 从vue中引入createApp,这里指定一下版本
    import { createApp } from "vue/dist/vue.esm-bundler"

    // 导入根组件【注意后缀名.vue不要掉】
    import App from "./vue/App.vue"

    ...

    const app = createApp(App)
    // 由于index.html中已经有咱们的挂载容器了,直接放选择器在里面
    app.mount("#app")

重新打包,发现报错,其实我们也能想明白,你现在是一个.vue文件,webpack肯定是不识别的,就像之前css文件,也就是需要loader,就是vue-loader

关于vue-loader

安装vue-loader

1
npm install vue-loader -D

安装完vue-loader,就需要去配置rules,回到webpack.config.js文件中

1
2
3
4
{
test: /\.vue$/,
loader: "vue-loader"
}, // 加载vue所需规则

再次打包,发现如下报错信息
vue-loader was used without the corresponding plugin. Make sure to include VueLoaderPlugin in your webpack config.

大致意思是说,确保你的webpack配置中包含VueLoaderPlugin插件
这里我们需要从vue-loader里面引入这个插件,并使用这个插件,回到webpack.config.js文件中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
...
// 引入VueLoaderPlugin插件,插件贯穿于整个webpack生命周期,它可以帮助vue-loader做一些事情
const { VueLoaderPlugin } = require("vue-loader/dist/index")
...
module.exports = {
...
module: { // 配置module
rules: [ // 注意rules是数组,以后会有多个规则
...
{
test: /\.vue$/,
loader: "vue-loader"
}, // 加载vue所需规则
{}, // 加载ts需要规则
// {
// test: /\.(css|less)$/,
// use: [
// "style-loader",
// "css-loader",
// "less-loader"
// ]
// }, // css、less合并写法
]
},
plugins: [
new CleanWebpackPlugin(), // 格式:根据CleanWebpackPlugin类创建出对象
new HtmlWebpackPlugin({
template: "./public/index.html",
title: "巧克力真美味" // 模板里的htmlWebpackPlugin.options.title中的htmlWebpackPlugin是new出来的对象,options就是传入的配置项,title就是我们这里配置的title
}), // 可以传入一个指定模板【不指定它有个默认模板】
new DefinePlugin({
BASE_URL: '"./"'
}), // 定义BASE_URL的值,注意这个引号里还要再加个引号,有点类似eval,会把引号里面内容当js语法解析
new CopyWebpackPlugin({
patterns: [
{
from: "public", // 从哪个文件夹里复制
to: "./", // 复制到哪个文件夹【注意它是在打包文件夹基础上,这里也可以不写,默认就是打包文件夹】
globOptions: {
ignore: [
"**/index.html" // 注意这两个**表示当前public文件夹下index.html以及子文件夹下的index.html
]
} // globOptions.ignore忽略某个文件,可以过滤掉某些不需要复制的文件如index.html模板
}
] // patterns匹配的意思
}), // 复制功能插件
new VueLoaderPlugin() // vue-loader插件,帮助vue-loader做一些事情
] // plugins是个数组【这个不用管顺序】,里面放的是一个个插件对象【其实它源码里面是拿到我们导出的这个大的对象,然后去取到我们所有的plugins,之后对它做了个for循环,for循环后对它做个注入,到时候就可以根据不同的hook的生命周期来回调这个插件里面对象的某个方法】
}

然后重新打包就能成功了,浏览器运行显示正常

真实开发是按照组件化开发,这里我们可以接着在vue文件夹参创建一个HelloWorld组件,注意组件名要使用大驼峰,编写一点代码

我打算把它先作为全局组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<!-- 使用 scoped 后,父组件的样式将不会渗透到子组件中。不过,子组件的根节点会同时被父组件的作用域样式和子组件的作用域样式影响。 -->
<div>
<h2>Hello World,我是全局组件</h2>
</div>
</template>

<script>
export default {
data() {
return {

}
}
}
</script>

<style scoped>

</style>

接着我们选择全局注册

  • 全局注册是在main.js文件中,需要使用到app应用实例的扩展方法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 导入根组件【注意后缀名.vue不要掉】
    import App from "./vue/App.vue"

    // 导入HelloWorld组件
    import HelloWorld from "./vue/HelloWorld.vue"

    const app = createApp(App)
    // 注册全局组件
    app.component("HelloWorld", HelloWorld)
    // 由于index.html中已经有咱们的挂载容器了,直接放选择器在里面
    app.mount("#app")

App.vue根组件中使用,注意官方文档有说过,大驼峰命名的组件在使用组件标签时是可以使用短横线连接的

1
2
3
4
5
6
7
<template>
<h2>Hello World</h2>
<h2>{{ message }}</h2>
<hello-world></hello-world>
</template>

...

重新打包,在浏览器运行正常

在到vue文件夹中创建一个NavBar.vue组件,来作为局部组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<!-- 使用 scoped 后,父组件的样式将不会渗透到子组件中。不过,子组件的根节点会同时被父组件的作用域样式和子组件的作用域样式影响。 -->
<div>
<h2>Hello World,我是局部组件</h2>
</div>
</template>

<script>
export default {
data() {
return {

}
}
}
</script>

<style>

</style>

App.vue文件中进行局部注册并使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<template>
<h2>Hello World</h2>
<h2>{{ message }}</h2>
<hello-world></hello-world>
<NavBarVue />
</template>

<script>
import NavBarVue from './NavBar.vue';

export default {
components: {
NavBarVue
},
data() {
return {
message: "Hello World"
}
},
methods: {

}
}
</script>

<style scoped>
h2 {
color: red;
}
</style>


重新打包,在浏览器运行正常

最后,还有一个问题,就是浏览控制台一直有一个警告,如下

1
2
3
4
Feature flags __VUE_OPTIONS_API__, __VUE_PROD_DEVTOOLS__ are not explicitly defined.
You are running the esm-bundler build of Vue, which expects these compile-time feature flags to be globally injected via the bundler config in order to get better tree-shaking in the production bundle.

For more details, see https://link.vuejs.org/feature-flags.

通过他给的链接,我们可以了解到:
Bundler 构建功能标志
3.0.0-rc.3 开始,esm-bundler构建现在公开了可以在编译时覆盖的全局功能标志:

  • __VUE_OPTIONS_API__(启用/禁用选项 API 支持,默认值true:)
  • __VUE_PROD_DEVTOOLS__(在生产中启用/禁用 devtools 支持,默认值false:)

该构建将在不配置这些标志的情况下工作,但是强烈建议正确配置它们以便在最终捆绑包中获得正确的 tree-shaking。要配置这些标志:

  • webpack:使用DefinePlugin
  • 汇总:使用@rollup/plugin-replace
  • Vite:默认配置,但可以使用define选项覆盖
    注意:替换值必须是布尔值,不能是字符串,否则捆绑器/压缩器将无法正确评估条件。

__VUE_OPTIONS_API__:它是来对vue2做适配的,其实现在写的templatedata都是options api,在vue3写的比较少,用的是setup,那么在我们项目里面到底有没有这个东西呢,它默认情况下是true,即是有这个东西的,那么到时候vue源代码里面是有一部分来做这个options api解析的代码的,但是如果你vue3写的都是setup代码,我就不需要options api这部分代码了,它推荐我们可以设置为false,它到时候可以做tree-shaking,警告里面有这个词,tree-shaking它在真正打包的时候可以把我们这部分代码本来是有的但是我发现你不需要有这个东西,它就会把我们这部分代码从我们源代码里面删除掉,那我们代码就可以变得更小一点

__VUE_PROD_DEVTOOLS__:生产环境要不要做devtool,它其实是一个vue调试工具,调试工具一般在开发阶段使用,生产环境一般是不需要让它生效的,它刚好默认值就是false,如果你想要生产环境生效就设置为true

怎么去除这个警告呢?
其实它上面也有写,webpack:使用DefinePlugin,这个DefinePlugin之前我们有给index.html设置BASE_URLfavicon.ico,这里回到webpack.config.js文件中配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
...
// `DefinePlugin`插件,这个插件是`webpack`内置的一个插件
const { DefinePlugin } = require("webpack")

module.exports = {
...
plugins: [
...
new DefinePlugin({
BASE_URL: '"./"',
__VUE_OPTIONS_API__: true, // 这个就是开启options api,如果都是setup,vue3代码,可以关闭,减小vue源码体积
__VUE_PROD_DEVTOOLS__: false // 这个默认就是false,可以不用设置
}), // 定义BASE_URL的值,注意这个引号里还要再加个引号,有点类似eval,会把引号里面内容当js语法解析
...
}

这时候我们再次打包就发现警告消除了

小细节说一下

在我们以.vue文件书写模板时,template不再是那种原先的配置项中的属性了【它原先解析还需要依靠特定的版本】,这种.vue文件最主要的是,他那个vue-loader会依赖一个@vue/compiler-sfc它会去解析template标签及里面的内容,所以在打包我们的源代码时,它加载就已经做了解析,就不再需要再多做一次解析了,所以从vue引入createApp那里可以改一下了
main.js文件diff如下:

1
2
3
4
5
6
7
...
// 从vue中引入createApp,这里指定一下版本
// import { createApp } from "vue/dist/vue.esm-bundler"
// 加载.vue文件可以直接从vue中引入,因为它不需要上面那个做解析,自己使用vue-loader依赖的@vue/compiler-sfc做解析
import { createApp } from "vue"

...

重新打包,浏览器运行显示正常,控制台也没有报错