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
- First, select a file locally and clone the code locally:
git clone https://github.com/GitHubGanKai/vue-jd-h5.git
Copy the code
-
π switch to branch vue-next and start playing (currently in the process of refactoring)! π
-
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
useuseRouter
Hooks 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
useuseRoute
Gets 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-loader
Working 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 globalaxios
Encapsulate 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:
- 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 -_-);
- Pay attention to us, not regular points good article;
- 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.