Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。另一方面,当与现代化的工具链以及各种支持类库结合使用时,Vue 也完全能够为复杂的单页应用提供驱动。
准备 vue-cli
使用 Vue 你可以通过 webpack 打包,也可以通过专用的 vue-cli 快速构建项目。如果想要使用 webpack 完全自定义配置可以参考>>> 关于 WebPack 你需要知道的一些事。
这里采用 vue-cli ,在开始前你需要知道:vue-cli 基于 webpack 和 webpack-dev-server ,并且配置好了基本的打包规则,它可以帮你用配置好的模板迅速搭建起一个项目工程来,避免了自己配置 webpack.config.js 的麻烦。
npm i @vue/cli -g // npm i 初始化项目后使用该命令安装 vue-cli
创建项目
vue create hello-world // 在文件夹下创建项目 -p, --preset <presetName> 忽略提示符并使用已保存的或远程的预设选项 -d, --default 忽略提示符并使用默认预设选项 -i, --inlinePreset <json> 忽略提示符并使用内联的 JSON 字符串预设选项 -m, --packageManager <command> 在安装依赖时使用指定的 npm 客户端 -r, --registry <url> 在安装依赖时使用指定的 npm registry -g, --git [message] 强制 / 跳过 git 初始化,并可选的指定初始化提交信息 -n, --no-git 跳过 git 初始化 -f, --force 覆写目标目录可能存在的配置 -c, --clone 使用 git clone 获取远程预设选项 -x, --proxy 使用指定的代理创建项目 -b, --bare 创建项目时省略默认组件中的新手指导信息 -h, --help 输出使用帮助信息
注意使用 vue create 创建项目时,程序会让你选择是使用默认方案创建还是自定义创建
- Default : 采用默认方案自动创建项目,可能会安装一下自己用不到的内容
- Manually select features : 采用自定义方案手动创建项目,需要自己根据提示一步步配置
创建完成后,项目目录大概如下图所示:
注意:与 webpack 不同的是,使用 vue-cli 创建的项目默认会生成一个 public 文件夹,该文件夹下的文件会在打包后直接复制到输出文件夹中,同时里面的 index.html 即是使用 webpack.config.js 文件中 HtmlWebpackPlugin 插件里配置的 html 文件
···
vue ui // 浏览器打开图形交互界面创建或管理项目,更直观
···
创建方式与命令行方式几乎没有差别,还可以管理项目。
插件
插件面板用于安装一些扩展 CLI 命令的插件,这些插件多为一些部署模板,用于快速安装并部署项目所需的依赖。你也可以自定义一些自己的模板方案>>>插件开发指南
vue add // 安装和调用 Vue CLI 插件
vue invoke // 调用 generator.js 快速生成配置
- @vue/cli-service :启动一个基于 webpack-dev-serve 的开发服务器并附带开箱即用的模块热加载,package.json 中的 serve、 build、lint 命令均源于此,是众多命令运行的基础。
- @vue/cli-plugin-babel :快速安装并配置 babel 依赖。
- @vue/cli-plugin-eslint :快速安装并配置 eslint 依赖。
使用 vue ui 打开图形界面管理项目时,上方还会提示未安装 vuex 与 vue-router,通过此处安装的话会先安装一下两个插件:
- @vue/cli-plugin-router :根据模板快速安装并配置 vue-router 依赖。
- @vue/cli-plugin-vuex :根据模板快速安装并配置 vuex 依赖。
默认 add 命令安装 CLI 插件后会自动运行生成器,安装并配置 vuex 及 vue-router。
由于 vue add 或 vue invoke 可能会修改项目文件,为了避免错误,建议运行前先提交下项目。
注意:这些插件也可以通过 npm 命令安装及卸载。
依赖
其他非 vue-cli 的插件直接通过 npm 命令安装卸载即可。
- core-js :JavaScript的模块化标准库,使用 ES3 语法兼容高版本语法,确保项目兼容性。
- vue :前端三大框架,快速开发。
- babel-eslint :基于 ESLint 对非 ES 标准的 Babel 代码规范。可以通过 .eslintrc 或 package.json 中的 eslintConfig 字段来配置。
- eslint :协同开发,代码规范,只针对已发布的 ES 标准,可以通过 .eslintrc 或 package.json 中的 eslintConfig 字段来配置。
- esling-plugin-vue :使用 ESLint 检查 .vue 文件以及 .js 文件中 <template> 和 <script> 的代码.
- vue-template-compiler :预编译 vue ,大多时候与 vue-loader 一起用,需要与 vue 版本一致。
项目配置面板可自定义配置各工具参数,比如这里的 Vue CLI 及 ESLint ,当然你也可以通过对应的配置文件进行配置。
任务面板显示当前的任务配置及运行情况:
- serve :开发环境,实时编译和热更新,对应 package.json 中的 “serve”: “vue-cli-service serve”,
- build :生产环境,编译并压缩代码,对应 package.json 中的 “build”: “vue-cli-service build”,
- lint :检查并修复文件中的代码错误,对应 package.json 中的 “lint”: “vue-cli-service lint”,
- inspect :序列化 webpack 配置文件并显示出来,注意它不是真实存在的 webpack.config.js ,需要自定义 webpack 配置需要修改 vue.config.js 中的 configureWebpack 项
vue-cli-service serve [options] [entry] --open 在服务器启动时打开浏览器 --copy 在服务器启动时将 URL 复制到剪切版 --mode 指定环境模式 (默认值:development) --host 指定 host (默认值:0.0.0.0) --port 指定 port (默认值:8080) --https 使用 https (默认值:false)
vue-cli-service build [options] [entry|pattern] --mode 指定环境模式 (默认值:production) --dest 指定输出目录 (默认值:dist) --modern 面向现代浏览器带自动回退地构建应用 --target app | lib | wc | wc-async (默认值:app) --name 库或 Web Components 模式下的名字 (默认值:package.json 中的 "name" 字段或入口文件名) --no-clean 在构建项目之前不清除目标目录 --report 生成 report.html 以帮助分析包内容 --report-json 生成 report.json 以帮助分析包内容 --watch 监听文件变化
vue-cli-service inspect [options] [...paths] --mode 指定环境模式 (默认值:development)
默认通过 vue create 命令创建的项目可以直接跑起来了。更多配置参考>>> vue.config.js
基础
一般项目中会使用 vue-router 管理路由,vuex 管理状态。默认 vue create 时没有安装这两个依赖,所以这里需要安装下。安装完成后查看 main.js 中的代码如下:
import Vue from 'vue' import App from './App.vue' import store from './store' import router from './router' Vue.config.productionTip = false new Vue({ store, router, render: h => h(App) }).$mount('#app')
- store :状态管理,引入的 vuex
- router :路由管理,引入的 vue-router
- render :渲染函数,通过 JavaScript 代码直接渲染 DOM
- $mount :挂载实例,将实例化后的 DOM 对象挂载到 #app 上,等同于 el 配置项
基础的模板包含 <template>、<script>、<style> 三部分。
template
模板结构,这里面的代码渲染后用于前端显示。
命令
- v-text : 解析文本,更新元素的 textContent
- {{ 文本内容 }} :解析文本,更新部分的 textContent
- v-html :解析 HTML 代码,更新元素的 innerHTML
- v-show :切换元素的 display
- v-if :条件渲染 DOM ,可与 v-else , v-else-if 配合使用
- v-for :遍历元素,注意 key
- v-on :绑定事件
- v-bind :绑定属性,可以是自定义属性,注意动态组件切换添加 is
- v-module :双向绑定,仅适用于 input 、 select 、 textarea
- v-pre : 跳过当前元素的编译
- v-once :当前元素只渲染一次
- ref :注册引用信息,通过 this.$refs 调用
内置组件
- slot : 插槽,<slot> 元素自身将被组件标签内部代码替换
- transition :为单个元素/组件添加过渡效果
- transition-group :为多个元素/组件添加过渡效果
- component :渲染一个“元组件”为动态组件。依 is 的值,来决定哪个组件被渲染
- keep-alive :缓存动态组件
script
模板行为,处理数据或动作效果的代码放这里。
- components :注册组件
- props :接收父组件数据
- data :数据对象,用于数据绑定
- methods :函数方法,根据需求处理组件中的数据
- computed :计算属性,计算需要的值
- watch :监听数据变化并自动执行方法
- 生命周期 :beforeCreate、created、beforeMount、mounted、beforeUpdate、updated、actived、deactivated、beforeDestroy、destroyed、errorCaptured
style
模板样式,注意使用 scoped 属性防止样式污染。
更多使用参考 >>> 官网教程
路由管理
vue-router 是 Vue.js 官方的路由管理器。所谓路由,就是点击链接页面导航到哪里去。
基础
script 标签引入
<script src="https://unpkg.com/vue/dist/vue.js"></script> <script src="https://unpkg.com/vue-router/dist/vue-router.js"></script>
npm 包管理工具安装
通过 npm install vue-router 命令安装路由,需要自行配置。
通过 vue add router 安装,程序会自动安装一个 @vue/cli-plugin-router 插件,然后自动下载 vue-router 并自动配置。默认创建的 router 文件夹下生成的路由配置文件如下:
import Vue from 'vue' import VueRouter from 'vue-router' import Home from '../views/Home.vue' Vue.use(VueRouter) const routes = [ { path: '/', name: 'Home', component: Home }, { path: '/about', name: 'About', // route level code-splitting // this generates a separate chunk (about.[hash].js) for this route // which is lazy-loaded when the route is visited. component: () => import(/* webpackChunkName: "about" */ '../views/About.vue') } ] const router = new VueRouter({ routes }) export default router
但是我们一般不这么使用,为了程序更好地拓展及后期维护,一般针对 views 下的每个视窗创建独立的路由文件并分别管理即嵌套路由。
Router 与 View 一一对应
统一入口文件
独立视窗路由
基本参数
路由中可配置的参数有:
- path :字符串,路由的跳转路径
- name :字符串,路由的名称,用于命名路由,通过名称来标识路由
- alias :字符串或数组,路由的别名,将其他路径指向该路由视图
- redirect :字符串,路由重定向,将当前路由地址重定向到一个新的路由地址
- component :路由指向的组件视图
- props:布尔、对象或函数,使用 props 将组件和路由解耦,实现动态路由,并传递一定参数
导航守卫
导航守卫是路由跳转过程中不同时机触发的一些钩子函数。
- beforeEach :全局前置守卫
- beforeResolve :全局解析守卫,与 beforeEach 类似,区别是:在导航被确认前,同时在所有组件内守卫和异步路由组件被解析之后,解析守卫就被调用
- afterEach :全局后置钩子
- beforeEnter :路由独享的守卫
- beforeRouteEnter :组件内的守卫,导航进入该组件的对应路由前调用
- beforeRouteUpdate :组件内的守卫,在该组件的当前路由改变前调用
- beforeRouteLeave :组件内的守卫,导航离开该组件的对应路由前调用
状态管理
对于应用中使用到的公共变量,我们可以通过 vuex 进行状态管理。
import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) export default new Vuex.Store({ state: { }, mutations: { }, actions: { }, modules: { } })
每一个 Vuex 应用的核心就是 store(仓库)。store 基本上就是一个容器,它包含着你的应用中大部分的状态。
基础知识
配置选项
- state :store 的状态属性,类似于 vue 中的 data ,具体组件通过 mapState 引用
- getters :store 的计算属性,类似于 vue 中的 computed ,具体组件通过 mapGetters 引用
- mutations :store 的方法属性,类似于 vue 中的 methods ,具体组件通过 mapMutations 引用,更改 state 的唯一方法是提交 mutation
- actions :store 的方法属性,类似于 mutations ,具体组件通过 mapActions 引用,主要用于异步操作
- modules :用于分割子模块(module)。每个模块拥有自己的 state、mutation、action、getter,方便管理
- namespaced :是否启用命名空间,分模块管理状态时建议启用,避免各模块中变量名冲突,在组件中引用时需带上模块名
- strict :是否启用严格模式,严格模式下,任何 mutation 处理函数以外修改 Vuex state 都会抛出错误
- plugins :一个数组,包含应用在 store 上的插件方法
- devtools :为某个特定的 Vuex 实例打开或关闭 devtools
实例属性
- state :Object,只读,根状态属性
- getters :Object,只读,暴露出注册的 getter
实例方法
- commit :提交 mutation
- dispatch :分发 action
- replaceState :替换 store 的根状态,仅用状态合并或时光旅行调试
- watch :响应式地侦听 fn 的返回值,当值改变时调用回调函数
- subscribe :订阅 store 的 mutation
- subscribeAction :订阅 store 的 action
- registerModule :注册一个动态模块
- unregisterModule :卸载一个动态模块
- hasModule :检查该模块的名字是否已经被注册
- hotUpdate :热替换新的 action 和 mutation
辅助函数
- mapState :将 state 映射为组件的计算属性
- mapGetters :将 getters 映射为组件的计算属性
- mapMutations :将 mutations 映射为组件的方法属性
- mapActions :将 actions 映射为组件的方法属性
- createNamespacedHelpers :创建基于命名空间的组件绑定辅助函数
实际应用
对于公共状态,如果项目比较大,为了方便管理,一般我们也建议分模块单独管理各自的状态,方便后期维护。
分模块管理公共状态
统一入口文件
独立状态管理
服务端渲染(SSR)
对于一个单页面应用(SPA:Single Page Application),服务端渲染(SSR:Server Side Render)相较于客户端渲染(CSR:Client Side Render)具有更快的首屏加载及更好的 SEO 等优势,但同时也伴随着开发条件限制、服务器压力增大等弊端。Vue.js 基于 node.js 可以通过 vue-server-renderer 实现服务端渲染的效果。因为即使不用服务端渲染也不影响前端显示效果,所以本文档暂不对 SSR 做深入探讨。
过渡/动画效果
在 CSS 中过渡(transition)与动画(animation)是有一定的差别的。过渡可以看作一些简单的动画,但只能设置起始关键帧间的动画效果,而动画则可以通过 @keyframe 定义关键帧逐帧设置动画效果;另外,过渡需要事件触发,而动画不需要。
Vue 中通过 transition 组件配置实现过渡和动画效果。
基础知识
transition 内置组件可定义组件/元素切换时的过渡效果,在进入/离开的过渡中,会有 6 个 class 切换:
- v-enter:定义进入过渡的开始状态
- v-enter-active:定义进入过渡生效时的状态
- v-enter-to:定义进入过渡的结束状态
- v-leave:定义离开过渡的开始状态
- v-leave-active:定义离开过渡生效时的状态
- v-leave-to:定义离开过渡的结束状态
注意:Vue 过渡效果是在使用 v-if 或 v-show 指令控制 DOM 元素显示隐藏时触发的。
transition 标签
使用 transition 标签包裹需要显示隐藏的元素,在其内部的内容根据设置可以实现过渡效果。
<!-- diyName:命名 transition,如果未命名则其过渡类名以 v- 开头,命名后以 diyName- 开头 toggleCondition:是用来自定义显示隐藏的判断条件 --> <transition name="diyName"> <div class="content" v-if="toggleCondition"> <h3>显示隐藏的内容</h3> </div> </transition>
transition 配置项
上面代码中用到的 name 属性是最常用的配置项,主要用来避免在同一组件中避免各元素切换效果实现的冲突。包含 name 属性外的配置项还有:
- name:命名 transition,如果未命名则其过渡类名以 v- 开头,命名后以 diyName- 开头
- duration:自定义进入和移出的持续时间
- mode:过渡模式,out-in 或 in-out
- in-out:新元素先进行过渡,完成之后当前元素过渡离开。
- out-in:当前元素先进行过渡,完成之后新元素过渡进入。
- appear:设置节点在初始渲染的过渡
- enter-class:自定义进入前的过渡类名
- enter-active-class:自定义进入时的过渡类名
- enter-to-class:自定义进入后的过渡类名
- leave-class:自定义离开前的过渡类名
- leave-active-class:自定义离开时的过渡类名
- leave-to-class:自定义离开后的过渡类名
原生 CSS 过渡
/** * CSS 过渡效果 */ .diyName-enter{ // 进入过渡的开始状态 opacity: 0; } .diyName-enter-active{ // 进入过渡生效时的状态 transition: all 3s ease; } .diyName-enter-to{ // 进入过渡的结束状态 opacity: 1; } .diyName-leave{ // 离开过渡的开始状态 opacity: 1; } .diyName-leave-active{ // 离开过渡生效时的状态 transition: all 3s ease; } .diyName-leave-to{ // 离开过渡的结束状态 opacity: 0; }
diyName 是自定义的 transition 名称
原生 CSS 动画
/** * CSS 动画效果 */ @keyframes cssAnimationName { // 自定义动画名 0% { transform: scale(0); } 50% { transform: scale(1.5); } 100% { transform: scale(1); } } .cssAnimation-enter{ // 进入动画的开始状态 transform: scale(0); } .cssAnimation-enter-active{ // 进入动画生效时的状态 animation: cssAnimationName 1s; } .cssAnimation-enter-to{ // 进入动画的结束状态 transform: scale(1); } .cssAnimation-leave{ // 离开动画的开始状态 transform: scale(1); } .cssAnimation-leave-active{ // 离开动画生效时的状态 animation: cssAnimationName 1s reverse; } .cssAnimation-leave-to{ // 离开动画的结束状态 transform: scale(0); }
cssAnimation 是自定义的 transition 名称,cssAnimationName 是自定义的动画名
第三方 CSS 库实现动画特效
可以设置 enter-class、enter-active-class、enter-to-class、leave-class、leave-active-class、leave-to-class 的类名为第三方 CSS 动画库(如 Animate.css)中的类名,以应用对应的动画效果。
<!-- 第三方 CSS 库实现动画特效 --> <div class="item"> <h3>第三方 CSS 库实现动画特效</h3> <button @click="cssThirdPartyShow=!cssThirdPartyShow">第三方 CSS 库动画</button> <div class="box"> <transition enter-active-class="animate__animated animate__zoomIn" leave-active-class="animate__animated animate__zoomOut"> <div v-show="cssThirdPartyShow"></div> </transition> </div> </div>
JavaScript 钩子
除了使用 CSS 实现过渡及动画效果,还可以使用 JavaScript 实现更丰富的效果,可用的钩子有:
- before-enter:进入前,
function (el) {}
- enter:进入时,
function (el, done) {}
- after-enter:进入后,
function (el) {}
- enter-cancelled:进入被打断时,
function (el) {}
- before-leave:离开前,
function (el) {}
- leave:离开时,
function (el, done) {}
- after-leave:离开后,
function (el) {}
- leave-cancelled:离开被打断时,
function (el) {}
当只用 JavaScript 过渡的时候,在 enter 和 leave 中必须使用 done 进行回调。否则,它们将被同步调用,过渡会立即完成。
第三方 JS 库实现动画特效
可以是会用 JavaScript 自定义一些动画效果,也可以引入一些第三方 JS 库实现动画效果,比如这里引入 anime.js 实现动画效果。
HTML 结构
// transition 中绑定 enter、leave 方法 <transition v-on:enter="enter" v-on:leave="leave"> <div v-show="jsThirdPartyShow"></div> </transition>
Script 内容
// 引入 anime.js,直接对获取的 el 对象使用动画效果即可 enter(el){ anime({ targets: el, rotate: { value: 360, duration: 1000, easing: 'easeInOutSine' }, }); }, leave(el, done){ anime({ targets: el, rotate: '2turn', }); setTimeout(done, 1000); // 当只用 JavaScript 过渡的时候,在 enter 和 leave 中必须使用 done 进行回调。否则,它们将被同步调用,过渡会立即完成。 },
列表过渡(transition-group)
transition 只能对他所包裹的单个 DOM 对象实现过渡动画效果,如果要对一个 DOM 列表中的每个元素都应用过渡动画效果则需要使用 transition-group 组件。
组件间传值
父向子传值(props) & 子向父传值($emit)
项目中组件间传值一般会通过 store 进行状态管理,但是封装一些基础组件的时候可能需要使用 vue 原生的 props 与 $emit 实现组件间传值。
父组件相关代码(PassData.vue)
<template> <div class="pass-data-container"> <div class="item"> <h3>父向子传值(props) & 子向父传值($emit) </h3> <div class="box"> <PassDataChild :passDataByProps="data" @passDataByEmit="getDataByEmit"/> <p>$emit 接收子组件 PassDataChild.vue 中的数据:<span>{{ dataFromChildByEmit }}</span></p> </div> </div> </div> </template> <script> import PassDataChild from './PassDataChild.vue' export default { components:{ PassDataChild }, data(){ return{ data: 'PassData.vue 组件中的数据', dataFromChildByEmit: '', otherParams: "其他 PassData.vue 传递给 PassDataChild.vue 的参数" } }, methods:{ getDataByEmit(childData){ // 默认的第一个参数为子组件传递过来的值 this.dataFromChildByEmit = childData } } } </script> <style lang="scss" scoped> .pass-data-container{ margin: 10px auto; .item{ span{ color: red; } } } </style>
子组件相关代码(PassDataChild.vue)
<template> <div class="pass-data-child-container" > <div class="item"> <p>props 接收父组件 PassData.vue 组件中的参数:<span>{{ passDataByProps }}</span></p> <button @click="sentDataByEmit()">PassDataChild.vue 组件中发送子组件数据</button> </div> </div> </template> <script> export default { props:{ passDataByProps: { type: String, required: false } }, data(){ return{ data: 'PassDataChild.vue 组件中的数据', } }, methods:{ sentDataByEmit(){ this.$emit('passDataByEmit', this.data) } } } </script> <style lang="scss" scoped> .pass-data-child-container{ margin: 10px auto; .item{ span{ color: red; } } button{ margin: 5px 10px 5px 0; background: #e9e9e9; border: 1px solid #999; border-radius: 3px; &:hover{ background: #f3f3f3; } } } </style>
provide / inject
props 与 $emit 一般用于父子组件间数据的通信,如果想要允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在其上下游关系成立的时间里始终生效,那么可能需要使用 provide / inject,注意:这俩选项需要一起使用。
- provide:提供其他组件要用的参数。
Object | () => Object
- inject:接收其他组件提供的参数。
Array | { [key: string]: string | Symbol | Object }
提供参数
provide(){ // 提供参数 return{ 'passDataByProvideAndInject' : 'PassData.vue 组件中的数据(组件间传值#(provide&inject))', } },
接收参数
inject:{ //inject后面可以是一个对象 passDataByProvideAndInject : { default : '默认值' //指定默认值 } },
注意:provide 与 inject 均写到 export default {} 中即可。
配置文件 vue.config.js
类似于 webpack.config.js ,如果你在项目中使用了 vue-cli ,那么就需要对 vue.config.js 进行配置。
- publicPath:部署应用包时的基本 URL,与 webpack 的 output.publicPath 一致
- outputDir:当运行 vue-cli-service build 时生成的生产环境构建文件的目录,对应 webpack 的 output.path
- assetsDir:放置生成的静态资源 (js、css、img、fonts) 的 (相对于 outputDir 的) 目录
- indexPath:指定生成的 index.html 的输出路径 (相对于 outputDir),也可以是一个绝对路径
- filenameHashing:生成的静态资源文件名是否包含 hash 值
- pages: 在 multi-page 模式下构建应用。每个 page 对应一个入口文件
- lintOnSave:是否在开发环境下通过 eslint-loader 在每次保存时 lint 代码
- runtimeCompiler:是否使用包含运行时编译器的 Vue 构建版本
- transpileDependencies:默认 babel-loader 会忽略所有 node_modules 中的文件。需要显式转译的可以在这个选项中列出来
- productionSourceMap:是否需要生产环境的 source map
- crossorigin:设置生成的 HTML 中 link 和 script 标签的 crossorigin 属性
- integrity:是否在生成的 HTML 中的 link 和 script 标签上启用子资源完整性(SRI)验证
- configureWebpack:如果是对象,则会通过 webpack-merge 合并到最终的配置中;如果是函数,则会接收被解析的配置作为参数
- chainWebpack:是一个函数,接收基于 webpack-chain 的 ChainableConfig 实例。允许对内部的 webpack 配置进行更细粒度的修改
- css:CSS 的相关配置
- css.requireModuleExtension:是否只有 *.module.[ext] 结尾的文件才会被视作 CSS Modules 模块
- css.extract:是否将组件中的 CSS 提取至一个独立的 CSS 文件中
- css.sourceMap:是否为 CSS 开启 source map
- css.loaderOptions:向 CSS 相关的 loader 传递选项,支持:css、postcss、sass、less、stylus
- devServer:对应 webpack-dev-server 的配置
- parallel:是否为 Babel 或 TypeScript 使用 thread-loader
- pwa:向 PWA 插件传递选项
- pluginOptions:这是一个不进行任何 schema 验证的对象,因此它可以用来传递任何第三方插件选项