Dakota Revisits Vue.js 4

15 阅读6分钟

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. Use Firebase as our backend

Firebase helps mobile and web app teams succeed. Search firebase auth rest api in Google to get more infomation.

Combine the Vuex with database

If we want to submit some info to backend, then we must submit firstly, then wait and make sure that we've succeed, after that commit a mutation to change store. In this way, we can ensure the info on the page is exactly the same as it in database.

Another approach is to submit and wait and then fetch data from the updated database, after get the updated data, first store it into Vuex and then render it onto the pages.

export default {
  async registerCoach(context, data) {
    const userId = context.rootGetters.userId; // 获取当前用户的ID

    const coachData = {
      firstName: data.first,
      lastName: data.last,
      description: data.desc,
      hourlyRate: data.rate,
      areas: data.areas
    };

    // 注意替换URL为你的实际API端点
    await fetch(`https://vue-http-demo-85e9e.firebaseio.com/coaches/${userId}.json`, {
      method: 'PUT', // 注意修正方法名称为大写
      body: JSON.stringify(coachData),
      headers: {
        'Content-Type': 'application/json'
      }
    });

    context.commit('registerCoach', coachData);
  }
};

So, why we need Vuex, it seems that we can exchange data with database through network directly. A good reason to do so is that we don't need to request data from back end every time, we can set a cache with resonable timeout in an action before we decide to fetch data from database.

By doing that, performance improves.

tip: base-loading

<template>
  <div v-if="isLoading">
    <base-spinner></base-spinner>
  </div>
  <ul v-else-if="hasCoaches">
    <coach-item
      v-for="coach in filteredCoaches"
      :key="coach.id"
      :id="coach.id"
      :first-name="coach.firstName"
      :last-name="coach.lastName"
      :rate="coach.hourlyRate"
      :areas="coach.areas"
    ></coach-item>
  </ul>
</template>

<script>
export default {
  computed: {
    isLoading() {
      // 返回加载状态
      return this.someLoadingState;
    },
    hasCoaches() {
      // 返回是否有教练数据
      return this.filteredCoaches.length > 0;
    },
    filteredCoaches() {
      // 返回过滤后的教练列表
      return this.someCoachesArray.filter(coach => {
        // 这里可以添加过滤逻辑
        return true; // 总是返回true作为示例
      });
    }
  }
};
</script>

Use v-if and v-else-if with spin to implement loading animation.

2. Better Router experiences

A reasonable structure when use router

<template>
  <the-header></the-header>
  <router-view v-slot="slotProps">
    <transition name="route">
      <component :is="slotProps.Component" />
    </transition>
  </router-view>
</template>

<script>
import TheHeader from './components/layout/TheHeader.vue';

export default {
  components: {
    TheHeader
  }
};
</script>

404 page with back button

<template>
  <section>
    <base-card>
      <h2>Page not found</h2>
      <p>
        This page could not be found. Maybe check out our coaches?
      </p>
      <router-link to="/coaches">Coaches</router-link>
    </base-card>
  </section>
</template>

<script>
export default {
  name: 'NotFound'
};
</script>

Two kinds of data

With authorization, we can differentiate the data into two parts: public and protected. When we access to protected data, we must have the authorization first. And we can use Vuex to help us do that.

Use Vuex to store user information

Firstly, dispatch an action to get user info from database, after that store the data(commit a mutation), thus we can access to these info in any components in application.

Organized Action Process:

  1. API Request: Define an asynchronous action in actions.js to send an authentication request.
  2. Handle Response: If the request is successful, parse the response and update the Vuex state; if it fails, log the error and throw an exception.
  3. Update State: Define a mutation in mutations.js to update user information.
  4. Error Handling: Log error information and throw an exception when the API request fails.

Code Implementation:

actions.js

