记一次vue3+vite+vant+axios移动端页面实践(vant上拉加载/下拉刷新,引入axios)

最近搞了个企业微信内部应用(OA),想着内部使用就拿了vue3来练手。

vue3 出来很久了,目前版本 @3.2⇲, 另外 中文网站 也出来了,作为一个英文差的伸手党也应该学习起来了,哈哈~~~

UI框架我选择了 Vant@3.1.4 ,其官网地址⇲

趁着周末,以 Vant上拉加载/下拉刷新 为切入点,慢慢铺开 vue3 的一些小知识点。

本文使用的 IDE 是 vscode, 对于vue3语法 ,官方更推荐 volar 插件支持。
注意 vue3 要求 node 版本 > 12

这篇 ~~高质量程序员~~🤡 的文章,能不能让我花三个小时得到你们的18个赞👍。
请添加图片描述


创建一个项目

Vite 创建一个 Vue3项目(名为vue_next), 只需要以下命令

# yarn 推荐
yarn create vite vue_next
# npm
npm init vite vue_next

我这样安装的vue版本是 3.0.4

请添加图片描述
项目初始目录:

|-node_modules      -- 项目依赖包的目录
|-public            -- 项目公用文件
  |--favicon.ico    -- 网站地址栏前面的小图标
|-src               -- 源文件目录,程序员主要工作的地方
  |-assets          -- 静态文件目录,图片图标,比如网站logo
  |-components      -- Vue3.x的自定义组件目录
  |--App.vue        -- 项目的根组件,单页应用都需要的
  |--index.css      -- 一般项目的通用CSS样式写在这里,main.js引入
  |--main.js        -- 项目入口文件,SPA单页应用都需要入口文件
|--.gitignore       -- git的管理配置文件,设置那些目录或文件不管理
|-- index.html      -- 项目的默认首页,Vue的组件需要挂载到这个文件上
|-- package-lock.json --项目包的锁定文件,用于防止包版本不一样导致的错误
|-- package.json    -- 项目配置文件,包管理、项目名称、版本和命令

照着指示,进入 => 安装依赖 => 运行 即可

cd vue_next
npm install (or `yarn`)
npm run dev (or `yarn dev`)

在这里插入图片描述
浏览器打开相应地址就行了。

vite 很香,自带热更新,配置项在根目录 vite.config.js (需要新建),细则见 官网

为了后面案例的展开,先补充一点点 vue3 的知识。


vue3基础知识

vue-router && vuex

这两个没什么大的变化,不做重点讲解。使用时移步官网即可。

  • 状态管理推荐 pinia 方案,vue2/3均支持。
  • 下面也会提到 基于 provide/injectsessionStorage 的小方案

向上兼容

注意,你仍然可以像写 vue2.x 那样写vue3。除了部分不兼容外(错误时会有提示)
个人认为 vue3 最大改动就是加入了 Composition API ,所以这将是本文的实践方式。

Composition API

Composition API 翻译过来就是 组合式接口,官方推荐使用代替原来的选项式(Options Api)书写。
其实这种方式更接近 js ,不同的就是就是写在了 setup 函数里。

setup

Composition API的大概意思就是数据和逻辑全部写在 setup函数 里面,把它当做一个函数即可,接受两个参数 props(属性), context(上下文)。
context主要有三个属性 attrs, slots, emit

//xxx.vue
<template>
</template>

<script>
export default {
  setup(props, { attrs, slots, emit }) {
    console.log(props.title, )
  }
}
</script>

props

  • 接受的自定义属性, 响应式的。

context.attrs

  • 所有传入的属性,非响应式。如果要根据变化做响应操作,建议 onUpdated 中调用

context.slots

  • 插槽,非响应式。如果要根据变化做响应操作,建议 onUpdated 中调用。

context.emit

  • 向父组件中提交事件,和原来 this.$emit 相同

生命周期

有一些 更改, 更改之后,与路由守卫周期,自定义指令周期都一致了。

2.x 周期选项式 APIsetup
beforeCreatebeforeCreateNot needed*(setup)
createdcreatedNot needed*(setup)
beforeMountbeforeMountonBeforeMount
mountedmountedonMounted
beforeUpdatebeforeUpdateonBeforeUpdate
updatedupdatedonUpdated
beforeDestroybeforeUnmountonBeforeUnmount
destroyedunmountedonUnmounted
errorCapturederrorCapturedonErrorCaptured
-renderTrackedonRenderTracked
-renderTriggeredonRenderTriggered
-activatedonActivated
-deactivatedonDeactivated

