1. 程式人生 > >Tsx寫一個通用的button元件

Tsx寫一個通用的button元件

一年又要到年底了,vue3.0都已經出來了,我們也不能一直還停留在過去的js中,是時候學習並且在專案中使用一下Ts了。

  如果說jsx是基於js的話,那麼tsx就是基於typescript的

  廢話也不多說,讓我們開始寫一個Tsx形式的button元件,

  ts真的不僅僅只有我們常常熟知的資料型別,還包括介面,類,列舉,泛型,等等等,這些都是特別重要的

  專案是基於vue-cli 3.0 下開發的,可以自己配置Ts,不會的話那你真的太難了

  

 

     我們再compenonts中新建一個button資料夾,再建一個unit資料夾,button放button元件的程式碼,unit,放一些公共使用模組

  我們再button資料夾下建立 ,index .tsx放的button原始碼,index.less放的是樣式,css也是不可缺少的

       

 

   分析一下button需要的一些東西

  第一個當然是props,還有一個是點選事件,所以我們第一步就定義一下這兩個型別

type ButtonProps = {
  tag: string,
  size: ButtonSize,
  type: ButtonType,
  text: String
}

type ButtonEvents = {
  onClick?(event: Event) :void
}
type ButtonSize = 'large' | 'normal' | 'small' | 'mini'
type ButtonType = 'default' | 'primary' | 'info' | 'warning' | 'danger'

  因為button是很簡單的元件,內部也沒有一些特別的狀態需要改變,所以我們用函式式元件的方式去寫(之後的render會用到這個方法)

function Button (h: CreateElement, props: ButtonProps, slots: DefaultSlots, ctx: RenderContext<ButtonProps>) {
  const { tag, size, type } = props
  let text
  console.log(slots)
  text = slots.default ? slots.default() : props.text
  function onClick (event: Event) {
    emit(ctx, 'click', event)
  }
  let classes = [size,type]
  return (
    <tag
      onClick = {onClick}
      class = {classes}
    >
      {text}
    </tag>
  )
}

  h 是一個預留引數,這裡並沒有用到 ,CreateElement  這個是vue從2.5之後提供的一個型別,也是為了方便在vue專案上使用ts

  props 就是button元件的傳入的屬性,slots插槽,ctx,代表的是當前的元件,可以理解為當前rendercontext執行環境this

  DefaultSlots是我們自定義的一個插槽型別

export type ScopedSlot<Props = any> = (props?: Props) => VNode[] | VNode | undefined;

export type ScopedSlots = {
  [key: string]: ScopedSlot | undefined;
}

  插槽的內容我們都是需要從ctx中讀取的,預設插槽的key就是defalut,具名插槽就是具體的name

  button放發內部還有一個具體的點選事件,還有一個emit方法,從名字我們也可以看的出,他是粗發自定義事件的,我們這裡當然不能使用this.emit去促發,

  所以我們需要單獨這個emit方法,我們知道元件內所以的自定義事件都是儲存在listeners裡的,我們從ctx中拿取到所以的listeners



  import { RenderContext, VNodeData } from 'vue' // 從vue中引入一些型別

function emit (context: RenderContext, eventName: string, ...args: any[]) { const listeners = context.listeners[eventName] if (listeners) { if (Array.isArray(listeners)) { listeners.forEach(listener => { listener(...args) }) } else { listeners(...args) } }

  這樣我們元件內部的事件觸發就完成了

  我們的button肯定是有一些預設的屬性,所以,我們給button加上預設的屬性

Button.props = {
  text: String,
  tag: {
    type: String,
    default: 'button'
  },
  type: {
    type: String,
    default: 'default'
  },
  size: {
    type: String,
    default: 'normal'
  }
}

  我們定義一個通用的functioncomponent 型別

type FunctionComponent<Props=DefaultProps, PropsDefs = PropsDefinition<Props>> = {
  (h: CreateElement, Props:Props, slots: ScopedSlots, context: RenderContext<Props>): VNode |undefined,
  props?: PropsDefs
}

  PropsDefinition<T>  這個是vue內部提供的,對 props的約束定義

  不管怎麼樣我們最終返回的肯定是一個物件,我們把這個型別也定義一下

  ComponentOptions<Vue> 這個也是vue內部提供的

 interface DrmsComponentOptions extends ComponentOptions<Vue> {
  functional?: boolean;
  install?: (Vue: VueConstructor) => void;
}

  最終生成一個元件物件

function transformFunctionComponent (fn:FunctionComponent): DrmsComponentOptions {
  return {
    functional: true, // 函式時元件,這個屬性一定要是ture,要不render方法,第二個context永遠為underfine
    props: fn.props,
    model: fn.model,
    render: (h, context): any => fn(h, context.props, unifySlots(context), context)
  }
}

  unifySlots 是讀取插槽的內容

// 處理插槽的內容
export function unifySlots (context: RenderContext) {
  // use data.scopedSlots in lower Vue version
  const scopedSlots = context.scopedSlots || context.data.scopedSlots || {}
  const slots = context.slots()

  Object.keys(slots).forEach(key => {
    if (!scopedSlots[key]) {
      scopedSlots[key] = () => slots[key]
    }
  })

  return scopedSlots
}

  當然身為一個元件,我們肯定是要提供全域性注入介面,並且能夠按需匯入

  所以我們給元件加上名稱和install方法,install 是 vue.use() 方法使用的,這樣我們能全部註冊元件

export function CreateComponent (name:string) {
  return function <Props = DefaultProps, Events = {}, Slots = {}> (
    sfc:DrmsComponentOptions | FunctionComponent) {
    if (typeof sfc === 'function') {
      sfc = transformFunctionComponent(sfc)
    }
    sfc.functional = true
    sfc.name = 'drms-' + name
    sfc.install = install
    return sfc 
  }
}

  index.tsx 中的最後一步,匯出這個元件

export default CreateComponent('button')<ButtonProps, ButtonEvents>(Button)

  還少一個install的具體實現方法,加上install方法,就能全域性的按需匯入了

function install (this:ComponentOptions<Vue>, Vue:VueConstructor) {
  const { name } = this
  Vue.component(name as string, this)
}

 

   最終實現的效果圖,事件的話也是完全ok的,這個我也是測過的

 

   程式碼參考的是vant的原始碼:https://github.com/youzan/vant

  該程式碼已經傳到git:   https://github.com/czklove/DrpsUI  dev分支應該是程式碼全的,master可能有些並沒有合併