Dakota Revisits Vue.js 3

39 阅读4分钟

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!

27. How to encapsulate a button Component

As the following code shows, when encapsulating a button component, typically we assign its type and class attributes from outside.

<template>
  <button :type="type" :class="mode"></button>
</template>

<script>
export default {
  props: ['type', 'mode']
}
</script>

<style scoped>
button {
  padding: 0.75rem 1.5rem;
  font-family: inherit;
}
</style>

28. Fall through

In vue, fall through means the events or atrributes bound on the encapsulated component will be passes to the root element of this encapsulated component.

<template>
  <base-card>
    <base-button type="submit" class="btn-submit" @click="setSelectedTab('stored-resources')">Store</base-button>
  </base-card>
</template>

It equals to be:

<template>
  <button type="submit" class="btn-submit" @click="setSelectedTab('stored-resources')">
    <slot></slot>
  </button>
</template>

29. The meaning of proxy

In vue, variables are proxyed by, so the following code can not can the component refreshing:

removeResource(resId){
  this.storedResources =this.storedResources.filter(
    (res)=> res.id !== resId
  );
}

Instead, we should use some immutable methods:

removeResource(resId){ 
  const resIndex = this.storedResources.findIndex(
    item => item.id === resId
  );
  this.storedResources.splice(resIndex, 1);
}

30. Tips when using provide

When using provide to pass through data, it's important to keep in mind that you can't define the data(properties or methods) directly in provide:

provide () {
  return {
    resources: this.storedResources,
    addResource: this.addResource,
    deleteResource: this.removeResource,
  }
}

31. Data convert within two-way binding

As you know, we can assign the type of input, such as 'text' 'number', etc. When the type of input is assigned and we also use two-way binding, the Vue will convert the string value to be a proper type.

<input id="age" name="age" type="number" v-model="userAge" ref="inputRef" />

Now, the type of the value of 'userAge' is number but not string. However, when you use 'inputRef' to get value directly from the DOM, the result is still string.

Tip: Three v-model modificators: lazy, number and trim.

32. Encapsulating a Component with Two-Way Binding

To encapsulate a component that supports two-way binding, you can use the following approach:

Start with the desired usage in your template:

<div class="form-control">
  <rating-control v-model="rating"></rating-control>
</div>

Next, let's convert this to an equivalent syntax that explicitly shows the modelValue prop and the corresponding update:modelValue event:

<div class="form-control">
  <rating-control :model-value="rating" @update:modelValue="rating = $event"></rating-control>
</div>

Here is the implementation of the encapsulated rating-control component:

<template>  
  <ul>  
    <li :class="{ active: modelValue === 'poor' }">  
      <button type="button" @click="activate('poor')">Poor</button>  
    </li>  
    <li :class="{ active: modelValue === 'average' }">  
      <button type="button" @click="activate('average')">Average</button>  
    </li>  
    <li :class="{ active: modelValue === 'great' }">  
      <button type="button" @click="activate('great')">Great</button>  
    </li>  
  </ul>  
</template>

<script>
export default {
  props: ['modelValue'],
  emits: ['update:modelValue'],
  data() {
    return {
      activeOption: null,
    };
  },
  methods: {
    activate(option) {
      this.activeOption = option;
      this.$emit('update:modelValue', option);
    }
  }
};
</script>

By defining the props and emits explicitly, you gain a clearer understanding of how they function:

props: ['modelValue'],
emits: ['update:modelValue'],

This setup allows the parent component to bind the rating variable using v-model, while the rating-control component emits updates to this variable via the update:modelValue event.

33. Firebase

Firebase is a comprehensive mobile and web development platform provided by Google. It offers a range of tools and services that help developers build, improve, and grow their apps efficiently. Firebase provides real-time databases, cloud storage, authentication, and hosting services, all of which are designed to be scalable and secure. With Firebase, developers can easily integrate various functionalities into their apps, such as push notifications, analytics, and crash reporting. Firebase also supports multiple platforms, including iOS, Android, and web, making it a versatile choice for cross-platform development. Overall, Firebase is an essential tool for developers who want to create high-quality, engaging apps quickly and efficiently.

34. Fetch basic use

The following code shows a basic use of fetch to request data from the network:

fetch(url, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    name: this.enteredName,
    rating: this.chosenRating,
  })
})
.then(function(response){
  if(response.ok){
    return response.json();
  }
})
.then(function(data){
  const results = [];
  for(const id in data) {
    results.push({
      id,
      name: data[id],
    })
  }
})

It's very common that sometimes we'll get a network error, in this case, we use a strategy named "catch then throw":

.then(function(response){
  if(response.ok){
    return response.json();
  } else {
    throw new Error("Could not save data!");
  }
})
.then(function(data){
  // ...
})
.catch( error => {
  this.error = error.message;
})

35. Mounted basic use

Two ways to use mounted hook:

onMounted(() => {});

or,

import { onMounted } from "vue";

mounted(){
  this.loadExperiences();
}

36. What's the true meaning of SPA

