Битва с корзиной покупок Vue3+Ts+Vite

Vue.js внешний фреймворк

Это статья по обмену практическим опытом.Некоторые специфические функциональные точки реализованы во фронтенде для упрощения.Бэкенда поддержки нет.Основная цель - изучитьVue3+Typescriptсочетаются в средеVuex@4.xтак же какVue-Router@4.xразработка. Общее требование состоит в том, чтобы смоделировать реализацию функции корзины.Функция относительно проста, но она все еще разрабатывается как упрощенная версия проекта.Если у вас есть какие-либо вопросы, пожалуйста, оставьте сообщение для обсуждения и исправления, спасибо .

Давайте сначала посмотрим на эффект:

Возможно~ Вы можете увидеть это в конце:

  • Как создать среду проекта, как в названии
  • Советы по быстрому созданию SFC путем установки шаблонов
  • улучшить пониманиеsetupИ сценарий использования (надеюсь, не вызвал у всех непонимания)
  • оviteНекоторые базовые знания о настройке
  • как использовать в тсvuex@4.x,vue-router@4.x
  • установить глобальный метод в vue3
  • more???

если правильноVue3Для тех, кто еще не изучил это, вы можете сначала прочитать эту статью:

Давайте начнем~

Создать проект

первое использованиеViteсоздание инструментаVue3+Typescriptсреда проекта

Уведомление:ViteНужна твойNodejsверсия >=12.0.0

// yarn 方式
yarn create @vitejs/app v3-ts --template vue-ts
cd v3-ts
yarn install

// yarn create vite-app v3-ts --template vue-ts 这个1.x的命令

// npm 
npm init @vitejs/app v3-ts --template vue-ts
cd v3-ts
npm install 

ViteТакже предоставляются некоторые другие шаблоны:

vanilla
vue
vue-ts 
react
react-ts
preact
preact-ts

Сначала наведите порядок в проекте и удалите официальную демку (HelloWorld удаления, связанные с компонентами).

добавить меньше

yarn add less less-loader --dev

// npm
npm install less less-loader --save-dev

Примечание: если не добавлено здесь--dev, пакет будет установлен наdependencies, что приведет к сбою компиляции. нужно бытьlessа такжеless-loaderпереехал вdevDependenciesповторно выполнитьyarnУстановить.

Улучшить структуру каталогов

Создайте в проекте несколько папок и соответствующие им шаблоны файлов vue.Структура папок примерно следующая (можно создатьpagesПапки, остальные будут создаваться последовательно):


Советы: Здесь, кстати, я представлю небольшой метод для vscode для быстрого создания страницы или шаблона vue (пожалуйста, игнорируйте, если вы уже это знаете)

Шаг 1: vscode> Настройки> Фрагменты пользователя

Шаг 2: Введите vue, найдите vue.json, чтобы открыть

Шаг 3: Установите шаблон кода (уже подготовлен для всех, скопируйте и вставьте!)

// vue.json
{
    "Vue Template":{
      "prefix":"vueTemplate",
      "body":[
      "<template>\n\t<div>\n\n\t</div>\n</template>",
      "<script lang=\"ts\">\nimport{ defineComponent }from 'vue';\nexport default defineComponent({\n\tname: \"\",\n\tsetup: () => {\n\n\t}\n})\n</script>",
      "<style lang=\"less\" scoped>\n\n</style>"
      ],
      "description":"生成vue文件"
    }
}

Шаг 4: ctrl (команда) + S (сохранить)

Шаг 5: Войдите в файл vue.vueTemplate, vscode выдаст подсказку, смело нажимайте Enter!

step6 : good job ~

// 模版长这样
<template>
  <div>

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

  }
})
</script>
<style lang="less" scoped>

</style>

Вы можете установить шаблон в соответствии с вашей собственной ситуацией.


добавить vue-маршрутизатор

Приступаем к настройке маршрутизации.

yarn add vue-router@4.0.1

