vue3+vite+vant4项目实战总结

一 vue3

1.vue3常用的响应式api

  • ref ref可以定义任何数据类型的变量,在js中我们一般通过.value获取值;在模板中不需要.value,会自动解包
  • reactive reactive只能定义对象类型的数据,并且解构后会丢失响应式
  • watch侦听器
js 复制代码
  watch([startDate, endDate, categoryList], () => {
    reset();
    getNewsList();
  });

比如,上面这种方式,控制台就会警告:因为首页没有传入这3个参数为undefined

改成getter的形式就不会了,但是没有触发watch了

js 复制代码
 watch([() => startDate, () => endDate, () => categoryList], () => {
    reset();
    getNewsList();
  });

响应式 API:工具函数

  • isRef 检查某个值是否为 ref

  • unref() 如果参数是 ref,则返回内部值,否则返回参数本身。这是 val = isRef(val) ? val.value : val 计算的一个语法糖。

  • toRef() 可以将值、refs 或 getters 规范化为 refs (3.3+)。

  • toValue() 将值、refs 或 getters 规范化为值。这与 unref() 类似,不同的是此函数也会规范化 getter 函数。如果参数是一个 getter,它将会被调用并且返回它的返回值。

  • toRefs() 将一个响应式对象转换为一个普通对象,这个普通对象的每个属性都是指向源对象相应属性的 ref。每个单独的 ref 都是使用 toRef() 创建的。

2.keep-alive缓存路由失效了

参考链接 原因分析:<keep-alive>includeexclude 属性根据组件的 name 进行匹配,注意这里是组件的name属性而不是路由的name属性。虽然vue3会自动为组件加上name属性,但是像重复的index.vue这样的就无法匹配了,如下

这样就需要手动设置name属性了,官方推荐如下

注意: defineOptions无法访问hook函数中导出的变量,会报错

当然也可以借助插件vite-plugin-vue-setup-extend。关于项目中一些常用的插件,也会单列一项整理。

3.列表页进入详情页后,切换回列表页,需要对列表页进行缓存,如果从首页进入列表页,就要重新加载列表页。

方式一:route的meta属性上增加一个字段isBackFromDetail编辑是否是从详情页进入

js 复制代码
   {
      path: '/newsList/:newsType',
      name: 'newsList',
      component: () => import('../views/newsList/index.vue'),
      meta: {
        requiresAuth: true,
        needCache: true,
        isBackFromDetail: false,
      },
    },

beforeRouteEnter里面进行判断

js 复制代码
defineOptions({
  name: 'newsList',
  beforeRouteEnter(to, from) {
    if (from.name === 'newsDetail') {
      to.meta.isBackFromDetail = true;
    } else {
      to.meta.isBackFromDetail = false;
    }
  },
});

注意:setup函数中无法直接使用beforeRouteEnter,需要在script代码块或像上面那样

激活钩子里面进行数据和dom的初始化

js 复制代码
onActivated(() => {
  // 第一次和非详情页进入页面时 isBack都为false,即需要重新请求数据
  if (!route.meta.isBackFromDetail) {
    // 清空查询条件和数据 并重新请求数据
    reset();
    getNewsList();
  }
  route.meta.isBackFromDetail = false;
});

上面这个方法虽然也能实现,但是有个不好的地方就是要初始化很多数据和状态,如果列表页面足够复杂,容易出现bug.

方式二:利用keep-alive的include属性

store存储需要缓存的路由

js 复制代码
import { defineStore } from 'pinia';

export const useCachedViewStore = defineStore('cached-view', () => {
  const cachedViewList = ref([]);
  function addCachedView(view) {
    // 不重复添加
    if (cachedViewList.value.includes(view.name)) return;
    if (view?.meta?.needCache) {
      cachedViewList.value.push(view.name);
    }
  }
  function delCachedView(view) {
    const index = cachedViewList.value.indexOf(view.name || view);
    return index > -1 && cachedViewList.value.splice(index, 1);
  }
  function delAllCachedViews() {
    cachedViewList.value = [];
  }

  // 移除缓存的路由组件的实例
  async function delCacheEntry(view) {
    if (delCachedView(view)) {
      await nextTick();
      addCachedView(view);
    }
  }

  return {
    cachedViewList,
    addCachedView,
    delCachedView,
    delCacheEntry,
    delAllCachedViews,
  };
});

