# vue3实战 **Repository Path**: pleaseanswer/vue3-actual-combat ## Basic Information - **Project Name**: vue3实战 - **Description**: vue3+js 京东到家 - **Primary Language**: JavaScript - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 2 - **Forks**: 2 - **Created**: 2022-03-17 - **Last Updated**: 2024-10-03 ## Categories & Tags **Categories**: Uncategorized **Tags**: 项目实战, vue3, vue3-京东 ## README # 一 “京东到家”项目首页开发 > 效果图 ## 1 工程初始化 * `vue create jingdong` * 自定义安装项 `router + vuex + css pre-processors + linter` * `3.x` * `scss` * `standard esLint` ## 2 工程目录代码简介及整理 * 安装插件 * ESLint * Vetur ## 3 基础样式集成及开发模拟器的使用 * 安装 `normalize.css` * `npm i normalize -S` > 使元素的渲染在多个浏览器下都能保持一致并且符合规范 ## 4 flex + iconfont 完成首页 docker 样式编写 > 从下载的文件夹中 copy * iconfont.css ```css @font-face { font-family: 'iconfont'; /* Project id 2408176 */ /* src: ... 项目中 copy */ } .iconfont { font-family: "iconfont" !important; font-size: .16rem; font-style: normal; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } ``` ## 5 使用 Scss 组织地址区域布局 ### 5.1 统一管理css变量 * viriables.scss ```scss $content-fontcolor: #333; ``` ### 5.2 css_mixin 的使用 * mixins.scss ```scss @mixin ellipsis { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } ``` ## 6 利用 CSS 技巧实现搜索及 banner 区域布局 ### 6.1 图片抖动 * 使用 `padding-bottom hack` 避免图片后面的内容抖动 ```scss .banner { height: 0; overflow: hidden; padding-bottom: 25.4%; // 宽 / 高 &__img { width: 100%; } } ``` ## 7 使用 flex 布局实现图标列表布局 ## 8 首页布局收尾 ## 9 首页组件的合理拆分 * App.vue ```vue ``` * home > Home.vue ```vue ``` ## 10 使用 v-for, v-html 指令精简页面代码 * `d-html` ```html
``` ## 11 CSS 作用域约束以及 Vue 开发者工具的安装使用 * `scoped` ### 11.1 扩展程序 vue devtools # 二 登录功能开发 ## 1 登陆页面布局开发 > 效果图 ## 2 路由守卫实现基础登陆校验功能 * 实现登录跳转 > login.vue ```js import { useRouter } from 'vue-router'; export default { name: 'Login', setup() { const router = useRouter(); const handleLogin = () => { localStorage.isLogin = true; router.push({ name: 'Home' }); } return { handleLogin } } } ``` * 使用全局路由守卫限制未登录时访问其他页面会跳转到 login 页面 > router > index.js ```js router.beforeEach((to, from, next) => { const { isLogin } = localStorage; (isLogin || to.name === 'Login') ? next() : next({ name: 'Login' }) }) ``` * 使用路由独享守卫限制登陆后不能访问 login 页面 ```js const routes = [ { path: '/', name: 'Home', component: Home }, { path: '/login', name: 'Login', component: Login, beforeEnter(to, from, next) { const { isLogin } = localStorage; isLogin ? next({ name: 'Home' }) : next() } } ] ``` ## 3 注册页面开发及路由串联复习 ## 4 使用 axios 发送登陆 Mock 请求 > Login.vue ```js import { reactive } from 'vue'; import { useRouter } from 'vue-router'; import axios from 'axios'; axios.defaults.headers.post['Content-Type'] = 'application/json' export default { name: 'Login', setup() { const data = reactive({ username: '', password: '' }) const router = useRouter(); const handleLogin = () => { // https://www.fastmock.site/mock/ae8e9031947a302fed5f92425995aa19/jd axios.post('.../api/user/login', { username: data.username, password: data.password }).then(() => { localStorage.isLogin = true; router.push({ name: 'Home' }); }).catch(() => { alert('失败') }) } return { data, handleLogin, handleRegisterClick } } } ``` ## 5 请求函数的封装 > utils > request.js ```js import axios from 'axios'; export const post = (url, data = {}) => { return new Promise((resolve, reject) => { axios.post(url, data, { baseURL: '...', headers: { 'Content-Type': 'application/json' } }).then((res) => { resolve(res.data) }, err => { reject(err) }) }) } ``` > login.vue ```js const handleLogin = async () => { try { const result = await post('/api/user/login', { username: data.username, password: data.password }) if(result?.errno === 0) { localStorage.isLogin = true; router.push({ name: 'Home' }); } else { alert('登录失败') } } catch(e) { alert('请求失败') } } ``` ## 6 Toast 弹窗组件的开发 > components > Toast.vue ```vue ``` > Login.vue ```js const showToast = (message) => { data.showToast = true; data.toastMessage = message; setTimeout(() => { data.showToast = false; data.toastMessage = ''; }, 2000); } const handleLogin = async () => { try { const result = await post('111/api/user/login', { username: data.username, password: data.password }) if(result?.errno === 0) { localStorage.isLogin = true; router.push({ name: 'Home' }); } else { showToast('登录失败') } } catch(e) { showToast('请求失败') } } ``` ## 7 通过代码拆分增加逻辑可维护性 ### 7.1 拆分 login 页面 toast 的相关代码 ### 7.2 将 toast 相关代码放到 Toast 组件单独管理 > Login.vue ```js import Toast, { useToastEffect } from '../../components/Toast.vue'; ``` > components > Toast.vue ## 8 Setup 函数的职责以及注册功能的实现 ### 8.1 职责 -- 代码执行的流程 * 将不同的逻辑分别放到各自的函数中,setup 函数仅用来做代码流程的控制 ```js const userLoginEffect = (showToast) => { const router = useRouter(); const data = reactive({ username: '', password: '' }) const handleLogin = async () => { try { const result = await post('111/api/user/login', { username: data.username, password: data.password }) if(result?.errno === 0) { localStorage.isLogin = true; router.push({ name: 'Home' }); } else { showToast('登录失败') } } catch(e) { showToast('请求失败') } } const { username, password } = toRefs(data) return { username, password, handleLogin } } const userRegisterEffect = () => { const router = useRouter(); const handleRegisterClick = () => { router.push({ name: 'Register' }); } return { handleRegisterClick } } export default { name: 'Login', components: { Toast }, setup() { const { show, toastMessage, showToast } = useToastEffect(); const { username, password, handleLogin } = userLoginEffect(showToast); const { handleRegisterClick } = userRegisterEffect(); return { username, password, show, toastMessage, handleLogin, handleRegisterClick } } } ``` # 三 商家展示功能开发(上) > 效果图 ## 1 首页附近店铺数据动态化-详情页准备 ### 1.1 封装 get 请求 1. 创建 axios 实例 ```js const instance = axios.create({ baseURL: 'https://www.fastmock.site/mock/ae8e9031947a302fed5f92425995aa19/jd', timeout: 10000 }) ``` 2. 封装 get 请求 ```js export const get = (url, params = {}) => { return new Promise((resolve, reject) => { instance.get(url, { params }, { }).then((res) => { resolve(res.data) }, err => { reject(err) }) }) } ``` ### 1.2 获取列表数据 1. 使用 `get` 方法获取列表数据 ```js const getNearbyList = async () => { const result = await get('/api/shop/host-list'); if(result?.errno === 0 && result?.data?.length) { nearbyList.value = result.data, result.data; } } ``` 2. 代码拆分 ## 2 动态路由,异步路由与组件拆分复用 ### 2.1 异步路由 ```js const routes = [ { path: '/', name: 'Home', component: () => import(/* webpackChunkName: "home" */ '../views/home/Home') }] ``` ### 2.2 组件拆分复用 * 将商品列表的代码拆分到单独的组件 * 可在首页、商品列表页使用 ### 2.3 动态类名 ```html
``` ## 3 搜索布局及路由跳转 ### 3.1 搜索布局 ### 3.2 路由跳转 1. 返回上一页 ```js import { useRouter } from 'vue-router' export default { setup() { const router = useRouter(); const handleBackClick = () => { router.back() } return { handleBackClick } } } ``` 2. 点击跳转进入商家详情 ```html ``` ## 4 路由参数的传递以及商家详情的获取 ### 4.1 路由传参 1. 设置 `router` ```js const routes = [{ path: '/shop/:id', name: 'Shop', component: () => import(/* webpackChunkName: "shop" */ '../views/shop/Shop') }] ``` 2. 路由跳转时传递 `id` ```html ``` ### 4.2 商家详情的获取 * 获取路由 `id` 从而请求获取相关数据 ```js import { reactive, toRefs } from 'vue' import { useRouter, useRoute } from 'vue-router' import { get } from '../../utils/request' const useShopInfoEffect = () => { const route = useRoute(); const data = reactive({ item: {} }) const getItemData = async () => { const result = await get(`/api/shop/${route.params.id}`) if(result?.errno === 0 && result?.data) { data.item = result.data } } const { item } = toRefs(data) return { item, getItemData } } ``` ## 5 商家页面核心样式开发 ## 6 样式的优化与代码复用 ## 7 商家详情页分类及内容联动的实现 1. 页面 `tab` 点击 ```html
{{item.name}}
``` 2. `js` 处理 ## 8 使用 watchEffect 巧妙的进行代码拆分 ### 8.1 初始化时获取列表 + tab变化时获取列表 ```js watchEffect(() => { getContentData() }) ``` ### 8.2 各司其职 1. 和 `tab` 切换相关的逻辑 ```js const useTabEffect = () => { const currentTab = ref(categories[0].tab) const handleTabClick = (tab) => { currentTab.value = tab } return { currentTab, handleTabClick } } ``` 2. 列表相关逻辑 ```js const useCurrentList = (currentTab) => { const route = useRoute() const shopId = route.params.id const content = reactive({ list: [] }) const getContentData = async () => { const result = await get(`/api/shop/${shopId}/products`, { tab: currentTab.value }) if(result?.errno === 0 && result?.data?.length) { content.list = result.data } } watchEffect(() => { getContentData() }) const { list } = toRefs(content) return { list } } ``` 3. 代码流程 ```js setup() { const { currentTab, handleTabClick } = useTabEffect() const { list } = useCurrentList(currentTab) return { categories, currentTab, handleTabClick, list } } ``` # 四 商家展示功能开发(下 ) > 效果图 ## 1 购物车的样式开发 ## 2 Vuex中购物车数据结构的设计 ### 2.1 store 存放购物车数据 ```js export default createStore({ state: { cartList: { // 商铺id: { // 商品id: { // // 商品内容及购物数量 // } // } } }, mutations: { // count +/- 1 changeCartItemInfo(state, payload) { const { shopId, productId, productInfo, num } = payload let shopInfo = state.cartList[shopId] || {}; let product = shopInfo[productId]; // store不存在该商品数据时,初始化product if(!product) { product = productInfo; product.count = 0; } product.count = product.count + num; shopInfo[productId] = product; state.cartList[shopId] = shopInfo; } }, } ``` ### 2.2 列表操作 ```js // 购物车相关逻辑 const useCartEffect = () => { const store = useStore(); const { cartList } = toRefs(store.state) const changeCartItemInfo = (shopId, productId, productInfo, num) => { store.commit('changeCartItemInfo', { shopId, productId, productInfo, num }) } return { cartList, changeCartItemInfo } } ``` ## 3 使用 computed 完成订单价格计算 ### 3.1 计算选中商品总数 ```js const total = computed(() => { const productList = cartList[shopId]; let count = 0 if(productList) { for (let i in productList) { const product = productList[i] count += product.count } } return count }) ``` ### 3.2 计算选中商品总价 ```js const price = computed(() => { const productList = cartList[shopId]; let count = 0 if(productList) { for (let i in productList) { const product = productList[i] count += (product.count * product.price) } } return count.toFixed(2) }) ``` ## 4 购物车及列表双向数据同步功能开发 ### 抽离购物车相关逻辑 > `Content.vue` 与 `Cart.vue` 公用 ```js export const useCommonCartEffect = () => { const store = useStore(); const { cartList } = toRefs(store.state) const changeCartItemInfo = (shopId, productId, productInfo, num) => { store.commit('changeCartItemInfo', { shopId, productId, productInfo, num }) } return { cartList, changeCartItemInfo } } ``` ## 5 根据购物车选中状态计算订单金额 ### 5.1 列表展示 ```html