The true meaning of SPA is only one HTML file, or be more accurate, only one main entrance. It's very different from another render strategy called SSR, where the backend provides many HTML files, thus the frontend can receive every page from the network directly.

37. Using router in Vue

First, install the dependency:

npm install --save vue-router@next

Secondly, a typical process for app to integrate the router:

import { createRouter, createWebHistory } from 'vue-router';
import App from './App.vue';

const router = createRouter({
  history: createWebHistory(),
  routes: [
    { path: '/teams', component: TeamsList },
    { path: '/users', component: UserList },
  ]
});

const app = createApp(App);

app.use(router);

app.mount('#app');

Then, write the router wrapper component:

<template>
  <the-navigation @set-page="setActivePage"></the-navigation>
  <main>
    <router-view></router-view>
  </main>
</template>

And the template of the custom TheNavigation component is:

<template>
  <header>
    <nav>
      <ul>
        <li><router-link to="/teams">Teams</router-link></li>
        <li><router-link to="/users">Users</router-link></li>
      </ul>
    </nav>
  </header>

</template>

The RouterLink component will be render as a tag eventually, so you when you want to add some styings in RouterLink, you can use a as a selector.

But, the RouterLink component prevent the default behavior of a tag, and shift to trigger another method to jump to other paths.

38. RouterLink stylings

When the current url is match to the value of :to attribute in RouterLink, Vue will automatically add some fixed class on the corresponding a tag, these classes are:

  • router-link-active
  • exact-active-class

However, router-link-exact-active will NOT be added to the <a> element! That only happens for a full match.

And you can change these preset name when create a router instance:

const router = createRouter({
  history: createWebHistory(),
  routes: [
    { path: '/teams', component: TeamsList },
    { path: '/users', component: UserList },
  ],
  linkActiveClass: 'router-link-active2'
});

39. Methods in $router

  • push(location, onComplete?, onAbort?)
  • replace(location, onComplete?, onAbort?)
  • go(n)
  • back()
  • forward()

You can use them like the following sample:

this.$router.push('/teams');
this.$router.forward()

40. Params in router

Use a colon to declare a certain part to be dynamic when designing the route:

const router = createRouter({
  ...,
  routes: [
    { path: '/users', component: UserList },
    { path: '/users/:userId', component: User },
  ],
});

Click an anchor like this:

<router-link :to="'/teams/'+ id">View Members</router-link>

Or use computed to generate the value of :to in realtime.

<template>  
  <li>  
    <h3>{{ name }}</h3>  
    <div class="team-members">{{ memberCount }} Members</div>  
    <router-link :to="teamMembersLink">View Members</router-link>  
  </li>  
</template>  
  
<script>  
export default {  
  props: ['id', 'name', 'memberCount'],  
  computed: {  
    teamMembersLink() {  
      return '/teams/' + this.id;  
    }  
  }  
}  
</script>

Then try to get the userId like this:

const userId = this.$route.params.userId;

Also, you can watch the value of route and refresh your component as soon as the datas in route change:

watch: {
  $route(newRoute) {
    this.load(newRoute);
  }
}

or,

import { watch, onMounted, nextTick } from 'vue';  
import { useRoute } from 'vue-router';  
  
export default {  
  setup() {  
    const route = useRoute();  
      
    // 使用 watch 监视 route 的变化  
    watch(  
      () => route,  
      (newRoute) => {  
        load(newRoute);  
      },  
      { immediate: true, deep: true }  
    );  
  
    // 定义 load 方法  
    function load(route) {  
      // 这里是你的加载逻辑  
      console.log('Loading route:', route);  
    }  
  
    // 如果 setup 返回对象,它们将被暴露给组件的模板和其他组合式 API 钩子  
    return {};  
  }  
};

or,

<template>  
  <!-- 你的模板代码 -->  
</template>  
  
<script setup>  
import { watch, ref } from 'vue';  
import { useRoute } from 'vue-router';  
  
// 使用 useRoute 钩子获取当前路由对象  
const route = useRoute();  
  
// 定义一个响应式引用来存储路由相关的数据(如果需要)  
// const someData = ref(null);  
  
// 使用 watch 监视 route 的变化,注意这里不需要显式的 setup 函数  
watch(  
  () => route,  
  (newRoute, oldRoute) => {  
    // 这里处理路由变化时的逻辑  
    load(newRoute);  
  },  
  { immediate: true, deep: true } // 立即执行并且深度观察  
);  
  
// 定义 load 函数来处理路由加载逻辑  
function load(route) {  
  // 这里是你的加载逻辑,可以使用 route 对象的信息  
  console.log('Loading route:', route);  
  // 根据路由执行你的逻辑,比如获取数据或更新状态  
}  
  
// 如果需要在模板中使用响应式数据,直接定义在这里就可以  
// 例如:const myData = ref('initial value');  
</script>  
  
<style>  
/* 你的样式代码 */  
</style>

41. Declare the params to be props

You can do that like this:

{ path:'/teams/:teamId',component: TeamMembers, props: true }

And the teamId automatically becomes one of the props:

