很久没输出了,今天来输出一把,缓解一下一我的的孤独。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
vue create athena
建立一个项目(起名雅典娜),雅典娜女神比较著名,以此祝我早日找到本身的女神吧。Manually select features
回车,咱们须要自定义配置,不使用默认配置。Use class-style component syntax? (Y/n)
是否使用class-style
语法,固然选择N
啊,咱们会彻底使用composition-api,不会借助class来作,而且我我的不是很喜欢使用装饰器跟类这一套方案,若是有喜欢的,应该有好些资料介绍的,这里不选它。Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)?
,默认是就能够。Use history mode for router?
,看本身项目需求吧,不想额外配置nginx可使用hash路由,这里我就默认了。Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default):
, 这里我选择第二个node-sass,由于我用sass比较多,有感情,dart-sass尝试过,深度选择器支持得不友好,因此不用。ESLint + Prettier
,固然若是你有特殊须要选本身喜欢的就好了。 Pick additional lint features:
, 两个选项保存时跟commit都选上就能够了。 Where do you prefer placing config for Babel, ESLint, etc.?
,固然是单独的文件夹呀,都放在package.json里咋维护啊。以上选择就是我一般的配置,各位能够根据需求自行选择。nginx
安装composition-api yarn add @vue/composition-api
vuex
在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,这也够用了。有兴趣能够看看控制台都打印出了什么东西。
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下面会有红色波浪线,这里暂时没理清怎么作,不过不会影响项目编译运行。若是有弄明白的能够下面留言解答一下。
vue3 / composition-api拥抱函数式编程,咱们使用新的技术也须要作开发方式的转换,若是vue3到时候仍是跟vue2如出一辙的写法和使用,那么还不如继续使用2呢。
hooks的使用场景
1. 拆分公用逻辑
用一个真实场景来吧,这几天咱们的系统有个小问题,dialog弹出框的每一个form表单都须要点开后自动聚焦在第一个input上,然而Element虽然提供了autofocus
的属性,但它并不会自动明聚焦。这就须要手动维护ref
,在mounted后,经过在nextTick
中手动调用组件的focus()
方法,只不过要改的组件不少,一个一个加太费力了。因此只能使用mixin
,而后在每一个dialog的首个input添加ref
为autofocus
的属性。
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
中写,乱七八糟一种逻辑分散在各处,维护起来成本也是挺大的。
这个工具是用来代替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函数,十分方便。
其实最近也看了很多同窗分享了本身的vue3相关的尝鲜文章,都是主要介绍响应式api的,但这里只会带一下。
响应式api,钩子函数等均可以在官网文档中找到,介绍的又全面又详细。 这里简单说一下使用:
const a = ref(true)
的使用:在模板中能够直接使用a这个值,在代码中对a更新则须要使用a.value = newvalue。ref通常用于普通类型的值,或者数组。其实若是ref中的值为数组或对象,最终在实现上都会转换为reactive。const aa = reactive({name: 'haha'})
,reactive只能对数组或对象使用,无论是在更新仍是使用时候都直接对其进行操做便可,没有向ref同样的.value
;这两个是最经常使用的,其余的若是你有什么疑问,官网是最好的解决疑问之处。
此次分享了一些vue-composition-api结合ts的使,须要注意的是要转换思惟,从配置式转为函数式,必定要思考以前的代码在新的框架应该怎么写。个人探索基本就是上面这种写法,或许你们会探索到更好的使用方式,欢迎到时候@艾特一下我
,让我跟着学习一下。另外推荐拉勾教育黄轶黄老师的vue3源码解析,这里不放连接,不放推广码,凭心推荐。你可能会收获更多。