// 查看历史版本
// npm(yarn) info vue-router versions

Обратите внимание на молниезащиту: не рекомендуется использовать4.0.0Версия (товарищи из инженерной компании, пожалуйста, не стесняйтесь ~).

vue-routerиз4.xВ версии представлен уже не класс, а набор функций. мы проходимcreateRouterСоздавайте маршруты.

1.существуетrouter/index.tsв, создав маршрут и экспортировав его.

Уведомление:существуетv3Есть некоторые изменения в определении асинхронных компонентов, иviteЗаменятьwebpack, так чтоrequireа такжеrequire.ensureНедопустимый способ реализации ленивой загрузки маршрутов.

  • ESсерединаimport
// 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;

  • пройти черезdefineAsyncComponentСоздайте
import { defineAsyncComponent } from 'vue'
...
 {
    path: '/shoppingCart',
    name: 'shoppingCart',
    component:defineAsyncComponent(() => new Promise((resolve,reject)=>{
    	...doSomething
        resolve({
          // 异步组件选项
          ...
        })
    })) ,
 },
  {
    path: '/shoppingCart',
    name: 'shoppingCart',
    component:defineAsyncComponent(() => import('.....')) , 
 },

Мы используем псевдонимы для введения компонентов, чтобы сделать путь болеелаконично, ясно. Это требует от нас некоторой настройки, сначала создайте в корневом каталоге проектаvite.config.js(более высокая версияViteавтоматически создаст этот файл илиvite.config.ts) и настройте следующим образом.

Уведомление:Возможны ошибки в плагине vetur с подсказкой импортировать некоторые пути через алиасы в vscode, но проблема не большая.Если путь правильный то на работу проекта это не повлияет.Причина не особо понятна мне.Если кто-нибудь знает, пожалуйста, оставьте сообщение и дайте мне знать На мгновение, большое спасибо!

// vite.config.js 
const path = require('path');
module.exports = { 
  alias:{
    // 注意这边一定要加双斜杠
    '/@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/',
  }
})

Вот список для васvite.config.tsОбщая конфигурация ,vite.config.jsКонфигурация примерно такая же, детали можно опуститьОфициальный сайтЭталонная конфигурация:

// vite.config.ts  
export default defineConfig({
  plugins: [vue()],
  // 项目根目录,可以是绝对路径也可以是相对配置文件所在的路径
  root?: '',
  // 运行编译模式 'development' || 'production' 
  mode?: 'development' , 
  // 路径别名
  alias?: {} , 
  // 全局定义变量替换
  define?:{
      '':''
  },
  // build选项
  build?:{
      base:'/', // 基础路径
      target:'modules', // 浏览器兼容模块
      outDir:'dist', // 输出路径
      assetsDir:'assets' // 静态资源路径
      ...
  },
  // 依赖优化项
  optimizeDeps?:{
      ...
  },
  // 开发服务器
  server?:{
      host:'', // 主机
      prot: 3000, // 端口
      https: true, // 是否开启 https
      open: false, // 是否在浏览器自动打开
      proxy: {
        '/api': {
          target: 'http://jsonplaceholder.typicode.com',
          changeOrigin: true,
          rewrite: (path) => path.replace(/^\/api/, '')
        },
      }
      ...
  }
})

Уведомление: В процессе разработки мы обнаружим, что введение файла vue в файл ts вызовет ошибку, что соответствующий модуль не может быть найден. Это связано с тем, что файл ts не может распознать файл vue, нам нужнонаучить его делать вещи,существуетsrcСоздайте файл shims-vue.d.ts в каталоге и отредактируйте его.

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

2.существуетmain.tsвведен вrouter:

// 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')

3.Наконец вapp.vueУстановите окно просмотра в

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

Здесь мы представляем наши пользовательские компонентыNavBar:

<template>
  <div class="nav-bar">
    <router-link 
      to="/" 
      :class="{ active: active === 'product' }"
    >商品列表</router-link>
    <router-link 
      to="/shoppingCart" 
      :class="{ active: active === 'shoppingCart' }"
    >购物车{{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>

Готово, приступаем к сборкеyarn dev, работает отлично!

Начать заполнение страницы~

Добавить библиотеку компонентов Vant

Чтобы эта демонстрация выглядела более «прилично», давайте добавим еще одну библиотеку компонентов.vant.

npm i vant@next -S

затем следуйтеофициальный сайт ВантШаги установки просто прекрасны.

Стоит отметить, что в нашем проекте используетсяVite, поэтому вам не нужно использовать загрузку по требованию.

Официальное примечание Ванта: В Vite нет необходимости рассматривать проблему внедрения по требованию. Vite автоматически удаляет неиспользуемые модули ESM с помощью Tree Shaking при создании кода. Все модули в Vant 3.0 написаны на основе ESM и, естественно, могут быть введены по запросу. Проблема, оставшаяся на этом этапе, заключается в том, что неиспользуемые стили компонентов не могут быть распознаны и удалены Tree Shaking, и мы рассмотрим возможность их поддержки с помощью плагинов Vite в будущем.

Поскольку мы не используемbabel-plugin-importЗагружайте по требованию, это нужно помнитьимпортировать стиль css! ! ! ! !

// main.ts
import 'vant/lib/index.css';

Внезапно я вспомнил, как мой коллега говорил, как добавлять глобальные методы при отработке v3? Давайте используем тост в качестве примера, oh huo ~ успешно установил засаду (наземную) ручку (свет) позади, и предоставим вам несколько методов.

  • существуетapp.config.globalPropertiesДобавьте свойства.
// main.ts 
const app = createApp(...);
// 添加全局方法
app.config.globalProperties.$toast = (msg)=>{
  return Toast(msg) // 根据需求自定义
};

// this.$toast('轻提示太舒服了');
  • Добавить кminixs
// utility/minix.ts
const mixin = {
  methods: {
   fn(){
     console.log('----doSomething-----');
   }
  }
}

export default mixin;

// 添加
import mixin from '/@src/utility/minix.ts';
export default defineComponent({
  name: "Products",
  mixins:[mixin],
  ...
})
  • Извлечение функций и экспорт
// utility/index.ts
import { Toast } from "vant"

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

// 调用
import { toast } from '/@src/utility/index.ts';

...
toast('轻提示');

Список продуктов

О рисовании страниц и говорить нечего.Восемь Бессмертных могут показать свои магические силы, когда пересекают море.Засучите рукава и сделайте это.

<template>
  <div class="products">
    <!-- 在v3里面,v-for,v-if已经可以这么干了,v-if总是优先于v-for -->
    <div class="product-list" 
      v-for="(item,index) in products" 
      :key="index" 
      v-if="!loading"
    >
      <!-- 作者比较懒没有封装成组件,大家请无视 -->
      <span class="name">{{item.title}}</span>
      <span class="price">{{item.price}}元</span>
      <van-button 
        type="primary" 
        size="small" 
        @click="addToCart(item)"
      >加入购物车</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);
    // 获取产品列表
    const getProducts = async () => {
      loading.value = true;
      products.value = await apiGetProducts();
      loading.value = false;
    }
    getProducts();
    return {
      loading, // 加载状态
      products // 商品列表
    }
  },
  methods:{
    addToCart(product:Product){
      console.log('加入购物车');
    }
  }
})
</script>
<style lang="less" scoped>
  ...
</style>

Я создал два новых файла со следующими путями:

  • /interface/index.tsопределенная модель
  • /api/index.tsСписок интерфейсов
// interface/index.ts

export interface Product {
  id:number, // id
  title:string, // 名称
  price:number, // 价格
  count:number // 购买数量
}
// api/index.ts

/**
 * 获取产品列表
 */