export default {
  async authenticate(context, credentials) {
    try {
      const response = await fetch('/api/authenticate', {
        method: 'POST',
        body: JSON.stringify(credentials),
        headers: {
          'Content-Type': 'application/json'
        }
      });
      
      if (!response.ok) {
        const responseData = await response.json();
        const error = new Error(responseData.message || 'Failed to authenticate.');
        throw error;
      }

      const responseData = await response.json();
      context.commit('setUser', {
        token: responseData.idToken,
        userId: responseData.localId,
        tokenExpiration: responseData.expiresIn
      });
    } catch (error) {
      console.error('Authentication failed:', error);
    }
  }
};

mutations.js

export default {
  setUser(state, payload) {
    state.token = payload.token;
    state.userId = payload.userId;
    state.tokenExpiration = payload.tokenExpiration;
  }
};

getters.js

export default {
  isAuth(state) {
    return state.token && state.tokenExpiration > Date.now();
  }
};

App.vue

<template>
  <div>
    <button @click="attemptLogin">Login</button>
  </div>
</template>

<script>
import { mapActions } from 'vuex';

export default {
  methods: {
    ...mapActions(['authenticate']),
    attemptLogin() {
      const credentials = {
        username: 'user',
        password: 'password'
      };
      this.authenticate(credentials);
    }
  }
};
</script>

Explanation:

  • The authenticate async function in actions.js handles the login logic, including calling the API, checking the response status, updating the state, or throwing an error.
  • The setUser mutation in mutations.js is used to update the user's authentication information.
  • The isAuth getter in getters.js is used to determine if the user is authenticated.
  • App.vue is an example component showing how to call the authenticate action.

When logout, we can clear or reset the user iformation in store:

logout(context){
  context.commit('setUser', {
    token: null,
    userId: null,
    tokenExpiration: null,
  })
}

use it in a component:

<script>
export default {
  computed: {
    isLoggedIn(){
      return this.$store.getters.isAuthenticated;
    }
  },
  methods: {
    logout() {
      this.$store.dispatch('logout');
    }
  }
}
</script>

Nornally, we need to jump to another page after logout:

this.$store.dispatch('logout');
this.$router.replace('/coaches');

Combine router guard with authorization info provided by Vuex

router.beforeEach(function (to, from, next) {
  if (to.meta.requiresAuth && !store.getters.isAuthenticated) {
    next('/auth');
  } else if (to.meta.requiresUnauth && store.getters.isAuthenticated) {
    next('/coaches');
  } else {
    next();
  }
});
export default router;

Login automatically

  1. Automatic Login After Registration:

    • Once the user completes the registration process, an automatic login request is sent using a code snippet. This is typically done to immediately give the user access post-registration.
  2. Login Persistence After Page Refresh:

    • Upon refreshing the page, the application needs to check for existing login information stored persistently, usually in localStorage.
    • If the necessary login tokens or user information exist in localStorage, the application should automatically re-authenticate the user by sending a login request using the stored information.

Code Implementation:

src/store/modules/auth/actions.js

export default {
  async register(context, credentials) {
    // Code to register the user...
    const response = await registerApiCall(credentials);
    if (response.success) {
      context.commit('login', response.data); // Assume server returns data needed for login
    }
  },
  async checkLoginOnRefresh(context) {
    // Check for stored tokens and user info in localStorage
    const token = localStorage.getItem('token');
    const user = localStorage.getItem('user');

    if (token && user) {
      const userData = JSON.parse(user);
      // Code to send login request with the stored token...
      try {
        const response = await loginApiCall(token);
        if (response.success) {
          context.commit('login', userData);
        }
      } catch (error) {
        console.error('Failed to re-authenticate on page refresh:', error);
        // Remove token and user if login fails after refresh
        localStorage.removeItem('token');
        localStorage.removeItem('user');
      }
    }
  }
};

src/store/modules/auth/mutations.js

export default {
  login(state, userData) {
    state.isAuthenticated = true;
    state.user = userData;
    localStorage.setItem('user', JSON.stringify(userData));
    localStorage.setItem('token', userData.token); // Assume userData includes a token
  },
  logout(state) {
    state.isAuthenticated = false;
    state.user = null;
    localStorage.removeItem('token');
    localStorage.removeItem('user');
  }
};

To incorporate an expiration time for the login credentials and replace the loginApiCall with a direct check against the stored expiration time, you can adjust the code as follows:

Updated Code Implementation:

src/store/modules/auth/actions.js

export default {
  async register(context, credentials) {
    // Code to register the user...
    const response = await registerApiCall(credentials);
    if (response.success) {
      context.commit('login', response.data); // Assume server returns data needed for login
    }
  },
  async checkLoginOnRefresh(context) {
    const token = localStorage.getItem('token');
    const userId = localStorage.getItem('userId');
    const tokenExpiration = localStorage.getItem('tokenExpiration');

    if (token && userId && tokenExpiration) {
      const expirationDate = parseInt(tokenExpiration);
      if (new Date().getTime() < expirationDate) {
        const userData = { token, userId };
        context.commit('login', userData);
      } else {
        // Remove token and user if the token is expired
        localStorage.removeItem('token');
        localStorage.removeItem('userId');
        localStorage.removeItem('tokenExpiration');
      }
    }
  }
};

src/store/modules/auth/mutations.js

export default {
  login(state, userData) {
    state.isAuthenticated = true;
    state.user = userData;
    localStorage.setItem('user', JSON.stringify(userData));
    localStorage.setItem('token', userData.token); // Assume userData includes a token
    const expiresIn = +userData.expiresIn * 1000;
    const expirationDate = new Date().getTime() + expiresIn;
    localStorage.setItem('tokenExpiration', expirationDate);
  },
  logout(state) {
    state.isAuthenticated = false;
    state.user = null;
    localStorage.removeItem('token');
    localStorage.removeItem('user');
    localStorage.removeItem('tokenExpiration');
  }
};

3. Deploy

what to deploy

  • HTML: (just one HTML file)
  • CSS: (often no separate files)
  • JavaScript: (built by Vue project setup)
  • A static websites hosting provider is needed

Refactors when you decide to deploy

  1. Test for Errors
  2. Refactor & "Don't repeat yourself"
  3. Consider using Asynchronous "Components"

There's an example shows how to use asynchronous component:

const BaseDialog = defineAsyncComponent(()=> import('./components/ui/BaseDialog.vue'));

wrap it rather than importing it directly,

import BaseDialog from'./components/ui/BaseDialog.vue';

Deployable resources

Normally, there are two kinds of deployable resources in an application:

  1. Custom HTML + CSS + JS Files: just deploy!
  2. More Complex Project (e.g. created with CLl): Optimize Your Code -> Build + automatically optimize code -> Deploy generated files

And you can use firebase to deploy your frontend application!

  1. npm install -g firebase-tools
  2. firebase login
  3. firebase init
  4. Hosting: Configure and deploy Firebase Hosting sites
  5. firebase deploy

Besides firebase, you can also use netlify yo deploy your application.

The frontend router and the backend router

In a Single Page Application (SPA), determining whether a URL is meant to be handled by the front-end router or the back-end server involves a combination of routing configurations and the application's behavior. Here's how it's typically managed:

  1. Front-End Routing:

    • In an SPA, front-end routing is often handled by the application's router, such as Vue Router for Vue.js or React Router for React applications.
    • These routers use the HTML5 History API to manage the application's state and URL without requiring a page reload. This is done using the pushState and replaceState methods, which allow you to change the URL while staying on the same page.
    • When you type a URL that matches one of the routes defined in your front-end router configuration, the router intercepts the navigation and handles it internally by rendering the appropriate component or view.
  2. Back-End Routing:

    • If the URL does not match any of the routes defined by the front-end router, the request is sent to the server. The server then determines how to handle the URL, which may involve serving a static file, a different HTML page, or an API endpoint.
    • For SPAs that are deployed, it's common to have the server return the same HTML entry file (e.g., index.html) for all routes that do not correspond to static assets or API endpoints. This is known as a catch-all route or a fallback route.
  3. Routing Recognition:

    • When you type a URL in the browser, the browser's address bar initially treats it as a request for the server to handle.
    • If the SPA's front-end router has a route that matches the URL, it takes over, and the browser's address bar is updated using the History API to reflect the new URL without actually hitting the server for that route.
    • If there is no matching route in the front-end router, the browser makes a request to the server, which then responds with the appropriate content or redirects to another route.
  4. Deployment and Server Configuration:

    • To ensure that all navigation happens through the front-end router after the initial page load, SPAs often require a special server configuration. For example, in a Node.js environment, you might set up an express server to serve the index.html file for all non-API routes.
    • This configuration ensures that when you type a URL that doesn't have a direct mapping to a static file or API endpoint, the server still serves the SPA's entry file, allowing the front-end router to take over.