定义数据和方法

数据可以有响应性和非响应性,但最后都得返回以暴露给 template 中使用

  • 非响应式数据
//xxx.vue
<template>
  <span>{{ name }}</span>
</template>

<script>
export default {
  name: 'App',
  setup(props) {
   const name = "非响应常量";
   return { name } // 暴露出去
  }
}
</script>
  • 响应式数据

对于响应式数据 vue 提供了两个API refreactive .个人认为基础数据类型用 ref, 否者用reactive 。写多了之后其实我都放在了 reactive 里面,因为优雅🐶)。

<template>
  <span>{{ name }}</span>
  <p v-for="item in state.arr" :key="item.id">{{item}}</p>
</template>

<script>
import { ref, reactive } from 'vue';
export default {
  name: 'App',
  setup(){
    let name = ref('响应式变量');
    // 在 setup内部使用 ref 包裹的变量要用 .value
    name.value = 'new name';
    const state = reactive({ arr: [{id: 1, name: '响应项'}] })
    return { name,  state }
  }
}
</script>
  • 语法糖 unref, toRefs

unref 主要是获取 非响应 和 响应 的值

import { unref } from 'vue';
// 相当于 val = isRef(name) ? name.value : name
const val= unref(name)

toRefs 是将响应式对象转换为普通对象。可以转化上面提到的 props,说是语法糖有点牵强,不过我常常把它当做语法糖用。如下书写后,便可以在模版内少写 state, 很香.

<template>
  <p v-for="item in arr" :key="item.id">{{item}}</p>
</template>

<script>
import { toRefs } from 'vue';
export default {
  name: 'App',
  setup(props){
	const title = toRef(props, 'title')
	console.log(title.value)
    const state = reactive({ arr: [{id: 1, name: '响应项'}] })
    return { ...toRefs(state) }
  }
}
</script>
  • 定义方法

和上面变量一样, 先定义后暴露即可

// template
<button @click="handleSubmit"></button>
// script	setup 内部
const handleSubmit = () => {/* somde code*/ }
return { 
 // 方法
 handleSubmit
}

全局变量和组件

在vue2.x 中 axios 引入就是通过 Vue.prototype.$http 定义全局属性以便在单文件组件中使用,注意这种方式在vue3中将不起作用, 相关 地址

作为替代: Vue.prototype 替换为 config.globalProperties

const app = createApp({})
app.config.globalProperties.$http = () => {}

其实这种方式 在 setup 中使用会有问题,考虑使用 provide/reject,下面会做介绍。

先介绍一下全局组件,全局注册

const app = Vue.createApp({})
app.component('component-a', {
  /* ... */
})

稍微可以封装一下

// src/common/thirdComponent.js
import HelloWorld from '../../components/HelloWorld.vue';
export default function initGloabalComponents(app) {
	app.component(HelloWorld);
}
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import initGloabalComponents from "./common/thirdComponent"

const bootstrap = () => {
  initGloabalComponents(app);
}
bootstrap();
app.mount('#app')

provide/reject(引入axios)

上面提到 app.config.globalProperties.$http 在 setup中使用会有问题, 可使用 provide/reject

这个⇲ 很好用,不仅能解决上面的问题,还可以 二次封装 第三方组件(引入vant时会提到),做一个自己的 mini vuex(后面会提到)。

当编写不依赖 vuex 的组件库时,这或许是 vue 提供的一个让人 🤞amazing🤸‍♂️ 的 Api!

使用 axios 为例。

引入axios

先安装

yarn add axios -S

只需要在main.js中注入

//main.js
import axios http from "axios";
app.provide('$http', axios);
<script>
import { provide, inject } from 'vue';
export default {
  name: 'App',
  setup(){
     // 如果要在template中使用便用return 暴露出去
     const _http = inject('$http');
     const fetchUser = () => {
      _http.get(url).then(res => {})
     }
     // 可以暴露一个本组件的全局变量以便其所有子组件(一层或多层)使用
     // 子组件用 inject('varible') 接收
     provide('varible');
  }
}
</script>

