TS + Composition-Api 实战体验

前言

很久没输出了,今天来输出一把,缓解一下一我的的孤独。javascript

Vue3虽然还没正式发布,但公布到如今也是蛮久了,虽然如今已经能够开始尝鲜,但因为周边生态还不完善,而且proxy没法被polyfill,致使它也不能支持IE11,若是是一些2C的产品使用了vue进行开发,就算3出了,可能也会因为考虑IE用户暂时没法升级。还有不少利用ElementUI等Vue2.x框架的产品,短期内也生态不完善也不太容易转移到vue3。css

不过值得高兴的是,vue3的核心功能composition-api是同时支持vue2与vue3两个主版本的。咱们已经能够在一些小项目中尝试使用composition-api来作开发了,体验与vue3基本一致,只不过不能用teleport,suspense等新的功能,但并不影响coding的愉悦之感。在尤大大刚直播vue3以后,跟不少小伙伴同样,火烧眉毛地进行了把玩,然而发现因为破坏性的改动致使例如elementUI等框架没法与vue3进行配合,虽然网上有说法利用cdn引入vue2.x兼容ElementUI,本身的组件可使用vue3来写,固然这样玩玩能够,但总让人有点不舒服的感受。其实倒也没必要能够追求3,由于咱们彻底可使用vue2.x + composition-api的方案来进行开发,而且兼容ElementUI等Vue2.x的UI框架。前端

因为本人所在团队只有我一个前端,技术的选择也是无比自由,最近也是用vue2.6 + composition-api + ts重构了一个项目,作了一个新的小项目,今天又尝试了一把使用这个方案作组件库(抽离出的公共功能作个小组件库),遇到了一些问题,但幸运地给解决掉了,又搞出了以前这个方案中遇到的JSX相关问题,因此抑制不住激动的心情,晚上仍是出来分享一下最近的使用体验吧。(实际上是一我的太孤独了,想找小姐姐聊天又找不到,孤独到难受,来写写文章舒缓一下心情)。vue

工欲善其事必先利其器

这里介绍一下vue-cli项目的建立,若是很是熟悉请跳过直接日后看。java

建立项目node

话很少说,接下来咱们就一块儿用vue-cli建立一个ts项目,开始前请保证你的vue-cli是最新版本。react

  1. vue create athena建立一个项目(起名雅典娜),雅典娜女神比较著名,以此祝我早日找到本身的女神吧。
  2. 接下来的选择比较重要,若是一直只是在公司大佬们建立的项目中新增功能,本身vue-cli用的比较少那仍是要注意下的。选择最后一项Manually select features回车,咱们须要自定义配置,不使用默认配置。
  3. 这里推荐一个我比较经常使用的一个项目依赖内容的选项组合吧,这几个选项估计你们也都明白是干啥的,只不过我写测试比较少,E2E更是没写过,因此通常不选,若是有须要也能够本身看状况处理。
  4. 选中以后回车,接下来会被问道Use class-style component syntax? (Y/n)是否使用class-style语法,固然选择N啊,咱们会彻底使用composition-api,不会借助class来作,而且我我的不是很喜欢使用装饰器跟类这一套方案,若是有喜欢的,应该有好些资料介绍的,这里不选它。
  5. 以后就会问你是否使用TSUse Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)?,默认是就能够。
  6. 而后问你使用hash路由仍是history路由,Use history mode for router?,看本身项目需求吧,不想额外配置nginx可使用hash路由,这里我就默认了。
  7. 而后就是询问使用哪一个css预处理器了,Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default):, 这里我选择第二个node-sass,由于我用sass比较多,有感情,dart-sass尝试过,深度选择器支持得不友好,因此不用。
  8. 如今被问到的是linter和formatter的选择,这里推荐倒数第二个ESLint + Prettier,固然若是你有特殊须要选本身喜欢的就好了。
  9. 而后会问啥时候去lint格式化你的代码, Pick additional lint features:, 两个选项保存时跟commit都选上就能够了。
  10. 测试框架jest,特殊需求请本身选择
  11. babel,eslint等配置放在哪里? Where do you prefer placing config for Babel, ESLint, etc.?,固然是单独的文件夹呀,都放在package.json里咋维护啊。