In summary, the distinction between front-end and back-end routes in an SPA is managed by the front-end router's configuration and the server's ability to either handle the route or serve the SPA's entry file for client-side routing to take place.

When using firebase to deploy, it asks you whether the application should be recognized as a SPA, if you choose yes, then the backend only give back index.html when access to any sub path of this application.

Change the html template before build

Before building your application, you can change the content of public/index.html, for example, add a title tag a meta description.

4. Composition API -- A different way of building components

Thus far, we used the Options API for building Vue apps / components. This approach is absolutely fine and you can stick to it! But you may face two main limitations / issues when build bigger Bue apps:

  1. Code that belongs together logically is split up across multiple options (data, methods, computed).
  2. Re-using logic across components can be tricky or cumbersome.

From Options API to Composition API, we have a new field named setup to include the original data methods computed watch content.

ref and reactive

import { ref } from 'vue';
export default {
  setup(){
    const user = ref({
      name: 'Maximilian',
      age: 31,
    });

    console.log(user.value); // 初始打印用户信息

    setTimeout(() => {
      user.value.name = 'Max'; // 更新用户名
      user.value.age = 32;     // 更新年龄
    }, 2000);

    // 在Vue组件中,你可能会返回这个响应式数据,使得它在模板中可以被访问
    return {
      userName: user.value.name,
      age: user.value.age,
      user: user
    };
  }
}

or,

import { reactive } from 'vue';
export default {
  setup(){
    const user = reactive({
      name: 'Maximilian',
      age: 31,
    });

    console.log(user.value); // 初始打印用户信息

    setTimeout(() => {
      user.name = 'Max'; // 更新用户名
      user.age = 32;     // 更新年龄
    }, 2000);

    // 在Vue组件中,你可能会返回这个响应式数据,使得它在模板中可以被访问
    return {
      userName: user.name,
      age: user.age,
    };
  }
}

Then, use the value of function setup:

<template>
  <section class="container">
    <h2>{{ userName }}</h2>
    <h3>{{ age }}</h3>
  </section>
</template>

Detect and transform

We can detect one variable is a ref or a reactive:

console.log(isRef(uAge));
console.log(isReactive(user),user.age);

We even can transform one to another: toRefs

const user = reactive({
  name: 'Maximilian',
  age: 31,
});
const userRefs = toRefs(user);

Do as React.js does

The following code shows we can implement useState in Vue.js:

setup () {
  const value = ref(0);

  const setValue = (newValue) => {
    value.value = newValue;
  }

  return {
    value,
    setValue,
  }
}

or,

import { ref } from 'vue';

const courseData = ref({
  goal: 'Finish the course!',
  goalVisibility: false,
});

function toggleGoalVisibility() {
  courseData.value.goalVisibility = !courseData.value.goalVisibility;
}

// 如果你想返回这些响应式数据和函数,可以像这样组织
return {
  goal: courseData.value.goal,
  goalIsVisible: courseData.value.goalVisibility,
  toggleGoalVisibility
};

The most obvious difference between them is that every time we want to change the value of a ref, we must access to the value property firstly.

Use Computed in setup

setup(){
  const uName = computed(function(){
    return firstName.value + "" + lastName.value;
  });

  return {
    uName,
  }
}

The value type of uName is ref, but it's readonly, and you shouldn't try to change its value.

Use ref to implement two way binding

const uAge = ref(31);

watch(uAge, function(newValue, oldValue){
  console.log(newValue, oldValue);
})

We can watch many items at once, and it has more advantages over Optional API:

const uAge = ref(31);
const uName = computed(()=>{return firstName.value + '' + lastName.value});

