vue3+vite2+ts+script 实践--todolist

3,052 阅读8分钟

这是我参与更文挑战的第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使用

  1. 安装 使用yarn add pinia@next
  2. 创建集成到vue中
const store = createPinia();
app.use(store)
  1. 定义store,定义state
import { defineStore } from "pinia";
export const useTodoStore = defineStore({
  id: "todo",
  state: (): TodoState => {
    return {
      total: [],
      testName: "测试一下",
    };
  },
});
  1. 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,//就有响应式
  };
}
  1. 注意事项,不能在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')
})
  1. 使用 $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的相关应用

参考 github.com/vuejs/jsx-n…

插槽的使用

  • 使用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推导出来相应的方法 错误信息如下:

Xnip2021-06-09_21-48-21.png 解决:

方法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的属性

const list = ()=> <></>
list.displayName = 'ScriptTsx'
export default list;

备注:

如果是一个函数组件需要加一个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样式不生效

展示如下:

Xnip2021-06-09_21-47-09.png 复现:这个可能是个bug,已经提交issue

bug链接

特别感谢

代码地址