以上选择就是我一般的配置,各位能够根据需求自行选择。nginx

安装composition-api yarn add @vue/composition-apivuex

src/main.ts中进行引入使用.vue-cli

...
import CompositionAPI from '@vue/composition-api';

Vue.use(CompositionAPI);
...
复制代码

安装官方推荐jsx工具 官方推荐了个jsx的工具,这个也须要安装yarn add babel-preset-vca-jsx -D

安装到dev依赖就行,打包后线上跑是不须要它的。固然不安装它也行,只不过涉及将将组件做为props传递给另一个组件就不知道你该怎么作了。这个在后面二次封装一个超级方便的通用table组件很是重要。

vuex插件安利 在使用composition-api过程当中,发现了vuex-composition-helpers神器,直接使用useXXXX函数,能够将vuex的state,actions, mutations,getters映射为响应式对象,用来代替经常使用的mapState,mapActions,mapMutations,mapGetters,固然,vuex中拆分的modules子store也有相应的useNamespacedXXX来替代。笔者最开始的时候还傻乎乎本身写了个useStats, useActions, useStore,而后坐地铁回家时忽然就看到了这个工具,简直是欣喜若狂啊,有兴趣的小伙伴还能够去看看源码,实现的很简洁清晰明了。

而后要作的固然是安装一下了 yarn add vuex-composition-helpers

babel.config.js稍做修改

module.exports = {
  presets: ["vca-jsx", "@vue/cli-plugin-babel/preset"]
};

复制代码

安装ElementUI 这就很少说了,官网打开,按教程安装并配置好 安装: yarn add element-ui 主题推荐建立一个scss文件:assets/style/_element-variables.scss,还能够很容易去覆盖一些主题色什么的。而后建立一个index.scss将这个文件引入,最后在main.ts中将scss文件引入就有了可配置的主题。

/* 改变 icon 字体路径变量,必需 */
$--font-path: '~element-ui/lib/theme-chalk/fonts';

@import "~element-ui/packages/theme-chalk/src/index";
复制代码
// main.ts
...
import ElementUI from 'element-ui';
import './assets/styles/index.scss'; // index.scss里包含element主题,也能够放一些reset的样式,公共样式或者其余

Vue.use(ElementUI, {
  size: 'small'
});
...
复制代码

基本工具安装好了,接下来就能够愉快的coding了。

defineComponent初体验

首先改写HelloWorld组件

<template>
  <div class="hello"> Hello world </div>
</template>

<script lang="ts"> import { defineComponent, getCurrentInstance } from "@vue/composition-api"; export default defineComponent({ name: "HelloWorld", props: { msg: String }, setup(props, ctx) { console.log(getCurrentInstance()); console.log(ctx); } }); </script>
复制代码

经过defineComponent进行组件的定义,setup函数有两个经常使用参数,第一个为props,第二个为setupContext, 这两个值跟vue3是同样的,能够经过getCurrentInstance获取当前组件实例,这个函数返回值为当前组件实例,打印出来后跟vue2的this内容是同样的,以前该有的参数都还在,只不过setup中没有this,只有ctx,这也够用了。有兴趣能够看看控制台都打印出了什么东西。

TSX体验

src/compnents/TestComp.tsx以tsx方式建立TestComp组件,注意属性comp会接收一个组件,咱们能够在props中规定类型为Object,但这并不够,咱们须要肯定comp详细的类型,那就能够在setup中从新规定一下props的类型。

import { defineComponent } from "@vue/composition-api";
import { VueConstructor } from "vue/types/umd";

type TestCompProps = {
  comp: VueConstructor<Vue>
}
export default defineComponent({
  name: "TestComp",
  props: {
    comp: {
      type: Object
    }
  },
  setup(props: TestCompProps) {
    const { comp: Comp } = props;
    return () => <Comp />;
  }
});
复制代码

或者直接规定在defineComponent的泛型参数中

import { defineComponent } from "@vue/composition-api";
import { VueConstructor } from "vue/types/umd";

