This is a practical experience sharing article. Some specific feature points are implemented on the front end for simplicity, with no back end support. The main purpose is to learn how to develop in conjunction with [email protected] and [email protected] in the Vue3+Typescript environment. The general demand is to simulate the realization of shopping cart function, the function is relatively simple, but still regard as some simplified version of the project to develop, if there is any problem, welcome to leave a message to discuss and correct, thank you.

First look at the effect:

You can see this at the end:

  • How to build a project environment like the title
  • Quick tips for creating SFC by setting up templates
  • To strengthen the understandingsetupAnd usage scenarios (hopefully not misleading)
  • aboutviteSome basic configuration knowledge
  • How to use it in TS[email protected].[email protected]
  • Set the global method in vue3
  • more???

If you are not familiar with Vue3, you can read this article first:

  • Vue3 Learning record

Come on ~

Create a project

Start by creating a Vue3+Typescript project environment with the Vite tool

Note: Vite requires your Nodejs version >=12.0.0

// Using yarn yarn created@vitejs /app v3-ts --template vue-ts CD v3-ts yarn install // yarn create vite-app v3-ts --template vue-ts --template vue-ts CD v3-ts NPM installCopy the code

Vite also provides some other templates:

vanilla
vue
vue-ts 
react
react-ts
preact
preact-ts
Copy the code

First, clean up the project and delete the official demo (delete all HelloWorld components).

Add less

yarn add less less-loader --dev

// npm
npm install less less-loader --save-dev
Copy the code

Note: If you do not add –dev here, the package will be installed in Dependencies, which will cause compilation to fail. Migrate less and less-Loader to devDependencies, and then install Yarn again.

Improving directory structure

Create several folders in the project and the corresponding vue file template. The directory structure is roughly as follows (you can create the pages folder first, others will be created later) :


Tips: here by the way to introduce a QUICK vscode page or the formation of vUE template small method (already know partners please ignore)

Step1: VScode > Preferences > User snippets

Step2: enter vue, find vue.json and open it

Step3: Set the code template (it has been prepared for you, please copy and paste!)

// vue.json
{
    "Vue Template": {"prefix":"vueTemplate"."body": ["<template>\n\t<div>\n\n\t</div>\n</template>".""."<style lang=\"less\" scoped>\n\n</style>"]."description":"Generate vUE file"}}Copy the code

Step4: CTRL (command)+s(save)

Step5: enter vueTemplate in the vue file. Vscode will give you a prompt.

step6 : good job ~

// The template looks like this
<template>
  <div>

  </div>
</template>
<script lang="ts">
import{ defineComponent }from 'vue';
export default defineComponent({
  name: "".setup: () = >{}})</script>
<style lang="less" scoped>

</style>
Copy the code

You can set the template according to your own situation.


Add the vue – the router

Let’s set up the route.

Yarn add [email protected] // View the historical version // NPM (yarn) info vue-router versionsCopy the code

Lightning protection: do not use version 4.0.0 (engineers please feel free ~).

Version 4.x of Vue-Router introduced a set of functions instead of a class. We create the route through createRouter.

1. Create a route on router/index.ts and export it.

Note: there are some changes to the definition of asynchronous components in V3, and vite replaced Webpack, so lazy route loading via require and require. Ensure is no longer available.

  • ESIn theimport
// index.ts 
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';
import Home from "/@pages/Products/index.vue";
const routes: Array<RouteRecordRaw> = [
  {
    path: '/'.name: 'product'.component: Home,
  },
  {
    path: '/shoppingCart'.name: 'shoppingCart'.component: () = > import('/@pages/ShoppingCart/index.vue'),},];const router = createRouter({
  history: createWebHistory(),
  routes,
});

export default router;

Copy the code
  • throughdefineAsyncComponentcreate