watch([uAge, uName], function(newValue, oldValue){
  console.log(newValue[0], oldValue[1]);
})

ref atrribute

Like React.js, you can add an attribute named ref to a certain component, and there's definately no colon before ref:

<input type="text" placeholder="Last Name" ref="lastNameInput" />

And usually the initial value of such refs is null:

const lastNameInput = ref(null);

Mix Optional and Composition API

We can mix these two kinds of API in one component or mix components using the different kind of API:

<script>
import { computed } from 'vue';

export default {
  props: ['firstName', 'lastName'],
  setup(props) {
    // 使用计算属性来合成名字
    const userName = computed(function() {
      return props.firstName + ' ' + props.lastName;
    });

    // 返回计算属性,使其在模板中可以被访问
    return { userName };
  }
};
</script>

Emit events in setup

The method setup accepts two arguments, props and context. We can use props to get the inputs and use context to emit events. They are both communication relevant.

<script>
import { computed } from 'vue';

export default {
  props: ['firstName', 'lastName', 'age'],
  setup(props, { emit }) { // 从context中解构出emit
    const userName = computed(() => {
      return props.firstName + ' ' + props.lastName;
    });

    // 使用emit发送事件
    console.log(emit);
    emit('save-data', 1); // 等同于 this.$emit('save-data', 1);

    return { userName };
  }
};
</script>

Provide and inject in setup

  1. provider
provide('userAge', uAge);
  1. receiver
const age = inject('userAge');

The value of age is a ref, and it's read only.

Comparisons

  1. data(){ ... } --> ref(), reactive()
  2. methods:{doSmth(){...}} --> function dosmth(){...}
  3. computed:{val(){...}} --> const val = computed()
  4. watch:{…} --> watch(dep,(val,oldV)=>{})
  5. provide:{…} / inject:[] --> provide(key,val), inject(key)
  6. beforeCreate, created --> Not Needed (setup() replaces these hooks)
  7. beforeMount, mounted --> onBeforeMount, onMounted
  8. beforeUpdate, updated --> onBeforeUpdate, onUpdated
  9. beforeUnmount,unmounted --> onBeforeUnmount,onUnmounted

ref and reactive

In Vue 3, both ref and reactive are essential for creating reactive data, but they serve different use cases and have distinct behaviors, especially when it comes to nesting and primitive vs. object types.

ref:

  • ref is used to create a reactive reference to a value, which can be either a primitive or an object.
  • It wraps the value, and the reference itself is reactive. For objects, you access and modify the value using .value property.
  • When used with arrays or objects, ref will make the root level reactive, but to make nested properties reactive, additional reactive calls are needed.
  • It is particularly useful for handling primitive data types (like strings, numbers) and when you need to toggle the reactivity on and off using .value property.

reactive:

  • reactive is used to create a deeply reactive object. This means that not only the object itself but also all nested properties become reactive.
  • It is ideal for complex data structures and objects where you need to ensure all nested properties are reactive down to any level.
  • reactive does not require the use of .value property to access or modify the reactive object; you interact with it directly.
  • It should be the first option for managing complex states or when you have an object with multiple nested properties that need to be reactive.

In summary, use ref for primitives or when you need a reactive reference to a single value, and use reactive for objects, especially those with nested properties, to ensure deep reactivity throughout the data structure.

Hooks in setup

Let's start with some examples first.

import {useRouter }from 'vue-router';

export default {
  setup(){
    const router =useRouter();
  }
}
import { useStore } from 'vuex';

export default {
  setup(){
    const store = useStore();

    function inc(){
      store.dispatch('increment');
    }

    const counter = computed(function(){
      return store.getters.counter;
    });

    return { 
      inc, 
      counter, 
    };
  }
}

Summary

  1. It's an alternative to the Options AP|: lt uses setup() to expose logic/ data to the template.
  2. lt's a function-based solution that allows you to keep logically related code together.
  3. Data can be managed as ref()s (individual values or obiects) or reactive() objects.
  4. Reactivity is a key concept-refs and reactive objects are reactive, their nested values are not.
  5. Methods become regular functions defined in setup().
  6. Computed properties and watchers are defined with imported functions (from vue).
  7. The setup() function is called by Vue when the component is created - it defines data + logic for the template.
  8. setup() receives two arquments (automatically): reactive props and context(attrs, slots, emit()).