type TestCompProps = {
  comp: VueConstructor<Vue>
}
export default defineComponent<TestCompProps>({
  name: "TestComp",
  props: {
    comp: {
      type: Object
    }
  },
  setup(props) {
    const { comp: Comp } = props;
    return () => <Comp />;
  }
});
复制代码

更多玩法请直接command + 点击或ctrl + 鼠标点击进入defineComponent声明文件进行探索。

注意咱们每次定义完一个组件后鼠标指上去看看是什么类型,通过观察实际上是VueConstructor<Vue>类型,这样在return 时候使用tsx的用法才不会报错。

经过属性传入组件 src/compnents/AA.vue建立AA.vue组件,写个普通的vue组件。

<template>
  <div class="hello"> AA Component </div>
</template>

<script lang="ts"> import { defineComponent } from "@vue/composition-api"; const HelloWorld = defineComponent({ name: "AA" }); export default HelloWorld; </script>

复制代码

在HelloWorld中引入AA组件与TestComp组件,而后将AA传递给TestComp

<template>
  <div class="hello"> Hello world <test-comp :comp="AA"></test-comp> </div>
</template>

<script lang="ts"> import { defineComponent, getCurrentInstance } from "@vue/composition-api"; import TestComp from "./TestComp"; import AA from "./AA.vue"; export default defineComponent({ name: "HelloWorld", props: { msg: String }, components: { TestComp, AA }, setup(props, ctx) { console.log(getCurrentInstance()); console.log(ctx); return { AA }; } }); </script>
复制代码

此时能够看到浏览器能够输出AA Component字样,说明成功。

tsx的另一种写法 再建立MM.tsx

const MM = () => {
  return () => <div>this is MM</div>
}

MM.name = 'MM';

export default MM;
复制代码

而后在HelloWord组件中引入它,一样的方法return出去(直接放在AA下面),而后将传递进TestComp组件的属性由AA替换为MM。保存,仍然OK。只不过此时代码不会报错可是Vetur插件会给咱们报个红色波浪线。因此我仍是推荐使用TestComp里的这种方式进行TSX组件定义。

其实这就是官方文档所说的setup返回一个函数的时候,这个函数会被当作render函数来使用,因此它就是vue2中的函数式组件了。

利用函数组件二次封装一个超级方便好用的表格组件

重要:渲染自定义table单元格组件的容器 TableCellRender.tsx

表格会传进来一个comp组件做为自定义的单元格,事先可能不知道啊这里要渲染什么,还会传进来scope数据

import { defineComponent } from "@vue/composition-api";
import { VueConstructor } from 'vue/types/umd';

type TableCellRenderProps = {
  scope: any;
  comp: VueConstructor<Vue>
}

export default defineComponent<TableCellRenderProps>({
  name: 'TableCellRender',
  props: {
    scope: {
      type: Object,
      required: true
    },
    comp: {
      type: Object,
      required: true
    }
  },
  setup(props) {
    const { comp: Comp } = props;
    console.log('props.scope', )
    return () => <Comp row={props.scope.row} />
  }
})

复制代码

TableBase.vue组件 通用组件,定义了四种单元格,一种为link类型的,一种为多选框,一种为自定义传进来的动态组件,最后一种为默认组件,外加一个翻页器,固然翻页器能够被隐藏。

<template>
  <div class="table-base"> <div class="table-container"> <el-table :size="size" v-loading="loading" :data="data" tooltip-effect="dark" style="width: 100%" @selection-change="handleSelectionChange" > <el-table-column v-if="multiple" type="selection" width="55" :selectable="checkSelectable" ></el-table-column> <template v-for="(column, index) in tableColumns"> <el-table-column v-if="column.comp" :key="index" :prop="column.key" :label="column.label" :width="column.width ? column.width : ''" :show-overflow-tooltip="!column.multipleline" > <template slot-scope="scope"> <table-cell-render :scope="scope" :comp="column.comp" ></table-cell-render> </template> </el-table-column> <el-table-column :key="index" v-else-if="column.active" :prop="column.key" :label="column.label" :width="column.width ? column.width : ''" :show-overflow-tooltip="!column.multipleline" > <template slot-scope="scope"> <span class="active-link" @click="() => handleClickActiveLink(scope.row)" >{{ scope.row[column.key] }}</span > </template> </el-table-column> <el-table-column v-else :key="index" :prop="column.key" :label="column.label" :width="column.width ? column.width : ''" :show-overflow-tooltip="!column.multipleline" ></el-table-column> </template> </el-table> </div> <div class="table-pagination" v-if="!noPagination"> <el-pagination class="pagination" background :layout="layout" :page-size="pageSize" @size-change="handleSizeChange" @current-change="handleCurrentChange" :current-page="currentPage" :total="total" ></el-pagination> </div> </div> </template>

