前言

老项目新谈,用vue3重构ing

主题演示

待填充…

构建工具+技术栈

编译软件:vscode
开发框架: vue3
打包工具:vite
UI:element-plus
icon:阿里巴巴
其他:pinia, vue-router,git
插件:pubsub,nanoid,less-loader
后端: nodejs,mysql

part0 路由&pinia

定义路由,以及重定向。
当时在router里面把路径写错了导致页面跳转问题出现。!!!
vue3的路由写法

1
2
3
4
5
6
7
8
9
10
11
12
import {
createRouter,
createWebHashHistory
} from "vue-router"
const routes = [{
xxxx
}]
const router = createRouter({
history: createWebHashHistory(),
routes,
});
export default router

pinia写法

part1 mainFrame

这个部分是关于整个页面框架的

element-plus

全局引入和按需引入,没什么好说的。注意的是官网的例子部分是ts,不太友好。

农历的类

使用到了一个可以返回农历值的方法,将他封装了起来,放进了plugins。最后暴露方法出来。

css变量

这次使用了css变量来定义全部的颜色变量,方便后期修改

注意使用css变量的框架不要设置style:scoped否则后面的读不出来数据。

part2 login

这个部分是关于login页面的

还待解决的问题:分辨率不同footok的标题向下位置不同
注意router的引入

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

注册的判断

关于id的正则表达式:/^[a-zA-Z0-9_-]{4,16}$/(只支持字母,数字,下划线,减号)
关于密码的正则:/^.*(?=.{6,})(?=.*\d)(?=.*[A-Z])(?=.*[a-z]).*$/(最少六位加大小写),并且注意不能空密码空账号
注意watch方法中,reactive的变量检测使用()=>obj.xxx
例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
watch(
() => userData.password,
(newValue) => {
let patt = /^.*(?=.{6,})(?=.*\d)(?=.*[A-Z])(?=.*[a-z]).*$/;
if (!patt.test(newValue)) {
passwordTips.value = "您的密码强度不够";
isSuccess.passWordSuccess = false;
} else {
passwordTips.value = "";
isSuccess.passWordSuccess = true;
}
}
);

登陆和注册以及axios

发送:注意发送的时候不要发响应式的对象,因为后端不需要你的响应式对象,它需要一个普通对象,所以可以使用toRaw将你的数据变成普通对象之后发送。
接收:axios的问题是要知道code和数据来自哪里。这里发现后端发来的数据中,code来自data

1
2
3
4
5
6
7
8
9
10
11
12
13
axios
.post("http://localhost:3000/api/register", rawData)
.then((res) => {
if (res.data.code === 200) {
sessionStorage.setItem("sid", res.data.data.user.id);
router.push({ name: "index" });
}
})
.catch((err) => {
userNameTips.value = "用户名已存在";
isSuccess.userNameSuccess = false;
console.log(err)
});

注册逻辑:200则注册成功,在session里面放sid,这样router里面就能放行,并且跳转。如果异常则是用户名已存在。(网络异常的判断?)
登陆逻辑:200登陆成功,否则用户名存在。
(登陆的优化?安全?token?jwt?)

part3 indexAside

这个部分是关于两侧aside页面的

用到了视口单位进行了响应式。

part4 foodSwiper

这个部分是关于轮播图页面的

三个需求

  1. 展示轮播图片 五个一循环 点击换一些 再切换五个
    目前还没解决的问题是 根据用户请求
  2. 点赞功能
  3. 收藏功能

轮播图片

采用的element的走马灯并进行了魔改。具体魔改的是下方的指示灯

