🔥Используйте семейную корзину vue3.0.0, чтобы воссоздать демо-версию vue2.6.1 в торговом центре!

Vue.js
🔥Используйте семейную корзину vue3.0.0, чтобы воссоздать демо-версию vue2.6.1 в торговом центре!

vue3-jd-h5

Когда Composition API для vue только что выпустили, я написал статьюНа основе бета-версии vue3.0.1 создайте проект электронной коммерции H5, имитирующий JD.com!В статье представлены некоторые новые функции Vue.Теперь выпущена официальная версия.В этом году у меня есть время начать использовать последнюю версию семейства Vue для реконструкции! Другие основные виды использования пропускаются напрямую, если вы не понимаете, вы можете прочитать это напрямую.китайский официальный сайтпример или япредыдущая статья!

Введение в проект

vue3-jd-h5Это интерфейсный проект страницы H5 для электронной коммерции, преобразованный из vue2.6.1 в vue3.0.0 на основеСемейная корзина Vue 3.0.0Vant 3.0.0выполнить!

📖Локальный оффлайн кодvue2.6в филиалеdemoв использованииmockjsДанные для разработки, пожалуйста, нажмите для визуализации 🔗здесь

⚠️Главная ветка — это код рабочей онлайн-среды, так как некоторые из фоновых интерфейсов были приостановлены😫, фактический эффект может быть не виден.

📌 у этого проекта много недостатков, если вы хотите внести свой вклад в это, партнеры также могут дать нам PR или выпуск;

🔑 Этот проект бесплатный и с открытым исходным кодом. Если есть партнеры, которые хотят развивать вторичную разработку на этой основе, они могут клонировать или форкнуть весь репозиторий. Буду очень рад, если смогу вам помочь. Если вы считаете, что этот проект хорош , пожалуйста, начните! 🙏

начать строить

  1. Сначала выберите локальный файл и локально клонируйте код:
git clone https://github.com/GitHubGanKai/vue-jd-h5.git 
  1. 👉 перейти на веткуvue-nextНачните экспериментировать (в настоящее время проводится постепенный рефакторинг)! 👈

  2. Запустите команду в командной строке IDEA:npm installЗагрузите и установите соответствующие зависимости;

Установите семейное ведро vue

Настроить установкуvue-router

// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'

const indexRouter = {
  path: '/',
  component: () => import('@/views/index'),
  redirect: '/index',
  children: []
}

const routes = [
  indexRouter,
  {
    path: '/*',
    name: '404',
    meta: {
      index: 1
    },
    component: () => import('@/views/error/404')
  },
]

const routerContext = require.context('./modules', true, /\.js$/)
routerContext.keys().forEach(route => {
  const routerModule = routerContext(route)
  indexRouter.children = [...indexRouter.children, ...(routerModule.default || routerModule)]
})

export default createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes
})

использоватьuseRouterХуки могут получать объекты маршрута:

import { onMounted } from "vue";
import { useRouter, useRoute } from "vue-router";

export default {
  name: "home",
  setup(props, context) {
    const $router = useRouter();
    
    const handleClick = id => {
      $router.push(`/classify/product/${id}`);
    };

    return {
      handleClick,
    };
  }
};

использоватьuseRouteПолучить объект параметра маршрута

import { onMounted } from "vue";
import { useRoute } from "vue-router";

export default {
  name: "home",
  setup(props, context) {
    // 可以拿到所有和路由相关的参数
    // 和useRouter()就差一个字母r,😅
    const $route = useRoute(); 
    
     onMounted(async () => {
      const { data } = await ctx.$http.get(
        `http://test.happymmall.com/product/${$route.params.id}`
      );
    });

    return {
      $route,
    };
  }
};

Настроить и установить vuex

// src/store/index.js
import { createStore } from 'vuex'

import cart from './modules/cart'
import search from './modules/search'

export default createStore({
  modules: {
    cart,
    search
  },
  strict: process.env.NODE_ENV !== 'production'
})