<script lang="ts"> import { defineComponent } from "@vue/composition-api"; import TableCellRender from "./TableCellRender"; export default defineComponent({ name: "MTableBase", components: { TableCellRender }, props: { layout: { type: String, default: "total, prev, pager, next, jumper" }, size: { type: String, default: "small" }, loading: { type: Boolean, default: false }, multiple: { type: Boolean, default: true }, tableColumns: { type: Array, default: () => [] }, data: { type: Array, default: () => [] }, pageSize: { type: Number, default: 10 }, pageSizes: { type: Array, default: () => [] }, currentPage: { type: Number, default: 0 }, total: { type: Number, default: 0 }, noPagination: { type: Boolean, default: false } }, setup(props, ctx) { const { emit } = ctx; const handleSelectionChange = (val: any) => emit('selection-change', val); const handleSizeChange = (val: number) => emit('current-change', val); const handleCurrentChange = (val: number) => emit('current-change', val); const handleClickActiveLink = ($event: MouseEvent, row: any) => emit('get-row-info', $event, row); const checkSelectable = (row: any) => row.name !== 'None'; return { handleSelectionChange, handleSizeChange, handleCurrentChange, handleClickActiveLink, checkSelectable } } }); </script>

<style lang="scss" scoped> .table-pagination { padding-top: 20px; .pagination { text-align: center; } } .table-container /deep/ { .el-table { font-size: 14px; } } </style>

复制代码

表格组件的使用

将home页面改造为ts,并使用defineComponent定义组件。之后表格组件不再用动了,每次只须要给特定的列定义本身的渲染组件就能够进行渲染了。

<template>
  <div class="home"> <HelloWorld msg="Welcome to Your Vue.js App" /> <table-base :tableColumns="column" :data="data"></table-base> </div>
</template>

<script lang="tsx"> import { defineComponent } from "@vue/composition-api"; import HelloWorld from "@/components/HelloWorld.vue"; import TableBase from "@/components/TableBase.vue"; const helloCell = defineComponent({ name: "HelloCell", props: { row: { type: Object } }, setup(props: { row: { hello: string } }) { console.log("cell inner", props); const hello = props.row.hello; return () => <el-button type="primary" size="mini">{hello}</el-button>; } }); export default defineComponent({ name: "Home", components: { HelloWorld, TableBase }, setup() { const column = [ { label: "Hello", key: "hello", comp: helloCell }, { label: "World", key: "world" } ]; const data = [ { hello: "hi", world: "wd" }, { hello: "hello", world: "world" } ] return { column, data } } }); </script>
复制代码

效果: ====================分割线==================== 还没写完,后面还想写写vuex-composition-helpers的简单使用,可是如今凌晨3点了。明天上班,先到这里,明天继续 ====================分割线==================== (我又回来了,分割线暂时就不删除了,能够伪装本身很辛苦的样子)

问题: 目前看似能够了,可是眼尖的小伙伴确定会发现一些猫腻,在TableCellRender中定义的Comp属性规定类型为VueConstructor<Vue>,此时它没有定义props,因此row下面会有红色波浪线,这里暂时没理清怎么作,不过不会影响项目编译运行。若是有弄明白的能够下面留言解答一下。

hooks助力解耦公用逻辑与复杂逻辑拆分

