杂谈 - panchaow/blog GitHub Wiki

覆盖 emotion 样式

在使用 emotion 作为组件库的 CSS 解决方案时,可能会遇到一个问题。emotion 生成的 classname 的形式是css-[hash],比如css-2nrfbr。如果指定了 label,生成的则是css-[hash]-[label],如css-jlndvo-Logo。如果是用于写 app 的样式,这个特性能尽量避免全局 classname 重复,是非常有用的。但是如果是用于组件库的样式,这样的 classname 会导致用户难以通过 class selector 选中元素进行样式覆盖。Material UI 库是通过增加额外的 classname 来实现允许用户使用 CSS 进行覆盖样式的功能的。

如图所示,Material UI 为允许用户自定义的元素上增加了一个冗余的 classname。

TypeScript 的 Generic 问题

假如有如下函数:

const prop =
  <O, K extends keyof O>(key: K) =>
  (o: O) =>
    o[key];

这里的 Generic 实际上是不正确的。比如prop("name")中,由于此时不能确定O,于是K只能被判断为never。那么,"name"作为一个string自然无法赋值给never。所以,TypeScript 会报错。可惜的是,这样的函数参数顺序在函数式编程是非常常见的。目前还没有得到解决的办法。

如何判断是否是 Class 对象

虽然本质上 Class 确实是一个函数,但是 Class 和普通的函数还是有一个明显的区别:Class 只能通过new来调用。那么,如何判断一个函数是不是 Class 呢?首先,利用Object.prototype.toString,我们可以排除一些函数类型:

Object.prototype.toString.call(class {}); // [object Function]
Object.prototype.toString.call(function () {}); // [object Function]
Object.prototype.toString.call(function* () {}); // [object GeneratorFunction]
Object.prototype.toString.call(async function () {}); // [object AsyncFunction]
Object.prototype.toString.call(() => {}); // [object Function]

可以看到,有三种类型函数,Object.prototype.toString都返回了"[object Function]"。可惜的是,目前没有很好的办法可以区别这三种类型。但有一个比较 dirty 的方法可以勉强完成任务:

// non-strict

Object.getOwnPropertyNames(function () {}); // [ 'length', 'name', 'arguments', 'caller', 'prototype' ]

Object.getOwnPropertyNames(class {}); // [ 'length', 'prototype', 'name' ]

Object.getOwnPropertyNames(() => {}); // [ 'length', 'name' ]

观察三条语句输出的内容,可以发现不包含 name 为prototype的属性的函数是箭头函数,剩下的不包含 name 为arguments/caller的属性的函数是 Class。只是箭头函数完全可能被定义一个 name 为prototype的属性,Class 对象也有可能被定义一个 name 为arguments或者caller的属性,而且在 strict 模式下,function() {}本身也不再包含 name 为arguments/caller的属性,因此以上的判断非常不可靠。

自定义组件样式

在使用 css preprosessor,比如 Less、Sass 作为组件库的 CSS 解决方案时,往往会使用到这些 preprosessor 的 variable 功能。这样一方面可以使代码更加 DRY,一方面也方便了用户自定义组件的样式。在使用 CSS-in-JS,比如 Styled components、Emotion 作为样式解决方案时,可以通过 Theme 实现类似的效果。

获取 React 组件的 Type

export type PropsOf<
  C extends keyof JSX.IntrinsicElements | React.JSXElementConstructor<any>
> = JSX.LibraryManagedAttributes<C, React.ComponentProps<C>>;

DistributiveOmit

经常可以看到定义如下的DistributiveOmit

type DistributiveOmit<T, K extends keyof T> = T extends unknown
  ? Omit<T, K>
  : never;

因为T extends unknown这个条件一定是满足的,所以这段代码会让人第一眼看着感觉有点奇怪。其实,这个是利用了 TypeScript 的 Conditional Types 的distributive特性:

When conditional types act on a generic type, they become distributive when given a union type.

官方文档中的 Example:

type ToArray<Type> = Type extends unkown ? Type[] : never;

type StrArrOrNumArr = ToArray<string | number>; // string[] | number[]

Higher-Order Components with TypeScript

最为基础的一个例子:

import React, { forwardRef } from "react";
import hoistStatics from "hoist-non-react-statics";

interface Theme {
  color: string;
}

interface InjectedProps {
  theme?: Theme;
}

export default function withTheme(theme: Theme) {
  return function <T extends InjectedProps = InjectedProps>(
    WrappedComponent: React.ComponentType<T>
  ) {
    // Try to create a nice displayName for React Dev Tools.
    const displayName =
      WrappedComponent.displayName || WrappedComponent.name || "Component";

    // Creating the inner component. The calculated Props type here is the where the magic happens.
    const ComponentWithTheme = forwardRef(
      (props: Omit<T, keyof InjectedProps>, ref) => {
        // Fetch the props you want to inject. This could be done with context instead.
        const injectedProps = {
          theme,
        };

        // props comes afterwards so the can override the default ones.
        return (
          <WrappedComponent {...injectedProps} {...(props as T)} ref={ref} />
        );
      }
    );

    ComponentWithTheme.displayName = `withTheme(${displayName})`;

    return hoistStatics(ComponentWithTheme, WrappedComponent);
  };
}

Vue 有而 React 没有的 Lifecycle —— deactived/active

Vue 和 React 都有 lifecycle 的概念,只是它们采用的术语可能有区别。在 Vue 的官方文档中,这个概念被称为 Lifecycle Hooks,React 社区内则更多使用 Lifecyecle Methods。除了名字的差异,相比起 React,Vue 提供的 Lifecycles 也更加丰富。其中的activedeactived可能对于只使用 React 的开发者来说就不太熟悉了。它们是用来配合keep-alive组件使用的。简单来说,被keep-alive组件包裹的组件实例被卸载时,会被“缓存”起来。vue 在需要重新加载那个组件的实例时,复用被缓存的实例。deactivedactive也就在被缓存和被复用时候触发。keep-alive常被用于根据条件在组件实例之间切换的情景。而对于 React 来说,这样的组件就很实现。因为 React 支持函数式组件,并且函数式组件的使用也越来越成为主流。所谓函数式组件只是一个返回ReactElement的普通函数。对其而言,不存在实例,甚至没有 lifecycle 的概念。React 团队也没有打算提供类似功能的想法。但是其实,这个功能可能还挺有用的,缓存卸载的组件除了带来运行效率上的收益,还可以保证实例的State不会丢失。如果要在React中实现类似的功能可能就要借助另外的数据存储的方式,比如redux了。总的来说,Vue 给我的印象是非常 Practical,对于开发者非常友好,可能这也是 Vue 在国内大范围流行的一个原因。

⚠️ **GitHub.com Fallback** ⚠️