vue3-jd-h5

When Vue’s Composition API was just released, I wrote an article about building an e-commerce H5 project imitating JINGdong based on Vue 3.0.1 beta! Now that the official version has been released, this year we have time to start refactoring with the latest VUE bucket! Other basic use will directly skip, do not understand the Chinese official website can directly see the example or I before that article!

Project introduction

Vue3-jd-h5 is an e-commerce H5 page front-end project, from VUe2.6.1 to vue3.0.0 for reconstruction, based on Vue 3.0.0 family bucket βž• Vant 3.0.0 implementation!

πŸ“– local offline code Vue2.6 was developed using MockJS data in the branch demo, and the renderings can be seen here at πŸ”—

⚠️master branch is the code of the online production environment, because part of the background interface has been suspended 😫, may not be able to see the actual effect.

πŸ“Œ There are still many deficiencies in this project. If you want to contribute to this project, you are also welcome to give us PR or issue.

πŸ”‘ This project is free and open source. If you have partners who want to carry out secondary development on the basis of secondary development, you can clone or fork the whole warehouse. I will be very glad if you can help. πŸ™

Begin to build

  1. First, select a file locally and clone the code locally:
git clone https://github.com/GitHubGanKai/vue-jd-h5.git 
Copy the code
  1. πŸ‘‰ switch to branch vue-next and start playing (currently in the process of refactoring)! πŸ‘ˆ

  2. Run the NPM install command on the IDEA cli to download and install dependencies.

Install vUE family bucket

Configure the installationvue-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
})

Copy the code

useuseRouterHooks can get the route object:

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, }; }};Copy the code

useuseRouteGets the route parameter object

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

export default {
  name: "home".setup(props, context) {
    // Get all routing parameters
    UseRouter () and useRouter() are separated by a letter r, πŸ˜…
    const $route = useRoute(); 
    
     onMounted(async() = > {const { data } = await ctx.$http.get(
        `http://test.happymmall.com/product/${$route.params.id}`
      );
    });

    return{ $route, }; }};Copy the code

Configure and install the 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'
})

Copy the code

Use the following in the file:

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

setup(props, context) {
    const { ctx } = getCurrentInstance();
    const $store = useStore();
    $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,
    };
  }
Copy the code

To use in the entry file 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' // Global import styles

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

usesvg-sprite-loaderWorking with SVG files

First configure svG-sprite-loader in vue.config.js:

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(); }},Copy the code

In the 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; ❌
    // Because props are reactive, you can't use ES6 deconstruction because it eliminates the responsiveness of prop.
    // If you need to deconstruct a prop, you can do so by using toRefs in the setup function:
    const { iconClass } = toRefs(initProps);
    const iconName = computed(() = > {
      return `#icon-${iconClass.value}`;
    });

    // Since className is an optional prop, there may be no className in the props passed.
    // In this case, toRefs will not create a ref for className and will need to use toRef instead.
    const className = toRef(initProps, "className");
    const svgClass = computed(() = > {
      if (className) {
        return "svg-icon " + className.value;
      } else {
        return "svg-icon"; }});return{ iconName, svgClass }; }};Copy the code

Write this as a plug-in that registers all SVG files as components for global use!

// 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)
  }
}
Copy the code

Uniformly register all components

In the SRC /components/index.js file:

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)
    })
  }
}
Copy the code

Configure globalaxiosEncapsulate asynchronous requests:

// 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('Login expired, please log in again')
      localStorage.removeItem('token')
      setTimeout(() = > {
        toLogin()
      }, 1000)
      break
    case 404:
      tip('Requested resource does not exist')
      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))

// Response interceptor
instance.interceptors.response.use(
  // The request succeeded
  response= > {
    return response.status === 200 ? Promise.resolve(response) : Promise.reject(response)
  },
  // The request failed
  error= > {
    const {
      response
    } = error
    if (response) {
      // The request has been issued, but it is outside the scope of 2xx
      errorHandle(response.status, response.data.message)
      return Promise.reject(response)
    }
  })

export default {
  install(app) {
    // This can be used instead of Vue 2.x Vue. Prototype in a single file:
    // const { ctx } = getCurrentInstance();
    / / CTX. $HTTP access
    app.config.globalProperties.$http = instance
  }
};
Copy the code

