vue官方文档解读及拓展
参考:
https://cn.vuejs.org/guide/introduction.html
https://blog.csdn.net/weixin_42371679/article/details/112408800
vue是一个js库,它基于标准html、css和js,并提供了一套声明式的、组件化的编程模型,可高效的开发用户界面。
简单应用举例
我们只需引入vue.min.js就可以使用vue。在这个例子中,我们引入vue.min.js,并使用vue来操作dom
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>vue</title>
<!-- 当我们引入这个js之后,浏览器内存中就多了一个Vue构造函数 --> <script src="https://cdn.staticfile.org/vue/2.2.2/vue.min.js"></script> </head> <body> <div id="app"> <p>{{ message }}</p> //使用{{}}语法,Vue实例中data下面的属性可以直接引用 </div> <script>
//创建一个Vue实例 new Vue({ el: '#app',//挂载点 data: {//data属性中存放el中要用到的数据 message: 'Hello Vue.js!' //通过Vue提供的指令渲染message到页面 } }) </script> </body> </html>
这里,我们创建了一个vue实例用于挂载这个dom。运行结果如下
一个疑问!!!既然有了vue.min.js,为啥平时开发中更多的使用import Vue from 'vue’来引入Vue,而不直接用vue.min.js呢?
在开发Vue.js应用程序时,通常会使用模块化的方式来组织代码。这种方式允许开发者通过import和export语句来导入和导出各个模块。使用import Vue from 'vue'来引入Vue是这种模块化开发方式的一部分。
以下是使用模块化导入Vue的几个原因:
- 代码分割和懒加载:当使用模块化系统和打包工具(如Webpack)时,可以利用代码分割(code splitting)和懒加载(lazy loading)的特性,这样可以按需加载应用的不同部分,从而减少应用的初始加载时间。
- 开发环境与生产环境:在开发环境中,通常使用未压缩的Vue版本,以便于调试和开发。而在生产环境中,会使用压缩后的版本(如vue.min.js)以减少文件大小。通过配置打包工具,可以在构建过程中自动切换这两个版本。
- 模块化的依赖管理:使用import语句可以更好地管理项目的依赖关系。打包工具可以分析这些依赖,并且只打包那些实际用到的Vue功能和组件,这有助于减少最终构建文件的大小。
- 更好的工具支持:模块化开发可以让开发者享受到现代JavaScript工具链提供的各种优势,比如热模块替换(HMR)、linting、类型检查等。
- ES模块的树摇(Tree Shaking):现代打包工具支持ES模块的树摇优化,这意味着未使用的代码可以在最终的构建中被排除掉,从而减少文件体积。
直接在HTML文件中通过<script>标签引入vue.min.js是一种简单快速的方式,适合于小型项目或是快速原型开发。但对于大型项目和生产环境,模块化开发提供了更多的灵活性和优化的可能性。
1 一些概念
1.1 单文件组件SFC
在vue中,我们可以创建一个.vue文件来创建一个组件,称为单文件组件。它的格式类似html格式,并且把js、html、css封装在同一个文件里,如下是一个例子
demo.vue
<script>
export default {
data() {
return {
count: 0
}
}
}
</script>
<template>
<button @click="count++">Count is: {{ count }}</button>
</template>
<style scoped>
button {
font-weight: bold;
}
</style>
需要注意的是,我们必须使用构建工具(webpack或者vue-cli-service)来对.vue文件进行处理,也就是说我们编写的.vue组件,经过webpack进行构建之后,会生成原生的js代码,才能够被浏览器执行。浏览器默认不认识.vue方式定义的组件。当然,我们可以不使用.vue和webpack来定义组件,方式是使用Vue.component()方法,详情见16.1.2小节
1.2 选项式API和组合式API
vue组件可以按照两种不同的风格来写:选项式 API 和组合式 API
选项式API是一个对象,该对象包含了data、methods、mounted等属性,这些属性都会暴露在函数内部的this上,这个this指向当前组件实例。
<script>
export default {
// data() 返回的属性将会成为响应式的状态
// 并且暴露在 `this` 上
data() {
return {
count: 0
}
},
// methods 是一些用来更改状态与触发更新的函数
// 它们可以在模板中作为事件监听器绑定
methods: {
increment() {
this.count++
}
},
// 生命周期钩子会在组件生命周期的各个不同阶段被调用
// 例如这个函数就会在组件挂载完成后被调用
mounted() {
console.log(`The initial count is ${this.count}.`)
}
}
</script>
<template>
<button @click="increment">Count is: {{ count }}</button>
</template>
组合式API
<script setup>
import { ref, onMounted } from 'vue'
// 响应式状态
const count = ref(0)
// 用来修改状态、触发更新的函数
function increment() {
count.value++
}
// 生命周期钩子
onMounted(() => {
console.log(`The initial count is ${count.value}.`)
})
</script>
<template>
<button @click="increment">Count is: {{ count }}</button>
</template>
2 使用vue脚手架创建vue项目
D:\2022\front\vue>npm init vue@latest
Need to install the following packages:
create-vue@latest
Ok to proceed? (y) y
Vue.js - The Progressive JavaScript Framework
√ Project name: ... vue-project
√ Add TypeScript? ... No / Yes
√ Add JSX Support? ... No / Yes
√ Add Vue Router for Single Page Application development? ... No / Yes
√ Add Pinia for state management? ... No / Yes
√ Add Vitest for Unit Testing? ... No / Yes
√ Add Cypress for both Unit and End-to-End testing? ... No / Yes
√ Add ESLint for code quality? ... No / Yes
Scaffolding project in D:\2022\front\vue\vue-project...
Done. Now run:
cd vue-project
npm install
npm run dev
D:\2022\front\vue>
安装依赖并启动项目
> cd vue-project
> npm install
> npm run dev
3 html中直接使用ES模块导入和使用vue
现在绝大多数浏览器都支持ES,下面的例子,我们使用了<script type="module">,并且导入vue
<div id="app">{{ message }}</div>
<script type="module">
import { createApp } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js'
createApp({
data() {
return {
message: 'Hello Vue!'
}
}
}).mount('#app')
</script>
这个例子,我们不需要使用构建工具(比如webpack)构建,直接在浏览器中打开即可
4 import maps
上面例子中,我们使用url导入vue,其实我们可以指定vue的多个源,通过<script type="importmap">设定
<script type="importmap">
{
"imports": {
"vue": "https://unpkg.com/vue@3/dist/vue.esm-browser.js"
}
}
</script>
<div id="app">{{ message }}</div>
<script type="module">
import { createApp } from 'vue'
createApp({
data() {
return {
message: 'Hello Vue!'
}
}
}).mount('#app')
</script>
5 根组件
每个应用都需要一个根组件,其他组件作为子组件
import { createApp } from 'vue'
// 从一个单文件组件中导入根组件
import App from './App.vue'
const app = createApp(App)
比如上例,根组件是App.vue,然后我们将这个组件作为createApp()的参数,createApp()作用是创建应用实例。
6 挂载
模板
<div id="app"></div>
js
app.mount('#app')
我们应该这么理解,我们创建了一个应用实例app,然后我们有一个html中的挂载点,我们将这个应用实例挂载到html的挂载点中。其中这个挂载点称为挂载容器。
当在未采用构建流程中使用vue时,可以在挂载容器中直接书写根组件模板
容器
<div id="app"> <button @click="count++">{{ count }}</button> </div>
js
import { createApp } from 'vue'
const app = createApp({
data() {
return {
count: 0
}
}
})
app.mount('#app')
当根组件没有template选项时,Vue 将自动使用容器的 innerHTML 作为模板
7 模板语法
vue使用一种基于html的模板语法,使我们能够声明式的将vue组件实例的数据绑定到dom树上。而且所有的vue模板可以看做合法的html文档,即使没有vue引擎,浏览器也能够正常解析它,只不过其中的{{}}绑定的数据不能够被替换罢了。
在底层,vue会将模板编译为优化了的js代码。结合响应式系统,当vue应用状态发生变更时,vue将会重新渲染这些dom。
7.1 模板中的数据绑定形式
文本插值
<span>Message: {{ msg }}</span>
文本插值是最基本的数据绑定形式,其语法为{{}},双大括号标签会被替换为组件实例中 msg 属性的值。同时每次模板中 msg 属性更改时它也会同步更新
文本插值和使用v-text能够达到相同的效果,但是如果有字符串拼接的话,v-text指令不满足要求,需要使用文本插值。此外,v-text没有闪烁问题,文本插值在网速慢的情况下会有闪烁问题。
原始html
如果我们想向模板中插入原始html标签(比如<span style="color: red">This should be red.</span>),需要使用v-html属性
<p>Using text interpolation: {{ rawHtml }}</p>//错误
<p>Using v-html directive: <span v-html="rawHtml"></span></p>//正确
这里我们做的事情简单来说就是:在当前组件实例上,将此元素的 innerHTML 与 vue组件实例的rawHtml 属性保持同步。span 的内容将会被替换为 rawHtml 属性的值,插值为纯 HTML——数据绑定将会被忽略
属性绑定
如果需要绑定模板中html标签的属性,需要如下操作
<div v-bind:id="dynamicId"></div>
上例中,将div元素的id属性和vue组件实例的dynamicId属性绑定在一起,如果vue组件实例的dynamicId是null或undefined,那么div元素的id属性将会被移除
vue中,将dynamicId看做一个js表达式执行,所以我们还可以这样写<div v-bind:id="dynamicId+'123'"></div>,来做一些灵活的变化。
因为v-bind非常常用,所以简写形式如下
<div :id="dynamicId"></div>
如果vue组件实例中的属性包含多个值,例如如下
data() {
return {
objectOfAttrs: {
id: 'container',
class: 'wrapper'
}
}
}
通过不带参数的 v-bind,你可以将它们绑定到模板的单个元素上
<div v-bind="objectOfAttrs"></div>
双向属性绑定
v-model是唯一一个可以进行双向属性绑定的指令。注意:v-model只能应用到表单元素中
使用js表达式
表达式都会以组件为作用域解析执行。
{{ number + 1 }}
{{ ok ? 'YES' : 'NO' }}
{{ message.split('').reverse().join('') }}
<div :id="`list-${id}`"></div>
在vue中javascript表达式可以被用于如下场景:
在文本插值中放到{{}}内部
在vue指令(v-开头的特殊属性)的属性值中
7.2 vue指令
vue指令是以v-开头的特殊属性。
指令的值必须是一个javascript表达式(除少数几个例外)。
一个指令的作用是在其表达式的值发生变化时,响应式的更新dom。
<p v-if="seen">Now you see me</p>
这里v-if指令会基于seen表达式的值来移除/插入该<p>元素。
指令参数
<a v-on:click="doSomething"> ... </a> <!-- 简写 --> <a @click="doSomething"> ... </a>
上例中,指令v-on的参数是click,参数值是doSomething。
动态参数
<a v-bind:[attributeName]="url"> ... </a> <!-- 简写 --> <a :[attributeName]="url"> ... </a>
在指令的参数上可以使用一个javascript表达式,这个表达式放到[]中,称为动态参数。上例中attributeName会作为一个javascript表达式被动态的执行,计算得到的值会被用作最终的参数。举例来说,如果attribteName表达式的值是href,则这个绑定就等价于v-bind:href
相似的还可以将一个函数绑定到动态的事件名称上
<a v-on:[eventName]="doSomething"> ... </a> <!-- 简写 --> <a @[eventName]="doSomething">
如果eventName表达式的值是focus,就等价于v-on:focus
修饰符
这里的修饰符指的是指令参数的修饰符
<form @submit.prevent="onSubmit">...</form>
上例中,.prevent就是修饰符,这个修饰符会告知v-on指令对触发的事件调用e.preventDefault()
8 响应式
8.1 状态
我们以选项式API为例
用 data 选项来声明组件的响应式状态。此选项的值应为返回一个对象的函数。Vue 将在创建新组件实例的时候调用此函数,并将函数返回的对象用响应式系统进行包装。此对象的所有顶层属性都会被代理到组件实例 (即方法和生命周期钩子中的 this) 上。(我们可以在浏览器console窗口执行window.vue查看,看看我们定义的vue实例下是否有data中的属性)
export default {
data() {
return {
count: 1
}
},
// `mounted` 是生命周期钩子,之后我们会讲到
mounted() {
// `this` 指向当前组件实例
console.log(this.count) // => 1
// 数据属性也可以被更改
this.count = 2
}
}
组件实例上的属性仅在实例首次创建时被添加,因此你需要确保它们都出现在 data 函数返回的对象上,如果值暂时未准备好,必要时可以使用null、undefined等占位。
8.2 方法
要为组件添加方法,我们需要用到 methods 选项。它应该是一个包含所有方法的对象
export default {
data() {
return {
count: 0
}
},
methods: {
increment() {
this.count++
}
},
mounted() {
// 在其他方法或是生命周期中也可以调用方法
this.increment()
}
}
vue自动为methods中的方法绑定了永远指向组件实例的this。需要注意的是,methods中的方法不能使用箭头函数,因为箭头函数没有自己的this上下文
export default {
methods: {
increment: () => {
// 反例:无法访问此处的 `this`!
}
}
}
模板上访问方法
<button @click="increment">{{ count }}</button>
8.3 DOM更新时机
当我们修改了响应式状态之后,dom会自动更新,但是,vue更新dom有自己的节奏,按照固定的时间周期更新。如果我们想访问更新后的dom,需要使用如下方式
import { nextTick } from 'vue'
export default {
methods: {
increment() {
this.count++
nextTick(() => {
// 访问更新后的 DOM
})
}
}
}
8.4 computed
export default {
data() {
return {
author: {
name: 'John Doe',
books: [
'Vue 2 - Advanced Guide',
'Vue 3 - Basic Guide',
'Vue 4 - The Mystery'
]
}
}
},
computed: {
// 一个计算属性的 getter
publishedBooksMessage() {
// `this` 指向当前组件实例
return this.author.books.length > 0 ? 'Yes' : 'No'
}
}
}
模板中的应用
<p>Has published books:</p> <span>{{ publishedBooksMessage }}</span>
在模板中使用计算属性的方式和使用一般属性的方式并无二致。Vue 会检测到 this.publishedBooksMessage 依赖于 this.author.books,所以当 this.author.books 改变时,任何依赖于 this.publishedBooksMessage 的绑定都将同时更新
computed属性缓存和方法比较
上例中,我们如果定义一个方法,同样可以达到一样的效果
// 组件中
methods: {
calculateBooksMessage() {
return this.author.books.length > 0 ? 'Yes' : 'No'
}
}
模板中调用方法
<p>{{ calculateBooksMessage() }}</p>
但是methods和computed是有区别的。computed属性是当状态属性发生变化时,触发computed属性的计算,然后缓存,如果状态属性没有变化,则会一直使用这个缓存。而methods会每次都会被调用
可写computed属性
computed属性默认是只读的。我们可以通过提供setter来使其可写。
export default {
data() {
return {
firstName: 'John',
lastName: 'Doe'
}
},
computed: {
fullName: {
// getter
get() {
return this.firstName + ' ' + this.lastName
},
// setter
set(newValue) {
// 注意:我们这里使用的是解构赋值语法
[this.firstName, this.lastName] = newValue.split(' ')
}
}
}
}
现在当你再运行 this.fullName = 'John Doe' 时,setter 会被调用而 this.firstName 和 this.lastName 会随之更新
8.5 绑定class
<div :class="{ active: isActive }"></div>
如果isActive=true的话,渲染结果是
<div :class=‘active’></div>
我们也可以在一个class上绑定多个值
data() {
return {
isActive: true,
hasError: false
}
}
配合如下模板
<div class="static" :class="{ active: isActive, 'text-danger': hasError }" ></div>
渲染结果为
<div class="static active"></div>
另外,一种更简便的方式是我们可以直接绑定一个对象
data() {
return {
classObject: {
active: true,
'text-danger': false
}
}
}
模板
<div :class="classObject"></div>
绑定数组
data() {
return {
activeClass: 'active',
errorClass: 'text-danger'
}
}
模板
<div :class="[activeClass, errorClass]"></div>
渲染结果
<div class="active text-danger"></div>
8.6 绑定内联样式
:style 支持绑定 JavaScript 对象值,对应的是 HTML 元素的 style 属性
data() {
return {
activeColor: 'red',
fontSize: 30
}
}
对应的模板
<div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
8.7 条件渲染v-if
v-if指令用于条件性的渲染一块内容。
<h1 v-if="awesome">Vue is awesome!</h1>
另外还有v-else和v-else-if
<button @click="awesome = !awesome">Toggle</button> <h1 v-if="awesome">Vue is awesome!</h1> <h1 v-else>Oh no 😢</h1>
一个 v-else 元素必须跟在一个 v-if 或者 v-else-if 元素后面,否则它将不会被识别
<template> 上的 v-if
如果我们想要切换不止一个元素,在这种情况下我们可以在一个 <template> 元素上使用 v-if,这只是一个不可见的包装器元素,最后渲染的结果并不会包含这个 <template> 元素
<template v-if="ok"> <h1>Title</h1> <p>Paragraph 1</p> <p>Paragraph 2</p> </template>
8.8 v-show
v-show和v-if效果差不多,都是根据一个布尔值决定元素的显式与否,但是他们是有区别的,v-if为false时,会移除dom元素,v-show为false时不会移除dom元素,只是隐藏该dom元素
v-if有较高的性能消耗,v-show有较高的初始性能消耗。如果元素频繁的进行显式和隐藏的切换,建议使用v-show。
8.8 v-for
data() {
return {
items: [{ message: 'Foo' }, { message: 'Bar' }]
}
}
模板中的使用
<li v-for="item in items">
{{ item.message }}
</li>
v-for 也支持使用可选的第二个参数表示当前项的位置索引
<li v-for="(item, index) in items">
{{ index }} - {{ item.message }}
</li>
可以在定义 v-for 的变量别名时使用解构
<li v-for="{ message } in items">
{{ message }}
</li>
<!-- 有 index 索引时 -->
<li v-for="({ message }, index) in items">
{{ message }} {{ index }}
</li>
也可以这样使用:item of items
<div v-for="item of items"></div>
也可以使用 v-for 来遍历一个对象的所有属性。遍历的顺序会基于对该对象调用 Object.keys() 的返回值来决定
data() {
return {
myObject: {
title: 'How to do lists in Vue',
author: 'Jane Doe',
publishedAt: '2016-04-10'
}
}
}
模板
<ul>
<li v-for="value in myObject">
{{ value }}
</li>
</ul>
可以通过提供第二个参数表示属性名 (例如 key)
<li v-for="(value, key) in myObject">
{{ key }}: {{ value }}
</li>
第三个参数表示位置索引
<li v-for="(value, key, index) in myObject">
{{ index }}. {{ key }}: {{ value }}
</li>
可以在v-for中使用范围值
<span v-for="n in 10">{{ n }}</span>
在该例中,v-for 直接接受一个整数值。Vue会将该模板基于1…10循环10次。
在<template> 上使用 v-for
<ul> <template v-for="item in items"> <li>{{ item.msg }}</li> <li class="divider" role="presentation"></li> </template> </ul>
通过key管理状态
Vue 默认按照“就地更新”的策略来更新通过 v-for 渲染的元素列表。当数据项的顺序改变时,Vue 不会随之移动 DOM 元素的顺序,而是就地更新每个元素,确保它们在原本指定的索引位置上渲染
默认模式是高效的,但只适用于列表渲染输出的结果不依赖子组件状态或者临时 DOM 状态 (例如表单输入值) 的情况
有时候,我们希望dom顺序进行重排,而不是按照默认顺序进行渲染。此时我们可以为每个元素对应的块提供一个唯一的 key
<div v-for="item in items" :key="item.id"> <!-- 内容 --> </div>
当你使用 <template v-for> 时,key 应该被放置在这个 <template> 容器上
<template v-for="todo in todos" :key="todo.name"> <li>{{ todo.name }}</li> </template>
9 事件处理
使用 v-on 指令 (简写为 @) 来监听 DOM 事件,并在事件触发时执行对应的 JavaScript。用法:v-on:click="methodName" 或 @click="handler"
本质上,methodName或者handler是一个js表达式,而js表达式是要有返回值的,所以,我们可以使用内联事件处理。
9.1 内联事件处理
data() {
return {
count: 0
}
}
模板
<button @click="count++">Add 1</button> <p>Count is: {{ count }}</p>
9.2 方法事件处理
data() {
return {
name: 'Vue.js'
}
},
methods: {
greet(event) {
// 方法中的 `this` 指向当前活跃的组件实例
alert(`Hello ${this.name}!`)
// `event` 是 DOM 原生事件
if (event) {
alert(event.target.tagName)
}
}
}
模板
<!-- `greet` 是上面定义过的方法名 --> <button @click="greet">Greet</button>
9.3 在内联事件处理中访问事件参数
有时我们需要在内联事件处理器中访问原生 DOM 事件。你可以向该处理器方法传入一个特殊的 $event 变量,或者使用内联箭头函数
<!-- 使用特殊的 $event 变量 -->
<button @click="warn('Form cannot be submitted yet.', $event)">
Submit
</button>
<!-- 使用内联箭头函数 -->
<button @click="(event) => warn('Form cannot be submitted yet.', event)">
Submit
</button>
9.4 事件修饰符
.stop
.stop这个修饰符可以阻止事件的向上冒泡传递。
<div @click="divHandler"> <input type="button" value="按钮" @click.stop="btnHandler" /> </div>
这个例子,点击事件将只作用于按钮,不会向上冒泡传递到div。
.prevent
这个修饰符作用是阻止默认事件
<a href="www.baidu.com" @click.prevent="linkClick">百度一下</a>
浏览器赋予了a链接的默认行为是跳转到href指定的链接,有时我们不希望浏览器默认行为,可以使用.prevent修饰符
.capture
这个修饰符作用是实现捕获触发事件机制。该机制和事件冒泡机制是反方向的。
<div @click.capture="divHandler"> <input type="button" value="按钮" @click="btnHandler" /> </div>
这个例子中,外层的div将会首先接收到事件,然后button才接收到事件。
.self
这个修饰符作用是,屏蔽掉冒泡事件,只有点击自身才会触发事件
<div @click.self="divHandler"> <input type="button" value="按钮" @click="btnHandler" /> </div>
这个例子中,当我们点击按钮,事件将不会传递到div。只有我们点击div时事件才会触发。
.once
事件只会触发一次
10 表单输入绑定
11 生命周期
vue组件实例是有生命周期的,组件实例从创建到销毁,会经历设置数据侦听、编译模板、挂载等过程。在数据改变的时候会更新dom。
vue为我们定义了很多生命周期钩子,让开发者能够在特定阶段运行自己的代码。
那么如何注册一个生命周期钩子呢?我们以mounted钩子为例说明
export default {
mounted() {
console.log(`the component is now mounted.`)
}
}
所有生命周期钩子函数的this都会自动指向当前vue组件实例。
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>vue</title> <script src="https://cdn.staticfile.org/vue/2.2.2/vue.min.js"></script> </head> <body> <div id="app"> <h3 id='h3'>{{ msg }}</h3> </div> <script> new Vue({ el: '#app', data: { msg: 'ok' }, methods: { show() { console.log('执行了show方法') } }, beforeCreate() { console.log('beforeCreate', 'Vue实例创建之前的生命周期钩子') console.log('beforeCreate', this.msg) // 打印undefined console.log('beforeCreate',this.show) // 打印undefined }, created() { console.log('created','Vue实例创建之后的生命周期钩子') console.log('created',this.msg) console.log('created',this.show) }, beforeMount() { console.log('beforeMount','templete模板已经在内存中编辑完成,但是尚未把模板渲染到页面') console.log('beforeMount',document.getElementById('h3').innerText) }, mounted() { console.log('mounted','将内存中编辑好的模板替换到浏览器的真实dom中去后,调用此钩子') console.log('mounted',document.getElementById('h3').innerText) }, beforeUpdate() { }, updated() { } }) </script> </body> </html>
12 侦听器
在选项式 API 中,我们可以使用 watch 选项在每次响应式属性发生变化时触发一个函数
export default {
data() {
return {
question: '',
answer: 'Questions usually contain a question mark. ;-)'
}
},
watch: {
// 每当 question 改变时,这个函数就会执行
question(newQuestion, oldQuestion) {
if (newQuestion.includes('?')) {
this.getAnswer()
}
}
},
methods: {
async getAnswer() {
this.answer = 'Thinking...'
try {
const res = await fetch('https://yesno.wtf/api')
this.answer = (await res.json()).answer
} catch (error) {
this.answer = 'Error! Could not reach the API. ' + error
}
}
}
}
模板
<p>
Ask a yes/no question:
<input v-model="question" />
</p>
<p>{{ answer }}</p>
即时回调的侦听器
export default {
// ...
watch: {
question: {
handler(newQuestion) {
// 在组件实例创建时会立即调用
},
// 强制立即执行回调
immediate: true
}
}
// ...
}
回调的触发时机
当你更改了响应式状态,它可能会同时触发 Vue 组件更新和侦听器回调。
默认情况下,用户创建的侦听器回调,都会在 Vue 组件更新之前被调用。这意味着你在侦听器回调中访问的 DOM 将是被 Vue 更新之前的状态。
如果想在侦听器回调中能访问被 Vue 更新之后的DOM,你需要指明 flush: 'post' 选项
export default {
// ...
watch: {
key: {
handler() {},
flush: 'post'
}
}
}
this.$watch()
我们也可以使用组件实例的 $watch() 方法来命令式地创建一个侦听器
export default {
created() {
this.$watch('question', (newQuestion) => {
// ...
})
}
}
13 过滤器filter
13.1 全局过滤器
定义
Vue.filter('过滤器的名字', function(data, ){}) //data是过滤器管道符前面的
过滤器的调用方式
{{ name | 过滤器的名字 }}
例子:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>vue</title> <script src="https://cdn.staticfile.org/vue/2.2.2/vue.min.js"></script> </head> <body> <div id="app"> <p>{{ msg | msgFormat('邪恶','疯狂') | secondFormat }}</p> </div> <script> Vue.filter('msgFormat', function(msg, arg1, arg2){ return msg.replace(/单纯/g, arg1+arg2) }) Vue.filter('secondFormat', function(msg){ return msg+'========' }) new Vue({ el: '#app', data: { msg: '曾经,我也是一个单纯的少年,单纯的我,傻傻的问,谁是世界上最单纯的男人' } }) </script> </body> </html>
如上例所示,过滤器可以通过管道符进行多次使用
13.2 私有过滤器
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>vue</title> <script src="https://cdn.staticfile.org/vue/2.2.2/vue.min.js"></script> </head> <body> <div id="app"> <p>{{ msg | msgFormat('邪恶','疯狂') }}</p> </div> <script> new Vue({ el: '#app', data: { msg: '曾经,我也是一个单纯的少年,单纯的我,傻傻的问,谁是世界上最单纯的男人' }, filters: { msgFormat: function(msg, arg1, arg2) { return msg.replace(/单纯/g, arg1+arg2) } } }) </script> </body> </html>
14 自定义指令
14.1 自定义全局指令Vue.directive
Vue中所有的指令在调用时都以v-开头,自定义指令也要遵守这个规则
自定义指令的语句是:Vue.directive('指令名称', {xxx})
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>vue</title> <script src="https://cdn.staticfile.org/vue/2.2.2/vue.min.js"></script> </head> <body> <div id="app"> <input v-focus></input> </div> <script> Vue.directive('focus', { bind:function(el) {//每当指令绑定到元素上时会执行,只执行1次 }, inserted:function(el) {//元素插入到DOM中时会执行,只执行1次,其中el是原生dom对象 el.focus() }, updated:function(el) {//当VNode更新的时候会执行,可能会触发多次 } }) new Vue({ el: '#app', data: { msg: '曾经,我也是一个单纯的少年,单纯的我,傻傻的问,谁是世界上最单纯的男人' }, filters: { msgFormat: function(msg, arg1, arg2) { return msg.replace(/单纯/g, arg1+arg2) } } }) </script> </body> </html>
自定义指令有一些钩子函数,我们还可以为这些钩子函数提供参数,可参考官网文档https://cn.vuejs.org/guide/reusability/custom-directives.html。
14.2 自定义私有指令directives
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>vue</title> <script src="https://cdn.staticfile.org/vue/2.2.2/vue.min.js"></script> </head> <body> <div id="app"> <h3 v-fontweight="100">fdfd</h3> </div> <script> new Vue({ el: '#app', data: { }, directives: { 'fontweight': { bind: function(el, binding) { el.style.fontWeight = binding.value } } } }) </script> </body> </html>
简写形式
对于自定义指令来说,一个很常见的情况是仅仅需要在 mounted 和 updated 上实现相同的行为,除此之外并不需要其他钩子。这种情况下我们可以直接用一个函数来定义指令。
<script> new Vue({ el: '#app', data: { }, directives: { 'fontweight': { // 普通形式 bind: function(el, binding) { el.style.fontWeight = binding.value } }, 'fontsize': function(el, binding) { //简写形式 el.style.fontSize = binding.value } } })
15 http请求
15.1 使用vue-resource发送请求
15.2 使用axios发送请求
16 创建组件
16.1 创建全局组件
16.1.1 使用Vue.extend()创建组件
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>vue</title> <script src="https://cdn.staticfile.org/vue/2.2.2/vue.min.js"></script> </head> <body> <div id="app"> <my-com1></my-com1> </div> <script> var com1 = Vue.extend({ template: '<h3>这是使用Vue.extend创建的组件</h3>' }) Vue.component('myCom1', com1) var vm = new Vue({ el: '#app' }); </script> </body> </html>
其中,我们可以简写为如下形式
Vue.component('myCom1', Vue.extend({ template: '<h3>这是使用Vue.extend创建的组件</h3>' }))
16.1.2 使用Vue.component创建组件
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>vue</title> <script src="https://cdn.staticfile.org/vue/2.2.2/vue.min.js"></script> </head> <body> <div id="app"> <my-com1></my-com1> </div> <script> Vue.component('myCom1', { template: '<h3>这是直接使用Vue.component创建的组件</h3>' }) var vm = new Vue({ el: '#app' }); </script> </body> </html>
16.1.3 使用<template>元素创建组件
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>vue</title> <script src="https://cdn.staticfile.org/vue/2.2.2/vue.min.js"></script> </head> <body> <div id="app"> <my-com1></my-com1> </div> <template id='tmp1'> <h3>这是通过template元素在外部定义的组件结构</h3> </template> <script> Vue.component('myCom1', { template: '#tmp1' }) var vm = new Vue({ el: '#app' }); </script> </body> </html>
16.2 创建私有组件
目前我们已经知道了组件所包含的一些属性,如下
<body> <div id="app"> </div> <script> var vm = new Vue({ el: '#app', data: {}, methods: {}, filters: {}, directives: {}, beforeCreate(){}, created(){}, //其他一些声明周期钩子函数省略 }); </script> </body>
现在我们再认识一个属性,用于定义私有组件
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>vue</title> <script src="https://cdn.staticfile.org/vue/2.2.2/vue.min.js"></script> </head> <body> <div id="app"> <login/> </div> <script> var vm = new Vue({ el: '#app', components: { login: { template: '<h1>这是私有的login组件</h1>' } } }); </script> </body> </html>
其中,template可以单独提取出来
<template id='tmpl'> <h1>这是私有的login组件</h1> </template> <script> var vm = new Vue({ el: '#app', components: { login: { template: '#tmpl' } } }); </script>
我们还可以这样写
<html> <head> <script src="https://cdn.staticfile.org/vue/2.2.2/vue.min.js"></script> </head> <body> <div id="app"> <login></login> </div> <script> var login = { template: '<h3>这是登陆组件</h3>' } var vm = new Vue({ el: '#app', components: { login } }) </script> </body> </html>
其中 components: { login } 相当于 components: { login:login }
17 组件中的data
组件可以有自己的data,但是组件中的data和实例化Vue根组件时的data写法有点不一样,组件中的data必须是一个函数,且这个函数返回一个对象
组件中的data和Vue根组件中的data使用方法一样,可以在<template>中通过{{}}引用
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>vue</title> <script src="https://cdn.staticfile.org/vue/2.2.2/vue.min.js"></script> </head> <body> <div id="app"> <my-com/> </div> <script> Vue.component('myCom',{ template: '<h1>这是全局组件-----{{ msg }}</h1>', data: function() { return { msg: '这是组件中的data定义的数据' } } }) var vm = new Vue({ el: '#app' }); </script> </body> </html>
注意组件中的data必须返回一个独立的对象,如果是共享的对象,则会出现问题
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>vue</title> <script src="https://cdn.staticfile.org/vue/2.2.2/vue.min.js"></script> </head> <body> <div id="app"> <counter></counter> <hr> <counter></counter> </div> <template id='tmpl'> <div> <input type='button' value='+1' @click='increment'> <h3>{{count}}</h3> </div> </template> <script> var obj = {count:0} Vue.component('counter',{ template: '#tmpl', data: function() { return obj }, methods: { increment() { this.count++ } } }) var vm = new Vue({ el: '#app' }); </script> </body> </html>
如下图所示,当我们点击一个组件时,另一个组件的count也在增加
所以,正确的做法是:
Vue.component('counter',{ template: '#tmpl', data: function() { return { count: 0 } }, methods: { increment() { this.count++ } } })
18 组件切换
这里我们用到一个vue中的新组件<component>来承载我们自定义组件。我们定义了2个a标签,点击标签通过改变data来改变<component>的:is属性绑定的值,从而改变要显示的自定义组件
/<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>vue</title> <script src="https://cdn.staticfile.org/vue/2.2.2/vue.min.js"></script> </head> <body> <div id="app"> <a href="" @click.prevent="comName='login'">登陆</a> <a href="" @click.prevent="comName='register'">注册</a> <component :is="comName"></component> </div> <script> Vue.component('login', { template: '<h3>登陆组件</h3>' }) Vue.component('register', { template: '<h3>注册组件</h3>' }) new Vue({ el: '#app', data: { comName: 'login' } }) </script> </body> </html>
19 组件传值
19.1 传递属性
父组件可以在引用子组件的时候,通过属性绑定(v-bind)的形式把需要传递给子组件的数据,以属性绑定的形式,传递到子组件内部,供子组件使用.
在子组件中需要定义props属性来接收父组件传递过来的值。
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>vue</title> <script src="https://cdn.staticfile.org/vue/2.2.2/vue.min.js"></script> </head> <body> <div id="app"> <com1 v-bind:parentmsg="msg"></com1> </div> <script> new Vue({ el: '#app', data: { msg: '123 啊-父组件中的数据' }, components: { com1: { template: '<h1>这是子组件 --- {{ parentmsg }}</h1>', props: ['parentmsg'] } } }) </script> </body> </html>
19.2 传递方法
除了把父组件data中的值传递到子组件,还可以把父组件中的方法传递到子组件。
父组件向子组件传递方法,使用的是事件绑定机制v-on
当我们自定义了一个事件属性之后,子组件就能够通过某些方式,来调用传递进去这个方法了
当点击子组件的按钮时,如何拿到父组件传递过来的func方法并调用呢?使用this.$emit('func')的方式
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>vue</title> <script src="https://cdn.staticfile.org/vue/2.2.2/vue.min.js"></script> </head> <body> <div id="app"> <com1 v-on:func="show"></com1> </div> <template id="tmp1"> <div> <h1>这是子组件</h1> <input type="button" value="这是子组件中的按钮 - 点击它,触发父组件传递过来的func方法" @click="myclick"></input> </div> </template> <script> var com1 = { template: '#tmp1', methods: { myclick() { this.$emit('func','abc') } } } new Vue({ el: '#app', data: {}, methods: { show(param) { console.log('调用了父组件的show方法'+param) } }, components: { com1 } }) </script> </body> </html>
上面例子中,我们在子组件中调用了父组件的方法,并且携带方法参数,通过这种方式,我们可以实现子组件向父组件传值。
20 this.$refs获取组件引用
我们在编写组件标签时,可以使用ref定义组件实例的引用。然后我们可以在其他组件中通过this.$refs获取组件的引用,进而可以访问组件中的data和调用组件的方法
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>vue</title> <script src="https://cdn.staticfile.org/vue/2.2.2/vue.min.js"></script> </head> <body> <div id="app"> <input value="获取元素" type="button" @click.prevent="getElement"> <login ref="mylogin"></login> </div> <script> var login = { template: '<h1>登陆组件</h1>', data() { return { msg: 'son msg' } }, methods: { show() { console.log('调用了子组件的方法') } } } var vm = new Vue({ el: '#app', data: {}, methods: { getElement() { console.log(this.$refs.mylogin.msg) this.$refs.mylogin.show() } }, components: { login } }) </script> </body> </html>
21 vue-router路由
路由是干啥的?路由是进行不同组件切换的。
我们可以直接在<script>标签中从cdn引入vue-router;也可以使用npm安装vue-router包
如果在一个模块化工程中使用vue-router,必须使用Vue.use明确的安装路由功能
import Vue from 'vue' import VueRouter from 'vue-router' Vue.use(VueRouter)
21.1 基本使用
当我们页面中引入了 <script src="https://unpkg.com/vue-router@3.0.1/dist/vue-router.js"></script> ,在window的全局对象上就有了一个路由的构造函数。叫VueRouter
下面我们通过一个实例来展示其基本用法
1 首先,创建VueRouter对象 routerObj ,创建时,我们传递一个对象,这个对象里面包含路由匹配规则
每个路由匹配规则是一个对象,这个对象上有两个必须的属性:path:表示监听哪个路由链接地址;component:表示如果路由匹配成功需要展示的组件
2 然后,通过在Vue实例中使用 router: routerObj 将路由规则对象注册到Vue实例上,用来监听URL地址变化,然后展示对应的组件
3 使用 <router-view> 标签作为一个占位符,用来替换路由匹配到的组件。 <router-view> 是vue-router提供的元素,专门用来当做占位符的,将来路由规则匹配到的组件,就会展示到这个router-view中去
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>vue</title> <script src="https://cdn.staticfile.org/vue/2.2.2/vue.min.js"></script> <script src="https://unpkg.com/vue-router@3.0.1/dist/vue-router.js"></script> </head> <body> <div id="app"> <a href="#/login">登陆</a> <a href="#/register">注册</a> <!-- 这是vue-router提供的元素,专门用来当做占位符的,将来路由规则匹配到的组件,就会展示到这个router-view中去 --> <router-view></router-view> </div> <script> var login = { template: '<h1>登陆组件</h1>' } var register = { template: '<h1>注册组件</h1>' } var routerObj = new VueRouter({ routes: [ //路由匹配规则 //每个路由匹配规则是一个对象,这个对象上有两个必须的属性:path:表示监听哪个路由链接地址;component:表示如果路由匹配成功需要展示的组件 {path: '/login', component: login}, //注意:component的属性值必须是一个组件模板对象,不能是组件的引用名称 {path: '/register', component: register} ] }) var vm = new Vue({ el: '#app', data: {}, methods: {}, router: routerObj //将路由规则对象注册到Vue实例上,用来监听URL地址变化,然后展示对应的组件 }) </script> </body> </html>
效果展示
执行过程:
1 点击登陆或者注册按钮,使得url地址发生改变
2 vue-router监听到url地址的变化,通过路由匹配规则,找到匹配到的组件
3 把匹配到的组件替换掉 <router-view> 占位符
21.2 router-link
上栗中,我们使用a标签来达到路由切换组件的目的,此外vue-router为我们提供了一个<router-link>标签达到相同的目的
<div id="app"> <router-link to="/login">登陆</router-link> <router-link to="/register">注册</router-link> <router-view></router-view> </div>
router-link默认渲染为一个a标签,我们还可以使用tag属性设置渲染为其他标签
<router-link to="/login" tag="span">登陆</router-link>
渲染后的结果如下所示
<div id="app"> <span class="router-link-exact-active router-link-active">登陆</span> <a href="#/register" class="">注册</a> <h1>登陆组件</h1> </div>
21.3 redirect重定向
上例中,我们可以设置路由为/时重定向到另一个路由
{path: '/', redirect: '/login'}
21.4 路由传参
21.4.1 方式1
我们可以在router-link中使用如下方式传递参数 <router-link to="/login?id=10&name=szj">登陆</router-link> 。然后在当前组件中使用Vue实例上的this.$route对象的query属性获取传递的参数。
<html> <head> <script src="https://cdn.staticfile.org/vue/2.2.2/vue.min.js"></script> <script src="https://unpkg.com/vue-router@3.0.1/dist/vue-router.js"></script> </head> <body> <div id="app"> <router-link to="/login?id=10&name=szj">登陆</router-link> <router-link to="/register">注册</router-link> <router-view></router-view> </div> <script> var login = { template: '<h1>登陆组件---{{ this.$route.query.id }}---{{ this.$route.query.name }}</h1>' } var register = { template: '<h1>注册组件</h1>' } var routerObj = new VueRouter({ routes: [ {path: '/login', component: login}, {path: '/register', component: register} ] }) var vm = new Vue({ el: '#app', data: {}, methods: {}, router: routerObj }) </script> </body> </html>
21.4.2 方式2
我们可以在router-link中使用如下方式传递参数 <router-link to="/login/10/szj">登陆</router-link> ,并且使用路由匹配规则方式为 {path: '/login/:id/:name', component: login} ,然后在当前组件中使用Vue实例上的this.$route对象的params属性获取传递的参数。
<html> <head> <script src="https://cdn.staticfile.org/vue/2.2.2/vue.min.js"></script> <script src="https://unpkg.com/vue-router@3.0.1/dist/vue-router.js"></script> </head> <body> <div id="app"> <router-link to="/login/10/szj">登陆</router-link> <router-link to="/register">注册</router-link> <router-view></router-view> </div> <script> var login = { template: '<h1>登陆组件---{{ $route.params.id }}---{{ $route.params.name }}</h1>' } var register = { template: '<h1>注册组件</h1>' } var routerObj = new VueRouter({ routes: [ {path: '/login/:id/:name', component: login}, {path: '/register', component: register}, {path: '/', redirect: '/login'} ] }) var vm = new Vue({ el: '#app', data: {}, methods: {}, router: routerObj }) </script> </body> </html>
21.5 子路由
路由匹配规则里面,我们使用children属性定义子路由
<html> <head> <script src="https://cdn.staticfile.org/vue/2.2.2/vue.min.js"></script> <script src="https://unpkg.com/vue-router@3.0.1/dist/vue-router.js"></script> </head> <body> <div id="app"> <router-link to="/account">账号</router-link> <router-view></router-view> </div> <template id="tmp1"> <div> <h1>账号</h1> <router-link to="/account/login">登陆</router-link> <router-link to="/account/register">注册</router-link> <router-view></router-view> </div> </template> <script> var account = { template: '#tmp1' } var login = { template: '<h3>登陆</h3>' } var register = { template: '<h3>注册</h3>' } var routerObj = new VueRouter({ routes: [ { path: '/account', component: account, children: [ {path: 'login', component: login}, {path: 'register', component: register} ] } ] }) var vm = new Vue({ el: '#app', data: {}, methods: {}, router: routerObj }) </script> </body> </html>
效果如下
21.6 使用命名视图实现经典布局
我们定义3个组件,分别是header、left、main。并且我们使用路由规则配置这三个组件的路由链接,实现经典的布局
我们定义3个<router-view>,并且我们为之设置name属性,达到分别放置header、left、main组件的目的
<html> <head> <script src="https://cdn.staticfile.org/vue/2.2.2/vue.min.js"></script> <script src="https://unpkg.com/vue-router@3.0.1/dist/vue-router.js"></script> </head> <body> <div id="app"> <router-view></router-view> <router-view name="left"></router-view> <router-view name="main"></router-view> </div> <script> var header = { template: '<h3>Header头部区域</h3>' } var leftBox = { template: '<h3>Left侧边栏区域</h3>' } var mainBox = { template: '<h3>Main主体区域</h3>' } var routerObj = new VueRouter({ routes: [ {path: '/', components: { 'default': header, 'left': leftBox, 'main': mainBox }} ] }) var vm = new Vue({ el: '#app', data: {}, methods: {}, router: routerObj }) </script> </body> </html>
结果如下所示
基本已经实现经典布局,缺的是样式,我们加上样式后的代码
<html> <head> <script src="https://cdn.staticfile.org/vue/2.2.2/vue.min.js"></script> <script src="https://unpkg.com/vue-router@3.0.1/dist/vue-router.js"></script> <style> .html, body { margin: 0; padding: 0; } .header { background: orange; height: 80px; } .h3 { margin: 0; padding: 0; font-size: 16px; } .container { display: flex; height: 600px; } .left { background: lightgreen; flex: 2; } .main { background: lightpink; flex: 8; } </style> </head> <body> <div id="app"> <router-view></router-view> <div class="container"> <router-view name="left"></router-view> <router-view name="main"></router-view> </div> </div> <script> var header = { template: '<h3 class="header">Header头部区域</h3>' } var leftBox = { template: '<h3 class="left">Left侧边栏区域</h3>' } var mainBox = { template: '<h3 class="main">Main主体区域</h3>' } var routerObj = new VueRouter({ routes: [ {path: '/', components: { 'default': header, 'left': leftBox, 'main': mainBox }} ] }) var vm = new Vue({ el: '#app', data: {}, methods: {}, router: routerObj }) </script> </body> </html>
效果如下所示
22 使用Vue实例的render方法渲染组件
<html> <head> <script src="https://cdn.staticfile.org/vue/2.2.2/vue.min.js"></script> </head> <body> <div id="app"> </div> <script> var login = { template: '<h3>这是登陆组件</h3>' } var vm = new Vue({ el: '#app', data: {}, methods: {}, render: function(createElements) { //createElement是一个方法,调用它能够把指定的组件模板渲染为html结构 var html = createElements(login) return html //这里的return结果,会替换页面中el指定的容器 } }) </script> </body> </html>
createElement是一个方法,调用它能够把指定的组件模板渲染为html结构
这里的return结果,会替换页面中el指定的容器
23 vue cli
官网:https://cli.vuejs.org/zh/guide/
vue cli是进行vue开发的工具,当我们安装完vue cli后,就可以使用里面的一些命令方便的进行一些创建和管理方面的操作。比如使用vue create命令进行新项目的创建;vue-cli-service build进行项目的构建等等。也可以参考官网进行理解
vue cli包含了如下3部分
- @vue/cli
- @vue/cli-service
- @vue/cli-plugin-xxx或vue-cli-plugin-xxx
23.1 安装
我们使用如下命令查看已安装的全局包
npm ls --global --depth 0
如果没有安装,则安装
npm install -g @vue/cli --registry=https://registry.npm.taobao.org
验证是否安装成功
D:\2022\front\pms-front>vue --version
@vue/cli 5.0.8
23.2 创建一个项目
vue create hello-world
23.3 vue ui
这个命令将会打开浏览器,在浏览器页面中我们可以创建和管理项目
23.4 插件
如果你查阅一个新创建项目的 package.json,就会发现依赖都是以 @vue/cli-plugin- 开头的。插件可以修改 webpack 的内部配置,也可以向 vue-cli-service 注入命令。在项目创建的过程中,绝大部分列出的特性都是通过插件来实现的。
23.5 vue-cli-service
如果我们安装了@vue/cli-service,我们就可以使用vue-cli-service命令
D:\2022\front\vuecli\hello-world\node_modules\.bin>vue-cli-service --help Usage: vue-cli-service <command> [options] Commands: serve start development server build build for production inspect inspect internal webpack config run vue-cli-service help [command] for usage of a specific command.
使用命令 vue-cli-service serve 启动一个开发服务器,并附带开箱即用的模块热重载。
D:\2022\front\vuecli\hello-world\node_modules\.bin>vue-cli-service help serve Usage: vue-cli-service serve [options] [entry] Options: --open open browser on server start --copy copy url to clipboard on server start --stdin close when stdin ends --mode specify env mode (default: development) --host specify host (default: 0.0.0.0) --port specify port (default: 8080) --https use https (default: false) --public specify the public network URL for the HMR client --skip-plugins comma-separated list of plugin names to skip for this run
其中命令行参数[entry]指定为唯一入口(默认值src/main.js)
使用 vue-cli-service build 用于构建用于生产环境的包
D:\2022\front\vuecli\hello-world\node_modules\.bin>vue-cli-service help build Usage: vue-cli-service build [options] [entry|pattern] Options: --mode specify env mode (default: production) --dest specify output directory (default: dist) --no-module build app without generating <script type="module"> chunks for modern browsers --target app | lib | wc | wc-async (default: app) --inline-vue include the Vue module in the final bundle of library or web component target --formats list of output formats for library builds (default: commonjs,umd,umd-min) --name name for lib or web-component mode (default: "name" in package.json or entry filename) --filename file name for output, only usable for 'lib' target (default: value of --name) --no-clean do not remove the dist directory contents before building the project --report generate report.html to help analyze bundle content --report-json generate report.json to help analyze bundle content --skip-plugins comma-separated list of plugin names to skip for this run --watch watch for changes --stdin close when stdin ends
23.6 环境变量
在vue cli创建的项目根路径下放置环境变量文件,如下
.env # 在所有的环境中被载入
.env.local # 在所有的环境中被载入,但会被 git 忽略
.env.[mode] # 只在指定的模式中被载入
.env.[mode].local # 只在指定的模式中被载入,但会被 git 忽略
比如,.env.production将用在生产环境中。
vue中采用第三方包dotenv(https://github.com/motdotla/dotenv#rules)实现环境变量的配置和加载。dotenv这个工具可以将.env中的配置加载到nodejs的全局环境变量process.env中。
但是,注入到process.env中的环境变量不一定就能够在vue项目中被使用。vue通过结合webpack的一个插件DefinePlugin(参考https://www.webpackjs.com/plugins/),可以实现只有VUE_APP开头的变量会被DefinePlugin静态的嵌入到代码中。
我们可以在代码中这样使用环境变量配置文件中的配置:
process.env.VUE_APP_SECRET
另外还有两个特殊的变量我们可以直接使用
NODE_ENV
- 会是"development"
、"production"
或"test"
中的一个。具体的值取决于应用运行的模式。BASE_URL
- 和vue.config.js
中的publicPath
对应,即你的应用会部署到的基础路径。
23.7 模式
默认情况下,一个vue cli项目有3种模式
development
模式用于vue-cli-service serve
test
模式用于vue-cli-service test:unit
production
模式用于vue-cli-service build
和vue-cli-service test:e2e
你可以通过传递--mode参数指定特定模式。例如,如果想在构建时使用开发环境变量:
vue-cli-service build --mode development
当运行vue-cli-service命令时,所有的环境变量都从对应的环境变量文件中载入。
NODE_ENV变量将决定web app运行的模式(开发、生产、测试)。这个变量可以在环境变量文件(.env)中配置,如果没有配置,会根据当前模式确定NODE_ENV的值。
23.8 html模板
public/index.html
文件是一个会被 html-webpack-plugin 处理的模板。在构建过程中,资源链接会被自动注入。
使用lodash template语法的html规则如下:
<%= BASE_URL %>
用来做不转义插值;<%- VALUE %>
用来做 HTML 转义插值;<% expression %>
用来描述 JavaScript 流程控制。
像BASE_URL、VUE_APP_XXX之类的环境变量都可以直接使用。
此外,html-webpack-plugin插件还提供了内置的一些变量供我们使用,参考:https://github.com/jantimon/html-webpack-plugin#writing-your-own-templates,示例
<html> <head> <%= htmlWebpackPlugin.tags.headTags %> </head> <body> <%= htmlWebpackPlugin.tags.bodyTags %> </body> </html>
23.9 静态资源
静态资源可以通过两种方式进行处理:
-
在 JavaScript 被导入或在 template/CSS 中通过相对路径被引用。这类引用会被 webpack 处理。
-
放置在
public
目录下或通过绝对路径被引用。这类资源将会直接被拷贝,而不会经过 webpack 的处理。webpack打包之后这些资源将会出现在打包路径的根路径下。
从相对路径导入的静态资源,该资源将会被包含到webpack的依赖视图中,在其编译的过程中,所有诸如<img src="...">
、background: url(...)
的资源将会被解析为一个模块依赖。例如
url(./image.png) 会被翻译为 require('./image.png')
<img src="./image.png"> 会被编译为一个hash值 h('img', { attrs: { src: require('./image.png') }}) 。在其内部,我们通过 webpack 的 Assets Modules 配置,用版本哈希值和正确的公共基础路径来决定最终的文件路径,并将小于 8KiB 的资源内联,以减少 HTTP 请求的数量。
23.10 URL转换规则
-
如果 URL 是一个绝对路径 (例如
/images/foo.png
),它将会被保留不变。 -
如果 URL 以
.
开头,它会作为一个相对模块请求被解释且基于你的文件系统中的目录结构进行解析。 -
如果 URL 以
~
开头,其后的任何内容都会作为一个模块请求被解析。这意味着你甚至可以引用 Node 模块中的资源:<img src="~some-npm-package/foo.png">
-
如果 URL 以
@
开头,它也会作为一个模块请求被解析。它的用处在于 Vue CLI 默认会设置一个指向<projectRoot>/src
的别名@
。(仅作用于模版中)
注:上述提到的模块请求,就是nodejs中的require(...)。(因为webpack编译过程中,全程是在nodejs的支持下完成的)。
23.11 webpack配置
在vue中配置webpack有两种方式
- vue.config.js
- webpack-chain
1 在vue.config.js中配置webpack
module.exports = { configureWebpack: { plugins: [ new MyAwesomeWebpackPlugin() ] } }
该对象会被webpack-merge合并入最终的webpack配置。
2 webpack-chain
module.exports = { chainWebpack: config => { config.module .rule('vue') .use('vue-loader') .tap(options => { // 修改它的选项... return options }) } }
24 .vuerc
对于@vue/cli的全局配置放在home目录下的.vuerc文件中。
我们可以使用 vue config 命令查看和修改这个文件中的配置。也可以直接使用文本编辑器修改这个文件。
25 vue.config.js
官网:https://cli.vuejs.org/zh/config/
这个配置文件放在项目根目录下,他会被@vue/cli-service自动加载。
我们也可以使用package.json中的vue字段进行配置。
在vue.config.js中,我们导出一个包含配置项的对象
module.exports = { // 选项... }
26 深入源码看启动过程
Vue是一个js库,我们使用Vue有两种方式:
方式1:全量引入,引入vue.min.js
方式2:在node中使用import Vue from 'vue'方式引入vue中的部分,然后通过构建工具打包拆分得到我们需要的js。
平时开发,我们一般使用方式2。
我们使用npm install vue安装了vue之后,vue被安装的位置在node_modules路径下。vue模块已经暴露了相关的对象供其他模块使用(export default Vue),当我们使用import Vue from 'vue'导入vue时,我们将导入Vue对象到当前模块中。
比如我们模块中主文件为main.ts
import Vue from 'vue' import App from './App.vue' new Vue({ render: h => h(App), }).$mount('#app')
首先,是构造Vue实例,使用new Vue方式创建,参考26.4 附:Vue实际的构造器。将传递进来的对象经过处理后赋给vm.$options对象。
然后,调用Vue.prototype.$mount函数进行虚拟dom的挂载,挂载到id=app这个节点。
可全局搜索Vue.prototype.$mount关键字可找到其定义位置。在\src\platforms\web\runtime\index.ts中定义
Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component { el = el && inBrowser ? query(el) : undefined return mountComponent(this, el, hydrating) }
26.1 附1:Vue对象和VueConstructor对象
和Vue实例创建相关的js对象有两个,Vue和VueConstructor。
(chatgpt的回答)
在 Vue.js 的类型定义中,Vue 和 VueConstructor 通常指代不同的概念:
Vue:通常指的是 Vue 实例的类型。当你创建一个新的 Vue 实例时,你会使用 new Vue(...)。在 TypeScript 中,Vue 类型描述了一个 Vue 实例的属性和方法,例如 $data, $props, $el, $mount, 等等。
VueConstructor:这是一个接口,用于描述 Vue 构造函数的类型。它定义了可以用来创建 Vue 实例的构造函数签名,以及 Vue 构造函数的静态属性和方法,例如 extend, component, directive, 等等。当你想要扩展 Vue 或者创建子类时,你会与 VueConstructor 打交道。
在 TypeScript 中,VueConstructor 接口可能会这样定义:
interface VueConstructor {
new (options?: VueComponentOptions<Vue>): Vue;
extend(options: VueComponentOptions<Vue>): VueConstructor;
// ...其他静态属性和方法
}
而 Vue 类型可能会这样定义:
class Vue {//如果es版本不支持class,此处可能是构造器的方式(function)
constructor(options?: VueComponentOptions<Vue>);
$mount(elementOrSelector?: string | Element, hydrating?: boolean): this;
// ...其他实例属性和方法
}
在实际的 Vue.js 类型定义中,Vue 类型通常是由 VueConstructor 创建的实例的类型。这意味着 VueConstructor 类型的对象(例如 Vue 构造函数本身)可以用来创建 Vue 类型的实例。
例如,当你导入 Vue 时:
import Vue from 'vue';
这里的 Vue 是一个对象,它符合 VueConstructor 接口。你可以使用它来创建 Vue 实例,也可以使用它的静态方法来定义全局组件或指令,或者通过 Vue.extend 来创建扩展的构造函数。
总结一下,Vue 是实例的类型,而 VueConstructor 是构造函数的类型。在 Vue.js 的类型系统中,它们共同工作,以确保无论是在创建实例还是扩展 Vue 时,类型都是正确和一致的。
在vue2.6.14源码中
VueConstructor定义如下
export interface VueConstructor<V extends Vue = Vue> { new < Data = object, Methods = object, Computed = object, Props = object, SetupBindings = {}, Mixin extends ComponentOptionsMixin = ComponentOptionsMixin, Extends extends ComponentOptionsMixin = ComponentOptionsMixin >( options?: ThisTypedComponentOptionsWithRecordProps< V, Data, Methods, Computed, Props, SetupBindings, Mixin, Extends > ): CombinedVueInstance< V, Data, Methods, Computed, Record<keyof Props, any>, SetupBindings, Mixin, Extends > extend< Data, Methods, Computed, Props, SetupBindings = {}, Mixin extends ComponentOptionsMixin = ComponentOptionsMixin, Extends extends ComponentOptionsMixin = ComponentOptionsMixin >( options?: ThisTypedComponentOptionsWithRecordProps< V, Data, Methods, Computed, Props, SetupBindings, Mixin, Extends > ): ExtendedVue< V, Data, Methods, Computed, Props, SetupBindings, Mixin, Extends > }
为便于理解简化一下
interface VueConstructor { new (options?: ThisTypedComponentOptionsWithRecordProps): CombinedVueInstance; }
其意思是这个是一个Vue构造函数的签名(在TypeScript的interface中使用 new 关键字的语法是用来定义一个类或对象的构造函数类型,一般写在d.ts中用于定义这个类长啥样),或者说定义了Vue构造函数长啥样。在使用new方式创建Vue实例时,传递一个options其类型为ThisTypedComponentOptionsWithRecordProps,并且返回一个CombinedVueInstance类型的实例。也就是说我们new Vue({el: '#app',render: h => h(App)})时,参数必须是指定的类型,创建的对象必须是指定类型的对象,不然会创建失败。
26.2 附1:Vue对象和Vue组件
Vue实例中的很多方法,Vue组件中也可以使用(比如$mount,$watch等),他们是如何实现的呢?
首先,我们在vue中创建一个组件,其本质是一个VueComponent的构造方法。在构造组件时,Vue帮我们执行了new VueComponent(options)。同时,vue还做了另一件事
VueComponent.prototype.__proto__===Vue.prototype
Vue修改了组件原型对象的隐式原型链,将其指向Vue的原型对象。使得组件实例对象可以使用Vue原型对象中的方法和属性。
26.3 附:*.d.ts文件的作用
在Vue 2源码中,*.d.ts
文件是TypeScript的声明文件。这些文件的作用是为JavaScript代码提供类型定义,使得TypeScript能够理解在JavaScript库或框架中使用的类型。即使Vue 2本身是用JavaScript编写的,这些声明文件也允许开发者在TypeScript项目中使用Vue,并从TypeScript提供的类型检查和代码编辑器的智能提示中受益。
具体来说,*.d.ts
文件中包含了:
-
接口(Interfaces):定义了对象的形状,包括它应该有哪些属性和方法,以及它们的类型。
-
类型别名(Type Aliases):为复杂的类型提供了简短的名称。
-
函数和方法的签名:描述了函数和方法的参数类型和返回类型。
-
类的定义:描述了类的构造函数、属性和方法的类型。
-
模块声明:指定了模块导出的内容,以及它们的类型。
-
全局变量和类型:定义了全局可用的变量和类型。
使用这些声明文件的好处包括:
-
类型安全:在开发过程中可以捕获到类型错误,减少运行时错误的可能性。
-
代码提示:编辑器可以提供自动补全、函数签名信息等,提高开发效率。
-
文档作用:声明文件可以作为库或框架的API文档,帮助开发者了解如何使用。
-
项目维护:有助于项目长期维护,特别是在团队协作和大型项目中。
在Vue项目中,如果你使用TypeScript,你会发现*.d.ts
文件对于理解和使用Vue API非常有帮助。如果你是库的作者,提供良好的类型声明文件也是一个很好的实践,因为它可以让你的库更容易被TypeScript用户使用。
26.4 附:Vue实际的构造器
在*.d.ts中,定义了Vue的签名,也就是Vue长啥样,有哪些元素和方法。而实际实例化Vue实例则在src/core/instance/index.ts中
function Vue(options) {this._init(options) }
_init函数则在src/core/instance/init.ts中
export function initMixin(Vue: typeof Component) { Vue.prototype._init = function (options?: Record<string, any>) { const vm: Component = this // a uid vm._uid = uid++ let startTag, endTag /* istanbul ignore if */ if (__DEV__ && config.performance && mark) { startTag = `vue-perf-start:${vm._uid}` endTag = `vue-perf-end:${vm._uid}` mark(startTag) } // a flag to mark this as a Vue instance without having to do instanceof // check vm._isVue = true // avoid instances from being observed vm.__v_skip = true // effect scope vm._scope = new EffectScope(true /* detached */) // #13134 edge case where a child component is manually created during the // render of a parent component vm._scope.parent = undefined vm._scope._vm = true // merge options if (options && options._isComponent) { //如果options有_isComponent参数并且为true,表明是内部组件,Vue对内部组件进行优化处理 // optimize internal component instantiation // since dynamic options merging is pretty slow, and none of the // internal component options needs special treatment. initInternalComponent(vm, options as any) } else { //否则,是用户定义组件 vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor as any), options || {}, vm ) } /* istanbul ignore else */ if (__DEV__) { initProxy(vm) } else { vm._renderProxy = vm } // expose real self vm._self = vm initLifecycle(vm) initEvents(vm) initRender(vm) callHook(vm, 'beforeCreate', undefined, false /* setContext */) initInjections(vm) // resolve injections before data/props initState(vm) initProvide(vm) // resolve provide after data/props callHook(vm, 'created') /* istanbul ignore if */ if (__DEV__ && config.performance && mark) { vm._name = formatComponentName(vm, false) mark(endTag) measure(`vue ${vm._name} init`, startTag, endTag) } if (vm.$options.el) { vm.$mount(vm.$options.el) } } }
27 vue构建过程
Vue.js的构建过程通常指的是将Vue源代码和应用代码转换成可以在浏览器中运行的JavaScript、CSS和HTML文件的过程。这个过程涉及多个步骤,通常由现代前端构建工具(如Webpack、Rollup或Vite)自动化完成。以下是Vue应用构建过程的一般步骤:
-
项目初始化:
- 使用Vue CLI或其他脚手架工具创建一个新的Vue项目。
- 安装项目依赖,包括Vue库、构建工具和插件。
-
编写源代码:
- 开发者编写
.vue
单文件组件,这些文件包含模板(HTML)、脚本(JavaScript)和样式(CSS)。 - 编写其他JavaScript代码、样式文件和静态资源。
- 开发者编写
-
预处理:
- 使用预处理器(如Babel、TypeScript、Sass等)处理源代码。例如,Babel可以将ES6+代码转换为兼容性更好的ES5代码,TypeScript可以编译为JavaScript,Sass可以编译为CSS。
-
模块打包:
- 使用Webpack、Rollup或Vite等模块打包工具将所有的
.vue
文件、JavaScript代码和其他资源打包成一个或多个bundle文件。 vue-loader
(Webpack的一个插件)会将.vue
文件中的模板编译成渲染函数,将脚本和样式提取出来。
- 使用Webpack、Rollup或Vite等模块打包工具将所有的
-
代码分割和懒加载:
- 在打包过程中,可以配置代码分割,将代码拆分成多个chunk,以实现按需加载,提高应用的加载速度。
-
优化和压缩:
- 对打包后的代码进行压缩,移除不必要的代码(如dead code),优化代码以减少文件大小。
- 压缩CSS和HTML,优化图片和其他静态资源。
-
生成静态资源:
- 构建工具生成最终的静态资源文件,包括JavaScript文件、CSS样式表、HTML入口文件等。
-
输出结果:
- 将构建生成的静态资源输出到指定的目录(通常是
dist
或build
目录)。
- 将构建生成的静态资源输出到指定的目录(通常是
-
本地开发服务器:
- 在开发过程中,可以使用热模块替换(HMR)启动一个本地开发服务器,以便实时预览更改。
-
部署:
- 将构建好的静态资源部署到服务器或静态网站托管服务。
整个构建过程可以通过配置文件(如webpack.config.js
、vue.config.js
、rollup.config.js
等)来自定义和优化,以满足项目的具体需求。构建过程的自动化和配置化使得开发者可以专注于编写源代码,而构建工具则负责将源代码转换为生产就绪的应用程序。
27.1 渲染函数
上面讲解构建过程时讲过,vue-loader会将.vue文件中的模板编译成渲染函数。如何理解呢?
vue-loader 使用 vue-template-compiler 将 <template> 块中的模板编译成JavaScript渲染函数。
编译后的渲染函数将成为组件选项的一部分,用于创建虚拟DOM。也就是这个渲染函数和组件中的data,computed,created()等options处于一个层级。
举例
main.ts
new Vue({ el: '#app', render: h => h(App) })
App.vue
其中没有<template>标签,而是使用render选项替换
<script> export default { name: 'App', render: function (createElement) { return createElement('div', { attrs: { id: 'app' } }, 'sss'); } } </script>
效果
从上栗我们可以看出,每个组件对应一个渲染函数,当生成虚拟DOM时,从main.ts开始执行渲染函数,嵌套执行,最终生成一个虚拟DOM,当状态发生变化时,采用diffing算法比较虚拟DOM和真实DOM,更新变化的部分。
27.2 vue-template-compiler是如何工作的
vue-template-compiler
是Vue.js的一个库,它的作用是将Vue组件中的 <template>
块编译成JavaScript渲染函数。这个编译过程涉及将模板字符串转换为虚拟DOM渲染函数的代码,这些代码最终会在Vue组件的渲染过程中被调用。以下是这个过程的大致步骤:
-
解析HTML:
vue-template-compiler
首先解析<template>
块中的HTML字符串,将其转换为AST(抽象语法树)。这个AST表示了模板的结构,包括元素、属性、指令、文本节点等。
-
优化AST:
- 接下来,编译器会遍历AST,并尝试优化它。例如,它会标记静态子树(不依赖组件状态的部分),这样在组件重新渲染时,静态部分可以被跳过,从而提高渲染性能。
-
生成代码:
- 编译器将AST转换为渲染函数的JavaScript代码。这个过程包括创建
_c
(或createElement
)函数的调用,这是Vue用于创建虚拟DOM节点的函数。 - 对于模板中的每个节点,编译器都会生成相应的
_c
调用。对于指令、事件监听器和绑定的属性,编译器会生成相应的代码来处理它们。
- 编译器将AST转换为渲染函数的JavaScript代码。这个过程包括创建
-
处理指令和事件:
- 对于Vue特有的指令(如
v-if
、v-for
、v-model
等),编译器会生成特定的代码来实现它们的功能。 - 对于事件监听器,编译器会确保生成的代码能够在运行时注册相应的事件处理函数。
- 对于Vue特有的指令(如
-
创建渲染函数:
- 最终,编译器生成的代码会被包装成一个渲染函数,这个函数在被调用时会返回一个虚拟DOM树的根节点。
- 这个渲染函数将作为组件选项的一部分,被Vue实例化过程中的渲染系统调用。
-
与Vue组件集成:
- 一旦渲染函数被创建,它就可以与Vue组件的其他部分(如数据、计算属性、方法等)集成在一起。
- 当组件需要更新时,Vue会调用这个渲染函数来生成新的虚拟DOM,然后与旧的虚拟DOM进行比较(diffing),并高效地更新真实DOM。
vue-template-compiler
的输出是一个可以直接被Vue运行时使用的渲染函数,这个函数是组件渲染和更新的核心。通过预编译模板,Vue可以在浏览器中避免编译开销,从而提高应用的性能。
27.3 渲染函数是如何工作的
渲染函数在Vue.js中是组件渲染的核心机制。它们工作的方式是通过返回虚拟DOM节点(VNodes)来描述组件的结构。Vue.js使用虚拟DOM来提高性能,因为它允许Vue在实际操作DOM之前,在JavaScript层面上进行节点的比较和计算。
以下是渲染函数的工作流程:
-
创建元素:
- 渲染函数使用
createElement
函数(通常简写为h
)来创建虚拟DOM节点。这个函数接受几个参数:元素标签名、一个包含属性/类/样式等数据的对象,以及子节点。
- 渲染函数使用
-
构建虚拟DOM树:
- 开发者可以通过嵌套
createElement
调用来构建整个组件的虚拟DOM树。每个createElement
调用都返回一个VNode,这些VNode可以作为子节点嵌套在其他VNode中。
- 开发者可以通过嵌套
-
响应式数据绑定:
- 渲染函数可以访问Vue实例的响应式数据。当这些数据变化时,Vue会自动重新执行渲染函数来更新视图。
-
事件处理:
- 在创建VNode时,可以绑定事件监听器。这些监听器可以是组件方法或任何有效的JavaScript函数。
-
条件渲染和列表渲染:
- 渲染函数可以使用JavaScript逻辑来执行条件渲染(如
if
语句)和列表渲染(如map
函数)。
- 渲染函数可以使用JavaScript逻辑来执行条件渲染(如
-
插槽和组件:
- 渲染函数可以包含插槽和其他组件的VNodes。这允许开发者构建复杂的组件结构。
-
返回VNode:
- 渲染函数最终返回一个VNode。这个VNode代表了组件的根节点。
-
虚拟DOM到真实DOM的映射:
- Vue维护一个虚拟DOM树和一个真实DOM树的映射。当状态变化时,Vue会重新执行渲染函数来获取新的虚拟DOM树,然后将其与旧的虚拟DOM树进行比较(diffing)。
-
DOM更新:
- Vue通过虚拟DOM的diff算法找出实际DOM需要变更的最小部分,并高效地更新这些部分。这个过程称为补丁(patching)。
-
组件更新:
- 如果组件的子组件也需要更新,Vue会递归地执行子组件的渲染函数,并应用相应的DOM更新。
渲染函数的这种工作方式使得Vue能够提供声明式的UI描述,同时保持高性能。开发者不需要直接操作DOM,而是通过改变组件的状态来间接地控制UI的更新。这种抽象层次的提升简化了前端开发,并使得代码更容易维护和理解。
待续...