export const apiGetProducts = ()=>{
  return new Promise<Product[]>((resolve,reject)=>{
    // 模拟接口请求
    setTimeout(()=>{
      resolve(data);
    },1000)
  })
}

данные находятся вapi/data.tsпостроен в,

добавить Vuex@следующий

npm install vuex@next --save

// yarn
yarn add vuex@next --save

1. Новыйstore/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();

export const store = createStore<State>({
  state:{
    shoppingCart:[] // 购物车列表
  },
})

export function useStore(){
  // 通过key给store提供类型
  return baseUseStore(key)
}

2. Добавьте vuex в App.vue

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

мы здесьvuexТакже успешно добавлены в проект, большая частьvuexПриложение такое же, как и в среде js, за исключением того, что в ts больше оценок типов и некоторые тонкие отличия, используемые в сочетании с v3, которые позже будут использоваться в реальных сценариях.

мы вvuexДобавьте некоторые из тех, которые мы будем использовать в первую очередьmutations,getters.

// store/index.ts
...
export const store = createStore<State>({
  state:{
    shoppingCart:[]
  },
  getters:{
    // 是否在购物车中已存在
    isInCart(state){
      return (data:any)=>{
        return state.shoppingCart.findIndex(item=>item.id === data.id) > -1 ? true : false;
      }
    }
  },
  mutations:{
    // 添加购物车
    ADD_TO_CARD(state,data){
      state.shoppingCart.push(data);
    },
    // 更新购物车数量
    CHANGE_COUNT(state,{type,data}){
      return state.shoppingCart.map(item=>{
        if(data.id=== item.id){
          item.count += type === 'add' ? 1 : -1;
        }
        return item;
      })
    },
    // 删除购物车
    REMOVE_BY_ID(state,id){
      state.shoppingCart = state.shoppingCart.filter(item=>item.id!==id);
    }
  }
})
export function useStore(){
  // 通过key给store提供类型
  return baseUseStore(key)
}

Далее продолжаем улучшать страницу списка товаров и реализовывать функцию добавления в корзину.

<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);
    // 获取产品列表
    const getProducts = async () => {
      loading.value = true;
      products.value = await apiGetProducts();
      loading.value = false;
    }
    getProducts();
    return {
      loading, // 加载状态
      products // 商品列表
    }
  },
  computed:{
    ...mapGetters(['isInCart'])
  },
  methods:{
    ...mapMutations(['ADD_TO_CARD']),
    addToCart(product:Product){
      // 如果已经存在
      if(this.isInCart(product)) return this.$toast('已存在');
      // 加入购物车
      this.ADD_TO_CARD({
        title:product.title,
        count:1,
        id:product.id
      })
      this.$toast('添加成功')
    }
  }
})
</script>
<style lang="less" scoped>
  ...
</style>

Сначала я написал код этой страницы так, но чем больше я его читаю, тем он становится более хаотичным и несуразным, почему я так говорю?

v3 дает нам новый хукsetup,существуетsetupтакже можно использовать вcomputed,methods, но я все же вывел функциональный пункт добавления в корзину, а бизнес-логику постарался сконцентрировать вsetupв, ноsetupНет вthis, и моя логика будет использоватьthis(Подсказка всплывающего окна настроена как глобальный метод), у меня нет возможности сделать это. Я считаю, что такие сценарии слишком распространены, так как же решить эту проблему и сделать код более элегантным и разумным? Какое-то время я был беспомощен, я был правsetupпонимание дляЛогика агрегации, но я смутно чувствую, что яsetupпонимание слишком "поверхностно". Поэтому я пошел на официальный сайт, чтобы учиться снова.

Логика агрегации,чтополимеризация? в области информатикиСвязанныйДанные анализируются и классифицируются. Ключ в том,Связанный",не то чтобы естьsetupбольше ничего писать не надоcomoputed,methodsохватывать, но мы должны по-настоящему понятьsetupдля чего это, что у наслогические проблемысобраться вместе. Так вот вопросthis.$toast()Действительно ли это наш основной родственный бизнес?