Encapsulate the global eventBus

In 2.x, Vue instances can be used to trigger handlers (on, ON, ON, off, and $once) that are added instructively by the event-triggering API. This creates the Event Hub, which is used to create global event listeners available throughout the application:

// src/utils/eventBus.js

const eventBus = new Vue()

export default eventBus
Copy the code

3. X completely removes the $on, $OFF, and $once methods from the instance. $EMIT is still included in the existing API because it is used to trigger event handlers added declaratively by the parent component. Existing Event Hubs, such as Mitt or Tiny-Emitter, can be replaced with external libraries that implement event-triggering interfaces.

Therefore, self-transformation is needed on the basis of 2.x (without mitt or tiny-Emitter):

import { getCurrentInstance } from 'vue'

class EventBus {
  constructor(app) {
    if (!this.handles) {
      Object.defineProperty(this.'handles', {
        value: {},
        enumerable: false})}this.app = app
    // Mapping of _uid to EventName
    this.eventMapUid = {}
  }
  setEventMapUid(uid, eventName) {
    if (!this.eventMapUid[uid]) {
      this.eventMapUid[uid] = []
    }
    this.eventMapUid[uid].push(eventName)
    // Push the event name of each _uid subscription into the array of its uid
  }
  $on(eventName, callback, vm) {
    // vm is used within the component when the component's current this is used to take _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();
      Intercepts beforeUnmount hooks that automatically destroy all their subscriptions
      this.$eventBus.$offVmEvent(currentInstance._uid)
    }
  })
}
export default $EventBus
Copy the code

In the SRC/views/the classify index. Vue file using the following:

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();
      // Use globally annotated $eventBus
      ctx.$eventBus.$emit("changeTag".1);
      // Use globally registered $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
    };
  }
};
Copy the code

Encapsulate a simple hooks: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;
}
Copy the code

The use of this

Inside setup(), this will not be a reference to the active instance, because setup() is called before parsing the other component options, so this inside setup() behaves completely differently than this in the other options. This can cause confusion when using setup() with other optional apis.

GetCurrentInstance can be used to get an instance of the current single file component, while CTX has some global properties hanging on it:

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) }; }};Copy the code

πŸ“¦ encapsulation better – scroll

<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
      });
      // Whether to send rolling events
      if (props.listenScroll) {
        scroll.on("scroll".pos= > {
          setupContext.emit("scroll", pos);
        });
      }
      // Whether to send the scroll to the bottom event for pull-up loading
      if (props.pullup) {
        scroll.on("scrollEnd".() = > {
          // Scroll to the bottom
          if (scroll.y <= scroll.maxScrollY + 50) {
            setupContext.emit("scrollToEnd"); }}); }// Whether to send a top drop-down event for drop-down refresh
      if (props.pulldown) {
        scroll.on("touchend".pos= > {
          // Pull down
          if (pos.y > 50) {
            setupContext.emit("pulldown"); }}); }// Whether to send the event that the list starts scrolling
      if (props.beforeScroll) {
        scroll.on("beforeScrollStart".() = > {
          setupContext.emit("beforeScroll"); }); }};const disable = () = > {
      // Proxy the better scroll disable methodscroll? .disable(); };const enable = () = > {
      // Proxy the better scroll enable methodscroll? .enable(); };const refresh = () = > {
      // The refresh method of the better Scroll proxyscroll? .refresh(); };const scrollTo = () = > {
      // Delegate the better scroll scrollTo methodscroll? .scrollTo.apply(scroll,arguments);
    };
    const scrollToElement = () = > {
      // Delegate the better Scroll scrollToElement methodscroll? .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>

Copy the code

To be continued…

Due to the lack of time, I have to go to work tomorrow. This project is just a small demo for practicing VUE3, and there is part of πŸ› in it. Welcome to share your opinions at any time! Github code click here.

❀️ Three things after reading: If you found this article inspiring, I’d like to ask you to do me a small favor:

  1. Like, so that more people can see this content, but also convenient to find their own content at any time (collection is not like, are playing rogue -_-);
  2. Pay attention to us, not regular points good article;
  3. Look at other articles as well;

πŸŽ‰ You are welcome to write your own learning experience in the comments section, and discuss with me and other students. Feel free to share this article with your friends if you find it rewarding.