这是我参与更文挑战的第1天,活动详情查看:更文挑战
简述
vue3和vite发布有一段时间了,打算使用一个简单的todolist demo尝鲜一下新特性,只涉及到常规的一些操作,暂时还没有涉及到特别高级的特性,主要目的是踩踩坑,代码已上传到codesandbox,作为模板使用
环境搭建
项目创建
- 创建项目命令:
create @vitejs/app
- 选择vue-ts模板
- 进入项目执行
yarn dev
启动
添加依赖
组件库:
"ant-design-vue": "2.1.2",
"element-plus": "^1.0.2-beta.46",
store管理:
"pinia": "^2.0.0-alpha.19",
vite插件安装和配置修改
按需加载:
本来想使用其中一个按需加载两个组件库,但是会出现问题
问题如下:
- "vite-plugin-importer": "^0.2.1", antd-vue好用 element样式加载错误
- "vite-plugin-style-import": "^0.10.0", element-plus好用 ,antd会全量加载,总是出现一个warning警告组件全量加载 解决方案: 分别各引用各的 修改vite.config.ts plugins中添加相应的配置:
import styleImport from "vite-plugin-style-import";
import usePluginImport from "vite-plugin-importer";
export default defineConfig({
plugins: [
usePluginImport({
libraryName: "ant-design-vue",
libraryDirectory: "es",
style: "css",
}),
styleImport({
libs: [
{
libraryName: "element-plus",
esModule: true,
ensureStyleFile: true,
resolveStyle: name => {
return `element-plus/lib/theme-chalk/${name}.css`;
},
resolveComponent: name => {
return `element-plus/lib/${name}`;
},
},
],
}),
],
})
支持jsx/tsx:
"@vitejs/plugin-vue-jsx": "^1.1.4",
修改vite.config.ts添加
import vueJsx from "@vitejs/plugin-vue-jsx";
export default defineConfig({
plugins: [
vueJsx(),
]
})
全局别名定义:
就像以前使用的那样,可以直接使用类似 component: () => import("@/views/elem-tmpl.vue"),
修改vite.config.ts添加
import path from "path";
export default defineConfig({
resolve: {
alias: [
{
find: "@",
replacement: path.resolve(__dirname, "src"),
},
],
},
})
此时
如果直接在ts中导入import { useTodoStore } from "@/store/modules/todo";
ts会报错,报错信息如下:
Cannot find module '@/store' or its corresponding type declarations.Vetur(2307)
解决方式:需要在tsconfig.json加入如下俩属性
"baseUrl": "src",
"paths": {
"@/*": ["./*"]
},
为所有less文件全局注入antd 主题色变量:
为了便于在项目中引用antd的预设颜色体系,比如这样
&.checked {
color: @success-color;
}
&.unchecked {
color: @primary-color;
}
而不必在每个less文件都通过@import(url)
的方式引入
修改vite.config.ts添加
import { generateModifyVars } from "./build/style/generateModifyVars";
export default defineConfig({
css: {
preprocessorOptions: {
less: {
modifyVars: generateModifyVars(),
javascriptEnabled: true,
},
},
},
})
新建一个文件./build/style/generateModifyVars.ts
用于导入并注入全局less
实现原理:使用less 特性hack属性进行注入
比如如下方式写就可以实现注入:
less: {
globalVars: {
hack: `true; @import '~@/assets/less/variables.less';`
}
}
由于antd已经有现成的方法,并已经有了hack属性,所以为了用他预设的颜色变量,我就直接导出getThemeVariables
方法返回的变量
代码如下:
import { getThemeVariables } from "ant-design-vue/dist/theme";
export function generateModifyVars(dark = false) {
const modifyVars = getThemeVariables({ dark });
return {
...modifyVars,
};
}
添加prettier配置
根目录新建一个prettier.conf.js
配置如下
module.exports = {
printWidth: 80, // 每行代码长度(默认80)
tabWidth: 2, // 每个tab相当于多少个空格(默认2)
useTabs: false, // 是否使用tab进行缩进(默认false)
singleQuote: false, // 使用单引号(默认false)
semi: true, // 声明结尾使用分号(默认true)
trailingComma: 'es5', // 多行使用拖尾逗号(默认none)
bracketSpacing: true, // 对象字面量的大括号间使用空格(默认true)
jsxBracketSameLine: false, // 多行JSX中的>放置在最后一行的结尾,而不是另起一行(默认false)
arrowParens: "avoid", // 只有一个参数的箭头函数的参数是否带圆括号(默认avoid)
};
开发经验
状态管理-pinia集成使用(也许是vuex5的样子)
pinia使用
- 安装 使用
yarn add pinia@next
- 创建集成到vue中
const store = createPinia();
app.use(store)
- 定义store,定义state
import { defineStore } from "pinia";
export const useTodoStore = defineStore({
id: "todo",
state: (): TodoState => {
return {
total: [],
testName: "测试一下",
};
},
});
- pinia删除了mutation定义,只留下action,定义action
actions: {
deleteItem(item: DataItem) {
const arr = this.total;
const index = arr.findIndex((item2: DataItem) => item2.id == item.id);
if (index != -1) {
arr.splice(index, 1);
}
},
clearData() {
this.$reset();
},
},
默认this指向的是当前实例,可以直接通过this.total
访问到state,也可以直接调用this.$reset
将数据进行清空
5. 应用,可以直接在composition api中调用usexxxStore进行使用,如下这样
export function useTodo() {
const todoStore = useTodoStore();
todoStore.restoreData();
const data: UnwrapRef<DataItem[]> = todoStore.total;
watch(todoStore.total, total => {//可以监听到store的变化
localStorage.setItem(StoreKey.TODO_LIST, JSON.stringify(total));
});
return {
total: data,//就有响应式
};
}
- 注意事项,不能在setup中使用解构
// ❌ This won't work because it breaks reactivity
// it's the same as destructuring from `props`
const { name, doubleCount } = store
同时也不能在路由守卫外面定义
// ❌ Depending on where you do this it will fail
const store = useStore()
router.beforeEach((to, from, next) => {
if (store.isLoggedIn) next()
else next('/login')
})
- 使用
$patch
进行批量更新state
cartStore.$patch((state) => {
state.items.push({ name: 'shoes', quantity: 1 })
state.hasChanged = true
})
script setup使用
- 标签设置setup属性
<script setup lang="ts">
<script lang="tsx" setup>//tsx需要设置lang=‘tsx’,否则无法解析tsx语法
- 引入的方法或者属性会自动绑定上,不用再return一个对象包含进去,比如这样,页面则可以直接调用
const { finishes, unfinish, add, total,clearAll } = useTodo();
- script setup使用tsx 需要用export代替之前的return返回结果,类似如下形式
export default ()=> <>
<Button type="danger" onClick={()=>clearAll()}>清除一切 </Button>
</>
hooks封装通用逻辑
由于多个页面都会用到相同的逻辑,所以将相同的逻辑和对应的响应属性统一封装到一起,便于多个页面复用,新建一个hooks文件夹,里面使用compositionApi实现通用逻辑,导出use方法, 示例:统一将表单提交所需的表单规则,表单状态,提交方法都封装到一起,用户只关心使用到什么就绑定什么方法就可以,无需关注内部实现
import { DataItem } from "@/types/model";
import { onMounted, reactive, ref, Ref, toRaw, UnwrapRef } from "vue";
import { useTodo } from "./useTodo";
export function useForm() {
const formRef = ref(); //引用页面的form实例
// const { add } = useTodo();
let formState: UnwrapRef<DataItem> = reactive({
title: "",
id: "",
finish: false,
});
const rules = {
title: [
{
required: true,
message: "请填写todo的title",
},
],
};
type SubmitType = () => Promise<DataItem>;
const onSubmit: SubmitType = () => {
return formRef.value.validate().then(() => {
return toRaw(formState);
});
};
const reset = () => {
formRef.value.resetFields();
};
return {
formRef,
formState,
rules,
reset,
onSubmit,
};
}
注意:
获取form实例方式变化:之前vue2的时候获取表单的实例是通过this.$refs.formRef
的方式获取,但是在vue3使用ref()赋值给一个变量,这个变量得名称就是在标签定义的名字,const formRef = ref();
,页面定义 <a-form ref="formRef"> </a-form>
而且这个变量是需要在mounted之后才能获取到真正的form实例。
tsx的相关应用
插槽的使用
- 使用v-slots,如下这样,key值是slot名,值是render函数
<List.Item
v-slots={{
actions: () => renderActions(item)
}}
>
</List.Item>
- 直接定义对象字面量,key是slot名
<List.Item.Meta description={`id=${item.id}`}>
{{
title: (): JSX.Element => {
return item.editing ? <Input v-model={[item.title, "value"]}
/> : <div onDblclick={() => edit(item)}>
{`${item.title}`}
</div>
}
}}
</List.Item.Meta>
v-model的使用
比如antd的form,template写法和jsx的写法对比如下:
template写法:
<a-input v-model:value="formState.title" />
jsx的写法:
<Input v-model={[this.formState.title, "value"]} />
常见问题(已解决)
antdv,menu警告的问题,只要是引用了menu组件就会报,警告如下
reactivity.esm-bundler.js:337 Set operation on key "default" failed: target is readonly.
解决:
- 相关issue:antd-vue的bug github.com/vueComponen…
- 升级vue版本解决 升级到3.0.10 github.com/vuejs/vue-n… 经过查询:是antd打包导致的问题,需要回退版本
- antdv回退到2.1.2
- vue使用3.0.10以上
• script setup中注册组件注册不上的问题
解决:template使用的组件是
<a-divider>
这种形式,但是导入的时候,是按照这种形式导入import { Divider } from "ant-design-vue";
,就需要重新注册一下,但是官方又没有提供单独的方法进行注册组件,于是只能手动写个方法进行全局注册, 方法如下:
interface Comp {
displayName?: string;
name?: string;
}
type RegisteComp = (comp: Comp & Plugin) => void;
const registeComp: RegisteComp = comp => {
const name = comp.displayName || comp.name;
if (name && !registed[name]) {
const instance = getCurrentInstance();
const app = instance?.appContext.app;
if (app) {
app.use(comp);
registed[name] = true;
}
}
};
Vue 3中 tsx 对自定义组件中事件的监听报类型错误问题
如果子组件只是定义了emits,则vue3没有办法正常在父组件使用ts推导出来相应的方法 错误信息如下:
解决:
方法1: 参考: juejin.cn/post/692093…
父组件:使用扩展运算法给属性赋值
<TodoList
data={unref(finishes)}
{...{
// 此种解决方案参考:https://juejin.cn/post/6920939733983969294
onDeleteItem: (item: DataItem): void => {
deleteItem(item);
},
}}
/>
子组件:正常定义props,赋值,但是需要将emits中的方法定义到props中,否则没有ts提示
const todoProps = {
data: {
type: Array as PropType<DataItem[]>,
},
onDeleteItem: {
type: Function as PropType<(item: DataItem) => void>
}
};
方法2: 子组件: 定义如下,setup中的props不加类型限制也可以进行推导, 将渲染函数都放到render方法里(猜测与此有关)(待验证)
const todoProps = {
data: {
type: Array as PropType<DataItem[]>,
},
onDeleteItem: {
type: Function as PropType<(item: DataItem) => void>
}
};
const list = defineComponent({
name: 'TodoListTSX',
props: todoProps,
emits: ['deleteItem'],
setup(props) {
//操作逻辑写到setup中,这里的作用是和视图连接,
const toggle = (item: DataItem) => {
if (item.finish) {
item.finish = false;
} else {
item.finish = true;
}
};
const { edit, finishEdit, cancelEdit } = useEdit(props.data as DataItem[]);
return {
edit, finishEdit, cancelEdit, toggle
};
},
render() {
//渲染逻辑写这里,导入setup中的方法
const { data, finishEdit, cancelEdit, edit, onDeleteItem, toggle } = this
}
})
script setup返回tsx报警告 切换路由总会有个警告:
vue-router.esm-bundler.js:72 [Vue Router warn]: Component "default" in record with path "/script-tsx" is a function that does not return a Promise. If you were passing a functional component, make sure to add a "displayName" to the component. This will break in production if not fixed.
解决 就是加一个displayName的属性
- issue github.com/vuejs/vue-r…
const list = ()=> <></>
list.displayName = 'ScriptTsx'
export default list;
备注:
- next.router.vuejs.org/api/#props 官方文档是这样描述的,
如果是一个函数组件需要加一个displayName属性,即使返回的是一个jsx if you want to use a functional component, make sure to add a displayName to it.
const HomeView = () => h('div', 'HomePage')
// in TypeScript, you will need to use the FunctionalComponent type
HomeView.displayName = 'HomeView'
const routes = [{ path: '/', component: HomeView }]
遗留问题
script返回tsx 开始变化,切换别的页面再切回来 列表数据不变化,
复现步骤:在使用script setup方式返回tsx只有路由第一次执行这里的方法,当第二次切换到这个页面就不会调用,现象是页面不跟随响应变化,但是实际上数据已经发生变化了,对比过defineComponent形式的setup,路由只要切换到当前页面则会自动执行setup方法,怀疑是vue的bug,暂未解决
在tsx组件中,引入less样式,deep不能生效
复现步骤:tsx中使用方式如下:
import './TodoList.less'
font-size: 24px; //tsx的时候deep不生效,只能使用这种方式
:deep(.anticon) {
font-size: 24px;
}
element popconfirm样式不生效
展示如下:
复现:这个可能是个bug,已经提交issue
bug链接
- element-plus :github.com/element-plu…
- vue3:github.com/vuejs/vue-n…
特别感谢
- 优秀的开源admin: github.com/anncwb/vue-…
代码地址
- codesandbox模板地址:codesandbox.io/s/vue3-vite…
- github地址:github.com/nabaonan/to…