如果想稍微封装一下或者想深入 axios, 推荐

dom元素获取之ref

获取元素是我们常见的需求,vue2.x 提供了 this.$refs.xxx 获取 ref="xxx" 的元素。
在vue3中,依然是 在 元素/组件 增加属性 ref="xxx", 获取时有一些不同。

// 注意一致性!!!
<template>
  <span ref="xxx0"></span>	
  <HelloWorld msg="Hello Vue 3.0 + Vite" ref="xxx1" />
</template>
<script>
import { ref } from 'vue';
export default {
  name: 'App',
  setup(){
    const xxx0 = ref(null);
    const xxx1 = ref(null);
    // 是一个对象, $el 是相应的dom
	console.log(xxx0.value, xxx1.value)
	return { xxx0, xxx1 }
  }
}
</script>

父子父组件的通信

父子组件常常是需要触发对方的方法或者传值, 前面提 setup 时提及到一些知识,这里完善一下,建议参考 组件基础⇲

  • 父组件传值给子组件
<template><!-- child.vue --></template>
<script>
import { toRef } from 'vue';
export default {
  name: 'App',
  setup(props){
  	console.log(props.title);
	const title = toRef(props, 'title')
	console.log(title.value)
    return { title }
  }
}
</script>
  • 父组件调用子组件方法&子组件调用父组件方法
// pareant.vue
<template>
	<child ref="childRef" @bindFunc="handleFromChild"></child>
</template>
<script>
import { ref } from 'vue';
export default {
  name: 'App',
  setup(){
  	const childRef= ref(null);
  	// 调用 child 组件的 childFunc 方法, 
  	childRef.value.childFunc(params2);
  	
  	const handleFromChild = (params1) => {}
    return { childRef, handleFromChild }
  }
}
</script>
// child.vue
<template>
	<span>child</span>