vue3 / composition-api拥抱函数式编程,咱们使用新的技术也须要作开发方式的转换,若是vue3到时候仍是跟vue2如出一辙的写法和使用,那么还不如继续使用2呢。

hooks的使用场景

1. 拆分公用逻辑

用一个真实场景来吧,这几天咱们的系统有个小问题,dialog弹出框的每一个form表单都须要点开后自动聚焦在第一个input上,然而Element虽然提供了autofocus的属性,但它并不会自动明聚焦。这就须要手动维护ref,在mounted后,经过在nextTick中手动调用组件的focus()方法,只不过要改的组件不少,一个一个加太费力了。因此只能使用mixin,而后在每一个dialog的首个input添加refautofocus的属性。

export default {
  name: 'AutoFocusMixin',
  mounted() {
    this.$nextTick(() => {
      this.$refs.autofocus.focus();
    });
  }
};
复制代码

这样作的好处很明显,共享了代码逻辑,可是后人维护时候可能会很蒙蔽,看到ref="autofocus"可是直接在文件中搜索却不能找到哪里用了它,若是没注意到mixin,那么删除了这个属性可能还会觉得优化了代码,最后只会致使问题重现。

可是当vue有了hook,一切就不同了,咱们能够将这段逻辑提取出来

// useAutofocus.ts
import { ref, Ref, onMounted } from '@vue/composition-api';
import { Input } from 'element-ui';

export function useAutofocus() {
  const focusEl:Ref<null | HTMLInputElement | Input> = ref(null);

  onMounted(() => {
    setTimeout(() => {
      if (focusEl.value) {
        focusEl.value.focus();
      }
    }, 0)
  })

  return focusEl;
}

复制代码

在须要使用的组件中引入

about.vue

<template>
  <div class="about"> <el-input ref="focusEl" placeholder="请输入内容" v-model="inputValue"/> </div>
</template>

<script lang="ts"> import { defineComponent, ref } from "@vue/composition-api"; import { useAutofocus } from "@/hooks/useAutofocus"; export default defineComponent({ name: "About", setup() { const inputValue = ref(""); const focusEl = useAutofocus(); return { inputValue, focusEl }; } }); </script>

复制代码

autofocus生效,完美。这样比mixin的好处就很明显了,最起码咱们能够找到变量在哪里定义的,怎样使用的,避免维护上的模糊与困难。

此外还有一点,就是mixin有时候会写不少的逻辑,可是hooks你能够尽管往细了拆分,你最终须要谁就引入谁进去。

2. 拆分复杂逻辑

若是你的项目很是复杂,在一个页面中可能写上千行的代码,那么安小功能能够将你每一个功能代码拆分到hooks中,依赖的数据经过参数进行传递,固然,hooks也能够返回多种多样的数据类型,好比函数,能够用个hook来写你的点击或者其余操做的业务逻辑,最终返回一个函数,点击时调用它。

有些极端的小伙伴甚至能将全部的业务逻辑所有拆分到hooks中,组件中只会留下一堆建立变量,导出变量和引用变量的信息。

拆分逻辑后,有可能在别的地方也会使用这些hooks,就算用不到,这也会给维护带来更多的便利性。毕竟一些函数一会写在mounted中一会又要在updated中写,乱七八糟一种逻辑分散在各处,维护起来成本也是挺大的。

vue-composition-helpers的使用

这个工具是用来代替mapState,mapActions等函数的替代品。 vue3中好像也是提供了相似的hook。

以一个模拟的用户登陆功能为例 建立src/store/modules/user.ts文件

import { Module } from 'vuex';
// 模拟的登陆api
const fakeLogin = () => {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve({name: '张三' })
    },300)
  })
}

// 声明state
interface State {
  loginPending: boolean;
  userInfo: {name: string} | null
}

