很久沒輸出了,今天來輸出一把,緩解一下一我的的孤獨。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源碼解析,這裏不放連接,不放推廣碼,憑心推薦。你可能會收穫更多。