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源碼解析,這裏不放連接,不放推廣碼,憑心推薦。你可能會收穫更多。

相關文章
相關標籤/搜索