// 建立子store
const UserModel: Module<State, {}> = {
  namespaced: true,
  state: {
    loginPending: false,
    userInfo: null
  },
  mutations: {
    setLoginPending: (state, loginPending: boolean) => {
      state.loginPending = loginPending;
    },
    setUserInfo: (state, userInfo: {name: string} | null) => {
      state.userInfo = userInfo;
    }
  },
  actions: {
    loginAction: async ({ commit, state }): Promise<any> => {
      if (state.loginPending) {
        return;
      }
      try {
        commit('setLoginPending', true);
        const res = await fakeLogin();
        commit('setUserInfo', res);
        commit('setLoginPending', false);
      } catch (exp) {
        commit('setLoginPending', false);
        console.error('error: ', exp);
        throw exp;
      }
    },
    logoutAction: () => {
      console.log('this is logout');
    }
  }
};

export default UserModel;

复制代码

src/store/index.ts中引入

import Vue from 'vue';
import Vuex from 'vuex';
import user from './modules/user';

Vue.use(Vuex);

const store = new Vuex.Store({
  state: {},
  mutations: {},
  actions: {},
  modules: {
    user
  }
});

export default store;
复制代码

以上就是vuex的基本使用了。 咱们能够经过建立一个useUserStore的hook,进一步使得咱们的代码更通用。下面举例使用了useNamespacedState, useNamespacedActions两个api,其他的给为能够查查文档,使用方式跟useState,useActions等如出一辙。

import { useNamespacedState, useNamespacedActions } from "vuex-composition-helpers";
import { Ref } from '@vue/composition-api';

export function useUserStore() {
  const {
    loginPending,
    userInfo
  } : {
    loginPending: Ref<boolean>;
    userInfo: Ref<{name: string} | null>;
  } = useNamespacedState("user", ["loginPending", "userInfo"]);

  const { loginAction } = useNamespacedActions("user", ["loginAction"])

  return {
    state: {
      loginPending,
      userInfo
    },
    actions: {
      loginAction
    }
  }
}
复制代码

为何不直接在具体的组件中使用上面函数内部的逻辑?若是是那样使用的话每次要用store中的内容都要重复一遍相同的操做,因此有重复逻辑,咱们就用hooks.

在about组件中使用咱们建立的useUserStore hook

<template>
  <div class="about" v-loading="loginPending"> <el-input ref="focusEl" placeholder="请输入内容" v-model="inputValue" /> <el-button @click="loginAction">登陆</el-button> <div>{{ JSON.stringify(userInfo) }}</div> </div>
</template>

<script lang="ts"> import { defineComponent, ref } from "@vue/composition-api"; import { useAutofocus } from "@/hooks/useAutofocus"; import { useUserStore } from "@/hooks/useUserStore"; export default defineComponent({ name: "About", setup() { const inputValue = ref(""); const focusEl = useAutofocus(); const { state: { loginPending, userInfo }, actions: { loginAction } } = useUserStore(); return { inputValue, focusEl, loginPending, userInfo, loginAction }; } }); </script>
复制代码

效果:

封装为hooks以后,若是想在其余地方使用,直接调用hook函数,十分方便。

关于响应式api

其实最近也看了很多同窗分享了本身的vue3相关的尝鲜文章,都是主要介绍响应式api的,但这里只会带一下。

响应式api,钩子函数等均可以在官网文档中找到,介绍的又全面又详细。 这里简单说一下使用:

  1. const a = ref(true)的使用:在模板中能够直接使用a这个值,在代码中对a更新则须要使用a.value = newvalue。ref通常用于普通类型的值,或者数组。其实若是ref中的值为数组或对象,最终在实现上都会转换为reactive。
  2. const aa = reactive({name: 'haha'}),reactive只能对数组或对象使用,无论是在更新仍是使用时候都直接对其进行操做便可,没有向ref同样的.value;

这两个是最经常使用的,其余的若是你有什么疑问,官网是最好的解决疑问之处。

总结

此次分享了一些vue-composition-api结合ts的使,须要注意的是要转换思惟,从配置式转为函数式,必定要思考以前的代码在新的框架应该怎么写。个人探索基本就是上面这种写法,或许你们会探索到更好的使用方式,欢迎到时候@艾特一下我,让我跟着学习一下。另外推荐拉勾教育黄轶黄老师的vue3源码解析,这里不放连接,不放推广码,凭心推荐。你可能会收获更多。

相关文章
相关标签/搜索