现阶段开发 vue3
项目使用 vite
+ ts
应该已成为标准范式了吧,新的语法体验 vue composition api
再配合 script setup
谁用谁知道啊,开发和构建方面,vite
作为下一代构建工具,想必大家也有所了解,使用 ES6 module
+ ESbuild
支持本地开发,速度和效率起飞啊,就一个字——爽,对于 TypeScript
,感觉都没必要说了,现在还没上车的赶紧的了~
前言
vite
作为一个构建工具,我们目前了解如何使用即可,该工具足够优秀,默认对很多功能都做到了支持(css module
、less
、scss
),且其作为 vue
之父尤大大的作品,对于 vue
也有着较好的支持,目前使用率也已经很高了,nuxt
等多个大项目都已支持,结合文档和社区,目前使用足够了,也无需担心疑难杂症~,快上车~
vue3
,感觉变化最大的就是全面拥抱了函数式编程,结合 composition api
现在真的可以做到对于复杂业务代码的轻松管理,抛弃以前的 this this 一大堆,不友好的mixin
,使用现在的 hooks
处理,逻辑复用、功能模块拆分简直太方便了,语法、api使用上也优雅方便了很多,很值得一试
另一大亮点就是 vue3
对 TS
的良好支持,现在项目可以全面拥抱 TS写法了,然后与 setup
再结合我接下来推荐的几款工具,组合起来使用,简直不要太爽
对于 TS
,首先要做到对类型的定义,这是和传统写JS有所不同的地方,但是这一步却是很必要也很值得的,这对你接下来的工作或者说对于这个项目的以后都是有很大的好处
如这样一个场景,和后端接口对接:
在前期,我们拿到接口文档,按照格式和类型定义好对应 TS类型,并结合 Mock写好接口和业务逻辑,使用时通过TS可以高效的完成代码开发,同时可以极大的避免犯错,对于后期维护迭代有很大的保障
import.meta
使用 vite做为构建工具,可以通过 import.meta
获取对应方法方便快速的处理业务
环境变量获取
- 在
package.json
中以命令行的形式定义("dev:development": "vite --mode development"
) - 在
.env
中以配置文件的形式定义(VITE_APP_ENV = dev
)
当.env.development
文件如下配置时:
VITE_APP_ENV = dev
VITE_APP_TITLE = 我是标题
注意:
当要读取在 .env.development
中配置的变量时,需要确定 vite --mode development
的 mode
参数为 development
在 vite.config.ts
中
可以用函数式的写法直接读取到 mode
参数(如果你想在该文件中读取.env.development
中的配置信息,可以通过结合dotenv
库实现)
import { UserConfig, ConfigEnv } from 'vite';
export default ({ command, mode }: ConfigEnv): UserConfig => {
console.log(command, mode);
return {
// ...
}
}
在组件中
console.log('my config env: ', import.meta.env)
打印内容如下
// console.log(import.meta.env)
{
BASE_URL: "/"
DEV: false
MODE: "development"
PROD: true
SSR: false
VITE_APP_ENV: "dev"
VITE_APP_TITLE: "我是标题"
}
注意:
配合.env
/.env.development
/.env.production
等文件设置环境变量使用时,变量Key
应该以VITE_
为前缀
{
"script":{
"dev": "vite --mode development"
}
}
为了防止意外泄漏 env 变量给客户端,只有以 VITE_为前缀的变量才会暴露给 Vite 处理的代码。只会向您的客户端源代码VITE_SOME_KEY公开import.meta.env.VITE_SOME_KEY,但DB_PASSWORD不会。
批量处理文件
import.meta.globEager
// 读取当前目录下的所有 .ts文件
const modules = import.meta.globEager('./**/*.ts')
ref和reactive
都可以用来定义响应式数据
ref
主要用于定义基本类型,使用时需要通过 .value
读取或修改
基本类型:除去Object,包括:String
、Number
、boolean
、null
、undefined
控制台打印数据结构为 RefImpl
// ref
const count = ref(0)
count.value++
console.log(count.value)
在定义基本类型时,响应式原理是与 vue2.x类似的 Object.defineProperty()
,通过 get
、 set
读取、修改数据
不过,ref也可以定义引用类型的数据,注意,当定义引用类型时,其内部实现则是借助 reactive
可以通过打印数据在控制台查看结构,分别为:RefImpl
、Proxy
reactive
只能定义引用类型,即Object,包括:Object
、Array
、Date
、function
,定义基本类型时会警告
使用时,直接通过属性读写
// reactive
const state = reactive({count:0})
state.count++
console.log(state.value)
reactive默认对对象内的所有属性都进行响应式处理,并可以实现深层监听
该响应式能力是通过 ES6 Proxy
实现的,其可以做到对属性的新增、删除监听,解决了 defineProperty
的缺陷,并且对嵌套属性有着良好的支持,可以轻松实现 a.b.c.d=xx
的响应式更新
Proxy
与Reflect
均为ES6语法,一般他俩是一起配合使用,可以很好的做到安全优雅的对属性做出劫持更新
小结
模板 tempalte 会自动解包,在模板中使用时,不需要 .value
对于引用类型来说,简单理解即:ref本质也是reactive,ref(obj)等价于reactive({value: obj}),ref
的底层实现就是 reactive
可以发现,所谓响应式其实就是对属性的劫持
ref
和reactive
定义的数据,每一层都是响应式的
watch、watchEffect
监听响应式数据的变化
watch
基本语法和 vue2类似,不过在这里有一些不同的使用方式
监听 ref定义的响应式数据(基本类型)
- 函数式写法需要
.value
,监听的是一个值的变化
const count = ref(0);
const str = ref('abc');
// 1. 普通写法
// watch可以省略 .value
watch(count, (val, old) => console.log({ val, old }));
// 2. 函数写法
watch(
() => count.value,
(val, old) => console.log({ val, old }),
);
// 3. 数组写法
watch(
() => [count.value, str.value],
(val, old) => console.log({ val, old }),
);
监听 ref定义的响应式数据(引用类型)
- 需明白的是,
ref
定义引用类型,内部是使用reactive
实现的,因此,需要通过.value
拿到响应式对象,再进行属性监听
const refState = ref({
count: 0,
str: 'abc',
});
// 1. 普通写法,无效
// => refState.value 有效
watch(refState, (val, old) => console.log({ val, old }));
// 2. 函数写法
watch(
() => refState.value.count,
(val, old) => console.log({ val, old }),
);
监听 reactive定义的响应式数据
- 需要针对属性监听
state.count
const state = reactive({
count: 0,
str: 'abc',
a: {
b: {
c: 'a-b-c',
},
},
});
// 1. 普通写法
// 结果:val, old 新旧值相同,
// watch(state, (val, old) => console.log({ val, old }));
// 2. 函数写法
// 结果:指定属性变化才会触发
watch(
() => state.value.a.b.c, // 只监听指定的 属性
(val, old) => console.log({ val, old }),
);
watchEffect
接收一个函数,不需要设置监听对象,该方法会自动接管函数内部使用到的依赖,当依赖发生更新时,触发该函数执行
该函数会初始化默认执行一次
watchEffect(()=>{
if(state.count>1){
// 只要 count变化,这个 watchEffect函数就会执行一次
// 当 count > 1时,做对应行为
}
})
watch、watchEffect 小结
使用watch
时,需要考虑的情况比较多
watch
更强调结果,watchEffect
强调过程
就用法方面来说 watchEffect
似乎更简单易用~
shallowRef和shallowReactive
- 递归监听和非递归监听
ref
和reactive
都属于递归监听,也就是数据的每一层都是响应式的,如果数据量比较大,非常消耗性能,非递归监听只会监听数据的第一层。
script setup 写法的 props 、context处理方式
在以 <script setup lang="ts">
形式使用 setup时,默认整个 script都是setup的函数作用域,我们不必再一一 return
定义的每个变量和方法,可以直接使用
但,对于 props
、emit
的定义,以及 ctx
属性的获取问题
vue也针对此,为我们提供了3个新API
defineProps
defineEmit
useContext
// 这3个api和setup 的属性一一对应
setup(props, { emit, ctx}){}
如果想通过父组件获取子组件的属性,需要在子组件中通过 defineExpose
定义需要暴露的属性
// 子组件 Child
const count = ref(0)
defineExpose({
count,
});
// 父组件
// <Child ref="ChildRef" />
const ChildRef = ref<RefType<{ count: number }>>(0);
const count = ChildRef.value.count
更多API见官方文档,说的很详细,这里就不再赘述了
props Type 类型定义问题
抛开 vue默认的几种基本类型,在一些特殊场景需要定义比较复杂的类型,需要通过 PropType
配合使用
如定义菜单路由类型
props: {
menuData: {
type: Array as PropType<MenuDataItem[]>,
default: () => [],
},
}
这里如果按照常规类型 Array
很难满足我们的需求(只知道是个数据,但是数据形状并不清楚),原始类型写法很难精确推导各属性的类型定义
prop、ref、emit 数据通信
prop
强调单项数据流(父=>子),类似react,主要用来传参给子组件
ref
两种用法:
-
以引用的方式把子组件的实例指给
ref
,使得可以在父组件中获取到子组件中的所有属性和方法,可以通过配合defineExpose
API来实现 -
用于获取DOM元素
// 如:在使用echarts绑定DOM节点时
// <div class="chart-box" ref="chartRef"></div>
const chartRef = ref<HTMLDivElement | null>(null);
echarts.init(unref(chartRef))
emit
主要用于子组件向父组件传递参数和通信emit
(子=>父),父组件通过事件方法@event
接收
<!-- 父组件 -->
emit('getMessage', '我是父组件!')
<!-- 子组件 -->
<child @event="handleMethod">
jsx 语法
在使用的过程中发现,jsx配合模板语法有着极大的灵活性,对于jsx语法,有 react开发经验的应该会感到很熟悉,开发体验上很相似
但是,对于vue来说,有着得天独厚的的优势,它本身作为模板语法,通过对实例方法的注入,指令的使用等,可以快速而高效的进行开发,在一些场景下, jsx语法 + vue模板语法有着完全不同的体验~
如:在模板中可以
<div @click="$router.push('xx')" v-auth="create"></div>
Table组件
以比较常见的table组件为例,我们封装好可复用的分页逻辑后,再把 columns
单出拆分出来用,使用jsx语法封装,根据不同组件使用做不同的配置,这样也更方便维护
export function columnsConfig(refresh: () => void) {
// ... 其他业务逻辑
const columns: ColumnProps[] = [
{
title: 'IP地址和端口',
dataIndex: 'ip',
width: 150,
customRender: ({ record }) => `${record.ip}:${record.port}`,
},
{
title: '操作',
key: 'action',
width: 200,
fixed: 'right',
customRender: ({ record }) =>
<Space>
<Button type="primary" onClick={() => router.push(`/app/product/detail/${record.id}`)}>详情</Button>
<Divider type="vertical" />
{
record.isSelf && <Popconfirm
title="你确定要退出网络吗?"
onConfirm={async () => {
const res = await fetchApi.delete(record.id);
if (res) {
message.success(`已申请退出网络`);
// 触发列表更新
refresh?.();
}
}}
>
<Button>删除</Button>
</Popconfirm>
}
</Space>
},
];
return columns;
}
当 action操作列 业务比较复杂时,需要频繁的和其他数据通信,我们也可以把action操作列剥离出,在 vue内部处理,再配合 Table组件的再封装处理
Table组件封装
<template>
<a-table :columns="columns">
<!-- 函数式写法自定义 操作列 -->
<template #action="{ record }">
<template v-for="(action, index) in getActions" :key="`${index}-${action.label}`">
<!-- 气泡确认框 -->
<a-popconfirm
v-if="action.enable"
:title="action?.title"
@confirm="action?.onConfirm(record)"
@cancel="action?.onCancel(record)"
>
<a @click.prevent="() => {}" :type="action.type">{{ action.label }}</a>
</a-popconfirm>
<!-- 按钮 -->
<a v-else @click="action?.onClick(record)" :type="action.type">{{ action.label }}</a>
<!-- 分割线 -->
<a-divider type="vertical" v-if="index < getActions.length - 1" />
</template>
</template>
</a-table>
</template>
<script lang="ts">
// action 操作列
const getActions = computed(() => {
return (toRaw(props.actions) || [])
.filter((action) => hasPermission(action.auth))
.map((action) => {
const { popConfirm } = action;
return {
type: 'link',
...action,
...(popConfirm || {}),
enable: !!popConfirm,
};
});
});
</script>
使用
// <Table :columns="columns" :actions="tableActions"/>
export const columns = [
// ...
{
title: '操作',
key: 'action',
width: 120,
slots: { customRender: 'action' },
},
]
const tableActions = ref([
{
label: '编辑',
auth: AuthEnum.user_update, // 配置按钮权限
onClick: async (row) => {
modalState.visible = true;
const res = await store.fetchDetail(row.id);
if (res) formModel.value = res;
},
}
// ...
]
这是我在上个项目中实战使用的一点心得,对于开发效率提升还是很明显的,维护起来也是很方便,更多用法也欢迎大家一起交流学习,就目前体验来说vue3很棒~
event bus
vue3中移除了实例中挂载 $emit
的行为,如果想继续使用可以单独下载对应的 npm包,如:mitt,该包很轻量,仅 200byte
api与用法类似,只是改变为了函数式创建,需要确保单个操作的 emitter创建唯一
import mitt from 'mitt'
const emitter = mitt()
export emitter
二级路径嵌套域名的部署配置
2022.03.09更新
简单举例
- 无嵌套域名访问时,举例:
www.exmple.com
- 存在嵌套域名访问时,举例
www.exmple.com/my
项目配置
针对生产环境,从vite build
入手
- 直接在
vite.config.ts
中配置base
属性为base: '/my/'
(base
默认为/
) - 在
package.json
中配置 scripts中的构建命令,动态传参配置"build": "vite build --base=/my/"
以上配置的属性值都可以通过 import.meta.env.BASE_URL
获取到
注意
当完成以上配置后,你会发现,通过链接和路由是可以正常访问的,但是存在两个问题
- 通过路由跳转的新的地址栏路径是缺少
/my/
路径的 - 手动刷新浏览器会返回
www.exmple.com
,以及返回404的情况
问题一:
是因为我们只配置了静态文件构建生成的路径参数,但是没有配置相关路由的,举例:路由跳转是 router.push('/home')
,现在应该跳转为 router.push('/my/home')
,这里需要同步配置 route
的base
属性保持一致。
直接在全局实例的时候统一配置即可:
// vue3.x 新版本路由通过 createWebHashHistory 函数参数配置
export const router = createRouter({
history: createWebHashHistory('/my/'),
routes,
});
// vue2.x 老版配置本如下
const router = new VueRouter({
mode: 'history',
base: '/my/',
routes
});
为了方便维护你也可以这样:createWebHashHistory(import.meta.env.BASE_URL)
问题二:
那是因为在history模式下,只是动态的通过js操作window.history来改变浏览器地址栏里的路径,并没有发起http请求,但是当我直接在浏览器里输入这个地址的时候,就一定要对服务器发起http请求,但是这个目标在服务器上又不存在,所以会返回404
不过通过以上配置过路由base
之后,保持路径和路由一致也一同解决了
结语
这篇文章其实相当于自己的学习笔记,也是为了加深印象,在使用的过程中记录了遇到的一些问题,希望可以给自己和大家带来一些帮助。就内容而言属于入门使用水平,目前暂未涉及到深水区,本文会根据使用情况持续更新