Если мы это поймем, я думаю, наш код можно оптимизировать так:

<template>
  <div class="products">
    ...
    <van-button 
        type="primary" 
        size="small" 
        @click="addHandle(item)"
      >加入购物车</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();
    // 获取产品列表
    const getProducts = async () => {
      loading.value = true;
      products.value = await apiGetProducts();
      loading.value = false;
    }
    // 加入购物车
    const addToCart = (product:Product) => {
      commit('ADD_TO_CARD',{
        title:product.title,
        count:1,
        id:product.id
      })
    }
    // 判断是否在购物车中已存在
    const isInCart = (product:Product)=>{
      return getters.isInCart(product);
    }
    getProducts();
    return {
      loading, // 加载状态
      products, // 商品列表
      addToCart, // 加入购物车
      isInCart // 是否在购物车中已存在
    }
  },
  methods:{
    addHandle(product:Product){
      // 如果已经存在
      if(this.isInCart(product)) return this.$toast('已存在');
      this.addToCart(product);
      this.$toast('添加成功')
    }
  }
})
</script>

Мы агрегировали соответствующую бизнес-логику добавления в корзину, ОК, так удобнее ~

Логика этого примера относительно проста и не объясняет настройку.Типичная сцена, но описанная выше проблема действительно существует на данный момент, оsetupПрименение кода и организация кода могут привести к гибкому реагированию на реальный сценарий спроса.Я надеюсь, что вы оставите много комментариев для обсуждения.

Мы также можем заменить метод извлечения функций выше.this.$toast():

<template>
  <div class="products">
    ...
    <van-button 
        type="primary" 
        size="small" 
        @click="addHandle(item)"
      >加入购物车</van-button>
    ...
  </div>
</template>
<script lang="ts">
...
import { toast } from '/@src/utility/index.ts';
export default defineComponent({
  name: "Products",
  setup(){
    ...
    // 处理函数
    const addHandle = (product:Product) => {
      // 如果已经存在
      if(isInCart(product)) return toast('已存在');
      addToCart(product);
      toast('添加成功')
    }
    getProducts();
    return {
      loading, // 加载状态
      products, // 商品列表
      addHandle // 添加购物车
    }
  }
})
</script>

Выглядит ли этот стиль кода завершенным и красивым?

Обновить App.vue

Чтобы мы могли убедиться, что корзина успешно добавлена, давайте обновим пользовательскийNavBar, который отображает количество списков корзины покупок в навигации.

// 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
    // 购物车中的商品种类
    const count = computed(():number=>{
      return store.state.shoppingCart.length;
    })
    // 当前路由的name
    const activeRouteName = computed(():string =>{
      return route.name?.toString() || '';
    })
    return {
      count,
      activeRouteName
    }
  }
})
</script>

страница корзины покупок

Говорите как можно меньше!

<template>
  <div class="shopping-cart">
    <h2>我的购物车</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)"
      >删除</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
    })
    // 更新购物车数量
    const changeCount = (type:string,data:Product) => {
      // 保证购物车中最小数量为1
      if(type === 'reduce' && data.count <= 1) return;
      commit('CHANGE_COUNT',{type,data})
    }
    // 删除购物车
    const removeCart = (data:Product) => {
      commit('REMOVE_BY_ID',data.id);
    }
    // 处理函数
    const removeHandle = (data:Product) => {
      removeCart(data);
      toast('删除成功')
    }
    return {
      shoppingCart, // 购物车列表
      changeCount, // 更新购物车数量
      removeHandle // 删除购物车
    }
  },
})
</script>
<style lang="less" scoped>
  @import './index.less';
</style>

ОК~ Все кончено.

Адрес источника

конец

Спасибо за ваше терпение, вы усердно работали, и я надеюсь, что у вас что-то получится.

Дорога проста, легка в познании и трудна в исполнении, единство знания и действия, успех.