Используйте в файле следующее:

import { useStore } from "vuex";
import { reactive, getCurrentInstance } from "vue";

setup(props, context) {
    const { ctx } = getCurrentInstance();
    const $store = useStore();
    // ctx.$store === $store  ==>true 其实是同一个对象!
    
    const ball = reactive({
      show: false,
      el: ""
    });

    const addToCart = (event, tag) => {
      $store.commit("cart/addToCart", tag);
      ball.show = true;
      ball.el = event.target;
    };

    return {
      ...toRefs(ball),
      addToCart,
    };
  }

во входном файлеmain.jsиспользуется в:

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import 'lib-flexible/flexible'

import Vant from 'vant'
import 'vant/lib/index.css' // 全局引入样式

const app = createApp(App);
app.use(Vant).use(store).use(router).mount('#app');

использоватьsvg-sprite-loaderОбрабатывать svg-файлы

первый вvue.config.jsСредняя конфигурацияsvg-sprite-loader:

module.exports = {
   chainWebpack: config => {
    const svgRule = config.module.rule("svg");
    svgRule.uses.clear();
    svgRule
      .use("svg-sprite-loader")
      .loader("svg-sprite-loader")
      .options({
        symbolId: "icon-[name]"
      })
      .end();
  },
}

существуетsrc/components/SvgIcon/index.vueсередина:

import { computed, toRefs, toRef } from "vue";
export default {
  name: "svg-icon",
  props: {
    iconClass: {
      type: String,
      required: true
    },
    className: {
      type: String
    }
  },
  setup(initProps) {
    // const { iconClass } = initProps;❌
    // 因为 props 是响应式的,你不能使用 ES6 解构,因为它会消除 prop 的响应性。
    // 如果需要解构 prop,可以通过使用 setup 函数中的 toRefs 来完成此操作:
    const { iconClass } = toRefs(initProps);
    const iconName = computed(() => {
      return `#icon-${iconClass.value}`;
    });

    // 由于 className 是可选的 prop,则传入的 props 中可能没有 className 。
    // 在这种情况下,toRefs 将不会为 className 创建一个 ref ,需要使用 toRef 替代它。
    const className = toRef(initProps, "className");
    const svgClass = computed(() => {
      if (className) {
        return "svg-icon " + className.value;
      } else {
        return "svg-icon";
      }
    });
    return {
      iconName,
      svgClass
    };
  }
};

Напишите это как плагин и единообразно зарегистрируйте все файлы svg как компоненты для глобального использования!

// src/icons/index.js
import SvgIcon from '@/components/SvgIcon'

const requireAll = requireContext => requireContext.keys().map(requireContext)

export default {
  install(app) {
    app.component('svg-icon', SvgIcon);
    const req = require.context('./svgs/', false, /\.svg$/)
    requireAll(req)
  }
}

Единая регистрация всех компонентов

существуетsrc/components/index.jsВ файле:

function capitalizeFirstLetter(str) {
  return str.charAt(0).toUpperCase() + str.slice(1)
}

function validateFileName(str) {
  return /^\S+\.vue$/.test(str) &&
    str.replace(/^\S+\/(\w+)\.vue$/, (rs, $1) => capitalizeFirstLetter($1))
}

const requireComponent = require.context('.', true, /\.vue$/)

export default {
  install(app) {
    requireComponent.keys().forEach(filePath => {
      const componentConfig = requireComponent(filePath)
      const fileName = validateFileName(filePath)
      const componentName = fileName.toLowerCase() === 'index' ?
        capitalizeFirstLetter(componentConfig.default.name) :
        fileName
      app.component(componentName, componentConfig.default || componentConfig)
    })
  }
}
function capitalizeFirstLetter(str) {
  return str.charAt(0).toUpperCase() + str.slice(1)
}