router.beforeEach添加需要缓存的路由

js 复制代码
 addCachedView(to);

组件导航守卫判断如果是从首页进入则删除该缓存的路由

js 复制代码
defineOptions({
  name: 'newsList',
  beforeRouteEnter(to, from) {
    if (from.name === 'home') {
      const { delCacheEntry } = useCachedViewStore();
      delCacheEntry(to);
    }
  },
});

这样可以清除从首页进入列表页,列表页的缓存;但是存在一个效果无论是点击首页的消息分类进入还是浏览器的前进后端都会清除;那如果想保留浏览器的前进后退的缓存呢!

直接在首页点击链接跳转前清除缓存,这样即可以实现从首页的消息分类按钮点击进入不缓存,但是保留浏览器前进后退的缓存操作了!

js 复制代码
const { delCacheEntry } = useCachedViewStore();
const handleGridClick = newsType => {
  delCacheEntry('newsList');
  router.push(`/newsList/${newsType}`);
};

vue-router

1.不同的历史记录模式

项目选择的是history模式

如果不是根目录,而是子目录,vite项目可以这样配置

2.params参数还是query参数?

项目一开始选择的是params参数,需要同步在routes中进行路由的定义;并且如果参数过多且复杂(有的参数是可选参数),在浏览器前进后退时会出现参数错位的情况,导致获取的值不正确。所以果断改成了query参数,参数体现在地址栏上,一眼就能看出传的是什么属性,再也不用看顺序了,好用,值得推荐!

params参数:

query参数

需要注意的是参数都将被字符串化! 比如传入的数字或者值为undefined的情况,都会被转成string,所以有些逻辑判断需要特别注意!

3.企业微信回调地址获取code参数的坑(TODO?:)

1.第一种情况访问的路由本身带有query参数,则code会被拼接在后面,此时在vue应用中可以通过query参数获取

'/login?redirect=/newsList?newsType=1001&code=123' (query参数)

Eg:

http://localhost:8080/login?redirect=/newsList?newsType=1001&code=123 (queryCode--- 123)(这个地址获取的location.search值为'')

这个路径为啥通过 getSearchParams(redirect, 'code')获取不到code? {redirect:"/newsList?newsType=1001"}

2.第二种情况访问的路由本身没有任何query参数,比如/home,此时code会被直接拼在?后面,vue中可以通过获取search参数的方式获取

login页面--redirect /home?code=123 (登录拦截)

eg:

http://172.23.10.63:5174/login?redirect=/home?code=123 (searchCode--- 123) (这个地址获取的location.search值为'')

{redirect:"/home?code=123"}

vite

1.项目中常用的vite插件

  • 1.vite-plugin-vue-setup-extend

解决vue3下 script setup语法糖 下 ,手动设置组件name不方便的问题

arduino 复制代码
npm i vite-plugin-vue-setup-extend -D

在文件vite.config.js中

arduino 复制代码
import vueSetupExtend from 'vite-plugin-vue-setup-extend';
export default defineConfig({
  plugins: [
    ...
    vueSetupExtend()
  ],
});
  • 2.unplugin-auto-import

实现vue函数的自动导入,这样如ref, computed,watch等就不用手动导入了

arduino 复制代码
 npm i unplugin-auto-import -D

在文件vite.config.js中

javascript 复制代码
import AutoImport from 'unplugin-auto-import/vite';
export default defineConfig({
  plugins: [
    ...
     AutoImport({
        include: [/\.[tj]sx?$/, /\.vue$/, /\.vue\?vue/, /\.md$/],
        imports: ['vue', 'vue-router', 'pinia'],
      }),
  ],
});
  • 3.unplugin-vue-components
  • 实现vue组件库的自动按需导入,这样就不用手动导入了