</template>
<script>
export default {
  name: 'App',
  setup(props, { emit }){
  	// 触发绑定的方法 bindFunc
  	emit('bindFunc', params1);
  	
  	const childFunc = (param2) => { // 子组件方法 }
  	return { childFunc }
  }
}
</script>

其它

一些API,子元素的方法什么的变更,限于篇幅我就不再提了。
当我们一个组件代码很多的时(后期很常见),需要做一些逻辑抽离,比如每个页面需要检测一下用户权限(当然完全可以在router 拦截里面做)。官网中也有提及

建议大家仔细阅读 迁移指南⇲即可


引入vant

全局引入

安装

yarn add vant@3.1.4 -D

全局引入

main.js

import { createApp } from 'vue'
import App from './App.vue'
import './index.css';
// 引入
import Vant from 'vant';
import 'vant/lib/index.css';

const app = createApp(App);
// 安装
app.use(Vant);
app.mount('#app');

使用

Helloword.vue 稍微改造一下

  • template
<template>
  <div class="list-wrapper">
    <van-pull-refresh v-model="refreshing" @refresh="onRefresh">
      <van-list
        v-model:loading="loading"
        :finished="finished"
        finished-text="没有更多了"
        @load="onLoad"
        <!-- 阻止onLoad首次加载 -->
        :immediate-check="false"
      >
        <van-cell v-for="item in list" :key="item.id">
          <span>{{ item.name + item.id }}</span>
        </van-cell>
      </van-list>
    </van-pull-refresh>
  </div>
  <div class="bottom-nav">底部导航</div>
</template>
  • script
<script>
import { reactive, toRefs, inject, onMounted } from 'vue';
export default {
  name: 'HelloWorld',
  setup(props) {
    const state = reactive({
      refreshing: false,
      loading: false,
      finished: false,
    })
    const list = [
      { id: 1, name: '张三' }
      , { id: 2, name: '张三' }
      , { id: 3, name: '张三' }
      , { id: 4, name: '张三' }
      , { id: 5, name: '张三' }
      , { id: 6, name: '张三' }
      , { id: 7, name: '张三' }
      , { id: 8, name: '张三' }
      , { id: 9, name: '张三' }
    ];

    onMounted(() => {
      fetchUser()
    })

    const onRefresh = () => {
      console.log('下拉刷新');
    }
    const onLoad = (refresh = false) => {
      console.log('上拉加载');
    }

    return {
      list,
      ...toRefs(state),
      // 方法
      onRefresh,
      onLoad
    }
  }
}
</script>
  • style

安装sass,vite 自带css预处理器解析

yarn add sass -D
<style lang="scss" scoped>
$bottom_nav_height: 10vh;
.list-wrapper {
  height: calc(100vh - #{$bottom_nav_height});
  overflow: auto;
  :deep .van-list {
    .van-cell:nth-child(2n) {
      background-color: skyblue;
    }
    .van-cell__value {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      height: 5rem;
    }
  }
}
.bottom-nav {
  width: 100%;
  height: $bottom_nav_height;
  background-color: #eee;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 1.5em;
}
</style>

目前没有下拉加载,但样式应该是出来了。

按需引入

按需引入分自动按需引入(用了什么它自动引入什么)和手动按需引入。因为有些组件我要统一配置,我也就手动按需引入了。

关于自动按需引入参考 vant-按需引入

新建文件 common/thirdComponent.js

// common/thirdComponent.js
/**
 * 全局组件导入
*/

import 'vant/lib/index.css';
import { 
  PullRefresh,
  List,
  Cell,
  Toast,
} from 'vant';

export default function initGloabalComponents(app) {
  let loadingToast;

  app.use(PullRefresh)
  app.use(List)
  app.use(Cell)
  app.use(Toast)
  
  //允许加载多个
  Toast.allowMultiple();
  // 加载,统一加载样式
  function show(cfg = {}){
    loadingToast = Toast.loading(Object.assign({
      duration: 0,
      message: '加载中...',
      forbidClick: true,
      loadingType: 'spinner',
    }, cfg))
  }
  function hidden(){
    loadingToast && loadingToast.clear()
  }
  /***
   * 运行 toast 实例
   * @params {} | string config  与Toast可接受参数一致 
  */
  function runToast(config = { message: '提示' }) {
    Toast(config)
  }
  const _toast = {
    _toast: runToast, // 提示
    on: show, // 加载
    off: hidden //关闭加载
  }
  app.provide('_vantToast', _toast)
}

main.js 注入

import { createApp } from 'vue'
import App from './App.vue'
import initGloabalComponents from "./common/thirdComponent"
const app = createApp(App);
const bootstrap = () => {
  // 上面提到的axios 也可在这里注入	 
  initGloabalComponents(app);
}
bootstrap();
app.mount('#app')

其它组件和上面使用方式一致,使用 toast 时需要像这样使用

//xxx.vue
<script>
import { inject} from 'vue';
export default {
  name: 'App',
  setup(){
     const _http = inject('$http');
     const $toast = inject('_vantToast');
     const fetchUser = () => {
       $toast.on()// 开启加载
      _http.get(url).then(res => {
		$toast.off()// 关闭加载
		$toast._toast('获取成功')
	  })
     }
  }
}
</script>

vant下拉刷新、上拉加载代码

这里我也踩了一些坑,比如

  1. 列表过长时下拉刷新和滑动区域的冲突处理
  2. 列表过短时下滑列表外区域不触发刷新。

针对1 上面的demo是不存在这个问题了,我项目中出现了,但是用了其它的方法处理,后面会提到;
针对 2 可将 van-pull-refresh 高度设置为 列表展示的最大高度;

关键代码(开头的效果关键代码)

// 注意 list 用 users 替代了
<script>
import { reactive, toRefs, inject, onMounted } from 'vue';
export default {
  name: 'HelloWorld',
  props: {
    msg: String
  },
  setup(props) {
    const $toast = inject('_vantToast');
    const state = reactive({
      refreshing: false,
      loading: false,
      finished: false,
      users: [],
      searchInfo: {
        page: 1,
        count: 0,
        size: 10,
      },
    })
    onMounted(() => {
      fetchUser()
    })
    const onRefresh = () => {
      console.log('下拉刷新');
      state.searchInfo.page = 1;
      onLoad(true);
    }
    const onLoad = (refresh = false) => {
      console.log('上拉加载');
      if (refresh) fetchUser()
      else {
        const ending = handlePage();
        !ending && fetchUser();
      }
    }
	// 控制页面的增减
    const handlePage = () => {
      const { page, size, count } = state.searchInfo;
      const totalPage = count % size > 0 ? ~~(count / size) + 1 : count / size;
      // 已到最后一页
      if (page < totalPage) state.searchInfo.page++;
      // 已到最后一页
      else {
        state.finished = true;
        $toast._toast('全部加载完成');
        return true;
      }
    }
    // settimeout 模拟远程拉取用户
    const fetchUser = () => {
      const { page, size } = state.searchInfo;
      state.loading = true;
      setTimeout(() => {
        if (page === 1) {
          state.users = [];
          state.refreshing && $toast._toast('刷新成功')
          state.refreshing = false;
        }
        // 产生模拟数据
        const data = Array(10).fill(0).reduce((acc, item, index) => {
          acc.push({ id: (page-1) * size + index, name: '张三' })
          return acc;
        }, [])
        console.log('data: ', data);
        const res = { success: true, data, count: 40 }
        if (res.success) {
          state.searchInfo.count = res.count;
          state.users.push(...res.data);
        } else {
          state.searchInfo.page--;
        }
        state.loading = false;
      }, 800)
    }
    return {
      ...toRefs(state),
      // 方法
      onRefresh,
      onLoad
    }
  }
}
</script>

上面说过,如果出现了列表过长时下拉刷新和滑动区域的冲突处理
利用 ref 获取dom 监听滚动事件,在合适的时候禁用下拉组件即可解决。

<van-pull-refresh v-model="refreshing" @refresh="onRefresh" :disabled="refreshDisabled ">
  <van-list ref="vanListRef">
    <!-- some code -->
  </van-list>
</van-pull-refresh>
//...
import { ref, toRefs, reactive, onMounted } from 'vue';
setup () {
    const vanListRef = ref(null);
	const state = reactive({
		refreshDisabled: false,
	}),
	return {
		vanListRef,
		...toRefs(state)
	}
}
onMounted(() => {
    vanListRef.value.$el.addEventListener('scroll', evt => {
      state.refreshDisabled = evt.target.scrollTop > 0 ? true : false;
   })
});
//注意需要在 onBeforeUnmount 销毁 该事件监听器。  

基于 provide/reject, sessionStorage 搞一个自己的状态存储

牢骚ing😁: 文章写着写着就这么长了,主要是代码太占地方了, csdn 还不支持 像思否那么的代码折叠,难受~~~(其实还想谢谢vue3对于vue2的改进)

言归正传。在 src/store/index.js, 写入如下代码

// 在 app.vue 里面全局注入
import { reactive } from "vue"
/**
 * 简单的状态管理
*/
 const store = {
  state: reactive({
    userInfo: {},
  }),

  /**---------------------------------------------
   * mutation
  */
  set_userInfo(newValue) {
    this.state.userInfo = newValue
  },
  clear_all_state(){
    state.userInfo = {};
  },
  clear() {
    window.sessionStorage.clear();
  }
}
//防止刷新丢失,方法在 app.vue 里面执行
export const saveStore = (state) => {
  // TODO: 键 & 值 MD5加密存储
  //在页面刷新时将vuex里的信息保存到sessionStorage里
  window.addEventListener("beforeunload", () => {
    window.sessionStorage.setItem("infoStore",JSON.stringify(state))
  })
      
  //在页面加载时读取sessionStorage里的状态信息
  const info = window.sessionStorage.getItem("infoStore")
  if(info){
    const infoObj = JSON.parse(info);
    for(let key in infoObj){
      state[key] = infoObj[key]
    }
  }
}
export default store
//app.vue
import { onBeforeMount,provide,reactive } from 'vue';
import store, { saveStore }  from "./store/index"
export default {
  name: 'App',
  setup(){
    const appStore = store;
    onBeforeMount(() =>{ saveStore(state) });
    provide('appStore', appStore);
  }
}

使用

// xxx.vue
import { reject} from 'vue';
export default {
  name: 'App',
  setup(){
    const _store = reject('appStore', appStore);
    _store.set_userInfo({ id: 1, name: '张三' })
    console.log(_store.state.userInfo) // { id: 1, name: '张三' }
  }
}

最后

Vue3还是挺香的,结合 Vite 食用更佳,不过生态还不全,可尝试性的搞起来了。


参考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值