Recently, I dedicated some time to revisit Vue.js, the first JavaScript framework I explored several years ago. Over the past two years, the framework has evolved, and revisiting it has provided me with a fresh and enriched perspective on its capabilities and applications. I am thrilled to share my insights and experiences with you!
1. Warning before leave
When you modify the form and exit unexpectly, you may lose the data. In this case, we can use router guard to reconfirm whether we really want to exit.
beforeRouteLeave(to, from, next) {
console.log('UsersList Cmp beforeRouteLeave');
console.log(to, from);
if (this.changesSaved) {
next(); // 如果没有未保存的更改,允许导航离开
} else {
// 如果有未保存的更改,询问用户是否确定离开
// 显示确认对话框,询问用户是否确定离开
const userWantsToLeave = prompt('Are you sure you want to leave? Unsaved changes will be lost!') === 'yes';
// const userWantsToLeave = confirm('Are you sure? You got unsaved changes!');
if (userWantsToLeave) {
next(); // 用户确认离开
} else {
next(false); // 用户取消离开
}
}
}
Now, we know why the function prompt
return a boolean value and can block the main thread.
Also, you can use another hook to decide whether the user really can see the page:
beforeRouteEnter(to, from, next) {
console.log('UsersList Cmp beforeRouteEnter');
console.log(to, from);
// 在这里可以进行一些操作,比如数据预加载
// 由于在这个阶段组件实例还没被创建,所以无法访问 this
// 可以通过传递回调给 next 来访问组件实例
next((vm) => {
// 现在可以访问组件实例 vm
console.log('Component instance is available as vm');
// 可以在这里调用组件的方法或访问数据
});
}
2. Render more than one RouterView
If we name the router-view component, then we can render more than just one router component at once:
routes: [
{
path: "/",
redirect: "/teams",
},
{
name: "teams",
path: "/teams",
meta: { needsAuth: true },
components: {
default: TeamsList,
footer: TeamsFooter,
},
children: [
// 在这里可以定义子路由
{
path: "detail",
component: TeamDetail,
},
{
path: "edit/:id", // 假设有一个编辑页面,需要传递id参数
component: TeamEdit,
},
// 可以继续添加更多的子路由
],
},
// 可以继续添加更多的顶级路由
];
That's to say, the value of property component
can be an object:
components: {
default: TeamsList,
footer: TeamsFooter
},
And default
is the key!
3. Use router guard to easily implement authorization
If we need to authorize some of the pages in our application while others are not need to, we can combine meta data and router guard to implement optional authorization easily:
router.beforeEach(function (to, from, next) {
console.log("Global beforeEach");
console.log(to, from);
// 检查目标路由是否需要认证
if (to.meta && to.meta.needsAuth) {
console.log("Needs auth!");
// 这里可以添加认证逻辑
// 例如,检查用户是否已经登录
if (isLoggedIn()) {
next(); // 如果已登录,允许导航
} else {
next({ path: "/login" }); // 如果未登录,重定向到登录页面
}
} else {
next(); // 如果不需要认证,直接允许导航
}
});
function isLoggedIn() {
// 这里应该实现检查用户登录状态的逻辑
// 例如,检查本地存储或全局状态
return !!localStorage.getItem("userToken");
}
4. Animation in Vue
Some fundamental knowledgement
We can start this journey from the transform
property in CSS!
.animate {
transform: translateX(-150px);
}
.block {
width: 8rem;
height: 8rem;
transition: transform 0.3s ease-out;
}
Or, use transition to change color:
/* 定义元素的初始颜色和过渡属性 */
.box {
width: 100px;
height: 100px;
background-color: blue; /* 初始颜色 */
transition: background-color 0.5s ease; /* 过渡效果 */
}
/* 定义悬停时的目标颜色 */
.box:hover {
background-color: red; /* 悬停时的颜色 */
}
The code above may be the most easy way to implement an animation in website. Transform tells the block how to move, and the transition tells that's the transform to move and how to move.
Besides tranform, there's indeed another way to implement animation.
.animate {
animation: slide-fade 0.3s ease-out forwards;
}
@keyframes side-fade {
0% {
transform: translateX(0) scale(1);
}
}
So, we actually have two methods to let block move: one is transition + transform
and the other one is animation + keyframes
. But what is the relationship between them?
Actually, we can use animation to do whatever transition can do. Because, transition defines two states while animation can define any numbers of state you want.
Animation in Vue
Vue provides with us a special component: transition to make precise control over the animation process. We wrap the target element into <transition></transition>
and then we can manipulate the whole process.
<div>
<transition>
<p v-if="paraIsVisible">This is only sometimes visible...</p>
</transition>
<button @click="toggleParagraph">Toggle Paragraph</button>
</div>
In different stage of the animation, Vue adds different class name to the target element.
In Vue.js, when using the <transition>
element, you can define animation effects for entering and leaving states. The keywords you provided seem to describe how to add class names for these states to achieve animation effects. Here is a detailed explanation of how to use these class names to create animation effects:
Defining Animation States
Vue.js provides several specific class names for the <transition>
element to control the animation of elements entering and leaving:
-
Entering States (Enter States)
*-enter-from
: Defines the starting state of the enter animation.*-enter-to
: Defines the ending state of the enter animation.*-enter-active
: Defines the duration and animation effect of the enter animation.
-
Leaving States (Leave States)
*-leave-from
: Defines the starting state of the leave animation.*-leave-to
: Defines the ending state of the leave animation.*-leave-active
: Defines the duration and animation effect of the leave animation.
*
refers to the name property of transition component. If the name is fade
then these classes will be: fade-enter-from fade-enter-to
, etc.
Example Code
Here is a complete example showing how to use these class names to implement a simple fade-in and fade-out animation:
<template>
<div>
<transition name="fade" @after-enter="afterEnter" @after-leave="afterLeave">
<p v-if="paraIsVisible" class="message">
This is only sometimes visible...
</p>
</transition>
<button @click="toggleParagraph">Toggle Paragraph</button>
</div>
</template>
<script>
export default {
data() {
return {
paraIsVisible: false,
};
},
methods: {
toggleParagraph() {
this.paraIsVisible = !this.paraIsVisible;
},
afterEnter(el) {
console.log("Entered");
},
afterLeave(el) {
console.log("Left");
},
},
};
</script>
<style>
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.fade-enter-to,
.fade-leave-from {
opacity: 1;
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s;
}
</style>
Code Explanation
<transition name="fade">
: Defines a transition namedfade
.v-if="paraIsVisible"
: Controls the display and hiding of the paragraph.@after-enter
and@after-leave
: These event hooks are called after the enter or leave transition is completed.- CSS Animation:
.fade-enter-from
and.fade-leave-to
: Set the opacity at the start or end of the animation..fade-enter-to
and.fade-leave-from
: Set the opacity at the end or start of the animation..fade-enter-active
and.fade-leave-active
: Set the duration and transition effect of the animation.
In this way, you can create smooth enter and leave animation effects. Make sure your CSS class names match the value of the transition's name
attribute, which is fade
in this example.
Futhermore, Vue can balance the animation with the life cycle hooks of the component, for example, the component will not unmount util the leaving animation ends. This is much more advanced than immediately unmounting the component especailly when there is a leaving animation if you don't use Vue but write the whole logic yourself.
And it's not neccessary to have the same prefix, we can assign the names of different state seperately:
<transition enter-active-class="some-class" enter-active-class="">
<p v-if="paraIsVisible">This is only sometimes visible...</p>
</transition>
Use transition to wrap dialog
It's a great idea to combine the dialog and transtion component:
<template>
<transition name="modal">
<div v-if="open" class="backdrop" @click="$emit('close')"></div>
<dialog v-if="open">
<slot></slot>
</dialog>
</transition>
</template>
<script>
export default {
props: ['open'],
emits: ['close']
}
</script>
<template>
<div>
<button @click="openModal = true">Open Modal</button>
<my-modal :open="openModal" @close="openModal = false">
<p>This is the content of the modal.</p>
</my-modal>
</div>
</template>
<script>
import MyModal from './MyModal.vue';
export default {
components: { MyModal },
data() {
return {
openModal: false
};
}
}
</script>
The number of elements included in transition
Normally, there's only one element being allowed to include in the transition component. But to be more accurate, there should be only one element rendered.
<template>
<transition>
<button v-if="!usersAreVisible" @click="showUsers">Show Users</button>
<button v-else="usersAreVisible" @click="hideUsers">Hide Users</button>
</transition>
</template>
Attention! You can't use v-if
to replace the v-else
above, although you know they're the same.
The mode of transition
you can choose the transition mode to be out-in
or in-out
:
<template>
<transition mode="out-in">
<button v-if="!usersAreVisible" @click="showUsers">Show Users</button>
<button v-else="usersAreVisible" @click="hideUsers">Hide Users</button>
</transition>
</template>
Using events rather than class names to control animation
We can use animation events provided by transition component to control the animation.
<template>
<transition
name="para"
@before-enter="beforeEnter"
@enter="enter"
@after-enter="afterEnter"
@before-leave="beforeLeave"
@leave="leave"
@after-leave="afterLeave"
@enter-cancelled="enterCancelled"
@leave-cancelled="leaveCancelled"
>
<p v-if="paraIsVisible">This is only sometimes visible...</p>
</transition>
</template>
<script>
export default {
data() {
return {
paraIsVisible: false,
};
},
methods: {
// 定义进入动画之前的逻辑
beforeEnter(el) {
console.log('beforeEnter');
console.log(el);
el.style.opacity = 0; // 设置初始透明度为0
el.style.transition = 'opacity 0.5s ease'; // 定义透明度变化的动画效果
},
// 定义进入动画的逻辑
enter(el) {
console.log('enter');
console.log(el);
let round = 1;
setInterval(() => { // 使用箭头函数以正确引用外部变量
el.style.opacity = round * 0.1;
round++;
if (round > 10) {
clearInterval(this.interval); // 当透明度超过1时,清除定时器
return;
}
}, 20);
},
enterCancelled(el) {
clearInterval(this.interval);
}
beforeLeave(el) {
console.log(el);
},
toggleVisibility() {
this.paraIsVisible = !this.paraIsVisible;
},
},
};
</script>
<style>
</style>
However, if we use events, we need to manually tell the Vue when to stop the animation. By using the second parameter of events callback function, we can tell Vue when the animation should stop.
enter(el, done) {
console.log('enter');
console.log(el);
let round = 1;
setInterval(() => {
el.style.opacity = round * 0.1;
round++;
if (round > 10) {
clearInterval(this.interval);
done();
return;
}
}, 20);
},
Sometimes, the event enterCancelled
doesn't work, that's because you use v-if
rather than v-show
in the target component.
If you decide to use events to fully control the animation process, then it's recommanded that you should avoid the css effects. Add another property css
to tell Vue that we don't want any class name effects.
<transition
:css="false"
@before-enter="beforeEnter"
@enter="enter"
@after-enter="afterEnter"
@before-leave="beforeLeave"
@leave="leave"
@after-leave="afterLeave"
@enter-cancelled="enterCancelled"
@leave-cancelled="leaveCancelled"
>
<p v-if="paraIsVisible">This is only sometimes visible...</p>
</transition>
transition group
Besides component transition, there's another component named transition-group. This component is often used as a list wrapper, and unlike transition component, the transition-group
component can be rendered as a true dom element.
<template>
<ul>
<transition-group name="user-list">
<li v-for="user in users" :key="user.id" @click="removeUser(user)">
{{ user.name }}
</li>
</transition-group>
</ul>
</template>
Since it can be rendered as true element, we can simplify the code as following:
<template>
<transition-group name="user-list" tag="ul">
<li v-for="user in users" :key="user.id" @click="removeUser(user)">
{{ user.name }}
</li>
</transition-group>
</template>
The attribute name
is the prefix of animation class name:
.user-list-enter-from {}
.user-list-enter-active {}
.user-list-enter-to {}
And the attribute tag means the dom element the transition-group want to be.
Router animation
What if we use transition to wrap router-view
component?
Router warn :
<rourer-view> can no longer be used directly inside
<transition> or <keep-alive>,
Use slot props instead:
<router-view v-slot="{ Component }'>
<transition>
<component :is="Component" />
</transition>
</router-view>
That'is pretty much clear. Just do as what the warning message tells you.
<template>
<router-view v-slot="slotProps">
<transition name="route" mode="out-in">
<component :is="slotProps.Component"></component>
</transition>
</router-view>
</template>
As you can see, the transition component can only wrap another common component rather than RouterView.
Aviod No Page Jump Animation
When refreshing the page, it always starts with a blank route before transitioning to the target route. This leads to the first entry into the page not being through a page jump. Consequently, a transition animation also occurs. To solve this problem, we can utilize the beforeRouteEnter hook to adjust the behavior during route transitions.
app.use(router);
router.isReady().then(function(){
app.mount('#app');
})
5. Vuex -- Better State Management with Vuex ( Replacing provide/ inject )
What is “Vuex"?
Vuex is a library for managing global state.
Why vuex?
Managing app-wide/global state can be difficult.
- Fat Components: App.vue (or similar components) contain lots of data and logic
- Unpredictable: It's not always obvious where data (state) gets changed in which way
- Error-prone: Accidental or missed state updates are possible
With vuex:
- Outsourced state management
- Predictable state management/flow
- Clearly defined data flow: Less errors
Start to use
Create a store object and then let app to use it:
const store = createStore({
state(){
return {
counter: 0
}
}
});
const app = createApp(App);
app.use(store);
The argument or the configuration of createStore
function has the same structure with the Vue instance.
After use the store, we can get the data in any components of this app:
<h3>{{ $store.state.counter }}</h3>
The data in store can also be used in computed or methods:
computed:{
counter() {
return this.$store.state.counter;
}
},
methods: {
addone(){
this.$store.state.counter++;
}
}
Change the data in store
this.$store.state
is readonly, which means you can not change the value of it directly. Use methods in mutation to change the value:
const store = createStore({
state() {
return {
counter: 0
};
},
mutations: {
increment(state) {
state.counter = state.counter + 2;
},
increase(state, payload){
state.counter = state.counter + payload.value;
},
}
});
We don't use these methods directly, we use it in a vary strange way:
<script>
export default {
methods: {
addOne() {
this.$store.commit('increment');
},
addTen() {
this.$store.commit('increase', { value: 10 });
},
addNine() {
this.$store.commit({ type: 'increase', value: 10 });
}
}
}
</script>
Combine vuex with computed
If we combine the vuex and computed, we get a powerful weapon:
export default {
computed: {
counter() {
return this.$store.state.counter * 2;
},
}
}
In this way, one component can refresh itself because in another component we submit some new data into the store.
Getter -- means computed
You don't want to manually multiply 2 in every place where you use the counter, right? Then Vue provides us an inset computed feature named getters:
getters: {
finalCounter(state){
return state.counter * 2;
}
}
Of course you can defined a new field(finalCounter) in state, but if you do so, you must update the relative data twice(counter and finalCounter). Thus, the getter actually build a more reasonable hierarchy of our state and also reduce the amount of code.
We can use a getter inside another getter through the second argument, and that is the hierarchy:
const store = createStore({
state() {
return {
counter: 0
};
},
mutations: {
increment(state) {
state.counter = state.counter + 2;
}
},
getters: {
finalCounter(state) {
return state.counter * 3;
},
normalizedCounter(state, getters) {
const finalCounter = getters.counter * 3;
if (finalCounter < 0) {
return 0;
}
if (finalCounter > 100) {
return 100;
}
return finalCounter;
}
}
});
How Vuex Works?
There are five parts help you to understand how it works:
- actions
- mutations
- center data store
- getters
- components
So what's the function of actions?
We all known in Vue the code can be split as sync or async. So the mutations handle the sync part and the actions handle the async part.
When I say the actions handle the async part, I mean in an action, firstly handle with the async code and get what we want and then call a certain mutation to change the state.
actions: {
increment(context) {
context.commit('increment');
}
}
Compare it with:
export default {
methods: {
addOne() {
this.$store.commit('increment');
},
}
}
It's easy to notice the context refers to the store itself. If we print the context into console, we get the following result:
{
commit: f,
dispatch: f,
getters: {},
rootGetters: {},
rootState: Proxy,
state: Proxy,
}
Although you can get access to state or rootState, you can not modify the data in it directly. Always use context.commit
to change the state.
The meaning of action
The following code shows why we need action in some situations:
const store = createStore({
state() {
return {
counter: 0
};
},
mutations: {
increment(state) {
state.counter += 2;
}
},
actions: {
increment(context) {
setTimeout(() => {
context.commit('increment');
}, 2000);
},
increase(context, payload) {
context.commit('increment', payload);
}
}
});
<template>
<div>
<p>Counter: {{ counter }}</p>
<button @click="addOne">Increment after 2 seconds</button>
<button @click="() => increaseBy({value: 2})">Increase by payload</button>
</div>
</template>
<script>
import { mapState } from 'vuex';
export default {
computed: {
...mapState(['counter'])
},
methods: {
addOne() {
this.$store.dispatch('increment');
},
increaseBy() {
this.$store.dispatch('increase', {value: 10});
}
increaseBy2(payload) {
this.$store.dispatch({
type: 'increase',
...payload,
});
}
}
};
</script>
Use commit to call a mutation and use dispatch to call an action, okay?
Inject actions mutations states in a certain component
It's not convenient to use the data in store like this:
$store.state.counter
We need some more effective ways:
import { mapGetters, mapState, mapActions } from 'vuex';
export default {
methods: {
// addone(){
// this.$store.dispatch('increment');
// }
...mapActions(['increment','increase'])
},
computed:{
...mapState(['counter']),
// counter(){
// return this.$store.getters.finalCounter,
// },
...mapGetters(['finalCounter']),
}
}
If we don't want the original name, we can change it while mapping:
...mapActions({
inc: 'increment',
increase: 'increase',
})
Vuex and Authorization
A good place to use vuex is authorization. Because it's definately a global state, and another good place is internationalization.
First of all, we define two actions in store. And the reason why don't use mutation is that the login and logout need communicate with network, these are async processes.
And then we need a mutation to change the login state in store:
const store = createStore({
state() {
return {
isAuth: false // 初始状态设为未认证
};
},
mutations: {
setAuth(state, payload) {
state.isAuth = payload.isAuth;
}
},
actions: {
login(context) {
context.commit('setAuth', { isAuth: true });
},
logout(context) {
context.commit('setAuth', { isAuth: false });
}
}
});
After doing that we can show the authoriztion state or change it in a certain component:
<template>
<div>
<button @click="handleLogin">Login</button>
<button @click="handleLogout">Logout</button>
<p>Auth Status: {{ isAuth ? 'Authenticated' : 'Not Authenticated' }}</p>
</div>
</template>
<script>
import { mapState } from 'vuex';
export default {
computed: {
...mapState(['isAuth'])
},
methods: {
handleLogin() {
this.$store.dispatch('login');
},
handleLogout() {
this.$store.dispatch('logout');
}
}
};
</script>
Combine stores to be a big one
Firstly, we create a sub store named counterModule
and then inject it into the rootStore:
// 定义一个模块 counterModule
const counterModule = {
namespaced: true,
state() {
return {
counter: 0
};
},
mutations: {
increment(state) {
state.counter = state.counter + 2;
},
increase(state, payload) {
state.counter = state.counter + payload.value;
}
}
};
// 创建store
const store = createStore({
modules: {
numbers: counterModule
}
});
// 可以添加一个示例模块
const authModule = {
namespaced: true,
state() {
return {
isLoggedIn: false
};
}
};
// 将authModule添加到store
store.module('auth', authModule);
Now, the getter in counterModule want to access to another getter belongs to another sub store, how can we do that? Actually each getter can accept 4 arguments, and we can use the last two ones to access to the rootState or the rootGetter:
getters:{
testAuth(state, getters, irootState, rootGetters) {
return state.isLoggedIn;
}
}
You may notice that we can assign a field named namespaced
when we create a sub store. The namespaced
option in Vuex modules serves to create an isolated module that has its own state, mutations, actions, and getters. When namespaced
is set to true
, the module is namespaced, meaning all its properties and functions are encapsulated and won't conflict with other modules.
Here's the role of namespaced
in Vuex modules:
-
Avoid Naming Collisions: It helps to avoid naming collisions with other modules, as each module's state and mutations are prefixed with the module's name.
-
Modular and Organized Code: It allows you to write modular and organized code by keeping related state and actions together, which is especially useful in large applications.
-
Prefixed Actions and Getters: When you dispatch actions or access getters from a namespaced module, you need to specify the module's name. This ensures that you're working with the correct module's properties.
-
Encapsulation: It provides a level of encapsulation, which is beneficial for maintaining and testing code, as each module can be developed and tested independently.
-
Improved Readability: It improves the readability of the code by making it clear which actions, mutations, or getters belong to which module.
In summary, the namespaced
property in Vuex is crucial for building large-scale applications, as it helps in organizing the state management in a clean, modular, and maintainable way.
export default {
computed: {
counter () {
return this.$store.getters['numbers/counter'];
},
concat () {
this.$store.dispatch('requests/contactCoach',{
email: this.email,
message: this.message,
coachId: this.$route.params.id,
});
this.$router.replace('/coaches');
}
}
}
or,
computed: {
...mapGetters('numbers', ['counter']),
},
methods: {
...mapActions('numbers', {
inc: 'increment',
increase: 'increase',
})
}
Encapsulate the store creation process
It's obvious that we shouldn't write the Vuex code in main.js
, instead, we need to encapsulate this process into a specific file, normally named store.js
import store form './store.js';
So, the hierarchy of Vuex files is like:
store/
counter/
actions.js
index.js
mutations.js
actions.js
getters.js
index.js
mutations.js
Tip: how to import a font in css?
- import:
@import url("https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&...")
- usage:
font-family: "Roboto", sans-serif;
Tip: the role of props: true
in router
In Vue Router, props: true
plays a crucial role in the component routing system by determining whether a route component should receive props. When set to true
, it enables the route component to receive props from the route's parameters, query, or state, providing a flexible way to pass data directly into the component.
By default, all route components are created each time the route changes. However, when props: true
is specified, Vue Router creates a single instance of the component and reuses it for all navigations within the same route, much like how you'd use a Vue component in a regular application. This behavior is controlled by the props
option.
When props
is set to true
, the component is treated as a functional component that receives route data as its props. This includes dynamic segments (:to
), query parameters (query
), and state (state
). For example, if you have a route like /user/:id
, with props: true
, the id
becomes a prop available in the component.
This approach has several advantages:
- Performance Optimization: By reusing the component, you avoid unnecessary re-rendering and initialization, which can lead to better performance.
- Consistent Component Instance: It ensures that the component instance remains the same across navigations, preserving the component's state and behavior.
- Direct Data Access: It allows direct access to route parameters as props, simplifying the data flow and making the component more declarative.
It's important to note that when using props: true
, the component should be designed to handle the props it receives, and it should not assume the presence of certain props. This is because the component will not receive any props when the route does not define them.
In summary, props: true
in Vue Router is a powerful feature that allows components to receive data directly from the route, promoting more efficient and maintainable code when navigating within the same route.
Example: how to combine the rootGetter and sub-getter
The following code shows how to judge whether the current user is a coach or not:
getters: {
isCoach(_1, getters, _2, rootGetters) {
const coaches = getters.coaches; // 获取教练列表
const userId = rootGetters.userId; // 获取当前用户的ID
return coaches.some(coach => coach.id === userId); // 检查当前用户是否在教练列表中
}
},