function validateFileName(str) {
  return /^\S+\.vue$/.test(str) &&
    str.replace(/^\S+\/(\w+)\.vue$/, (rs, $1) => capitalizeFirstLetter($1))
}
const requireComponent = require.context('.', true, /\.vue$/)
export default {
  install(app) {
    requireComponent.keys().forEach(filePath => {
      const componentConfig = requireComponent(filePath)
      const fileName = validateFileName(filePath)
      const componentName = fileName.toLowerCase() === 'index' ?
        capitalizeFirstLetter(componentConfig.default.name) :
        fileName
      app.component(componentName, componentConfig.default || componentConfig)
    })
  }
}

настроить глобальныйaxiosИнкапсулировать асинхронный запрос:

// src/plugins/axios.js
import axios from 'axios'
import router from '../router/index'
import { Toast } from 'vant'
const tip = msg => {
  Toast({
    message: msg,
    duration: 1000,
    forbidClick: true
  })
}
const errorHandle = (status, other) => {
  switch (status) {
    case 401:
      toLogin()
      break
    case 403:
      tip('登录过期,请重新登录')
      localStorage.removeItem('token')
      setTimeout(() => {
        toLogin()
      }, 1000)
      break
    case 404:
      tip('请求的资源不存在')
      break
    default:
      console.log(other)
  }
}
const instance = axios.create({
  baseURL: process.env.VUE_APP_BASE_URL,
  // baseURL: '',
  timeout: 1000 * 12
})
instance.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8'
instance.interceptors.request.use(
  config => {
    const token = localStorage.token
    token && (config.headers.token = token)
    return config
  },
  error => Promise.error(error))

// 响应拦截器
instance.interceptors.response.use(
  // 请求成功
  response => {
    return response.status === 200 ? Promise.resolve(response) : Promise.reject(response)
  },
  // 请求失败
  error => {
    const {
      response
    } = error
    if (response) {
      // 请求已发出,但是不在2xx的范围
      errorHandle(response.status, response.data.message)
      return Promise.reject(response)
    }
  })

export default {
  install(app) {
    // 这可以代替 Vue 2.x Vue.prototype 在单文件中,可以这样使用:
    // const { ctx } = getCurrentInstance();
    // ctx.$http访问
    app.config.globalProperties.$http = instance
  }
};

Инкапсулирует глобальную шину событий

существует2.xсередина,VueЭкземпляры можно использовать для запуска функций обработчика, которые императивно добавляются API, запускаемым событием (on,на,выкл и $один раз). Это создает концентратор событий для создания глобальных прослушивателей событий, доступных во всем приложении:

// src/utils/eventBus.js

const eventBus = new Vue()

export default eventBus

3.xполностью удален из экземпляра$on,$offа также$onceметод.$emitВсе еще включен в существующий API, поскольку он используется для запуска обработчиков событий, которые декларативно добавляются родительским компонентом. Существующий концентратор событий можно заменить внешней библиотекой, реализующей интерфейс запуска событий, напримерmittилиtiny-emitter.

Так что нужно2.xна основе самопреобразования (без использованияmittилиtiny-emitter):

import { getCurrentInstance } from 'vue'

class EventBus {
  constructor(app) {
    if (!this.handles) {
      Object.defineProperty(this, 'handles', {
        value: {},
        enumerable: false
      })
    }
    this.app = app
    // _uid和EventName的映射
    this.eventMapUid = {}
  }
  setEventMapUid(uid, eventName) {
    if (!this.eventMapUid[uid]) {
      this.eventMapUid[uid] = []
    }
    this.eventMapUid[uid].push(eventName)
    // 把每个_uid订阅的事件名字push到各自uid所属的数组里
  }
  $on(eventName, callback, vm) {
    // vm是在组件内部使用时组件当前的this用于取_uid
    if (!this.handles[eventName]) {
      this.handles[eventName] = []
    }
    this.handles[eventName].push(callback)
    this.setEventMapUid(vm._uid, eventName)
  }
  $emit() {
    let args = [...arguments]
    let eventName = args[0]
    let params = args.slice(1)
    if (this.handles[eventName]) {
      let len = this.handles[eventName].length
      for (let i = 0; i < len; i++) {
        this.handles[eventName][i](...params)
      }
    }
  }
  $offVmEvent(uid) {
    let currentEvents = this.eventMapUid[uid] || []
    currentEvents.forEach(event => {
      this.$off(event)
    })
  }
  $off(eventName) {
    delete this.handles[eventName]
  }
}