import { defineAsyncComponent } from 'vue'. {path: '/shoppingCart'.name: 'shoppingCart'.component:defineAsyncComponent(() = > new Promise((resolve,reject) = >{
    	...doSomething
        resolve({
          // Asynchronous component options. })})),}, {path: '/shoppingCart'.name: 'shoppingCart'.component:defineAsyncComponent(() = > import('... ')),},Copy the code

We use alias to introduce components to make the path more concise and clear. To do this, we need to do some configuration, first create vte.config.js in the project root directory (higher versions of vite will automatically create this file, or vtE.config.ts) and configure it as follows.

Note: in VScode, there may be vetur plug-in prompt some errors of introducing path through alias, but it is not a big problem, if the path is correct, it will not affect the project operation, I am not particularly clear about the reason, if there are friends who know, please leave a message to inform me, thank you very much!

// vite.config.js 
const path = require('path');
module.exports = { 
  alias: {// Make sure you add a double slash
    '/@pages/':path.resolve(__dirname,'./src/pages'),
    '/@components/':path.resolve(__dirname,'./src/components')}}// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  alias: {'@src':'/src/',}})Copy the code

Here by the way to give you a list of common configuration of vte.config.ts, vte.config.js configuration is roughly the same, details can jump to the official website for reference configuration:

// vite.config.ts  
export default defineConfig({
  plugins: [vue()],
  // The project root directory, which can be either an absolute path or a relative configuration file pathroot? :' './ / run compilation mode 'development' | | 'production'mode? :'development' , 
  // Path alias
  alias?: {} , 
  // Globally define variable substitutiondefine? : {' ':' '
  },
  / / build optionsbuild? : {base:'/'.// Base path
      target:'modules'.// Browser compatibility module
      outDir:'dist'.// Output path
      assetsDir:'assets' // Static resource path. },// Rely on optimizationsoptimizeDeps? : {... },// Develop the serverserver? : {host:' './ / host
      prot: 3000./ / port
      https: true.// Whether to enable HTTPS
      open: false.// Whether to open it automatically in the browser
      proxy: {
        '/api': {
          target: 'http://jsonplaceholder.typicode.com'.changeOrigin: true.rewrite: (path) = > path.replace(/^\/api/.' ')}}... }})Copy the code

Note: During development, we noticed that introducing the vue file into the TS file would prompt an error indicating that the corresponding module could not be found. This is because the ts file does not recognize the vue file, so we need to teach it to do something. Create the shims-vue.d.ts file in the SRC directory and edit it.

// shims-vue.d.ts
declare module '*.vue' {
  import { Component } from 'vue'
  const component: Component
  export default component
}
Copy the code

2. Introduce router in main.ts:

// main.ts
import { createApp } from 'vue'
import App from './App.vue'
import router from './router';

const app = createApp(App);
app.use(router);
app.mount('#app')
Copy the code

3. Finally, set the view window in app.vue

// App.vue
<template>
  <nav-bar :count="0" :active="'product'"></nav-bar>
  <div class="body">
    <router-view />
  </div>
</template>
Copy the code

Here we introduce our custom component NavBar:

<template>
  <div class="nav-bar">
    <router-link 
      to="/" 
      :class="{ active: active === 'product' }"
    >List of goods</router-link>
    <router-link 
      to="/shoppingCart" 
      :class="{ active: active === 'shoppingCart' }"
    >Shopping cart {{count? `(${count})`:''}}</router-link>
  </div>
</template>
<script lang="ts">
import{ defineComponent }from 'vue';
export default defineComponent({
  name: "NavBar".props: {count: Number.active: String}})</script>
<style lang="less" scoped>.</style>
Copy the code

Finish, start compiling yarn dev, and it works perfectly!

Start fleshing out the page

Add a Vant component library

To make this demo look more “decent”, let’s add another component library, Vant.

npm i vant@next -S
Copy the code

Then follow the installation steps on the Vant website.

It is worth mentioning that we are using Vite in this project, so you do not need to use the on-demand loading method.

Vant official note: There is no need to worry about introducing on demand in Vite. When Vite builds code, it automatically removes unused ESM modules through Tree Shaking. All of Vant 3.0’s internal modules are written on an ESM basis and naturally have the ability to be introduced on demand. The remaining problem at this stage is that unused component styles cannot be recognized and removed by Tree Shaking, and we will consider supporting this with Vite plug-ins

Since we’re not using babel-plugin-import to load on demand, it’s important to remember to introduce the CSS style !!!!!

// main.ts
import 'vant/lib/index.css';
Copy the code

Remember how to add global methods in v3 practice? We would like to propose a toast to you.

  • inapp.config.globalPropertiesAdd properties.
// main.ts 
constapp = createApp(...) ;// Add the global method
app.config.globalProperties.$toast = (msg) = >{
  return Toast(msg) // Customize according to requirements
};

// this.$toast(' light tips are too comfortable ');
Copy the code
  • Add minixs
// utility/minix.ts
const mixin = {
  methods: {
   fn(){
     console.log('----doSomething-----'); }}}export default mixin;

/ / add
import mixin from '/@src/utility/minix.ts';
export default defineComponent({
  name: "Products".mixins:[mixin],
  ...
})
Copy the code
  • Extract the function and export it
// utility/index.ts
import { Toast } from "vant"

export const toast = (msg:string) = > {
  return Toast(msg);
}

/ / call
import { toast } from '/@src/utility/index.ts'; . toast('Light hints');
Copy the code

List of goods

Page of things are not what to say, the eight immortals across the sea can show the avatar, roll up his sleeves to do it ~

<template>
  <div class="products">
    <! -- In v3, v-for, V-if can already do this,v-if always takes precedence over V-for -->
    <div class="product-list" 
      v-for="(item,index) in products" 
      :key="index" 
      v-if=! "" loading"
    >
      <! -- The author is lazy and did not encapsulate components, please ignore -->
      <span class="name">{{item.title}}</span>
      <span class="price">{{item. Price}}</span>
      <van-button 
        type="primary" 
        size="small" 
        @click="addToCart(item)"
      >Add to cart</van-button>
    </div>
    <van-loading v-else />
  </div>
</template>
<script lang="ts">
import { defineComponent,ref }from 'vue';
import { Product } from '/@src/interface';
import { apiGetProducts } from '/@src/api/index';

export default defineComponent({
  name: "Products".setup(){
    const products= ref<Product[]>([]);
    const loading = ref(false);
    // Get the list of products
    const getProducts = async () => {
      loading.value = true;
      products.value = await apiGetProducts();
      loading.value = false;
    }
    getProducts();
    return {
      loading, // Loading status
      products // The list of products}},methods: {addToCart(product:Product){
      console.log('Add to cart'); }}})</script>
<style lang="less" scoped>.</style>

Copy the code

I created two new files with the following path:

  • /interface/index.tsDefined model
  • /api/index.tsThe interface list
// interface/index.ts

export interface Product {
  id:number, // id
  title:string, / / name
  price:number, / / price
  count:number // Purchase quantity
}
Copy the code
// api/index.ts

/** * Get the list of products */
export const apiGetProducts = () = >{
  return new Promise<Product[]>((resolve,reject) = >{
    // Simulate the interface request
    setTimeout(() = >{
      resolve(data);
    },1000)})}Copy the code

I constructed the data in API /data.ts,

Add Vuex @ next

npm install vuex@next --save

// yarn
yarn add vuex@next --save
Copy the code

1. Newstore/index.ts

import { InjectionKey } from 'vue';
import { createStore, useStore as baseUseStore, Store} from 'vuex';
import { Product } from 'src/interface';

export interface State{
  shoppingCart: Product[]
}

export const key: InjectionKey<Store<State>> = Symbol(a);export const store = createStore<State>({
  state: {shoppingCart: []// Shopping cart list}})export function useStore(){
  // Provide a type for the store through the key
  return baseUseStore(key)
}
Copy the code

2. Introduce vuex in app. vue

import { store , key} from './store/index'; . app.use(store,key); .Copy the code

Here, we also successfully added vuex into the project. Most vuEX is used in the same way as in JS environment. There is only more type judgment in TS and some subtle differences used in V3, which will be used in actual scenarios later.

What we’re going to do in VUex is add some mutations, getters that we’re going to use.

// store/index.ts.export const store = createStore<State>({
  state: {shoppingCart:[]
  },
  getters: {// Whether it already exists in the cart
    isInCart(state){
      return (data:any) = >{
        return state.shoppingCart.findIndex(item= >item.id === data.id) > -1 ? true : false; }}},mutations: {// Add the shopping cart
    ADD_TO_CARD(state,data){
      state.shoppingCart.push(data);
    },
    // Update the number of shopping carts
    CHANGE_COUNT(state,{type,data}){
      return state.shoppingCart.map(item= >{
        if(data.id=== item.id){
          item.count += type === 'add' ? 1 : -1;
        }
        returnitem; })},// Delete the shopping cart
    REMOVE_BY_ID(state,id){
      state.shoppingCart = state.shoppingCart.filter(item= >item.id! ==id); }}})export function useStore(){
  // Provide a type for the store through the key
  return baseUseStore(key)
}
Copy the code

Then we continue to improve the list of items page, to achieve the added shopping cart function.

<template>
  ...
</template>
<script lang="ts">
import { defineComponent ,ref }from 'vue';
import { mapMutations, mapGetters } from 'vuex'
import { Product } from '/@src/interface';
import { apiGetProducts } from '/@src/api/index';

export default defineComponent({
  name: "Products".setup(){
    const products= ref<Product[]>([]);
    const loading = ref(false);
    // Get the list of products
    const getProducts = async () => {
      loading.value = true;
      products.value = await apiGetProducts();
      loading.value = false;
    }
    getProducts();
    return {
      loading, // Loading status
      products // The list of products}},computed: {... mapGetters(['isInCart'])},methods: {... mapMutations(['ADD_TO_CARD']),
    addToCart(product:Product){
      // If it already exists
      if(this.isInCart(product)) return this.$toast('Existing');
      // Add to cart
      this.ADD_TO_CARD({
        title:product.title,
        count:1.id:product.id
      })
      this.$toast('Add successful')}}})</script>
<style lang="less" scoped>.</style>

Copy the code

At first, I wrote the code of this page like this, but the more I see, the more confused, the more uncomfortable, why say so?

V3 provides us with a new hook, setup, which also uses computed and methods in setup, but I still picked out the shopping cart function point, and I tried to focus the business logic in setup, but there was no this in setup. My logic would use this (make the toast prompt global) and I had no choice but to do so. I’m sure this scenario is all too common, so how do you solve this problem and make your code look more elegant and reasonable? For a moment I was at a loss. My understanding of Setup was for aggregation logic, but I felt vaguely that MY understanding of setup was “shallow.” So I went to the official website to study.

Aggregation logic, what is aggregation? In information science, the analysis and classification of relevant data. The key is correlation, not that there’s no need to write other comoputed methods with setup, but we need to really understand what setup is for, to bring together our logical concerns. So the question here is is this.$toast() really our core relevant business?

With that in mind, I think our code could be optimized as follows:

<template>
  <div class="products">.<van-button 
        type="primary" 
        size="small" 
        @click="addHandle(item)"
      >Add to cart</van-button>.</div>
</template>
<script lang="ts">.export default defineComponent({
  name: "Products".setup(){
    const products= ref<Product[]>([]);
    const loading = ref(false);
    const { commit, getters } = useStore();
    // Get the list of products
    const getProducts = async () => {
      loading.value = true;
      products.value = await apiGetProducts();
      loading.value = false;
    }
    // Add to cart
    const addToCart = (product:Product) = > {
      commit('ADD_TO_CARD', {title:product.title,
        count:1.id:product.id
      })
    }
    // Check whether it already exists in the shopping cart
    const isInCart = (product:Product) = >{
      return getters.isInCart(product);
    }
    getProducts();
    return {
      loading, // Loading status
      products, // The list of products
      addToCart, // Add to cart
      isInCart // Whether it already exists in the cart}},methods: {addHandle(product:Product){
      // If it already exists
      if(this.isInCart(product)) return this.$toast('Existing');
      this.addToCart(product);
      this.$toast('Add successful')}}})</script>
Copy the code

We’ve put together the relevant business logic to add to the shopping cart, and OK, it looks a lot more comfortable

The logic of this example is relatively simple, and it is not a typical scenario for explaining setup, but the problems described above do exist at this point, and the use of setup and the organization of the code are more likely to be flexible to the actual requirements of the scenario, so I hope you can leave a lot of comments.

We can also replace this.$toast() with an extract function as described above:

<template>
  <div class="products">.<van-button 
        type="primary" 
        size="small" 
        @click="addHandle(item)"
      >Add to cart</van-button>.</div>
</template>
<script lang="ts">.import { toast } from '/@src/utility/index.ts';
export default defineComponent({
  name: "Products".setup(){...// Processing function
    const addHandle = (product:Product) = > {
      // If it already exists
      if(isInCart(product)) return toast('Existing');
      addToCart(product);
      toast('Add successful')
    }
    getProducts();
    return {
      loading, // Loading status
      products, // The list of products
      addHandle // Add the shopping cart}}})</script>
Copy the code

This code style does not look complete and beautiful.

Update the App. Vue

To verify that the shopping cart has been added, let’s update the custom NavBar entry to display the number of shopping cart lists in the navigation.

// App.vue

<template>
  <nav-bar :count="count" :active="activeRouteName"></nav-bar>
  <div class="body">
    <router-view/>
  </div>
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue'
import { useRoute } from 'vue-router'
import { useStore } from '/@src/store/index'
import NavBar from "/@components/NavBar/index.vue"

export default defineComponent({
  name: 'App'.components:{
    NavBar
  },
  setup(props,context) {
    const store = useStore();
    const route  = useRoute(); // this.$route
    // The items in the cart
    const count = computed(():number= >{
      return store.state.shoppingCart.length;
    })
    // The name of the current route
    const activeRouteName = computed(():string= >{
      return route.name?.toString() || ' ';
    })
    return {
      count,
      activeRouteName
    }
  }
})
</script>
Copy the code

Shopping cart page

Speak as little as you can!

<template>
  <div class="shopping-cart">
    <h2>My shopping cart</h2>
    <div 
      class="product-info" 
      v-for="item in shoppingCart" 
      :key="item.id"
    >
      <span>{{item.title}}</span>
      <div class="btn-box">
        <button @click="changeCount('reduce',item)">-</button>
        <span>{{item.count}}</span>
        <button @click="changeCount('add',item)">+</button>
      </div>
      <van-button 
        type="danger" 
        size="small" 
        @click="removeHandle(item)"
      >delete</van-button>
    </div>
  </div>
</template>
<script lang="ts">
import { defineComponent, computed } from 'vue';
import { Product } from 'src/interface';
import { useStore } from '/@src/store/index';
import { toast } from '/@src/utility/index.ts';

export default defineComponent({
  name: "ShoppingCart".setup: () = > {
    const { state,commit } = useStore();
    const shoppingCart = computed(() = >{
      return state.shoppingCart
    })
    // Update the number of shopping carts
    const changeCount = (type:string,data:Product) = > {
      // Ensure that the minimum quantity in the shopping cart is 1
      if(type === 'reduce' && data.count <= 1) return;
      commit('CHANGE_COUNT',{type,data})
    }
    // Delete the shopping cart
    const removeCart = (data:Product) = > {
      commit('REMOVE_BY_ID',data.id);
    }
    // Processing function
    const removeHandle = (data:Product) = > {
      removeCart(data);
      toast('Deletion succeeded')}return {
      shoppingCart, // Shopping cart list
      changeCount, // Update the number of shopping carts
      removeHandle // Delete the shopping cart}}})</script>
<style lang="less" scoped>
  @import './index.less';
</style>
Copy the code

OK ~ Now all ends.

The source address

At the end of

Thank you for your patience, hard work, hope you have a harvest.

The road to simple, easy to know difficult to do, knowing and doing unity, get success.