最近搞了个企业微信内部应用(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 create vite-app vue_next
npm init vite-app 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
这两个没什么大的变化,不做重点讲解。使用时移步官网即可。
后面会提到 基于 provide/inject ,sessionStorage 的小方案
向上兼容
注意,你仍然可以项写 vue2.x 那样写vue3。除了部分不兼容外(错误时会有提示) 个人认为 vue3 最大改动就是加入了 Composition API ,所以这将是本文的实践方式。
Composition API
Composition API 翻译过来就是 组合式接口,官方推荐使用代替原来的选项式(Options Api)书写。 其实这种方式更接近 js ,不同的就是就是写在了 setup 函数里。
setup
Composition API 的大概意思就是数据和逻辑全部写在 setup函数 里面,把它当做一个函数即可,接受两个参数 props (属性), context (上下文)。 context 主要有三个属性 attrs , slots , emit
<template>
</template>
<script>
export default {
setup(props, { attrs, slots, emit }) {
console.log(props.title, )
}
}
</script>
props
context.attrs
context.slots
context.emit
生命周期
有一些 更改, 更改之后,与路由守卫周期,自定义指令周期都一致了。
2.x 周期 | 选项式 API | setup |
---|
beforeCreate | beforeCreate | Not needed*(setup) | created | created | Not needed*(setup) | beforeMount | beforeMount | onBeforeMount | mounted | mounted | onMounted | beforeUpdate | beforeUpdate | onBeforeUpdate | updated | updated | onUpdated | beforeDestroy | beforeUnmount | onBeforeUnmount | destroyed | unmounted | onUnmounted | errorCaptured | errorCaptured | onErrorCaptured | - | renderTracked | onRenderTracked | - | renderTriggered | onRenderTriggered | - | activated | onActivated | - | deactivated | onDeactivated |
定义数据和方法
数据可以有响应性和非响应性,但最后都得返回以暴露给 template 中使用
<template>
<span>{{ name }}</span>
</template>
<script>
export default {
name: 'App',
setup(props) {
const name = "非响应常量";
return { name }
}
}
</script>
对于响应式数据 vue 提供了两个API ref , reactive .个人认为基础数据类型用 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('响应式变量');
name.value = 'new name';
const state = reactive({ arr: [{id: 1, name: '响应项'}] })
return { name, state }
}
}
</script>
unref 主要是获取 非响应 和 响应 的值
import { unref } from 'vue';
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>
和上面变量一样, 先定义后暴露即可
<button @click="handleSubmit"></button>
const handleSubmit = () => { }
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', {
})
稍微可以封装一下
import HelloWorld from '../../components/HelloWorld.vue';
export default function initGloabalComponents(app) {
app.component(HelloWorld);
}
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中注入
import axios http from "axios";
app.provide('$http', axios);
<script>
import { provide, inject } from 'vue';
export default {
name: 'App',
setup(){
const _http = inject('$http');
const fetchUser = () => {
_http.get(url).then(res => {})
}
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);
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>
<template>
<child ref="childRef" @bindFunc="handleFromChild"></child>
</template>
<script>
import { ref } from 'vue';
export default {
name: 'App',
setup(){
const childRef= ref(null);
childRef.value.childFunc(params2);
const handleFromChild = (params1) => {}
return { childRef, handleFromChild }
}
}
</script>
<template>
<span>child</span>
</template>
<script>
export default {
name: 'App',
setup(props, { emit }){
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>
<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
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()
}
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 = () => {
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上面的demo是不存在这个问题了,我项目中出现了,但是用了其它的方法处理,后面会提到; 针对 2 可将 van-pull-refresh 高度设置为 列表展示的最大高度;
关键代码(开头的效果关键代码)
<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;
}
}
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;
})
});
基于 provide/reject, sessionStorage 搞一个自己的状态存储
文章写着写着就这么长了,主要是代码太占地方了, csdn 还不支持 像思否那么的代码折叠,难受~~~
言归正传。在 src/store/index.js, 写入如下代码
import { reactive } from "vue"
const store = {
state: reactive({
userInfo: {},
}),
set_userInfo(newValue) {
this.state.userInfo = newValue
},
clear_all_state(){
state.userInfo = {};
},
clear() {
window.sessionStorage.clear();
}
}
export const saveStore = (state) => {
window.addEventListener("beforeunload", () => {
window.sessionStorage.setItem("infoStore",JSON.stringify(state))
})
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
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);
}
}
使用
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)
}
}
最后
Vue3还是挺香的,结合 Vite 食用更佳,不过生态还不全,可尝试性的搞起来了。
参考
🔑 | Vant - 轻量、可靠的移动端 Vue 组件库 🔑 | Vue - 渐进式JavaScript 框架
|