let $EventBus = {}
$EventBus.install = (app) => {
  app.config.globalProperties.$eventBus = new EventBus(app)
  app.mixin({
    beforeUnmount() {
      const currentInstance = getCurrentInstance();
      // 拦截beforeUnmount钩子,自动销毁自身所有订阅的事件
      this.$eventBus.$offVmEvent(currentInstance._uid)
    }
  })
}
export default $EventBus

существуетsrc/views/classify/index.vueФайл используется следующим образом:

import ListScroll from "@/components/scroll/ListScroll";
import { ref, reactive, onMounted, toRefs, getCurrentInstance } from "vue";
import { useRouter } from "vue-router";

export default {
  name: "classify",
  components: {
    ListScroll
  },
  setup(props) {
    
    const { ctx } = getCurrentInstance();
    const $router = useRouter();

    const searchWrap = ref(null);

    const state = reactive({
      categoryDatas: [],
      currentIndex: 0
    });

    const selectMenu = index => {
      state.currentIndex = index;
    };

    const setSearchWrapHeight = () => {
      const { clientHeight } = document.documentElement;
      searchWrap.value.style.height = clientHeight - 100 + "px";
    };

    const selectProduct = sku => {
      $router.push({ path: "/classify/recommend", query: { sku } });
    };

    onMounted(async () => {
      setSearchWrapHeight();
      // 使用全局注过的$eventBus
      ctx.$eventBus.$emit("changeTag", 1);
      // 使用全局注册过的$http
      const { data } = await ctx.$http.get(
        "http://test.happymmall.com/category/categoryData"
      );
      const { categoryData } = data;
      state.categoryDatas = categoryData;
    });

    return {
      searchWrap,
      ...toRefs(state),
      selectProduct,
      selectMenu
    };
  }
};

Инкапсулируйте простой хук:useClickOutside

// src/hooks/useClickOutside.js
import { onMounted, onUnmounted, ref } from "vue";

export default useClickOutSide = (domRef) => {
  const isOutside = ref(false);

  const handler = (event) => {
    if (domRef.value) {
      if (domRef.value.contains(event.target)) {
        isOutside.value = false;
      } else {
        isOutside.value = true;
      }
    }
  }

  onMounted(() => {
    document.addEventListener('click', handler);
  });

  onUnmounted(() => {
    document.removeEventListener('click', handler);
  });

  return isOutside;
}

использовать этот

существуетsetup()внутренний,thisне будет ссылкой на активный экземпляр, потому чтоsetup()вызывается перед разбором других параметров компонента, поэтомуsetup()Внутреннийthisповедение такое же, как и в других вариантахthisполностью отличается. Этот и другие вариантыAPIиспользовать вместеsetup()может вызвать путаницу.

в состоянии пройтиgetCurrentInstanceПолучить экземпляр текущего компонента одного файла, в то время какctxВышеупомянутое зависает некоторые глобальные свойства:

import { getCurrentInstance,  onMounted, reactive, toRefs } from 'vue'

export default {
  name: "classify",
  setup(props) {
    const { ctx } = getCurrentInstance();
    
    const state = reactive({
      categoryDatas: [],
      currentIndex: 0
    });
    
    onMounted(async () => {
      const { data } = await ctx.$http.get("http://test.happymmall.com/category/categoryData");
      const { categoryData, page } = data;
      state.categoryDatas = categoryData;
      state.currentIndex = page;
    });
    
    return {
      ...toRefs(state)
    };
  }
};

