Vue2.x内使用tsx及相关组件库总结

1,385 阅读3分钟

其他说明

  • shims-vue.d.ts 主要用于让 ts 识别 .vue 文件
  • shims-tsx.d.ts 在 ts 中可以使用 jsx 语法
// 解决自定义 this.$xxx 不识别问题
declare module 'vue/types/vue' {
  type Abc = () => void;
  interface Vue {
    $abc: Abc;
  }
}
  • global.d.ts 新建来定义一些不识别的模块。如:.scss|css|png
    • declare module '*.scss';
  • 如果使用了@vue/cli-plugin-babel就不用再安装jsx相关依赖了
    • 它内部使用了babel-preset-app库,而这个库已经依赖了jsx相关的包

vue-property-decorator/vue-class-component

  • 官网文档:链接 链接
  • @Component({})可配置所有的vue.options内容
    • 使用tsx语法不用手动配置components内容,直接引入即可
import { Vue, Component, Prop, Emit, Ref, Provide, Inject, Watch } from 'vue-property-decorator';
// 在 .tsx 内不要在 @Component({ components: {} }) 配置了
import { Button } from 'vant';
// cssModule 方式
import styles from './TodoItem.module.scss';

@Component
export class TodoItem extends Vue {
  // props 注意使用 ! 修饰符,表示一定有数据
  @Prop()
  index!: number;
  @Prop({ type: Object, default: () => ({}) })
  data!: ITodoItem;
  
  // data
  count: number = 0;
  
  // watch
  @Watch('person', { immediate: true, deep: true })
  onPersonChanged(val: Person, oldVal: Person) {}
  
  // ref
  @Ref()
  readonly refCmp!: RefCmp;
  // 直接使用 this.refCmp 就可以了。好处是可以进行类型提示,获取 RefCmp 暴露的方法
  // 或者不用@Ref装饰器 (this.$refs.refCmp as RefCmp).xxx 也能获取类型提示

  // provide/inject
  @Provide()
  delItem(id: number) {}
  @Inject()
  readonly delItem!: (id: number) => void;
  
  // methods
  private add(): void {
    this.count += 1;
  }

  // computed
  public get total(): number {
    return this.count + 1;
  }
  public set total(v: number) {
    this.count = v;
  }

  private render() {
    {/* ref */}
    return <RefCmp ref="refCmp"></RefCmp>;
  }
}

vuex-module-decorators/vuex-class

  • 创建store时 vuex-module-decorators(dev-save) 链接
    • 推荐配合getModule使用动态模块
  • 使用时 vuex-class 链接
// 方式一: 只使用 vuex-module-decorators 库,按动态注册的方式
import { Module, Mutation, VuexModule, getModule } from 'vuex-module-decorators';

export interface ITodoState {
  list: ITodoItem[];
  listState: Mode;
}

@Module({ dynamic: true, name: 'todo', store })
export class Todo extends VuexModule implements ITodoState {
  // state
  list: ITodoItem[] = [];
  // getter
  public get listByState(): ITodoItem[] {
    return this.list;
  }
  // mutations 注意:传参也只能有一个,因为内部调用的 mutationFunction.call(state, payload)
  @Mutation
  add(payload: ITodoItem): void {
    // this.xxx = payload
  }
  // action
  @Action
  async fetchNewWheels(wheelStore: string) {
    const wheels = await get(wheelStore)
    this.context.commit('addWheel', wheels)
  }
}
// 供外部使用
// 使用这样的方式就可以从 TodoModule.xxx 上获取内,并且可以提供类型提示
export const TodoModule = getModule(Todo)

// 方式二:使用 vuex-module-decorators 创建,使用时使用 vuex-class(感觉好麻烦,都需要重复定义)
export default class TodoList extends Vue {
  // 是需要调用的类里定义
  @Getter('listByState', { namespace: 'todo' })
  readonly todoList!: ITodoItem[];

  @Mutation('changeListState', { namespace: 'todo' })
  private mutationsChangeListState!: (state: null | 0 | 1) => void;
}