遇到的问题:

  1. axios请求图片之后需要滑动走马灯才显示的问题
    原因:没有设置渲染条件,走马灯没读到数据。
    解决方法:v-if数据的长度,有数据就显示
  2. 点击换一些过一会屏幕空白
    原因:没有初始化数据数组长度
    解决方法:先初始化再赋值
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    var getfood = () => {
    foodMsgObj.foodMsg = [];
    axiosFoodMsg()
    .then((res) => {
    foodMsgObj.foodMsg = res;
    }) //初始化走马灯文字
    .then(() => {
    let btn = document.getElementsByClassName("el-carousel__button");
    for (let i = 0; i < btn.length; i++) {
    btn[i].innerText = wordObj[i];
    }
    });
    };
  3. 魔改的指示灯不显示
    原因:赋值完dom还没有渲染完,所以魔改的灯没有值。
    解决方法:如上,使用异步。
  4. 魔改的指示灯背景色 这个是css问题 已经解决。
    亮点:封装了axios方法到hook,方便后面请求使用。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    import axios from 'axios'

    export default async function () {
    var arr = [];
    let idObj ={
    id:parseInt(sessionStorage.getItem('sid'))
    }
    await axios.post("/api/swiper",idObj).then((res) => {
    // if(res.status ===200){
    // arr = res.data;
    // }
    if (res.data.code === 200) {
    arr = res.data.data.records;
    }
    console.log(arr);
    }).catch((err) => {
    console.log(err);
    })
    return arr
    }

    axios注意事项:

  5. 注意res的data和data里面的data问题。
  6. 注意return,它返回的是一个promise对象,解析promise对象需要使用then方法

    点赞功能

    性能优化:防抖后确认和第一次的状态不一致才发送请求改变状态

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    var updateLikeTime = null;
    var likeInit;
    var zan = (id) => {
    let arr = foodMsgObj.foodMsg.filter((item) => {
    return item.id === id;
    })[0];
    //如果是第一次 那么记录初始点赞状态
    if (!arr.zanfirstClick) {
    likeInit = arr.islike;
    arr.zanfirstClick = true;
    }
    //点赞和取消点赞
    if (!arr.islike) {
    arr.likenum++;
    arr.islike = 1;
    clearTimeout(updateLikeTime);
    } else {
    arr.likenum--;
    arr.islike = 0;
    clearTimeout(updateLikeTime);
    }
    //如果最后一次操作和初始状态不一样 才设置定时器发送请求
    if (likeInit !== arr.islike) {
    updateLikeTime = setTimeout(() => {
    let updateData = {
    userid: sessionStorage.getItem("sid"),
    id: id,
    islike: arr.islike,
    };
    axios.post("/api/updatelike", updateData).then((res) => {
    console.log(res);
    if (res.status === 200) {
    arr.zanfirstClick = false;
    console.log(id,"点赞转换成功");
    }
    });
    }, 3000);
    }
    };

    收藏功能

    同上 逻辑类似不赘述

    part5 foodMap

    这个部分是关于美食地图的

    需求如下:
  7. 热点地图 点击切换区域
  8. axios请求后显示相应的收录数,收录排行前二的数据
  9. 点击数据 跳转对应的详情页(产生通用接口传id查sql跳转)
  10. 查看所有直接跳转美食目录

    热点地图

    这个玩意当时纠结了很久,怎么点击一个地图的热点区域并高亮该区域
    目前知道的方法就是map标签,里面添加area标签以及poly属性处理多边形。前提还要和图片进行一个绑定。原理是通过点击热点区域动态改变图片src
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    //代码太长删掉了一部分 值得一提的是usemap和map中的id绑定,但此时只有写name即可视为绑定。
    <img :src="mainMap" alt="" usemap="#map" id="mainmap" />
    <map name="map">
    <area
    shape="poly"
    :coords="huanan1"
    class="huanan"
    @click.native="changeMap"
    />
    <area
    shape="poly"
    :coords="huanan2"
    class="huanan"
    @click.native="changeMap"
    />
    </map>
    写到这你以为很简单?看到上面的coords属性了吗,这个是坐标的区域,也就意味着每个多边形的坐标都需要知道,以至于…

    这只是一部分,后面的坐标还有很多。是通过ps的方式找坐标的,也就意味着要一个一个点打…

交互部分

剩下的功能倒不是很难实现,也没什么值得一提的地方,除了后端这个数据

前端就只能慢慢的解构赋值了

热点地图自适应(已解决)

原先问题:没有办法解决当缩放的时候,失去热点区域的问题,因为图片被缩放了,但是热点区域没有发生改变,原来的坐标还是在屏幕对应的位置 所以会出错。
解决方法:先记录图片初始化的大小,x坐标乘现在宽/初始宽的比例,y坐标乘现在高/初始高的比例,得到缩放后的比例。
过程中遇到的问题和注意事项:

  1. 坐标是字符串 转数组才能操作 操作完要转回字符串。
  2. 需要暴露两个obj 一个是初始坐标obj,另外一个暴露的obj会变成响应式后面使用。如果只暴露一个,那么响应式的就算暂存了地址中的数据也会发生变化。
  3. 替换坐标的时候,不能采用obj = function返回的obj这样的形式,会丢失响应式。建议直接obj.xx = xxfunction(xx)...这样的形式保证不会出错。
  4. 缩放设置定时器避免重复计算。
    上代码
    hook里面的
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    const coordsObj = {
    huanan1: huanan1,
    huanan2: huanan2,
    huanan3: huanan3,
    huabei: huabei,
    huazhong: huazhong,
    huadong: huadong,
    xibu: xibu
    }
    const initCoords = {
    huanan1: huanan1,
    huanan2: huanan2,
    huanan3: huanan3,
    huabei: huabei,
    huazhong: huazhong,
    huadong: huadong,
    xibu: xibu
    }
    function setCoords(arr, w, h) {
    let img = document.getElementById("mainmap");
    let newWidth = img.width;
    let newHeight = img.height;
    let widthpercent = newWidth / w;
    let heightpercent = newHeight / h;
    arr = arr.split(',')
    for (let i = 0; i < arr.length; i++) {
    // x坐标
    if (i % 2 !== 0) {
    arr[i] = Math.round(parseInt(arr[i]) * widthpercent);
    } else {
    //y
    arr[i] = Math.round(parseInt(arr[i]) * heightpercent);
    }
    }
    var newPosition = "";
    for (var j = 0; j < arr.length; j++) {
    newPosition += arr[j];
    if (j < arr.length - 1) {
    newPosition += ",";
    }
    }
    return newPosition;
    }
    export {
    coordsObj,
    initCoords,
    setCoords,
    }
    vue里面的
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    let mycoordsObj = reactive(coordsObj);
    //热点区域自适应
    //初始化热点
    let time = null;
    function initCoord(delay) {
    clearTimeout(time);
    time = setTimeout(() => {
    mycoordsObj.huanan1 = setCoords(initCoords.huanan1, 767, 586);
    mycoordsObj.huanan2 = setCoords(initCoords.huanan2, 767, 586);
    mycoordsObj.huanan3 = setCoords(initCoords.huanan3, 767, 586);
    mycoordsObj.huabei = setCoords(initCoords.huabei, 767, 586);
    mycoordsObj.huazhong = setCoords(initCoords.huazhong, 767, 586);
    mycoordsObj.huadong = setCoords(initCoords.huadong, 767, 586);
    mycoordsObj.xibu = setCoords(initCoords.xibu, 767, 586);
    }, delay);
    }
    initCoord(200);
    //当缩放的时候 自适应 且设置定时器避免多次计算
    window.onresize = function () {
    initCoord(1500);
    };

    性能优化

  5. png转webp,从400k变100k直升四倍,注意格式工厂的webp转换底色会变绿
  6. 依旧是定时器,避免重复计算。