📦 Обернуть лучше-листайте

<template>
  <div ref="wrapper" class="scroll-wrapper">
    <slot></slot>
  </div>
</template>
<script>
import BScroll from "better-scroll";
import { onMounted, nextTick, ref, watchEffect } from "vue";
export default {
  props: {
    probeType: {
      type: Number,
      default: 1
    },
    click: {
      type: Boolean,
      default: true
    },
    scrollX: {
      type: Boolean,
      default: false
    },
    listenScroll: {
      type: Boolean,
      default: false
    },
    scrollData: {
      type: Array,
      default: null
    },
    pullup: {
      type: Boolean,
      default: false
    },
    pulldown: {
      type: Boolean,
      default: false
    },
    beforeScroll: {
      type: Boolean,
      default: false
    },
    refreshDelay: {
      type: Number,
      default: 20
    }
  },
  setup(props, setupContext) {
    const wrapper = ref(null);

    const initScroll = () => {
      if (!wrapper.value) return;
      const scroll = new BScroll(wrapper.value, {
        probeType: props.probeType,
        click: props.click,
        scrollX: props.scrollX
      });
      // 是否派发滚动事件
      if (props.listenScroll) {
        scroll.on("scroll", pos => {
          setupContext.emit("scroll", pos);
        });
      }
      // 是否派发滚动到底部事件,用于上拉加载
      if (props.pullup) {
        scroll.on("scrollEnd", () => {
          // 滚动到底部
          if (scroll.y <= scroll.maxScrollY + 50) {
            setupContext.emit("scrollToEnd");
          }
        });
      }
      // 是否派发顶部下拉事件,用于下拉刷新
      if (props.pulldown) {
        scroll.on("touchend", pos => {
          // 下拉动作
          if (pos.y > 50) {
            setupContext.emit("pulldown");
          }
        });
      }
      // 是否派发列表滚动开始的事件
      if (props.beforeScroll) {
        scroll.on("beforeScrollStart", () => {
          setupContext.emit("beforeScroll");
        });
      }
    };
    const disable = () => {
      // 代理better-scroll的disable方法
      scroll?.disable();
    };
    const enable = () => {
      // 代理better-scroll的enable方法
      scroll?.enable();
    };
    const refresh = () => {
      // 代理better-scroll的refresh方法
      scroll?.refresh();
    };
    const scrollTo = () => {
      // 代理better-scroll的scrollTo方法
      scroll?.scrollTo.apply(scroll, arguments);
    };
    const scrollToElement = () => {
      // 代理better-scroll的scrollToElement方法
      scroll?.scrollToElement.apply(scroll, arguments);
    };
    onMounted(() => {
      nextTick(() => {
        initScroll();
      });
    });
    return {};
  }
};
</script>
<style lang="scss" type="text/scss" scoped>
.scroll-wrapper {
  width: 100%;
  height: 100%;
  overflow: hidden;
  overflow-y: scroll;
}
</style>

Продолжение следует. . .

Из-за нехватки времени я сначала написал это сюда, а завтра мне нужно идти на работу.Этот проект - всего лишь небольшая демонстрация, используемая для практики vue3.В нем еще есть некоторые 🐛.Вы можете поделиться своим мнением в любое время. ! щелчок по коду githubздесь.

❤️ Прочитав три вещи: Если вы найдете этот контент вдохновляющим, я хотел бы пригласить вас сделать мне небольшую услугу:

  1. Ставьте лайки, чтобы этот контент увидело больше людей, а также вам было удобно найти этот контент в любое время (фавориты не лайкаются, они все хулиганят -_-);
  2. Следите за нами и время от времени распространяйте статьи;
  3. Также смотрите другие статьи;

🎉Вы можете написать свой собственный опыт обучения в области сообщений, чтобы обсудить его со мной и другими учениками. Если вы чувствуете, что что-то приобрели, вы также можете поделиться статьей со своими друзьями.