export default {
  
  inject: ['users', 'teams'],
  props: ['teamId'],
  components:{
    UserItem,
  }
}

42. Redirect or Alias

They're very alike, but there is some difference:

Using redirect means '/' first and then '/teams':

  { path: '/', redirect: '/teams'}

But using alias means '/' equals to '/teams', which means they're exactly the same. When you type '/', it won't redirct to '/teams'.

  { path: '/teams', component: TeamsList, alias:'/'}

43. Fallback routing path

When user type a url that doesn't match any route, using a fallback routing to tell him the currect situation:

{ path:'/:notFound(.*)', component: NotFound }

44. The children routes and relative router

You can define a child route like this:

const routes = [  
  {  
    path: '/teams',  
    component: TeamsList,  
    name: 'Teams', // 可选的,用于命名路由  
    meta: { // 可选的,用于存储元数据  
      requiresAuth: true, // 示例:这个路由可能需要认证  
    },  
    children: [  
      {  
        path: ':teamId',  
        component: TeamMembers,  
        name: 'TeamMembers', // 可选的,命名子路由  
        props: true, // 将路由参数作为组件的 props 传递  
        meta: { // 可选的,子路由的元数据  
          // 可以添加与子路由相关的元数据  
        },  
      },  
    ],  
  },  
  // ...其他路由配置  
];

You should notice that there is not slash before :teamId, which means we are using a relative path.

45. The name of route

The path of route will change, so that all the place using this route must be changed after that. It's very annoying and prone to error.

We need another method to avoid such issue. We can give each route a name, then we can use the name instead of path.

const routes = [  
  {  
    path: '/teams',  
    component: TeamsList,  
  }
]
...
<router-link :to="'/teams'"></router-link>

You have to write all the code after the path changing:

const routes = [  
  {  
    path: '/teams2',  
    component: TeamsList,  
  }
]
...
<router-link :to="'/teams2'"></router-link>

But with name, you donn't have to:

const routes = [  
  {  
    path: '/teams',  
    name: 'Team',
    component: TeamsList,  
  }
]
...
<router-link :to="{name: 'Team'}"></router-link>

We can refactor it further:

<router-link :to="teamMembersLink">View Members</router-link>
...
export default {
  props: ['id', 'name', 'memberCount'],
  computed: {
    teamMembersLink(){
      return {
        name: 'Team',
        params: {
          teamId: this.id,
        }
      }
    }
  }
}

If you don't want to use a RouterLink component, you can use this.$router.push():

this.$router.push({
  name: 'Team',
  params: {
    teamId: this.id,
  }
})

46. Use query but params

If you just want to pass data and don't want to change the main part of your route, then you should use query but params:

this.$router.push({
  name: 'Team',
  params: {
    teamId: this.id,
  },
  query: {
    sort: 'ascend',
  }
})

47. Use params or query as meta data of a certain page

We can acquire the data passed by route, and then use it to request data from network.

computed(){
  teamId(){
    return this.$route.query.teamId;
  }
},
created(){
  // this.$route.path // /teams/t1
  this.loadTeamMembers(this.teamId);
  console.log(this.$route.query);
}

48. Use more than one RouterView

We can set a name attribute to RouterView to make it named. After that, we can use many named RouterView other than just one default RouterView.

<template>
  <the-navigation></the-navigation>
  <main>
    <router-view></router-view>
  </main>
  <footer>
    <router-view name="fqoter"></router-view>
  </footer>
</template>

Now, we can assign differant components to different RouterViews:

routes: [
  {
    name: 'teams',
    path: '/teams',
    components: {
      default: TeamsList,
      footer: TeamsFooter,
    },
    children: [
      {
        name: 'team-members',
        path: ':teamId',
        component: TeamMembers,
        props: true,
      }
    ]
  }
]

You can consider the RouterView as slot.

49. Scroll Behavior

We can interfere the scroll behaviors while jumping to another route. We can do it while we create the router instance:

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes,
  linkActiveClass: 'active',
  scrollBehavior( to, from, savedPosition ){
    if(savedPosition){
      return savedPosition;
    }
    return {
      left: 0,
      top: 0,
    }
  }
});

50. Router Gaurd

Unlike in AngularJS, define a router gaurd is pretty easy in Vue.js. After creating a router instance, we just call the router.beforeEach() function and pass the guard into it.

router.beforeEach(
  function(to, from, next){
    next({
      name: "team-members",
      params: { teamId: "t2" },
    })
  }
)
...

app.use(router);

In the code above, the next function means pass to next route or step.

Another examples:

router.beforeEach(
  function(to, from, next){
    if (to.name === "team-members") {
      next();
    } else {
      next({
        name: "team-members",
        params: { teamId: "t2" },
      })
    }
  }
);

51. Hooks of component about routing

The following code refers two routing hooks of a certain component:

beforeRouteEnter(to, from, next){
  console.log('UsersList Cmp beforeRouteEnter');
  console.log(to, from);
  next();
}
beforeRouteUpdate(to, from, next){
  this.loadTeamMembers(to.params.teamId);
  next();
}