主题
Vue3
相关
Vite、
路由管理: Vue-Router
VS Code+Volar 编辑器 + 语法提示工具作为上层开发工具;使用 Vite 作为工程化工具;使用 Chrome 进行调试
hello world
https://mp.weixin.qq.com/s/x68-rP8XvdKn-BcgJCkFcQ
shell
nvm use 18.17.1
npm create vite@latest
选择 Vue + TypeScript
npm install vue-router@next vuex@next
js
<template>
<div>
<h1 @click="add">{{count}}</h1>
</div>
</template>
<script setup>
import { ref } from "vue";
let count = ref(1)
function add(){
count.value++
}
</script>
Composition Api
ref、reactive、computed、watch
ref
js
import { ref } from 'vue'
export default {
setup() {
const count = ref(0)
console.log(count) // { value: 0 }
console.log(count.value) // 0
function increment() {
count.value++
console.log(count.value) // 1
}
return {
count,
increment
}
}
}
<div>{{ count }}</div>
<script setup>
vue
<script setup>
import { ref } from 'vue'
const count = ref(0)
function increment() {
count.value++
}
</script>
<template>
<button @click="increment">
{{ count }}
</button>
</template>
reactive
js
import { reactive } from 'vue'
const state = reactive({ count: 0 })
<button @click="state.count++">
{{ state.count }}
</button>
js
const raw = {}
const proxy = reactive(raw)
// proxy is NOT equal to the original.
console.log(proxy === raw) // false
// calling reactive() on the same object returns the same proxy
console.log(reactive(raw) === proxy) // true
// calling reactive() on a proxy returns itself
console.log(reactive(proxy) === proxy) // true
const proxy = reactive({})
const raw = {}
proxy.nested = raw
console.log(proxy.nested === raw) // false
js
const count = ref(0)
const state = reactive({
count
})
console.log(state.count) // 0
state.count = 1
console.log(count.value) // 1
集合
js
const books = reactive([ref('Vue 3 Guide')])
// need .value here
console.log(books[0].value)
const map = reactive(new Map([['count', ref(0)]]))
// need .value here
console.log(map.get('count').value)
计算属性 computed
vue
<script setup>
import { reactive, computed } from 'vue'
const author = reactive({
name: 'John Doe',
books: [
'Vue 2 - Advanced Guide',
'Vue 3 - Basic Guide',
'Vue 4 - The Mystery'
]
})
// a computed ref
const publishedBooksMessage = computed(() => {
return author.books.length > 0 ? 'Yes' : 'No'
})
</script>
<template>
<p>Has published books:</p>
<span>{{ publishedBooksMessage }}</span>
</template>
组件通信-传值
通信方式:
- props:父传子(子组件接收的数据只读)
- emit:子传父
- provide/inject:跨代传值
- vuex/pinia:跨组件传值
- 全局事件总线(mitt 或 tiny-emitter):不推荐使用
- v-model
- refs
一、props & emit
https://cn.vuejs.org/api/sfc-script-setup.html#defineprops-defineemits
- 父组件:
:list
传值 - 子组件:
defineProps
接收父组件值,defineEmits
调用父组件方法传值到父组件中 eg: proxy.$emit('handle-succ', data);
父组件
vue
<template>
<h3>{{ count }}</h3>
<HelloWorld :list="[{ name: '小郑' }]" @add="handleAdd" />
</template>
<script setup>
import { ref } from "vue";
import HelloWorld from "./components/HelloWorld.vue";
const count = ref(0);
function handleAdd(data) {
console.log(data);
count.value++;
}
</script>
<style scoped></style>
子组件
vue
<template>
<h3>{{ list }}</h3>
<button @click="handleSubmit">提交数据</button>
</template>
<script setup>
// 父传子
const props = defineProps({
list: {
type: Array,
required: false,
default: () => [],
},
});
// 子传父
const emits = defineEmits(["add"]);
function handleSubmit() {
emits("add", "child");
}
// 或直接通过 proxy.$emit("add", "child");
</script>
<style scoped></style>
二、v-model 父子组件双向绑定
- 父组件:
v-model
- 子组件:
props
接收父组件值modelValue
,proxy.$emit("update:modelValue", 666);
传值
tips: 如果父组件是
v-model:num
,那么子组件的modelValue
变更为num
uniapp中可通过 props 来获取页面参数 (tips:子组件内无法通过这种方式获取到路径参数!) eg: /pages/index/index?code=xxx
==> const props = defineProps({ code: { type: String, required: false } });
父组件
vue
<template>
<h3>父组件:{{ data }}</h3>
<HelloWorld ref="helloRef" v-model="data" />
</template>
<script setup>
import HelloWorld from "./components/HelloWorld.vue";
const data = ref(0);
</script>
<style scoped></style>
子组件
vue
<template>
<h3>子组件:{{ modelValue }}</h3>
<button @click="changeData">click</button>
</template>
<script setup>
const { proxy } = getCurrentInstance();
const props = defineProps({
modelValue: {
type: Number,
required: false,
default: () => 0,
},
});
function changeData() {
proxy.$emit("update:modelValue", 666);
}
</script>
<style scoped></style>
三、provide/inject:跨代传值
父组件
js
import { provide } from 'vue'
provide('msg', xxx)
子子组件
js
import { inject } from 'vue'
const msg = inject('msg')
const msg = inject('msg', 'hello') // 没值的话使用默认值hello
父组件调用子组件方法
如果父组件想要调用子组件的方法
- 子组件为选项式api 可以在父组件中使用
proxy.$refs.helloRef.changeData();
调用 - 子组件为组合式api 需在子组件中使用
defineExpose
暴露需要调用的方法
父组件
vue
<template>
<HelloWorld ref="helloRef" />
<button @click="handleClick">click</button>
<br />
<button @click="$refs.helloRef.changeData()">调用子组件方法</button>
</template>
<script setup>
const { proxy } = getCurrentInstance();
import HelloWorld from "./components/HelloWorld.vue";
function handleClick() {
proxy.$refs.helloRef.changeData();
}
</script>
<style scoped></style>
子组件
vue
<template>
<h3>count:{{ count }}</h3>
</template>
<script setup>
const count = ref(0);
function changeData() {
count.value++;
}
// 暴露方法
defineExpose({
changeData,
});
</script>
<style scoped></style>
子组件调用父组件方法
父组件
vue
<template>
<HelloWorld @ok="handleOk" />
</template>
<script setup>
import { ref } from "vue";
import HelloWorld from "./components/HelloWorld.vue";
function handleOk(data) {
console.log(data);
}
</script>
<style scoped></style>
子组件
法一:
js
<script setup>
const { proxy } = getCurrentInstance();
function changeData() {
proxy.$emit('ok', 'hello');
}
</script>
法二:
js
<script setup>
const { proxy } = getCurrentInstance();
const emits = defineEmits(["ok"]);
function changeData() {
emits("ok", "hello");
}
</script>
插槽
父组件
vue
<template>
<HelloWorld>
<template v-slot:left>具名插槽-left</template>
<template #right>具名插槽-right</template>
<template #default="{ data }">作用域插槽:{{ data }}</template>
<template #[xx]>动态插槽</template>
</HelloWorld>
</template>
<script setup>
import HelloWorld from "./components/HelloWorld.vue";
let xx = ref("dynamic"); // 这里可以随时变 dynamic/left => xx
</script>
<style scoped></style>
子组件
vue
<template>
<h3>hello</h3>
<slot name="left"></slot>
<br />
<slot name="right"></slot>
<div v-for="item in list" :key="item.id">
<slot :data="item"></slot>
<!-- 作用域插槽可回调值给父组件使用 <template #right-show="{ isShow }"></template> -->
<!-- <slot name="right-show" :is-show="isShowRightMenu" /> -->
</div>
<slot name="dynamic"></slot>
</template>
<script setup>
let list = ref([
{ id: 1, name: "小张" },
{ id: 2, name: "小李" },
]);
</script>
<style scoped></style>
生命周期钩子
https://cn.vuejs.org/api/composition-api-lifecycle.html#onmounted
vue
<template>
<h3>hello</h3>
</template>
<script setup>
console.log(111);
onMounted(() => {
console.log(222);
});
</script>
<style scoped></style>
Teleport
可以将一个组件内部的一部分模板“传送”到该组件的 DOM 结构外层的位置去。
vue
<template>
<div class="body"></div>
<button @click="open = true">Open Modal</button>
<Teleport to=".body">
<div v-if="open" class="modal">
<p>Hello from the modal!</p>
<button @click="open = false">Close</button>
</div>
</Teleport>
</template>
<script setup>
let open = ref(false);
</script>
<style scoped>
.body {
width: 200px;
height: 200px;
background-color: #0ee757;
}
.modal {
background-color: #1789ca;
}
</style>
动态组件
https://cn.vuejs.org/guide/essentials/component-basics.html#dynamic-components
有些场景会需要在两个组件间来回切换,比如 Tab 界面
vue
<template>
<div class="home">
<ul>
<li v-for="(item, index) in tabList" :key="index" @click="change(index)">
{{ item.name }}
</li>
</ul>
<component :is="currentComponent.com"></component>
</div>
</template>
<script setup>
import { reactive } from "vue";
import A from "./A.vue";
import B from "./B.vue";
let tabList = reactive([
{ name: "显示组件A", com: markRaw(A) },
{ name: "显示组件B", com: markRaw(B) },
]);
let currentComponent = reactive({
com: A,
});
const change = (index) => {
currentComponent.com = tabList[index].com;
};
</script>
异步组件
在大型项目中,我们可能需要拆分应用为更小的块,并仅在需要时再从服务器加载相关组件。
访问资源
3种方式
js
import testVideo from '@/assets/test.mp4';
videoSrc: require('@/assets/test.mp4'),
async mounted() {
const videoSrc = await import('@/assets/test.mp4');
this.videoSrc = videoSrc.default;
}
js
import { ref } from 'vue';
export default {
name: 'Counter',
props: ['initialCount'],
setup(props) {
const count = ref(props.initialCount);
function increment() {
count.value++;
}
return {
count,
increment,
};
},
};
使用props和emit
js
import { ref } from 'vue';
export default {
name: 'ChildComponent',
props: ['message'],
setup(props, context) {
const localMessage = ref(props.message);
function updateMessage(newMessage) {
localMessage.value = newMessage;
context.emit('update:message', newMessage);
}
return {
localMessage,
updateMessage,
};
},
};
响应式API
vue
<template>
<h3>{{ count }}</h3>
<h3>{{ data }}</h3>
<hr />
<h3>{{ getStr }}</h3>
<h3>{{ getStr2 }}</h3>
<input type="text" v-model="getStr2" />
<hr />
<h3>{{ name }}</h3>
<button @click="changeData">修改</button>
</template>
<script setup>
import { ref, reactive, computed, watch, watchEffect, toRefs } from "vue";
// ref:定义基本数据类型的响应式数据
const count = ref(0);
// reactive:定义“数组/对象/map”复杂数据类型的深层响应式数据,shallowReactive:浅层响应式(只保留对这个对象顶层次访问的响应性)
const data = reactive({
name: "小王",
age: 18,
girlfriends: [{ name: "小张" }],
});
// toRefs:解构响应式,没有的话,无法修改name值,修改的时候使用 name.value 修改
const { name } = toRefs(
reactive({
name: "小郑",
})
);
// computed:计算属性
const str = ref("hello");
const getStr = computed(() => {
console.log("计算属性执行了...");
return str.value;
});
// 如果要修改计算属性值,上面的方式会报错 Write operation failed: computed value is readonly
// 使用下面的方式
const getStr2 = computed({
get() {
console.log("计算属性执行了...");
return str.value;
},
set(val) {
str.value = val;
},
});
// watch:监听器
watch(
count, // ref
// () => geoObj.value.lng, // ref 对象中的某一个属性值
// data, // reactive
// () => data.age, // reactive 对象中的某一个属性值
// [count, data], // 监听多个数据
// () => props.list, // 监听defineProps中的数据
// () => proxy.$router.currentRoute.value, // 监听路由变化
(newValue, oldValue) => {
console.log("监听器执行了... ", newValue, oldValue);
},
{
immediate: true, // 初始化执行一次
// deep: true, // 深度监听 -- eg: 监听数组里面的数据变更
}
);
// watchEffect:副作用函数,里面涉及到的属性有变更就会被触发执行
watchEffect(() => {
console.log("watchEffect执行了... ", data.age);
});
function changeData() {
count.value++;
data.age++;
data.girlfriends = [{ name: "小甜" }, { name: "小李" }];
data.girlfriends.push({ name: "哈基米" });
name.value = "小郑变了";
str.value += "1";
}
</script>
<style scoped></style>
模板语法
vue
<template>
<h3>{{ text }}</h3>
<h3>{{ user }}</h3>
<!--
指令 v-
v-model 数据双向绑定
v-if 判断表达式的值,true则显示,false则隐藏 -- 控制dom元素的创建和销毁,应避免频繁切换状态
v-show 和v-if区别 -- 始终会被渲染并保留在dom中,只是css被隐藏了 "display: none;" 一次性的
v-for 循环
v-bind 绑定属性或对象
v-on 注册事件
-->
<input v-model="user.age" />
<span v-if="user.age == 18">成年人:{{ user.age }}</span>
<span v-else-if="user.age < 18">未成年</span>
<span v-else>长大了...</span>
<span v-show="user.age >= 18">
和v-if区别:始终会被渲染并保留在dom中,只是css被隐藏了 "display: none;"
</span>
<!-- <button v-bind:disabled="true">v-bind</button> -->
<button :disabled="true">v-bind</button>
<h6
v-for="(value, key, index) in user.friends"
:key="index"
style="color: rgb(70, 238, 146)"
>
{{ value }} - {{ key }} - {{ index }}
</h6>
<!-- <button v-on:click="addFriend">添加好友</button> -->
<button @click="addFriend">添加好友</button>
<p :class="{ active: isActive }">Class 与 Style 绑定</p>
</template>
<script setup>
import { ref, reactive } from "vue";
const text = ref("HelloWorld");
const isActive = ref(true);
const user = reactive({
name: "小郑",
age: 18,
friends: [
{
name: "小张",
age: 18,
},
{
name: "小李",
age: 20,
},
],
});
function addFriend() {
user.friends.push({
name: "Patrick",
age: 18,
});
}
</script>
<style scoped>
.active {
color: rgb(134, 17, 250);
}
</style>
$ref语法糖 告别 .value
一、配置
法一
vue3.4版本之后废除
vite.config.js
js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [
vue({
reactivityTransform: true, // 启用响应式语法糖 $ref $computed $toRef ...
})
]
})
法二
https://vue-macros.sxzz.moe/zh-CN/features/reactivity-transform.html
tips: store(pinia版) 中使用
$ref
无法正常持久化数据!!!
shell
cnpm i -D @vue-macros/reactivity-transform
vite.config.js
js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ReactivityTransform from '@vue-macros/reactivity-transform/vite';
export default defineConfig({
plugins: [
vue(),
ReactivityTransform(), // 启用响应式语法糖 $ref ...
]
})
解决ESLint警告: '$ref' is not defined.
.eslintrc.cjs
js
module.exports = {
globals: { $ref: 'readonly', $computed: 'readonly', $shallowRef: 'readonly', $customRef: 'readonly', $toRef: 'readonly' },
};
二、测试
原本 .value 响应式
vue
<template>
<h1>{{ count }}</h1>
<button @click="handleClick">click</button>
</template>
<script setup>
let count = ref(0);
function handleClick() {
count.value++;
}
</script>
现在 $ref 去除 .value
vue
<template>
<h1>{{ count }}</h1>
<button @click="handleClick">click</button>
</template>
<script setup>
let count = $ref(0);
function handleClick() {
count++;
}
</script>
三、注意事项
$ref 在以下情况无法直接使用
- store pinia
- watch 监听器
unplugin-auto-import插件解决ref大量引入问题
解决 import { ref , reactive ..... } from 'vue'
大量引入的问题
配置后可以不用引入,直接使用
shell
cnpm i -D unplugin-auto-import
vite.config.js
js
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import AutoImport from "unplugin-auto-import/vite";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
AutoImport({
imports: ["vue", "vue-router"],
}),
],
});
CSS处理
内联样式
vue
<template>
<div :style="{ color: activeColor, fontSize: fontSize + 'px' }">Hello!</div>
</template>
<script>
export default {
data() {
return {
activeColor: 'red',
fontSize: 14,
};
},
};
</script>
<style>
标签 在 .vue 文件中,你可以使用 <style>
标签来定义组件的样式。默认情况下,这些样式是全局的,但你可以使用 scoped 属性来限制样式仅作用于当前组件
vue
<template>
<div class="greeting">Hello!</div>
</template>
<style scoped>
.greeting {
color: red;
}
</style>
CSS Modules
Vue 3 支持 CSS Modules,这是一种将 CSS 类名与组件绑定的技术,可以避免类名冲突
在模板中,你可以使用对象的属性名作为类名,而实际的类名将在运行时由 Vue DevTools 生成
vue
<template>
<div :class="styles.greeting">Hello!</div>
</template>
<style module="styles">
.greeting {
color: red;
}
</style>
使用 PostCSS Vue CLI 默认配置了 PostCSS,这意味着你可以使用 CSS 预处理器如 SCSS、SASS、LESS 等,也可以使用 CSS Next 的特性,如变量和函数。
js
<style lang="scss">
$primary-color: red;
.greeting {
color: $primary-color;
}
</style>
自动导入全局样式
在 vue.config.js 中,你可以配置自动导入全局样式文件,这样就不需要在每个组件中手动引入
js
// vue.config.js
module.exports = {
css: {
loaderOptions: {
scss: {
additionalData: '@import "@/styles/variables.scss";',
},
},
},
};
使用 CSS-in-JS 解决方案
虽然 Vue 不直接支持 CSS-in-JS,但你可以使用第三方库,如 styled-components 或 emotion,来实现这一目的
深度选择器
在使用 scoped 样式时,你可能会遇到需要穿透作用域去影响子组件的情况。你可以使用 /deep/
或 ::v-deep
来实现深度选择器
css
<style scoped>
/* 使用 /deep/ */
/deep/ .child-component-class {
color: blue;
}
/* 或使用 ::v-deep */
::v-deep .child-component-class {
color: blue;
}
</style>
新特性
1、响应式系统
Vue 2 的响应式机制是基于 Object.defineProperty() 这个 API 实现的,此外,Vue 还使用了 Proxy,这两者看起来都像是对数据的读写进行拦截,但是 defineProperty 是拦截具体某个属性,对不存在的属性无法拦截,所以 Vue 2 中所有数据必须要在 data 里声明。Proxy 才是真正的“代理”。Proxy 存在一些兼容性问题,这也是为什么 Vue 3 不兼容 IE11 以下的浏览器的原因,还好现在 IE 用的人不多了。
2、自定义渲染器
Vue 2 内部所有的模块都是揉在一起的,这样做会导致不好扩展的问题,Vue 3 使用拆包的方法解决这个问题,即使用 monorepo 管理方式,响应式、编译和运行时全部独立了
3、全部模块使用 TypeScript 重构
Vue 2 那个时代基本只有两个技术选型,Facebook 家的 Flow.js 和微软家的 TypeScript。Vue 2 选 Flow.js 没问题,但是现在 Flow.js 被抛弃了。Vue 3 选择了 TypeScript。
基于TypeScript的强类型,类型系统带来了更方便的提示;第二点是,类型系统让代码更健壮
4、Composition API 的组合语法
Vue 2 中 Options API 的写法简单好懂,但 Options API 的写法也有几个很严重的问题:由于所有数据都挂载在 this 之上,因而 Options API 的写法对 TypeScript 的类型推导很不友好,并且这样也不好做 Tree-shaking 清理代码。新增功能基本都得修改 data、method 等配置,并且代码上 300 行之后,会经常上下反复横跳,开发很痛苦。代码不好复用,Vue 2 的组件很难抽离通用逻辑,只能使用 mixin,还会带来命名冲突的问题。
使用 Composition API 后,虽然看起来烦琐了一些,但是带来了诸多好处:所有 API 都是 import 引入的。用到的功能都 import 进来,对 Tree-shaking 很友好,没用到功能,打包的时候会被清理掉 ,减小包的大小。不再上下反复横跳,可以把一个功能模块的 methods、data 都放在一起书写,维护更轻松。代码方便复用,可以把一个功能所有的 methods、data 封装在一个独立的函数里,复用代码非常容易。Composotion API 新增的 return 等语句,在实际项目中使用 <script setup>
特性可以清除。
5、新的组件
Vue 3 还内置了 Fragment、Teleport 和 Suspense 三个新组件。Fragment: Vue 3 组件不再要求有一个唯一的根节点,清除了很多无用的占位 div。Teleport: 允许组件渲染在别的元素内,主要开发弹窗组件的时候特别有用。Suspense: 异步组件,更方便开发有异步请求的组件。
6、新一代工程化工具 Vite
Vite 不在 Vue 3 的代码包内,和 Vue 也不是强绑定,Vite 的竞品是 Webpack。Webpack 的工作原理,就是根据你的 import 依赖逻辑,形成一个依赖图,然后调用对应的处理工具,把整个项目打包后,放在内存里再启动调试。由于要预打包,所以复杂项目的开发,启动调试环境需要 3 分钟都很常见,Vite 就是为了解决这个时间资源的消耗问题出现的。基于 ES6 的 import 语法,在调试环境下,不需要全部预打包,只是把你首页依赖的文件,依次通过网络请求去获取,整个开发体验得到巨大提升,做到了复杂项目的秒级调试和热更新。
使用自动化升级工具进行 Vue 的升级
js
"dependencies": {
- "vue": "^2.6.12",
+ "vue": "^3.2.19",
+ "@vue/compat": "^3.2.19"
...
},
"devDependencies": {
- "vue-template-compiler": "^2.6.12"
+ "@vue/compiler-sfc": "^3.2.19"
}
js
// vue.config.js
module.exports = {
chainWebpack: config => {
config.resolve.alias.set('vue', '@vue/compat')
......
}
}
这时你就会在控制台看到很多警告,以及很多优化的建议。参照建议,挨个去做优化就可以了。
初始化项目
- Vite4.4.5 https://cn.vitejs.dev/guide/
shell
# 创建项目
npm create vite@latest my-vue-app -- --template vue
# 安装依赖
npm i
# 运行
npm run dev
使用引入的 ref 函数包裹数字,返回的 count 变量就是响应式的数据,使用 add 函数实现数字的修改。对于 ref 返回的响应式数据,我们需要<mark>修改 .value </mark>
才能生效,而在 <script setup>
标签内定义的变量和函数,都可以在模板中直接使用。ref 和 computed 等功能都可以从 Vue 中全局引入,所以我们就可以把组件进行任意颗粒度的拆分和组合
Suspense
Vue 3 引入了 Suspense 组件,它是一个用于异步数据加载和资源加载时优雅地展示加载状态的组件。Suspense 提供了一个机制来协调异步组件的加载和渲染,同时允许你在等待组件加载完成时展示一个默认的“加载中”UI,当组件加载完毕后自动切换到组件的真实内容。
如何使用 Suspense 定义异步组件 Vue 3 中可以使用箭头函数定义异步组件,该函数返回一个 Promise,该 Promise 解析为组件定义对象
js
const AsyncComponent = () => import('./MyComponent.vue');
使用 Suspense 包裹异步组件 将你的异步组件包裹在 <Suspense>
标签内,并可以指定 default 和 fallback 槽来分别显示正常内容和加载中状态
vue
<template>
<Suspense>
<!-- 正常内容 -->
<template #default>
<AsyncComponent />
</template>
<!-- 加载中状态 -->
<template #fallback>
<div>Loading...</div>
</template>
</Suspense>
</template>
如何工作
当你访问包含 Suspense 的组件时,Vue 会开始加载所有的异步子组件。在所有异步组件加载完成之前,Suspense 的 fallback 槽会被渲染。一旦所有异步组件加载完成,Vue 会切换到渲染 default 槽的内容,即真实的组件内容。
注意事项
如果你的应用中有很多异步组件,使用 Suspense 可以确保页面不会因为等待某个组件的加载而阻塞整个页面的渲染。 Suspense 组件支持嵌套,这意味着你可以根据需要在应用的不同层级使用多个 Suspense 组件。 Suspense 对于提高用户体验非常有帮助,因为它允许你立即显示一个加载指示器,而不是让用户面对空白屏幕等待组件加载。 通过使用 Suspense,你可以使你的 Vue 3 应用更加流畅和响应迅速,尤其是在处理大型或复杂的数据加载场景时。
代码分割 Code Splitting
指将应用程序的代码分成多个较小的包,以便按需加载。这通常意味着只加载用户当前需要的部分代码,而不是一开始就加载整个应用的所有代码。
实现方法: 动态导入 (import() 语法) 使用ES6的动态导入语法,可以在运行时异步加载模块。这允许你将代码分割成不同的块,只有当需要的时候才加载它们
js
const loadModule = async () => {
const module = await import('./module.js');
// 使用module中的功能
};
Webpack 配置 如果你使用 Webpack,可以配置它自动分割代码。例如,使用splitChunks插件来自动分析和分割代码
js
optimization: {
splitChunks: {
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all'
}
}
}
}
懒加载(Lazy Loading)
懒加载是一种延迟加载策略,通常与代码分割一起使用,它确保了只有当特定部分的代码被请求时才会加载
路由懒加载
在Vue Router中,可以使用动态组件和异步组件结合的方式实现懒加载
js
const routes = [
{
path: '/user',
component: () => import(/* webpackChunkName: "user" */ './User.vue')
},
// ...
];
在React Router中,可以使用React的lazy和Suspense组件
js
import { lazy, Suspense } from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
const User = lazy(() => import('./User'));
function App() {
return (
<Router>
<Suspense fallback={<div>Loading...</div>}>
<Switch>
<Route path="/user">
<User />
</Route>
</Switch>
</Suspense>
</Router>
);
}
图片和资源的懒加载
对于图片和其他资源,可以使用JavaScript库如lozad.js或者CSS的loading属性来实现懒加载
单元测试
Mocha
Mocha 是一个流行的 JavaScript 测试框架,主要用于编写单元测试和集成测试。它具有高度可配置性,支持多种测试风格,并且可以与各种断言库(如 Chai、Should.js 或 Jest)配合使用
安装
shell
npm install --save-dev mocha
Mocha 的测试文件通常以 .spec.js
或 .test.js
结尾。每个测试文件可以包含多个测试套件(test suites),每个套件可以包含多个测试用例(test cases)。
js
// my-test.spec.js
const assert = require('assert');
describe('Array', function() {
describe('#indexOf()', function() {
it('should return -1 when the value is not present', function() {
assert.equal(-1, [1, 2, 3].indexOf(4));
});
});
});
describe 函数用于定义测试套件,it 函数用于定义测试用例。assert 是一个断言库,用来检查期望的结果是否与实际结果匹配。
配置Mocha
可以通过 mocha.opts
文件或 .mocharc.json
文件进行设置。这些文件可以包含各种选项,比如测试文件的模式、全局变量、测试运行的超时时间等。
例如,在根目录下创建一个 .mocharc.json 文件
json
{
"spec": "test/**/*.spec.js",
"timeout": 2000,
"ui": "bdd"
}
运行单元测试
shell
npx mocha
或者在package.json中配置
json
"scripts": {
"test": "mocha"
}
npm test
Mocha 还支持许多高级功能:
异步测试:使用 done 回调函数或 async/await 来处理异步代码。
测试间共享状态:使用 beforeEach 和 afterEach 来设置和清理测试环境。
测试覆盖率:与 Istanbul 或 nyc 配合使用,生成代码覆盖率报告。
断言库:选择你喜欢的断言库,如 Chai,以获得更丰富的断言方法
Jest
Jest 是一个非常受欢迎的 JavaScript 测试框架,尤其适合于现代前端开发,因为它提供了快照测试、模拟(mocks)、异步测试支持以及代码覆盖率报告等功能
安装
shell
npm install --save-dev jest
配置Jest
在项目根目录下创建一个 jest.config.js
文件,用于配置 Jest 的选项
js
module.exports = {
preset: 'ts-jest',
testEnvironment: 'jsdom',
testPathIgnorePatterns: ['<rootDir>/node_modules/'],
setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'],
};
在这个配置文件中,指定了 TypeScript 的预设(如果项目使用 TypeScript),测试运行的环境(对于前端应用,通常是 jsdom),忽略测试路径中的 node_modules 目录,以及在所有测试前执行的设置文件。
编写测试文件
Jest 支持使用.test.js
或 .spec.js
扩展名的测试文件。
测试文件中,你可以使用 describe 和 it 或 test 函数来组织和编写测试用例。Jest 提供了丰富的断言方法,例如 expect。
示例: 被测函数
js
// sum.js
export default function sum(a, b) {
return a + b;
}
测试文件
js
// sum.test.js
import sum from './sum';
describe('Sum function', () => {
test('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3);
});
test('handles large numbers', () => {
expect(sum(100000, 200000)).toBe(300000);
});
});
运行测试
在 package.json 中添加一个脚本
json
"scripts": {
"test": "jest"
}
npm test
Jest 提供了许多高级功能,包括:
快照测试:允许你捕获组件或函数的输出并保存为“快照”,之后的测试会比较输出是否与快照相同。
Mock 模拟:可以模拟模块、函数或对象的行为,以便于隔离测试。
异步测试:使用 async/await 或者 done 回调来测试异步代码。
Vue Devtools 调试
检查组件树 打开 DevTools 的 Vue 面板,你可以看到当前页面上的所有 Vue 组件及其层次结构。这可以帮助你快速定位到你关心的组件。
查看组件属性 选择一个组件,你可以查看它的属性(Props)、数据(Data)、计算属性(Computed)、方法(Methods)、侦听器(Watchers)以及自定义事件。这有助于理解组件的内部状态和行为。
修改组件状态 在组件的属性面板中,你可以直接修改组件的数据和计算属性,实时观察变化对界面的影响。
追踪生命周期钩子 Vue DevTools 可以显示组件的生命周期钩子调用顺序,这对于理解组件的加载和更新过程非常有用。
性能分析 使用 Vue DevTools 的 Profiler 功能,你可以记录组件渲染的性能,包括渲染次数和时间消耗。这对于优化应用性能特别有帮助。
源代码调试 Vue DevTools 还集成了源代码调试功能,允许你设置断点,逐步执行代码,查看调用堆栈,以及查看变量值。这与标准的 JavaScript 调试非常相似,但增加了对 Vue.js 特定功能的支持。
版本兼容性 确保你正在使用的 Vue DevTools 版本与你的 Vue.js 应用版本兼容。在 Vue 3 中,你可能需要
启用实验性功能
才能使用 DevTools。
在 Vue 3 中,为了兼容性原因,默认情况下,DevTools 可能不会自动检测你的应用。你可能需要在你的应用中显式启用它。在你的应用入口文件(如 main.js 或 main.ts)中加入以下代码
js
import { createApp } from 'vue';
import App from './App.vue';
const app = createApp(App);
if (import.meta.hot) {
import.meta.hot.accept();
import.meta.hot.dispose(() => {
app.unmount();
});
}
app.mount('#app');