css 复制代码
npm i unplugin-vue-components -D

在文件vite.config.js中

javascript 复制代码
import Components from 'unplugin-vue-components/vite';
import { VantResolver } from '@vant/auto-import-resolver';
export default defineConfig({
  plugins: [
    ...
    Components({
      resolvers: [VantResolver()]
    }),
  ]
});
  • 4.vite-plugin-svg-icons

  • svg图标插件

    npm install vite-plugin-svg-icons -D

在文件vite.config.js中

arduino 复制代码
//使用svg-icons插件
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons';

  plugins: [
    ...
      // 注册所有的svg文件生成svg雪碧图
      createSvgIconsPlugin({
        iconDirs: [path.resolve(process.cwd(), 'src/assets/icons')], //svg图片存放的目录
        symbolId: 'icon-[name]', // symbol的id
        inject: 'body-last', // 插入的位置
        customDomId: '__svg__icons__dom__', // svg的id
      }),
  ]
});

具体用法可参照vue3-h5-template

2.vite环境变量配置

项目一般有开发环境,测试环境,生产环境(具体根据实际情况而定),不同环境下请求的api地址不同。所以可以在项目根目录下新建.env.文件来定义,vite项目中可以通过 *import.meta.env**来读取环境变量。

环境变量

模式

环境变量和模式是2个不同的概念

可以简单理解为不同模式下的变量可以通过环境变量读取

项目配置

json 复制代码
 "scripts": {
    "dev": "vite",
    "dev:test": "vite --mode test",  //开发服务器上运行测试环境(模式)
    "build": "vite build",
    "build:test": "vite build --mode test",  //打包后的产物运行测试环境(模式)
    "preview": "vite preview",
    "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore",
    "format": "prettier --write src/"
  },

3.vite静态资源

  • 资源引入可以使用绝对公共路径(基于开发期间的项目根路径),也可以使用相对路径。
  • new URL(url, import.meta.url)
  • 案例一:vant ui 组件库的图片组件van-image使用绝对地址和相对地址都无法正确展示图片,需要一个资源的url,如上就可以解决啦!
  • 案例二 项目部署生产环境后静态资源无法获取,报错

问题代码

原本以为是glob批量导入和文件目录导致的,后来经过翻看文档和测试,发现不能向上面这样写,因为vite不会解析,原样输出后找不到对应的资源

调整后

ini 复制代码
//文件后缀 doc -> 文件类型 doc - > 图标地址
const getIconUrl = format => {
  const fileSuffix = format.slice(1);
  const fileType = getFileType(fileSuffix);
  return new URL(`../assets/image/${fileType}.png`, import.meta.url).href;
};

一试,果然可以,但是资源过小是base64的地址,这个是vite生产构建自动做的

如果不想使用base64,则可以按文档的方式配置

二 Pinia

store

1.store 是一个用 reactive 包装的对象,我们不能直接在setup函数中对其进行解构,否则破坏了它的响应性,eg

js 复制代码
import {useNewsStore} from '@/stores/news'
const {newsType,getNewTypes} = useNewsStore()
getNewTypes();
//实际上页面数据不会进行相应式更新
js 复制代码
import { storeToRefs } from 'pinia'

//正确做法,借助storeToRefs
const newsStore = useNewsStore()
// `name` 和 `doubleCount` 是响应式的 ref 
// 同时通过插件添加的属性也会被提取为 ref 
// 并且会跳过所有的 action 或非响应式 (不是 ref 或 reactive) 的属性
const {newsType} = storeToRefs(newsStore)
// 作为 action 的 getNewTypes 可以直接解构
const {getNewTypes} = newsStore
getNewTypes()


// ✅ 这样写是响应式的 
// 💡 当然你也可以直接使用 `store.doubleCount`
const newsType = computed(()=>newsStore.newsType)

上面这一点和组合式函数不同,组合式函数里面如果用的是ref定义的变量进行解构不会丢失响应式。