5. Reusable code

In Optional API, we can use Mixins to implement resuable code while in Composition API we use custom hooks.

Mixins

First, we create a new file:

mkdir src/mixins
touch src/mixins/alert.js

And the content in alert.js is

export default {
  // components: {
  //   UserAlert
  // },
  data() {
    return {
      alertIsVisible: false,
    };
  },
  methods: {
    showAlert() {
      this.alertIsVisible = true;
    },
    hideAlert() {
      this.alertIsVisible = false;
    }
  }
};

You can't try to share sub component in a mixin as the comment part shows.

Then we can reuse it in another component:

<script>
import UserAlert from './UserAlert.vue';
import alertMixin from '../mixins/alert.js';
export default {
  components: {
    UserAlert,
  },
  mixins: [alertMixin]
};
</script>

If there are two components share the same mixin, then their data is forced to share with each other.

Automatically mixture

The data or method in a certain mixin will be mixtured with the original fields defined in the component.

import UserAlert from './UserAlert.vue';
import alertMixin from '../mixins/alert.js';
export default {
  components: {
    UserAlert,
  },
  data(){
    return {
      alertTitle: 'Delete User?',
    }
  },
  mixins: [alertMixin],
};

If the name of a data in component is the same with that in mixins, then it will cover the previous one without any errors.

Global mixins

As the name hints, we can let all the components have the same mixin when we create the Vue instance:

import App from './App.vue';
import loggerMixin from './mixins/logger.js';

const app = createApp(App);

app.mixin(loggerMixin);

app.mount('#app');

Custom hooks in Vue

The logic reuse solution for the Options API is mixins, but it can potentially cause risks in large-scale projects. In contrast, the Composition API allows us to create custom hooks.

mkdir src/hooks
touch src/hooks/alert.js

Then we can create a custom hook in alert.js

import { ref } from 'vue';

export default function useAlert(startingVisibility = false) {
  const alertIsVisible = ref(startingVisibility);

  function showAlert() {
    alertIsVisible.value = true;
  }

  function hideAlert() {
    alertIsVisible.value = false;
  }

  return {
    alertIsVisible,
    showAlert,
    hideAlert
  };
}

Then we can use it in a certain component:

setup(){
  const [alertIsVisible,showAlert,mideAlert]= useAlert();

  return {};
}

Mixins share data implicitly, which can lead to unforeseen issues. However, data from hooks is explicitly returned and accepted using const.

Reactive in hooks

import { computed } from 'vue';

export default {
  components: {
    Product
  },
  props: ['user'],
  setup(props) {
    const projects = props.user ? props.user.projects : [];

    const {
      enteredSearchTerm,
      availableItems,
      updateSearch
    } = useSearch(projects, 'title');

    const hasProjects = computed(() => {
      return props.user && props.user.projects && availableItems.value.length > 0;
    });

    return {
      hasProjects,
      enteredSearchTerm,
      updateSearch
    };
  }
};

Here, props are reactive, but the user inside them is not, so when its value changes, useSearch in setup will no longer run.

To make it reactive, firstly we need to convert props to be a ref and use computed to calculate projects:

import { computed, toRefs } from 'vue';

export default {
  props: ['user'],
  setup(props) {
    const { user } = toRefs(props);
    
    const projects = computed(() => {
      return user.value ? user.value.projects : [];
    });

    const { enteredSearchTerm, availableItems, updateSearch } = useSearch(projects, 'title');

    const hasProjects = computed(() => {
      return props.user && projects.value.length > 0 && availableItems.value.length > 0;
    });

    return {
      enteredSearchTerm,
      availableItems,
      updateSearch,
      hasProjects
    };
  }
};

And now the hook is reactive because it's called with a computed material(projects).

Another usage example

The following hook is used to filter the list:

import { ref, computed } from 'vue';