jsx 语法

  • JSX 的本质就是 JavaScript,所以它具有 JavaScript 的完全编程能力
  • 在jsx语法内一切皆是prop统一了使用规则。编译器最终会把props转为不同的内容
  • 基础用法:不在使用v-bind|v-on语法,变量、事件等绑定使用xXx={this.xxx}
  • 内置指令: 只支持v-show|v-model
    • v-if|v-for使用JS逻辑去实现
    • v-html使用<div domPropsInnerHTML={xxx}>实现
{/* v-model 并设置 trim|number 修饰符用“_”连接 */}
<input type="text" vModel_trim_number={this.value} />

{/* v-model 手动实现 */}
<input type="text" onInput={this.onManualInput} />
private onManualInput(e: InputEvent) {
  // as 强制定义
  const value = (e.target as HTMLInputElement).value;
  this.inputValue = value;
}

{/* v-show 使用 */}
<p vShow={this.isShow}>is show</p>

{/* v-for 及 key */}
const todoItem = this.list.map((item, index) => {
  return <TodoItem key={item.id} index={index} data={item} />;
});
<div class="todo-list__scroll">{todoItem}</div>
  • 事件绑定:使用onEventName={this.methodName}方式,自己去处理修饰符的功能(建议使用)
    • 也可使用vOn:eventName_modifier_modifier={this.methodName}方式,可配置vue默认的修饰符
    • 原生事件nativeOnXXX={this.xxx}绑定在组件的根标签上
{/* 默认绑定:第一个参数默认是 event */}
<a href="" onClick={this.clickADom}></a>
// 事件处理方式
private clickADom(e: PointerEvent) {
  // 阻止默认事件
  e.preventDefault();
  // 阻止事件冒泡
  e.stopPropagation();
}

{/* 箭头函数:可手动调整 event 位置,建议放第一个 */}
// 会有一定的性能消耗。如果这个函数当作 prop 传给子组件会造成重新渲染问题
<div onClick={(e: PointerEvent) => this.clickTarget(e, 'abc')}></div>
private clickTarget(e: PointerEvent, str: string) {}

{/* 显示绑定:默认最后一个是 event */}
<div class="c2" onClick={this.onClickBind.bind(this, 'abc', 333)}></div>
private onClickBind(str: string, num: number, e: PointerEvent) {}

{/* emit 组件内部 this.$emit('callback', params) */}
<ChildCmp onCallback={this.callback} />
  • 自定义 directive
    • 修饰符也是通过_连接传递vPermission_a_b
// premission.ts
export const permission: DirectiveOptions = {
  inserted(el, binding) {
    console.log(el, binding);
  }
};

// AllProps.tsx
// 非全局指令需要先注册
@Component({
  directives: {
    permission
  }
})
export class AllProps extends Vue {
  private render() {
    return (
      <ChildCmp vPermission_a_b="xxx" />
    )
  }
}
  • $arrts|$listeners 绑定
    • 一切皆props:在JSX内标签上所有 onXxx 都会进入 listeners内,其他非语法内置props或组件内未设置@Prop接受的都会进入listeners 内,其他非语法内置 props 或组件内未设置`@Prop`接受的都会进入 attrs
{/* 方式一:需要告诉解析器,处理的是 attrs */}
const cusAttrs = { staticParam: 'abc', reactiveParam: this.isShow };
<CusCmp {...{ attrs: cusAttrs }} />

{/* 方式二(推荐):数据合并方式 */}
const cusAttrs2 = { ref: 'input', class: ['a', 'b'], attrs: {/..传递的属性../}, on: {/..传递的事件../} };
<CusCmp {...cusAttrs2} />
// 组件内部使用
<p vShow={this.$attrs.isShow}>c 1</p>
<p onClick={this.$listeners.testFn}>count++</p>
  • 插槽slot
    • 注意:作用域插槽的使用方式
// 通过函数方式定义 scopedSlots.scopedSlotName: () => {}
const cusScopedSlots = {
  footer: (props) => <p>this is scoped slot {props.value}</p>;
};

