vue2
vue是用于构建用户界面的渐进式框架。vue的核心库只关注视图层。
安装脚手架
需要先安装node
安装:npm i -g @vue/cli
查看版本:vue -V
创建项目:vue create projectName
vue的优点?
- 渐进式(通俗的理解就是想用什么就用什么,不强制要求,比如vuex,可用可不用)js框架
- 数据与视图分开
- 单页面路由
- 虚拟DOM
- 响应式
- 组件化
vue的缺点?
不利于seo(搜索引擎) 不支持IE8及以下的浏览器(因为vue使用的是ES5中的Object.defineProperty()方法,IE8不支持此方法)
首屏加载时间长(解决办法:首页加载一个loading动画,性能优化)
mvvm的理解?
Model-View-ViewModel:数据模型-视图-视图模型
三者与vue的对应:
- Model对应Data
- View对应template
- ViewModel对应new vue()
三者的关系:
- Model可以通过数据绑定的方式影响View
- View可以通过事件绑定的方式影响Model
- ViewModel是把View和Model连接起来的连接器
mvc
model-view-controller
- 用户操作view去改变数据
- view通过controller去改变model
- model改变后通知view去渲染页面
mvvm、mvc
- mvvm:双向的通信
- mvc:单向的通信
- vm不是替代了controller,而是vm抽离里controller中的数据渲染,其他业务逻辑还是在controller里
为什么只对对象劫持,而要对数组重写?
因为对象最多几十个属性,拦截起来数量不多,但是数组可能会有成百上千项,拦截起来非常消耗性能,所以重写数组原型上的方法,是比较节省性能的方案。
对象添加、删除属性,无法更新视图?
原因:Object.defineProperty没有对对象新属性进行属性劫持
- 在组件中使用this.delete(obj, key)
- 数组使用this.delete(arr, index)
虚拟DOM
虚拟DOM本质是js对象,用js对象模拟DOM树,通过diff算法比较新旧DOM树上的差异,把差异应用到真实DOM树上。 优点:效率高、节约性能
data为什么必须是一个函数?
因为一个组件可能会被复用,为了避免数据污染,所以data是一个函数并且返回一个对象
data() {
return {}
}
配置环境变量和模式
// 和src同级目录
.env 在所有的环境中被载入
.env.local 在所有的环境中被载入,但会被 git 忽略
.env.[mode] 只在指定的模式中被载入
.env.[mode].local 只在指定的模式中被载入,但会被 git 忽略
// 比如常用
.env.development
.env.production
.env.test // 必写NODE_ENV,不然报错
// 默认有两项
BASE_URL: "/" // 修改不成功
NODE_ENV: "development" // 修改成功
VUE_APP_*: // 自定义的变量
修饰符和修饰键
自定义事件修饰符:
.sync 主要用于子组件直接改变父组件的值,目前常用两种方式: 父组件:
第一种使用方式:
父组件
<Child :str.sync="str" />
data() {
return {
str: "lxh",
}
}
子组件
<button @click="change">改变父组件的str</button>
methods: {
change() {
// update: 固定写法
this.$emit("update:str", "LXH");
}
}
第二种使用方式
父组件
<Child v-bind.sync="obj" />
data() {
return {
obj: {
title: "lxh",
age: 25,
},
}
}
子组件
<button @click="change">改变父组件的str</button>
methods: {
change() {
// update: 固定写法
this.$emit("update:title", "LXH");
this.$emit("update:age", 26);
}
}
v-model的修饰符:
- .lazy 失去焦点后才会修改数据
- .trim 去掉首尾空格
- .number 将值的类型转为数值类型
- .number两种特殊情况:
- 先输入数字,只取前面数字部分,值为number类型
- 先输入除数字外的任意字符,.number无效,值为string类型
事件修饰符:
- .stop:阻止冒泡
- .prevent:阻止默认事件(比如a标签的的跳转)
- .capture:事件默认是由内往外冒泡,这个的作用是由外往内捕获
- .self:只有点击事件绑定的本身(event.target)才会触发
- .once:事件只执行一次
- .passive:当我们监听元素滚动事件的时候,会一直触发onscroll事件:
- 在pc端没有什么问题,所以基本上用不到这个修饰符
- 在移动端会让网页变卡,所以可以使用这个修饰符,相当于给onscroll事件加了.lazy修饰符
注意:修饰符的使用顺序很重要,比如:
- @click.prevent.self 会阻止所有的点击
- @click.self.prevent 只会阻止对元素自身的点击
按键修饰符:
一般结合@keyup和@keydown使用
- .enter
- .tab
- .delete (捕获“删除”和“退格”键)
- .esc
- .space
- .up
- .down
- .left
- .right
系统修饰键
- .ctrl
- .shift
- .alt
- .meta(windows键)
- .exact 精确的匹配系统修饰符
鼠标按钮修饰符
- .left
- .right
- .middle
内部指令
- v-show 显示与隐藏,切换元素的css样式属性display
- v-if 显示与隐藏,创建、销毁DOM元素实现切换效果
- v-else-if 前一个兄弟元素必须有v-if或v-else-if
- v-else 前一个兄弟元素必须有v-if或v-else-if
- v-for 遍历数组、对象、数字、字符串
- v-model 双向绑定输入框的值
- v-on 缩写@,绑定事件
- v-bind 缩写:,动态绑定变量
- v-slot 缩写#,插槽名
- v-once 元素和组件只渲染一次
- v-html 更新元素的innerHTML
- v-text 相当于{{}},更新元素的textContent
- v-pre 跳过这个元素和它的子元素的编译过程
- v-cloak 这个指令保持在元素上,直到关联实例结束编译
v-cloak和css规则如:
[v-cloak]: { display: none; }
这个指令可以隐藏未编译的标签,直到实例准备完毕
注意:
- v-if和v-for一起使用时,v-for比v-if的优先级高,不推荐一起使用。
- 频繁显隐使用v-show,否则使用v-if。
组件之间传值
父子组件
- 父组件传值给子组件,子组件使用props进行接收
- 子组件传值给父组件,子组件使用 $emit+事件 分发的方式对父组件进行传值
- 组件中可以使用 children 获取到父组件实例和子组件实例,进而获取数据
- 使用 Vuex 进行状态管理
- 使用 $refs 获取组件实例,进而获取数据
- 使用 listeners,在对一些组件进行二次封装时可以方便传值
父组件使用v-mode给子组件传参:
父组件:
<Child v-model="obj">
data() {
obj: {
title: "lxh",
}
}
子组件:
<button @click="btn">子组件改变父组件的obj</button>
props: {
/**
* 父组件通过v-model的方式传给子组件
* 默认名叫value
* 修改默认写法在model对象中的prop
* 不再此处(props)声明
* 可以通过this.$attrs.value访问
**/
value: {
type: Object,
},
},
// model 不需要修改默认值就可以不用写model
model: {
prop: "value", // 默认值 value
event: "input", // 默认值 input
},
methods: {
btn() {
/**
* 此处修改父组件的obj
* input是默认的写法
* 修改默认写法在model对象中的event
**/
this.$emit("input", { title: "LXH"})
}
}
兄弟组件
- Vuex
- $parent
- 使eventBus 进行跨组件触发事件,进而传递数据,使用如下:
- $emit() 发送
- $on() 接收
- $off() 移除
跨层级组件
- vuex
- eventBus
- root.name="data",取this.$root.name(此方法不推荐)
- 使用 provide 和 inject,官方建议我们不要用这个,但是ElementUI源码大量使用
路由
路由懒加载
- component:
() => import('路径') - component:
(resolve) => require(['路径'], resolve) - component:
r => require.ensure([], () => r(require('路径')), 'login')
路由模式
history模式
- 通过pushState和replaceState切换url
- 优点:符合url地址规范,看起来美观
- 缺点:
- 兼容性比较差
- 当用户手动输入地址和刷新页面都会发起url请求,后端需要配置index.html页面,用于用户匹配不到静态资源的情况,否者会出现404错误。
hash模式
- 通过hashChange()事件监听hash值的变化,根据路由表对应的hash值来判断加载对应的路由和组件。
- 优点:
- 兼容性比较好
- 只需要前端的配置,不需要后端的参与
- 缺点:
- 不符合url地址规范
- 不美观
动态绑定class和style
- 动态class对象:
<div :class="{'isActive': true, 'red': isRed}"></div> - 动态class数组:
<div :class="['isActive', isRed ? 'red' : 'pink' ]"></div> - 动态style对象:
<div :style="{ color: textColor, fontSize: '18px' }"></div> - 动态style数组:
<div :style="[{ color: tColor, fontSize: '18px' }]"></div>
computed和watch
computed是依赖已使用的变量,来计算一个结果,并且计算的结果会被缓存,依赖变量的值不改变,直接从缓存中读取结果。computed不支持异步操作。
computed传参数的写法:
computed: {
lxh() {
return (e) => {
// e就是传过来的数据
return e
}
}
}
写成箭头函数的形式:
computed: {
lxh: (_this) => (e) => _this.xxx + e
}
watch监听一个变量的变化,并执行相应的回调函数。watch支持异步操作。
watch的完整写法:
watch: {
lxh: {
deep: true, // 开启深度监听
immediate: true, // 立即监听
hander() {
// 异步操作
}
}
}
vue生命周期
- beforeCreate 创建了实例(此时this有值),但没有初始化数据和响应式处理
- created 数据初始化和响应式处理完成,可以访问/修改到数据(data)
- beforeMount render函数在这里被调用,生成虚拟DOM,但还没有转成真实DOM
- Mounted 真实DOM挂载完毕
- beforeUpdate 新的虚拟DOM生成,但还没有跟旧的虚拟DOM做对比打补丁
- updated 新旧虚拟DOM对比打补丁后,进行真实DOM的更新
- beforeDestroy 实例销毁之前调用,此时还能访问到数据
- destroyed 实例销毁后调用,访问不到数据
被keep-alive包裹的组件多两个周期函数
- activated 组件激活时调用
- deactivated 组件停用是调用
下面这个周期函数用得少了解一下:errorCaptured 子孙组件发生错误时被调用,此时会收到三个参数:错误对象、发生错误组件的实例、错误来源信息的字符串,此钩子可以返回false阻止错误向上传播
vuex
- state 设置初始状态
- getters 相当于state的计算属性,接收参数state
- mutations 唯一更改store中状态的方法,是同步函数,接收两个参数state、value(传过来的数据)
- actions 用于提交mutations,而不是直接变更状态,可以包含任意异步操作,接收两个参数context(里面有state、commit等属性)、value(传过来的数据)
- modules 模块化的使用store
不使用模块化
import { mapState, mapGetters, mapMutations, mapActions } from "vuex";
computed: {
// 获取state的两种方式
state() {
return this.$store.state
}
...mapState(['name'])
// 获取getters的两种方式
getters() {
return this.$store.getters
}
...mapGetters(['age'])
}
methods: {
...mapMutations(['increment'])
...mapActions(['decrement'])
// commit 操作mutations:this.$store.commit('increment', 传过去的值)
// dispatch 操作actions:this.$store.dispatch('decrement', 传过去的值)
}
// 使用
{{name}} -- {{age}}
使用模块化
第一步开启命名空间:namespaced: true
需要使用到vuex里的modules属性
import { mapState, mapGetters, mapMutations, mapActions } from "vuex";
computed: {
// 获取state的两种方式
lxh() {
return this.$store.state.lxh
}
...mapState('lxh', ['name'])
// 获取getters的两种方式
getters() {
return this.$store.getters["lxh/getLxhName"]
}
...mapGetters('lxh', ['age'])
}
methods: {
...mapMutations('lxh', ['increment'])
...mapActions('lxh', ['decrement'])
// commit 操作mutations:this.$store.commit('lxh/increment', 传过去的值)
// dispatch 操作actions:this.$store.dispatch('lxh/decrement', 传过去的值)
}
// 使用
{{name}} -- {{age}}
通过require.context引入全部模块
store/index.js 每个引入modules下的文件
const modulesFiles = require.context('./modules', true, /\.js$/)
const modules = modulesFiles.keys().reduce((modules, modulePath) => {
const moduleName = modulePath.replace(/^\.\/(.*)\.\w+$/, '$1')
const OBJ = modulesFiles(modulePath)
modules[moduleName] = OBJ.default ? OBJ.default : OBJ[moduleName]
return modules
}, {})
持久化
将store/index.js文件改成下面格式导出,方便下面的paths属性
const store = new Vuex.Store({
state: {},
getters: {},
mutations: {},
actions: {},
mudules: {},
plugins: {},
})
export default store;
function getStateKeys() {
setTimeout(() => {
return Object.keys(store.state)
});
}
这里介绍插件:vuex-persistedstate
安装:npm i vuex-persistedstate
引入:import createPersistedState from "vuex-persistedstate"
使用:
plugins: [createPersistedState({
storage: window.sessionStorage, // 存储位置,默认localStorage
storage: { // 加密的话可以这样写,btoa加密atob解密window的方法
getItem: (key) => window.atob(sessionStorage.getItem(key)),
setItem: (key, val) => sessionStorage.setItem(key, window.btoa(val)),
removeItem: (key) => sessionStorage.removeItem(key),
},
key: 'vuex-keys', // 存在sessionStorage的key值
// paths: getStateKeys(),// 存哪些数据,可以省略不写
})],
定义不需要响应式的数据(死数据)
第一种定义在data中,但是在return之上:
data() {
this.deadData = "死数据";
return {}
}
第二种定义在data中:
data() {
return {
deadData: Object.freeze("死数据")
}
}
父子组件执行顺序
父beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount->子mounted->父mounted
插槽
匿名插槽
父组件:
<Child>lxh</Child>
// 旧语法
<Child><div slot='default'>lxh</div></Child>
<Child><template slot='default'>lxh</template></Child>
// 新语法
<Child><template #default>lxh</template></Child>
<Child><template v-slot:default>lxh</template></Child>
子组件:
<slot></slot>
<slot name="default"></slot>
默认是default
具名插槽
子组件:
<slot name="one"></slot>
父组件:
<Child>
// 旧语法
<template slot="one"></template>
<div slot="one"></div>
// 新语法
<template #one></template>
<template v-slot:one></template>
</Child>
作用域插槽
子组件:
<slot :childData="{name: 'lxh'}"></slot>
父组件:
<Child>
// 旧语法
<template slot-scoped="{childData}">{{childData.name}}</template>
// 新语法
<template #default="{childData}">{{childData.name}}</template>
</Child>
自定义指令
局部指令
directives: {
lxh: {
/**
* el dom
* binding {name,value,oldValue,expression,arg,modifiers}
* 详细含义看文档https://cn.vuejs.org/v2/guide/custom-directive.html
* vnode 虚拟节点
* oldVnode 上一个虚拟节点
**/
bind(el, binding, vnode) {},
inserted(el, binding, vnode) {vnode.context.$nextTick(() => {});},
update(el, binding, vnode, oldVnode) {},
componentUpdated(el, binding, vnode, oldVnode) {},
unbind(el, binding, vnode) {},
},
// 想在bind和update触发相同行为,不关注其他钩子,这些缩写
lxh(el, binding) {
// 改变字体颜色
el.style.color = "red";
}
}
全局指令
Vue.directive("lxh", {
bind(el, binding, vnode) {},
inserted(el, binding, vnode) {vnode.context.$nextTick(() => {});},
update(el, binding, vnode, oldVnode) {},
componentUpdated(el, binding, vnode, oldVnode) {},
unbind(el, binding, vnode) {},
})
常用指令
export default {
install(Vue) {
// 点击复制
Vue.directive('cope', {
bind(el, { value }) {
el.$value = value
el.handle = () => {
if(!el.$value) {
console.log('暂无数据')
}
navigator.clipboard.writeText(el.$value).then(() => {
console.log("复制成功");
});
}
el.addEventListener('click', el.handle)
},
componentUpdated(el, { value }) {
el.$value = value
},
unbind(el) {
el.removeEventListener('click', el.handle)
},
})
// 防抖
Vue.directive("debounce", {
bind(el, { value }) {
let timer;
el.handle = () => {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
value();
}, 1000);
};
el.addEventListener("click", el.handle);
},
unbind(el) {
el.removeEventListener("click", el.handle);
},
});
}
}
nextTick
将回调延迟到下次DOM更新循环之后执行this.$nextTick(() => {})
props
基本类型数据不能改,会报错;
引用类型数据可以操作里面的属性
修改可以直接this.obj.name = 'xxx'
新增属性:this.$set(this.obj, 'age', '18')
删除属性:this.$delete(this.obj, 'age')
// 完整版
props: {
lxh: {
type: Array, // 类型大写
default: () => [1, 2, 3], // 默认值
required: true, // 必传
validator(e) { // 验证
// e是lxh的值,必须返回一个真,不然会报错
return true;
}
}
}
// 缩写形式
props: {
lxh: String,
}
// 数组形式
props: ['xxx', 'yyy', 'lxh'],
hook
常用取消定时器:
let timer = setInterval(() => {}, 1000);
this.$once('hook:beforeDestroy', () =>{
clearInterval(timer);
timer = null;
})
常用在子组件的周期函数里执行父组件的某一函数
父组件:
<Child @hook:mounted='childMountedHandler'></Child>
methods: {
childMountedHandler() {}
}
provide和inject
祖先组件:
provide: {}, // 不推荐这种写法
provide() {
return {
// 值是基本类型,不是响应式的数据,引用类型就是响应式的数据
property: 'value',
xxx: this.changeValue,
// 通过函数的方式也可以,注意:函数作为value,而不是this.changeValue()
}
},
// 推荐写法
provide() {
return {
parentData: () =>({
str: 'xxx',
obj: {
name: 'xxx',
}
})
}
}
后代组件:
inject: ['property'],
inject: {
xxx: {
from: 'property', // 重命名
default: '默认值',
}
},
// 推荐接收写法
inject: ['parentData'],
computed: {
parentObj() {
return this.parentData()
}
}
// 这样就直接用this.parentObj.str就是具有响应式的数据
this.parentObj.obj.name = 'yyy' // 可以修改
this.parentObj.str = 'yyy' // 修改不了
// 传过来的对象可以修改里面的属性,不能直接修改传过来的变量
transition
// 默认name是v,appear是开始就执行
<transition name="v" appear>
<div>xxx</div>
</transition>
.v-enter-active {
animation: ani 2s;
}
.v-leave-active {
animation: ani 1s reverse;
}
@keyframes ani {
from {
transform: translate(-100%);
}
to {
transform: translate(0);
}
}
// 进入开始
.v-enter {
transform: translate(-100%);
}
// 进入结束
.v-enter-to {
transform: translate(0);
}
// 进入行为
.v-enter-active {
transition: transform 2s linear;
}
// 离开开始
.v-leave {
transform: translate(0);
}
// 离开结束
.v-leave-to {
transform: translate(100%);
}
// 离开行为
.v-leave-active {
transition: transform 1s linear;
}
vue3的改变
.v-enter -> v-enter-from
.v-leave -> v-leave-from
第三方动画animate.css
安装:npm install animate.css --save
mixins
组件和混入冲突:
- 数据合并,优先级:组件、局部、全局;
- 生命周期函数合并成数组,都执行,执行顺序:全局、局部、组件;
methods(方法)、directives(指令)等,合并成对象,优先:组件、局部、全局;
mixin.js
export default {
data() {
return {}
},
mounted() {},
methods: {},
}
// 局部混入
import mixin from './plugins/mixin.js'
mixins: [mixin],
// 全局混入(不推荐)
Vue.mixin({
data() {
return {}
}
})
自定义插件
src/plugins.js(文件名自定义)
export default {
install(Vue) {
/**
* 可以定义全局过滤器、全局指令、定义混入、Vue原型上添加方法等
**/
Vue.prototype.$api = "http://127.128.0.0:8888"
}
}
src/main.js
import plugins from "./plugins.js";
Vue.use(plugins) // 使用插件
vue.config.js(跨域、优化等)
根目录下新建vue.config.js文件
const path = require("path");
const resolve = (dir) => path.join(__dirname, dir);
const isPro = process.env.NODE_ENV === "production"; // 判断当前环境
let externals = {};
let cdn = { css: [], js: [] };
if (isPro) {
cdn = {
/**
* 国内:
* BootCDN网站(https://www.bootcdn.cn)
* 七牛云(http://staticfile.org/)
* 国外:
* unpkg网站(https://unpkg.com)
* cdnjs网站(https://cdnjs.com/)
* jsdelivr网站(https://www.jsdelivr.com/)
**/
css: [
"https://unpkg.com/element-ui/lib/theme-chalk/index.css", // element-ui
],
js: [
"https://cdn.jsdelivr.net/npm/vue@2.6.11/dist/vue.min.js", // vue
"https://cdn.jsdelivr.net/npm/vuex@3.4.0/dist/vuex.min.js", // vuex
"https://cdn.jsdelivr.net/npm/vue-router@3.2.0/dist/vue-router.min.js", // vue-router
"https://unpkg.com/element-ui/lib/index.js", // element-ui
"https://cdn.bootcdn.net/ajax/libs/axios/0.24.0/axios.min.js", // axios
],
};
externals = {
// cdn引入 键值对:键是你要cdn引用的插件名,值是固定的,只能百,'包名':'在项目中引入的名字'
vue: "Vue",
vuex: "Vuex",
"vue-router": "VueRouter",
"element-ui": "ELEMENT",
"element-plus": "ElementPlus",
echarts: "echarts",
axios: "axios",
swiper: "swiper",
xlsx: "XLSX",
};
}
module.exports = {
publicPath: isPro ? "./" : "/", // 打包配置
outputDir: "dist",
indexPath: "index.html",
lintOnSave: false,
productionSourceMap: false,
devServer: {
open: true,
proxy: {
"/api": {
target: "http://localhost:3000",
changOrigin: true, //允许跨域
pathRewrite: { "^/api": "" },
},
},
},
configureWebpack: {
name: "我的vue2模板", // 设置项目名
externals: externals,
},
chainWebpack: (config) => {
if (isPro) {
// 移除 prefetch 插件
config.plugins.delete("prefetch");
// 移除 preload 插件
// config.plugins.delete("preload");
// 压缩代码
config.optimization.minimize(true);
// 分割代码
config.optimization.splitChunks({
chunks: "all",
});
// 去除console.log
config.optimization.minimizer("terser").tap((args) => {
args[0].terserOptions.compress.drop_console = true;
return args;
});
}
config.plugin("preload").tap(() => [
{
rel: "preload",
fileBlacklist: [/\.map$/, /hot-update\.js$/, /runtime\..*\.js$/],
include: "initial",
},
]);
// 注入cdn变量 (打包时会执行)
config.plugin("html").tap((args) => {
args[0].cdn = cdn; // 配置cdn给插件
return args;
});
config.resolve.alias
.set("@", resolve("src"))
.set("utils", resolve("src/utils"))
.set("views", resolve("src/views"))
.set("store", resolve("src/store"))
.set("assets", resolve("src/assets"))
.set("router", resolve("src/router"))
.set("components", resolve("src/components"));
},
};
public/index.html
<title><%= webpackConfig.name %></title>
<!-- 引入样式 -->
<% for(var css of htmlWebpackPlugin.options.cdn.css) { %>
<link rel="stylesheet" href="<%=css%>" />
<% } %>
<!-- 引入JS -->
<% for(var js of htmlWebpackPlugin.options.cdn.js) { %>
<script src="<%=js%>"></script>
<% } %>
filters
vue3中已移除过滤器
{{ str | fnName }}
<div :str="str | fnName"></div>
filters: {
fnName(value) {
return value;
},
},
SSR
- 服务器渲染
- 基于node.js serve服务器环境开发,所有html代码在服务器渲染
- 数据返回给前端,然后前端进行“激活”,即可成为浏览器识别的html代码
- 优点:首次加载更快,有更好的用户体验,有更好的seo优化。因为爬虫能看到整个页面的内容,如果是vue项目,由于数据还要经过解析,然而爬虫不会等待数据加载完成,所以vue项目的seo体验不是很好。
vue3
vue2和vue3的区别:
- 性能更比Vue2强。
- 打包更科学不再打包没用到的模块
- Composition API(组合API)
- Fragment、Teleport、Suspense
- 更友好的支持兼容TS
- Custom Renderer API(自定义渲染API)