思考:共享状态应该存储在pinia中还是使用自定义hook呢?

bug: 1.项目时遇到一个奇怪的bug,从列表页到详情页时请求详情页相关的接口是正常的,但是奇怪的是还会额外请求列表页的接口。接口请求逻辑是写在一个hook函数中,通过watch监听入参变化就会请求list。一开始以为是路由参数是接口入参,路由跳转参数变了导致触发了watch,即离开页面时watch副作用函数并没有销毁,后面经过验证我的猜想并没有得到证实,但是看到一个api effectScope 官网,可以再试试

vueuse

vant4

  1. Field输入框

type为number类型时,v-model绑定的值还是string类型。加了number修饰符后formatter的value参数依然是string类型。只有在最后提交时才是number类型。validator 校验也是number类型。

js 复制代码
 :rules="[{ validator: val => val >= 0.001 && val <= 100.0, message: '请输入0.001~100.0之间的值' }]"
 @input="handleInputRatio"
 :formatter="formatRatio"

2.Dialog组件的confirm事件会默认关闭弹窗,如果等需要校验表单,即使没有校验通过也会关闭。可以在before-close事件中进行处理或者通过插槽定义按钮和事件

3.Dialog弹窗宽度默认为320px。加了一个弹窗后默认宽度失效了。

经排查发现是首页路由映射的组件没有使用懒加载的方式,改成了懒加载的方式就可以了。为什么呢?

企业微信内部应用

1.如何接入企业微信sdk

注意

哪个页面要使用sdk,都需要先注册sdk。企业微信会自动读取页面的url。不能直接在全局注册sdk(App.vue或main.js,会导致获取的url不对。

2.附件下载和预览

  1. 使用sdk方式,只支持移动端 下载失败,请检查网络后重试
js 复制代码
export async function previewFile(url, size) {
  ww.previewFile({
    url,
    size,
    success(res) {
      console.log('previewFile---success---res', res);
    },
    fail(res) {
      console.log('previewFile---fail---res', res);
    },
  });
}

2.下载接口的方式,直接location.href 后端提供的地址。移动端是直接预览的方式,pc端是下载保存附件的形式

3.window.open 和 location.href一个跨域的资源地址,在pc端是可以的。但是安卓和ios都无法打开

TODO:

vueuse换肤 # prefers-color-scheme

1.定义不同主题的css变量

2.存储用户选择的是浅色模式还是深色模式,并动态设置html的class类,如果是深色模式,则有dark类

document.documentElement.classList.add("dark");

prefers-color-scheme css媒体特性

developer.mozilla.org/zh-CN/docs/...

window.matchMedia("(prefers-color-scheme: dark)").matches;

color-scheme: dark; 操作系统颜色方案的常见选择为"亮色"和"暗色",或"日间模式"和"夜间模式"。当用户选择其中一种颜色方案时,操作系统会对用户界面进行调整,包括表单控件、滚动条和 CSS 系统颜色的使用值。

developer.mozilla.org/zh-CN/docs/...

相关推荐
咖啡星人k1 小时前
【无标题】
前端·ai·github
颜进强1 小时前
LangChain 从入门到实践:用最小案例理解 RAG 的 5 个核心抽象
前端·后端·ai编程
颜进强1 小时前
MCP 从入门到实践:用 TypeScript 实现第一个 MCP Server
前端·后端·ai编程
朕的剑还未配妥2 小时前
CSS 实现渐变毛玻璃:从 backdrop-filter 到多层 mask
前端·css
程序员黑豆2 小时前
鸿蒙开发实战:使用 List 组件构建新闻列表
前端·华为·harmonyos
xiaobaoyu2 小时前
谈谈你对iframe的了解
前端
GISer_Jing2 小时前
前端转全栈须知后端知识
前端·后端·ai·前端框架
爱勇宝2 小时前
我做了一个排版器,也踩了一遍富文本复制的坑
前端·后端·微信
铁皮饭盒3 小时前
面试官:如何用 Bun + JS 实现安全的文件 MCP 工具集
前端·javascript·后端