part6 foodCatalog

这个部分是关于美食目录的


需求如下:

  1. 分页显示图片,内容,点击图片跳转详情页
  2. 根据不同标签显示不同种类图片

    骨架屏的使用

    第一次使用element的骨架屏,用法也比较简单,需要在el-skeleton标签内放两个template,一个用于骨架显示,一个用于实际显示。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    <el-skeleton :loading="loading" animated :throttle="500">
    <!-- 骨架显示 -->
    <template #template>
    <el-skeleton-item
    variant="image"
    style="
    width: 24%;
    height: 32%;
    margin-bottom: 5px;
    margin-right: 5px;
    "
    v-for="item in 12"
    :key="item"
    />
    </template>
    <!-- 实际图片显示 -->
    <template #default>
    <div class="foodBox">
    <el-card :body-style="{ padding: '0px' }" v-for="item in foodData" :key="item.id"
    ><el-image :src="item.foodbigimg" />
    <span>
    <p>{{ item.foodname }}</p>
    </span>
    </el-card>
    </div>
    </template>
    </el-skeleton>
    <el-pagination
    background
    class="pagin"
    layout="prev, pager, next"
    @current-change="handleCurrentChange"
    :currentPage="currentPage"
    :total="pageSize"
    />
    遇到的问题:
    项目中一页显示12个,骨架屏里面也有十二个内容,想着切换的条件应该是图片loading完毕就显示。所以给el-image的load事件绑定了loading,但是实际上并没有用。猜想可能是图片不是同时渲染,所以给出的boolean值也随时间变化,不是固定值,所以骨架屏整个的切换无法进行。
    解决方法:
    不等图片渲染完再取消了,直接收到请求之后取消。

    分页器

    element的分页器也是第一次使用,当时遇到的问题就是不知道怎么响应当前页数,就是点到第几页显示对应的数字。后面从文档中了解到使用@current-change="handleCurrentChange"绑定当前页,默认传入的是当前页的值val 这样就可以传参到后端了。

性能优化

  1. 骨架屏:请求回来之前使用骨架屏,响应之后渲染图片,增加用户体验
  2. css的loading,这个和骨架屏的原理其实差不多,选了骨架屏
  3. 依旧是png转webp,可以到达更小的size其实,只需要缩略图

vite

链接hash问题

vite会自动把链接变成哈希值,所以是不能直接引入某某链接的,需要使用一个方法

1
2
3
4
5
//getAssetsImages.js
export default function (fileName,imgName) {
return new URL(`/src/assets/img/${fileName}/${imgName}`,
import.meta.url).href;
}

这个js由于使用的场景太多 直接封装之后放入了hook使用。后面考虑全局使用。

vite配置项的问题

vite的配置项和之前vuecli不太一样,而且确保能完全使用它的功能,还需要将vite的版本升到最新。所以当时配置的时候删库重新init了vite项目,这个问题需要非常注意。
检验的方法是:init之后康康有没有vite.config.js

具体配置

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import {
defineConfig
} from 'vite'
import vue from '@vitejs/plugin-vue'

// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
base: './',
build: {
target: 'modules',
outDir: 'dist', //指定输出路径
assetsDir: 'assets', // 指定生成静态资源的存放路径
minify: 'terser' // 混淆器,terser构建后文件体积更小
},
server: {
cors: true, // 默认启用并允许任何源
open: true, // 在服务器启动时自动在浏览器中打开应用程序
//反向代理配置,注意rewrite写法,开始没看文档在这里踩了坑
proxy: {
// '/api': {
// target:'http://isinpc.natappfree.cc',
// changeOrigin: true,
// rewrite: (path) => path.replace(/^\/api/, '')
// }
'/api': {
target: 'http://localhost:8081',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
}
})

这里的proxy是配置转发,前端使用ajax或者axios的时候,就会将localhost:3000自动转换成对应的值。这里方便合作还使用到了内网穿透的api,来换切换使用。