{/* 作用域插槽:要在组件根上配置 */}
<SlotCmp scopedSlots={cusScopedSlots}>
  {/* 默认插槽 */}
  <p>this is default slot</p>
  {/* 命名插槽 */}
  <div slot="header">this is named slot</div>
</SlotCmp>

{/* 组件内部使用 */}
<div class="slot-cmp-inner">
  <p>slot cmp</p>
  {/* 默认 this.$slots.default */}
  <div class="slot-default">{this.$slots.default}</div>
  {/* 命名 this.$slots.name */}
  <div class="slot-named">{this.$slots.slotName ? this.$slots.slotName : null}</div>
  {/* 作用域 this.$scopedSlots.scopedSlotsName( props ) */}
  <div class="slots-scoped">{this.$scopedSlots.slotName ? this.$scopedSlots.slotName(this.itemData) : null}</div>
</div>

cssModule

  • 编译的结果是把className按对象形式传递出来{ todo-item: "TodoItem-module_todo-item_H_L6b" },本质内容是包含嵌套关系的
    • 可通过在vue.config.js内配置css选项修改localIdentName: '[path][name]__[local]--[hash:base64:5]'
  • :local/:global为作用域选择器
    • :global .className就不在本模块内出现,变为全局样式(既命名规则不在按照localIdentName
  • 注意: 不支持vue的深度样式选择器,因为不在.vue内编写

image.png

部分装饰器实现逻辑

  • vuex-class 的@State装饰器逻辑
    • 主要逻辑是把@State('list', { namespace: 'todo' }) readonly todoList!
    • 转为为computed: { todoList: mapState({ todoList: 'list' })['todoList'] }
      • mapState({ todoList: 'list' })返回的是个对象
export var State = createBindingHelper('computed', mapState);

function createBindingHelper(bindTo, mapFn) {
  // bindTo -> computed, mapFn -> mapState
  function makeDecorator(map, namespace) {
    // 装饰器需要是个可执行函数
    //  createDecorator(factory) 返回一个 (target, key, index) => {}
    // target 是 vueComponetInstance,key 是类里绑定的名称 todoList( @State('list') readonly todoList )
    // factory ->  Ctor.__decorators__.push(options => factory(options, key, index));
    return createDecorator(function (componentOptions, key) {
      if (!componentOptions[bindTo]) {
        componentOptions[bindTo] = {};
      }
      // 最后 mapObject = _a
      // mapObject = { todoList: 'list' }
      // mapState({ todoList: 'list' }) | mapSate(namespace, { todoList: 'list' })
      // mapState({ todoList: 'list' })['todoList'] 在获取出来
      // 最终 computed: { todoList: mapState({ todoList: 'list' })['todoList'] }
      var mapObject = (_a = {}, _a[key] = map, _a);
      componentOptions[bindTo][key] = namespace !== undefined
        ? mapFn(namespace, mapObject)[key]
      : mapFn(mapObject)[key];
      var _a;
    });
  }
  // @State('list', { namespace: 'todo' })
  function helper(a, b) {
    if (typeof b === 'string') {
      var key = b;
      var proto = a;
      return makeDecorator(key, undefined)(proto, key);
    }
    var namespace = extractNamespace(b);
    var type = a;
    // makeDecorator('list', 'todo')
    return makeDecorator(type, namespace);
  }
  return helper;
}
  • vuex-module-decorators 的 @Mutation逻辑
    • 把 mutations 关联到module.mutations[key]=Fn
export function Mutation<T extends Object, R>(
  target: T,
  key: string | symbol,
  descriptor: TypedPropertyDescriptor<(...args: any[]) => R>
) {
  const module = target.constructor as Mod<T, any>
  if (!module.hasOwnProperty('mutations')) {
    module.mutations = Object.assign({}, module.mutations)
  }
  const mutationFunction: Function = descriptor.value!
  const mutation: Mut<typeof target> = function (state: typeof target, payload: Payload) {
    mutationFunction.call(state, payload)
  }
  module.mutations![key as string] = mutation
}