export default function useSearch(items, searchProp) {
  // 传入的应该是ref,并且应该使用其value
  const enteredSearchTerm = ref('');
  const activeSearchTerm = ref('');

  // 确保搜索词响应式,使用延迟设置来避免初始渲染时的不必要过滤
  watch(enteredSearchTerm, (newVal) => {
    activeSearchTerm.value = newVal;
  });

  const availableItems = computed(() => {
    let filteredItems = [];
    if (activeSearchTerm.value) {
      filteredItems = items.value.filter(item => item[searchProp].includes(activeSearchTerm.value));
    } else {
      filteredItems = items.value;
    }
    return filteredItems;
  });

  function updateSearch(searchTerm) {
    enteredSearchTerm.value = searchTerm;
  }

  return {
    enteredSearchTerm,
    availableItems,
    updateSearch
  };
}

And update the filter conditions:

watch(user, function() {
  updatesearch('');
});

6. Vue3 - What changed?

Here are some of the main changes in Vue 3, described in English:

  1. Vue Application Instance Creation:

    • In Vue 3, Vue application instances are created using the createApp() method.
  2. Data Definition:

    • For components, the data option must now always be a function, allowing each instance to maintain its own independent data state.
  3. Registration of Components and Directives:

    • Components, directives, and third-party modules are now registered on the app instance instead of the global Vue object.
  4. Naming Change for Transitions:

    • The transition v-enter has been renamed to v-enter-from to align with v-leave-to.
  5. Vue Router Integration:

    • Vue Router is now created using the createRouter() function, and the way transitions work has changed.
  6. Vuex State Management:

    • The Vuex store is now created with the createStore() function.

These changes reflect Vue 3's focus on improved Composition API, better modularization support, and refined API design.

Vue3 - What's new?

In Vue 3, several new features and improvements have been introduced that enhance the framework's capabilities and developer experience. Here's an overview of what's new:

  1. New Composition API:

    • Vue 3 introduces a new Composition API that can be used as an alternative to the Options API, offering more flexibility and better code organization for complex components.
  2. Teleport Component:

    • <teleport> is a new built-in component that allows developers to transport fragments of the DOM to different parts of the DOM tree, no matter where the component is located in the application.
  3. Fragments:

    • Vue 3 supports multiple root nodes for a single component, enabling the creation of components that return multiple elements without the need for a common wrapper element.
  4. Improved TypeScript Support:

    • Vue 3 has been internally re-written with TypeScript, providing better type inference, type-checking, and overall support for TypeScript in Vue applications.
  5. Performance Optimizations:

    • Vue 3 includes various performance optimizations, such as tree shaking, smaller bundle sizes, and a more efficient virtual DOM implementation.
  6. Global API Reactivity:

    • The global API has been restructured to be more consistent and intuitive, making it easier to create and manage Vue applications.

These new features and improvements aim to make Vue 3 more powerful, flexible, and developer-friendly, positioning it well for the future of web development.

7. Some tiny points reminding in Vue.js

Use exact attribute in RouterLink

<router-link to="/" exact>Home</router-link>

Mount after the router is ready

router.isReady().then(()=> {
  app.mount('#app');
})

The process to use store

const app = createApp(App);

app.use(store);
app.mount('#app');

Array methods that can cause component updating

import { ref, computed } from 'vue';

setup() {
  // 创建一个响应式的目标列表
  const goals = ref([]);

  // 创建一个计算属性,用于过滤掉包含"Angular"或"React"的目标
  const filteredGoals = computed(function() {
    return goals.value.filter((goal) => 
      !goal.text.includes("Angular") && !goal.text.includes("React")
    );
  });

  // 定义一个方法来添加新目标
  function addGoal(text) {
    const newGoal = {
      id: new Date().toISOString(),
      text: text,
    };
    goals.value.push(newGoal); // 使用.value来操作ref引用的数组
  }

  // 返回响应式数据和方法,使其在模板中可以被访问
  return {
    goals,
    filteredGoals,
    addGoal,
  };
}

The code goals.value.push(newGoal) can result in the recalculation of filteredGoals.