# 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
# 二 登录功能开发
## 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
### 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
## 6 样式的优化与代码复用
## 7 商家详情页分类及内容联动的实现
1. 页面 `tab` 点击
```html
## 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
## 1 确认订单页面创建及顶部布局
### 1.1 页面创建
1. 跳转
```html
## 3 页面布局及展示逻辑开发
```html