胖蔡说技术
随便扯扯

TypeScript 中文使用手册 高级类型(一)

amiko阅读(257)

交叉类型(Intersection Types)

交叉类型是将多个类型合并为一个类型。 这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。 例如,Person & Serializable & Loggable同时是PersonSerializableLoggable。 就是说这个类型的对象同时拥有了这三种类型的成员。

我们大多是在混入(mixins)或其它不适合典型面向对象模型的地方看到交叉类型的使用。 (在JavaScript里发生这种情况的场合很多!) 下面是如何创建混入的一个简单例子(“target”: “es5”):

function extend<First, Second>(first: First, second: Second): First & Second {
    const result: Partial<First & Second> = {};
    for (const prop in first) {
        if (first.hasOwnProperty(prop)) {
            (result as First)[prop] = first[prop];
        }
    }
    for (const prop in second) {
        if (second.hasOwnProperty(prop)) {
            (result as Second)[prop] = second[prop];
        }
    }
    return result as First & Second;
}

class Person {
    constructor(public name: string) { }
}

interface Loggable {
    log(name: string): void;
}

class ConsoleLogger implements Loggable {
    log(name) {
        console.log(`Hello, I'm ${name}.`);
    }
}

const jim = extend(new Person('Jim'), ConsoleLogger.prototype);
jim.log(jim.name);

联合类型(Union Types)

联合类型与交叉类型很有关联,但是使用上却完全不同。 偶尔你会遇到这种情况,一个代码库希望传入numberstring类型的参数。 例如下面的函数:

/**
 * Takes a string and adds "padding" to the left.
 * If 'padding' is a string, then 'padding' is appended to the left side.
 * If 'padding' is a number, then that number of spaces is added to the left side.
 */
function padLeft(value: string, padding: any) {
    if (typeof padding === "number") {
        return Array(padding + 1).join(" ") + value;
    }
    if (typeof padding === "string") {
        return padding + value;
    }
    throw new Error(`Expected string or number, got '${padding}'.`);
}

padLeft("Hello world", 4); // returns "    Hello world"

padLeft存在一个问题,padding参数的类型指定成了any。 这就是说我们可以传入一个既不是number也不是string类型的参数,但是TypeScript却不报错。

let indentedString = padLeft("Hello world", true); // 编译阶段通过,运行时报错

在传统的面向对象语言里,我们可能会将这两种类型抽象成有层级的类型。 这么做显然是非常清晰的,但同时也存在了过度设计。 padLeft原始版本的好处之一是允许我们传入原始类型。 这样做的话使用起来既简单又方便。 如果我们就是想使用已经存在的函数的话,这种新的方式就不适用了。

代替any, 我们可以使用_联合类型_做为padding的参数:

/**
 * Takes a string and adds "padding" to the left.
 * If 'padding' is a string, then 'padding' is appended to the left side.
 * If 'padding' is a number, then that number of spaces is added to the left side.
 */
function padLeft(value: string, padding: string | number) {
    // ...
}

let indentedString = padLeft("Hello world", true); // errors during compilation

联合类型表示一个值可以是几种类型之一。 我们用竖线(|)分隔每个类型,所以number | string | boolean表示一个值可以是numberstring,或boolean

如果一个值是联合类型,我们只能访问此联合类型的所有类型里共有的成员。

interface Bird {
    fly();
    layEggs();
}

interface Fish {
    swim();
    layEggs();
}

function getSmallPet(): Fish | Bird {
    // ...
}

let pet = getSmallPet();
pet.layEggs(); // okay
pet.swim();    // errors

这里的联合类型可能有点复杂,但是你很容易就习惯了。 如果一个值的类型是A | B,我们能够_确定_的是它包含了AB中共有的成员。 这个例子里,Bird具有一个fly成员。 我们不能确定一个Bird | Fish类型的变量是否有fly方法。 如果变量在运行时是Fish类型,那么调用pet.fly()就出错了。

类型守卫与类型区分(Type Guards and Differentiating Types)

联合类型适合于那些值可以为不同类型的情况。 但当我们想确切地了解是否为Fish时怎么办? JavaScript里常用来区分2个可能值的方法是检查成员是否存在。 如之前提及的,我们只能访问联合类型中共同拥有的成员。

let pet = getSmallPet();

// 每一个成员访问都会报错
if (pet.swim) {
    pet.swim();
}
else if (pet.fly) {
    pet.fly();
}

为了让这段代码工作,我们要使用类型断言:

let pet = getSmallPet();

if ((pet as Fish).swim) {
    (pet as Fish).swim();
} else if ((pet as Bird).fly) {
    (pet as Bird).fly();
}

用户自定义的类型守卫

这里可以注意到我们不得不多次使用类型断言。 假若我们一旦检查过类型,就能在之后的每个分支里清楚地知道pet的类型的话就好了。

TypeScript里的_类型守卫_机制让它成为了现实。 类型守卫就是一些表达式,它们会在运行时检查以确保在某个作用域里的类型。

使用类型判定

要定义一个类型守卫,我们只要简单地定义一个函数,它的返回值是一个_类型谓词_:

function isFish(pet: Fish | Bird): pet is Fish {
    return (pet as Fish).swim !== undefined;
}

在这个例子里,pet is Fish就是类型谓词。 谓词为parameterName is Type这种形式,parameterName必须是来自于当前函数签名里的一个参数名。

每当使用一些变量调用isFish时,TypeScript会将变量缩减为那个具体的类型,只要这个类型与变量的原始类型是兼容的。

// 'swim' 和 'fly' 调用都没有问题了

if (isFish(pet)) {
    pet.swim();
}
else {
    pet.fly();
}

注意TypeScript不仅知道在if分支里petFish类型; 它还清楚在else分支里,一定_不是_Fish类型,一定是Bird类型。

使用in操作符

in操作符可以作为类型细化表达式来使用。

对于n in x表达式,其中n是字符串字面量或字符串字面量类型且x是个联合类型,那么true分支的类型细化为有一个可选的或必须的属性nfalse分支的类型细化为有一个可选的或不存在属性n

function move(pet: Fish | Bird) {
    if ("swim" in pet) {
        return pet.swim();
    }
    return pet.fly();
}

typeof类型守卫

现在我们回过头来看看怎么使用联合类型书写padLeft代码。 我们可以像下面这样利用类型断言来写:

function isNumber(x: any): x is number {
    return typeof x === "number";
}

function isString(x: any): x is string {
    return typeof x === "string";
}

function padLeft(value: string, padding: string | number) {
    if (isNumber(padding)) {
        return Array(padding + 1).join(" ") + value;
    }
    if (isString(padding)) {
        return padding + value;
    }
    throw new Error(`Expected string or number, got '${padding}'.`);
}

然而,必须要定义一个函数来判断类型是否是原始类型,这太痛苦了。 幸运的是,现在我们不必将typeof x === "number"抽象成一个函数,因为TypeScript可以将它识别为一个类型守卫。 也就是说我们可以直接在代码里检查类型了。

function padLeft(value: string, padding: string | number) {
    if (typeof padding === "number") {
        return Array(padding + 1).join(" ") + value;
    }
    if (typeof padding === "string") {
        return padding + value;
    }
    throw new Error(`Expected string or number, got '${padding}'.`);
}

这些_typeof类型守卫_只有两种形式能被识别:typeof v === "typename"typeof v !== "typename""typename"必须是"number""string""boolean""symbol"。 但是TypeScript并不会阻止你与其它字符串比较,语言不会把那些表达式识别为类型守卫。

instanceof类型守卫

如果你已经阅读了typeof类型守卫并且对JavaScript里的instanceof操作符熟悉的话,你可能已经猜到了这节要讲的内容。

_instanceof类型守卫_是通过构造函数来细化类型的一种方式。 比如,我们借鉴一下之前字符串填充的例子:

interface Padder {
    getPaddingString(): string
}

class SpaceRepeatingPadder implements Padder {
    constructor(private numSpaces: number) { }
    getPaddingString() {
        return Array(this.numSpaces + 1).join(" ");
    }
}

class StringPadder implements Padder {
    constructor(private value: string) { }
    getPaddingString() {
        return this.value;
    }
}

function getRandomPadder() {
    return Math.random() < 0.5 ?
        new SpaceRepeatingPadder(4) :
        new StringPadder("  ");
}

// 类型为SpaceRepeatingPadder | StringPadder
let padder: Padder = getRandomPadder();

if (padder instanceof SpaceRepeatingPadder) {
    padder; // 类型细化为'SpaceRepeatingPadder'
}
if (padder instanceof StringPadder) {
    padder; // 类型细化为'StringPadder'
}

instanceof的右侧要求是一个构造函数,TypeScript将细化为:

  1. 此构造函数的prototype属性的类型,如果它的类型不为any的话
  2. 构造签名所返回的类型的联合

可以为null的类型

TypeScript具有两种特殊的类型,nullundefined,它们分别具有值nullundefined。 默认情况下,类型检查器认为nullundefined可以赋值给任何类型。 nullundefined是所有其它类型的一个有效值。 这也意味着,你阻止不了将它们赋值给其它类型,就算是你想要阻止这种情况也不行。 null的发明者,Tony Hoare,称它为价值亿万美金的错误

--strictNullChecks标记可以解决此错误:当你声明一个变量时,它不会自动地包含nullundefined。 你可以使用联合类型明确的包含它们:

let s = "foo";
s = null; // 错误, 'null'不能赋值给'string'
let sn: string | null = "bar";
sn = null; // 可以

sn = undefined; // error, 'undefined'不能赋值给'string | null'

注意,按照JavaScript的语义,TypeScript会把nullundefined区别对待。 string | nullstring | undefinedstring | undefined | null是不同的类型。

可选参数和可选属性

使用了--strictNullChecks,可选参数会被自动地加上| undefined:

function f(x: number, y?: number) {
    return x + (y || 0);
}
f(1, 2);
f(1);
f(1, undefined);
f(1, null); // error, 'null' is not assignable to 'number | undefined'

可选属性也会有同样的处理:

class C {
    a: number;
    b?: number;
}
let c = new C();
c.a = 12;
c.a = undefined; // error, 'undefined' is not assignable to 'number'
c.b = 13;
c.b = undefined; // ok
c.b = null; // error, 'null' is not assignable to 'number | undefined'

类型守卫和类型断言

由于可以为null的类型是通过联合类型实现,那么你需要使用类型守卫来去除null。 幸运地是这与在JavaScript里写的代码一致:

function f(sn: string | null): string {
    if (sn == null) {
        return "default";
    }
    else {
        return sn;
    }
}

这里很明显地去除了null,你也可以使用短路运算符:

function f(sn: string | null): string {
    return sn || "default";
}

如果编译器不能够去除nullundefined,你可以使用类型断言手动去除。 语法是添加!后缀:identifier!identifier的类型里去除了nullundefined

function broken(name: string | null): string {
  function postfix(epithet: string) {
    return name.charAt(0) + '.  the ' + epithet; // error, 'name' is possibly null
  }
  name = name || "Bob";
  return postfix("great");
}

function fixed(name: string | null): string {
  function postfix(epithet: string) {
    return name!.charAt(0) + '.  the ' + epithet; // ok
  }
  name = name || "Bob";
  return postfix("great");
}

本例使用了嵌套函数,因为编译器无法去除嵌套函数的null(除非是立即调用的函数表达式)。 因为它无法跟踪所有对嵌套函数的调用,尤其是你将内层函数做为外层函数的返回值。 如果无法知道函数在哪里被调用,就无法知道调用时name的类型。

CSS中position: sticky与overflow发生冲突如何解决

amiko阅读(322)

今天遇到一个css的问题:之前通过sticky设置的吸顶效果突然不见了,经过一番查证后发现这是因为在其父布局中添加了overflow属性导致sticky失效了。接下来,我们来了解一下sticky的设置、失效原因以及如何解决sticky失效。

Sticky吸顶

Sticky是一个比较会常用到的功能,其实现也比较简单,我们只需要给一个容器设置成”position: sticky;”,指定系统条件即可实现一个简易的吸顶效果,实现代码如下:

// css 代码

    <style>
        .sticky {
            width: 900px;
            position: -webkit-sticky;
            /* Safari */
            position: sticky;
            top: 0px;
            height: 46px;
            line-height: 46px;
            box-shadow: 5px 2px 2px #f3f3f3;
            background-color: aqua;
            color: white;
            font-weight: bold;
        }

        .content {
            width: 100%;
            height: 800px;
        }
    </style>

// html代码
<body>
    <div class="main">
        <div class="sticky">这是一个吸顶控件</div>
        <div class="content">
            主题内容
        </div>

    </div>
</body>

什么会导致Sticky不生效?

sticky不生效总结可以大致分为如下三类问题:

  • 父元素或祖先元素中设置了overflow:hidden;overflow-x: hidden; overflow-y:
  • 没有设置定位属性:top/bottom/left/right属性进行粘性定位
  • 父元素的高度不能低于sticky元素的高度

如何解决Sticky不生效问题

一般出现overflowsticky冲突的问题,我们通常是通过监听页面容器的滑动判断滑动高度,并通过生成克隆层实现顶部粘粘,达到指定效果,统一的当页面滑动下来的时候再将克隆层删除实现页面的下滑。

/* 粘顶导航栏 */
/* 克隆的是class="sticky"的一个navbar */
var pscroll = 0; //滚动高度
var iscroll = 0; //循环因子
$(window).scroll(function(){
    pscroll = $(this).scrollTop();
    if(0<pscroll && iscroll === 0){
        var stickyClone = $(".sticky").clone(true).removeClass("sticky").addClass("stickyClone").css("top","0").css("visibility","hidden");
        $(".sticky").before(stickyClone);
        $(".sticky").css("z-index","1020");
        $(".sticky").css("position","fixed");
        $(".sticky").css("top","0");
        $(".sticky").css("right","0");
        $(".sticky").css("left","0");
        iscroll = 1;
    }
    if(0<pscroll){
        //下滚
    }else{
        $(".sticky").css("position","relative"); //可设为默认"static"
        $(".stickyClone").remove();
        iscroll = 0;
    }
});

TypeScript中如何使用hasOwnProperty方法

amiko阅读(343)

当我们在ts中需要判断一个对象是否存在一个属性的时候,使用js中的hasownPropperty方法发现报红显示:

Do not access Object.prototype method 'hasOwnProperty' from target object.(eslintno-prototype-builtins)

下面介绍几种ts中使用hasOwnProperty的方法。

1、直接使用,不去管报错

当我们的调用对象本身是javascript中的Object对象,在ts转换成js后是可以直接运行的。

const hasProp = obj.hasOwnProperty(prop)

2、强制转换

因为ts中默认对象并不是Object类型,我们只需要将其强制转换成Object即可直接使用hasOwnProperty方法:

const hasProp = (obj as Object).hasOwnProperty(prop)

3、通过改变方法指向进行执行

我们可以通过使用call、apply等改变hasOwnProperty的方法指向,来执行该方法:

const hasProp = Object.prototype.hasOwnProperty.call(obj, prop)

TypeScript如何给对象扩展方法

amiko阅读(395)

我们通过一个Leetcode的题目来了解一下如何在TypeScript中给对象扩展新的方法。

Arrary扩展一个分组方法groupBy

declare global {
    interface Array<T> {
        groupBy(fn: (item: T) => string): Record<string, T[]>
    }
}

Array.prototype.groupBy = function(fn) {
    const result: any = {}

    for(let i=0; i< this.length; i++) {
        const item = this[i]
        const key = fn(item)
        if(result[key]) result[key].push(item)
        else result[key] = [item]
    }
    return result
    
}

如上,通过declare 给Array声明一个groupBy方法,然后通过prototype实现该方法,即可实现对象的方法属性扩展。使用如下:

[1,2,3].groupBy(String) // {"1":[1],"2":[2],"3":[3]}

Vue3中使用a-select封装分页下拉框

amiko阅读(430)

背景

常规开发过程中,一般性质的下拉框已经满足我们的日常使用需求,但涉及到复杂场景、大数据量的时候,为了考虑前端的渲染压力和交互的友好性,我们需要对现有的下拉框进行一定的改造,实现下拉分页的功能。这里,我使用的是 "ant-design-vue": "^4.0.3" 组件的 Select 组件,其他UI框架也类似。

环境依赖

  • Vue3 + TypeScript 开发环境
  • 采用Ant-design-Vue UI组件
  • 使用axios作为网络请求框架

期望实现

  • 正常下拉框的所有功能
  • 配置实现下拉分页
  • 请求数据格式转换
  • 加载时机自主选择
  • 额外请求参数配置

功能实现

1、属性设置

根据如上设想,我给当前的自定义组件添加了如下的自定义属性:

export interface PageSelectPagination {
  url?: string // 远程请求地址
  method?: string // 请求方法
  query?: Record<string, any> // 额外请求参数,path地址
  data?: Record<string, any> // 额外请求参数,json data数据
  pageSize?: number // 配置分页器分页大小,默认为10, 建议分页数要 >= 10, selct下拉需要足够的高度
  pageFiled?: string // 配置分页请求页码请求参数名
  pageSizeFiled?: string // 配置分页请求分页器请求参数名
  resultFiled?: string // 数组参数,支持多级
  totalFiled?: string // 总条数参数,支持多级
  searchTextFiled?: string // search字段名
}

/**
 * 设置默认的分页配置
 */
export const DefaultPagination: PageSelectPagination = {
  pageSize: 10,
  pageFiled: 'pageNo',
  pageSizeFiled: 'pageSize',
  method: 'POST',
  resultFiled: 'data.list',
  totalFiled: 'data.total',
  searchTextFiled: 'keyword',
}

export enum LoadingType {
  DEFAULT = 'default', // 默认加载,beforeOnMount
  EXT_PARAMS_CHNAGE = 'ext-params-change', // extParam参数发生改变
  CUSTOM = 'custom', // 主动发起
  DROPDOWN = 'dropdown', // 下拉展开菜单的时候

}

/**
 * 数据转换
 */
export type TransformFunc = (model: Record<string, any>) => any


const pageSelectProps = {

  pagination: {

    type: [Object, Boolean] as PropType<PageSelectPagination | false>,

    default: false as const,

  },

  placeholder: {

    type: String,

    default: '请选择',

  },

  value: {

    type: [Object, String, Boolean, Symbol, Number],

  },

  extParams: {

    // 单独放出来,不与pagination中的配置混淆

    type: Object as PropType<Record<string, any>>,

  },

  loadingType: {

    // 首次加载的方式

    type: String as PropType<LoadingType>,

    default: LoadingType.DEFAULT,

  },

  /**

   * 数据转换

   */

  transform: {

    type: Function as PropType<TransformFunc>,

  },

  request: {

    type: Function,

  },

  remoteSearch: {

    type: Boolean,

    default: true,

  },

}

export type PageSelectProps = ExtractPublicPropTypes<typeof pageSelectProps> & SelectProps

2、组件绘制

通过tsx实现组件属性传递,基本传递原有Select的所有功能,使其不影响原有组件属性的使用。

    return () => {
      const { placeholder } = props
      const newAttrs = { ...attrs }
      if (newAttrs.class)
        delete newAttrs.class
      if (newAttrs.style)
        delete newAttrs.style

      const selectProps: any = {
        filterOption: !props.remoteSearch,
        onDropdownVisibleChange,
        onPopupScroll: handlePopupScroll,
        placeholder,
      }
      if (attrs['showSearch'] || attrs['show-search'])
        selectProps.onSearch = onSearch

      return (
        <div class={attrs.class} style={`${attrs.style} ;position:relative;`} >
          <Select
            options={options.value}
            v-model:value={value.value}
            class={attrs.class}
            {...newAttrs}
            {...emit}
            {...selectProps}
            notFoundContent={
              fetching.value
                ? undefined
                : (
                <Empty style={{ margin: 0 }} image={Empty.PRESENTED_IMAGE_SIMPLE} />
                  )
            }
          >
            {{
              ...selectSlots,
              ...slots,
            }}
          </Select>
        </div>
      )
    }

3、下拉分页

我这里使用a-selectonPopupScroll事件监听实现触底加载,其实也可以使用pagination分页器实现手动分页,功能类似。

    const handlePopupScroll = (e: Record<string, any>) => {
      const { target } = e
      const { scrollTop, scrollHeight, clientHeight } = target
      if (scrollTop + clientHeight === scrollHeight) {
        // 触底
        // 请求下一页
        const t = total.value
        if (t && options.value && t > options.value?.length) {
          loading.value = true
          const p = page.value + 1
          page.value = p
          onPageChange(p)
        }
      }
    }

4、完整实现

如下为该组件的完整代码:

import type { SelectProps } from 'ant-design-vue'
import { Empty, Select, Spin } from 'ant-design-vue'
import type { ExtractPublicPropTypes, PropType } from 'vue'
import { defineComponent, h } from 'vue'
import type { AxiosRequestConfig } from 'axios'
import { LoadingOutlined } from '@ant-design/icons-vue'
import { debounce } from 'lodash'
import type { PageSelectPagination, TransformFunc } from './typing'
import { DefaultPagination, LoadingType } from './typing'
import axios from '~/utils/request'
const pageSelectProps = {
pagination: {
type: [Object, Boolean] as PropType<PageSelectPagination | false>,
default: false as const,
},
placeholder: {
type: String,
default: '请选择',
},
value: {
type: [Object, String, Boolean, Symbol, Number],
},
extParams: {
type: Object as PropType<Record<string, any>>,
},
loadingType: {
// 首次加载的方式
type: String as PropType<LoadingType>,
default: LoadingType.DEFAULT,
},
/**
* 数据转换
*/
transform: {
type: Function as PropType<TransformFunc>,
},
request: {
type: Function,
},
remoteSearch: {
type: Boolean,
default: true,
},
}
export type PageSelectProps = ExtractPublicPropTypes<typeof pageSelectProps> & SelectProps
export type PageSelectEmits = ['update:value', 'update:ext-params']
const PageSelect = defineComponent<PageSelectProps, PageSelectEmits>(
(props: PageSelectProps, context) => {
const { attrs, expose, emit, slots } = context
const page = ref<number>(1) 
const pageSize = ref<number>(10) 
const total = ref<number>(0)
const paginationConfig = shallowRef<PageSelectPagination>()
const fetching = shallowRef<boolean>(false) 
const options = ref<SelectProps['options']>([]) 
const loading = shallowRef<boolean>(false)
const searchText = ref<string>() 
const extParams = ref<Record<string, any>>(props.extParams || {})
const value = ref(props.value)
watch(
() => props.extParams,
(newVal, _) => {
extParams.value = newVal || {}
page.value = 1
fetching.value = true
total.value = 0
options.value = []
if (props.loadingType === LoadingType.EXT_PARAMS_CHNAGE)
refresh()
},
{ deep: true },
)
watch(
() => props.value,
(newVal, _) => {
value.value = newVal
},
{ deep: true },
)
watch(value, (newVal, _) => {
emit('update:value', newVal)
}, { deep: true })
const getQueryParams = () => {
let params: Record<string, any> = {}
if (paginationConfig.value && paginationConfig.value.url) {
params[paginationConfig.value.pageFiled!] = page.value
params[paginationConfig.value.pageSizeFiled!] = pageSize.value
}
if (paginationConfig.value && searchText.value && String(searchText.value).trim()) {
params[paginationConfig.value.searchTextFiled!] = searchText.value
}
const extP = extParams.value
if (extP && Object.keys(extP).length)
params = Object.assign({}, params, extP)
return params
}
const onPageChange = () => {
// eslint-disable-next-line @typescript-eslint/no-use-before-define
fetchData()
}
const fillResult = (result: any) => {
const { code, msg } = result || {}
if (String(code) !== '0') {
console.error(msg || '请求失败,请检查接口配置')
options.value = []
fetching.value = false
loading.value = false
return
}
const p = page.value
let _data = result[paginationConfig.value!.resultFiled!]
const _total = result[paginationConfig.value!.totalFiled!]
total.value = Math.max(0, _total) 
const transformFunc = props.transform
if (transformFunc && typeof transformFunc === 'function')
_data = (_data || []).map((item: any) => transformFunc(item))
if (p === 1) {
options.value = _data
}
else if (_data && Array.isArray(_data) && _data.length) {
options.value = options?.value ? options?.value?.concat(_data) : _data // 拼接
}
else {
if (options?.value && options?.value?.length > _total) {
page.value = 1
onPageChange(1) 
}
}
}
const fetchData = () => {
if (props.request) {
props.request().then((res: any) => {
options.value = res 
}).finally(() => {
setTimeout(() => {
fetching.value = false
loading.value = false
}, 200)
})
return
}
if (!paginationConfig.value || !paginationConfig.value.url) {
loading.value = false
setTimeout(() => {
fetching.value = false
}, 400)
return
}
const _data = getQueryParams()
const config: AxiosRequestConfig = {
url: paginationConfig.value.url,
method: paginationConfig.value.method,
params: paginationConfig.value.query,
data: paginationConfig.value.data,
}
if (String(config.method).toUpperCase() === 'GET')
config.params = Object.assign({}, config.params || {}, _data)
else config.data = Object.assign({}, config.data || {}, _data)
axios(config)
.then((res: any) => {
setTimeout(() => {
fillResult(res)
}, 200)
})
.finally(() => {
setTimeout(() => {
fetching.value = false
loading.value = false
}, 200)
})
}
const handlePopupScroll = (e: Record<string, any>) => {
const { target } = e
const { scrollTop, scrollHeight, clientHeight } = target
if (scrollTop + clientHeight === scrollHeight) {
const t = total.value
if (t && options.value && t > options.value?.length) {
loading.value = true
const p = page.value + 1
page.value = p
onPageChange(p)
}
}
}
const refresh = () => {
page.value = 1
total.value = 0
options.value = []
fetching.value = true
fetchData()
}
const onSearch = debounce((text: any) => {
if (props.remoteSearch) {
searchText.value = text
refresh()
}
}, 300)
const onDropdownVisibleChange = (open: boolean) => {
if (props.loadingType === LoadingType.DROPDOWN && open) {
if (!options.value || options.value.length === 0) {
refresh()
}
}
}
const initConfig = () => {
const { pagination } = props
if (pagination) {
const newConfig =  Object.assign({},DefaultPagination,pagination) 
paginationConfig.value = newConfig
page.value = 1
pageSize.value = newConfig?.pageSize || DefaultPagination.pageSize!
}
}
onBeforeMount(() => {
initConfig()
if (props.loadingType === LoadingType.DEFAULT)
refresh()
})
expose({
refresh,
})
const selectSlots = {
notFoundContent: () => {
if (fetching.value)
return <Spin size="small" />
else
return []
},
dropdownRender: ({ menuNode: menu }) => {
// console.log('get menuNode:', menu)
const indicator = h(LoadingOutlined, {
style: {
fontSize: '14px',
},
spin: true,
})
return (
<div>
{h(menu)}
{loading.value
? (
<div class="loading flex-center">
<Spin size="small" indicator={indicator} />
</div>
)
: (
<div></div>
)}
</div>
)
},
}
return () => {
const { placeholder } = props
const newAttrs = { ...attrs }
if (newAttrs.class)
delete newAttrs.class
if (newAttrs.style)
delete newAttrs.style
const selectProps: any = {
filterOption: !props.remoteSearch,
onDropdownVisibleChange,
onPopupScroll: handlePopupScroll,
placeholder,
}
if (attrs['showSearch'] || attrs['show-search'])
selectProps.onSearch = onSearch
return (
<div class={attrs.class} style={`${attrs.style} ;position:relative;`} >
<Select
options={options.value}
v-model:value={value.value}
class={attrs.class}
{...newAttrs}
{...emit}
{...selectProps}
notFoundContent={
fetching.value
? undefined
: (
<Empty style={{ margin: 0 }} image={Empty.PRESENTED_IMAGE_SIMPLE} />
)
}
>
{{
...selectSlots,
...slots,
}}
</Select>
</div>
)
}
},
{
name: 'PageSelect',
inheritAttrs: false,
props: pageSelectProps,
emits: ['update:value', 'update:ext-params'],
},
)
export * from './typing'
export default PageSelect

5、使用

如下为一个简单的使用:

import type { PageSelectProps } from '~@/components/page-select'
import PageSelect, { LoadingType } from '~@/components/page-select'
const pageSelectRef = ref()
const pageQuery = ref({})
const id = ref()
const psProps: PageSelectProps = {
pagination: {
url: '<api-path>',
searchTextFiled: 'keywords',
resultFiled: 'data',
totalFiled: 'total',
},
loadingType: LoadingType.DEFAULT,
allowClear: true,
showSearch: true,
dropdownMatchSelectWidth: false,
optionLabelProp: 'title',
placeholder: '选择',
onChange: (val: any) => {
},
}
<template>
<PageSelect
ref="pageSelectRef"
v-model:value="id "
:ext-params="pageQuery"
:get-popup-container="(trigger) => trigger.parentElement.parentElement"
class="ml-0px w-200px"
v-bind="psProps"
/>
</template>

Leetcode题目:最大和查询

胖蔡阅读(355)

题目描述

给你两个长度为 n 、下标从 0 开始的整数数组 nums1 和 nums2 ,另给你一个下标从 1 开始的二维数组 queries ,其中 queries[i] = [xi, yi] 。

对于第 i 个查询,在所有满足 nums1[j] >= xi 且 nums2[j] >= yi 的下标 j (0 <= j < n) 中,找出 nums1[j] + nums2[j] 的 最大值 ,如果不存在满足条件的 j 则返回 -1 。

返回数组 answer ,其中 answer[i] 是第 i 个查询的答案。

示例 1:

输入:nums1 = [4,3,1,2], nums2 = [2,4,9,5], queries = [[4,1],[1,3],[2,5]]
输出:[6,10,7]
解释:
对于第 1 个查询:xi = 4 且 yi = 1 ,可以选择下标 j = 0 ,此时 nums1[j] >= 4 且 nums2[j] >= 1nums1[j] + nums2[j] 等于 6 ,可以证明 6 是可以获得的最大值。
对于第 2 个查询:xi = 1 且 yi = 3 ,可以选择下标 j = 2 ,此时 nums1[j] >= 1 且 nums2[j] >= 3nums1[j] + nums2[j] 等于 10 ,可以证明 10 是可以获得的最大值。
对于第 3 个查询:xi = 2 且 yi = 5 ,可以选择下标 j = 3 ,此时 nums1[j] >= 2 且 nums2[j] >= 5nums1[j] + nums2[j] 等于 7 ,可以证明 7 是可以获得的最大值。
因此,我们返回 [6,10,7]

示例 2:

输入:nums1 = [3,2,5], nums2 = [2,3,4], queries = [[4,4],[3,2],[1,1]]
输出:[9,9,9]
解释:对于这个示例,我们可以选择下标 j = 2 ,该下标可以满足每个查询的限制。

示例 3:

输入:nums1 = [2,1], nums2 = [2,3], queries = [[3,3]]
输出:[-1]
解释:示例中的查询 xi = 3 且 yi = 3 。对于每个下标 j ,都只满足 nums1[j] < xi 或者 nums2[j] < yi 。因此,不存在答案。 

提示:

  • nums1.length == nums2.length 
  • n == nums1.length 
  • 1 <= n <= 105
  • 1 <= nums1[i], nums2[i] <= 109 
  • 1 <= queries.length <= 105
  • queries[i].length == 2
  • xi == queries[i][1]
  • yi == queries[i][2]
  • 1 <= xi, yi <= 109

实现

function maximumSumQueries(nums1: number[], nums2: number[], queries: number[][]): number[] {
let result: number[] = Array(queries.length).fill(-1)
let minSum = null
let minX = null
let minY = null
let minEnd = null
let maxEnd = null
for(let j=0;j< queries.length;j++){
const x = queries[j][0]
const y = queries[j][1]
const sum = x + y
const mix = Math.max(x, y)
if(j === 0) {
minX = x
minY = y
minSum = sum
minEnd = mix
// maxEnd = mix
}else {
minSum = Math.min(minSum, sum)
minX = Math.min(minX, x)
minY = Math.min(minY,y)
minEnd = Math.min(minEnd, mix)
maxEnd = Math.max(maxEnd, mix)
}
}
const length = nums1.length
let indexArray: any[] = []
for(let i=0;i< length;i++) {
const n1 = nums1[i]
const n2 = nums2[i]
const sum = n1+n2
const mix = Math.max(n1 , n2)
if(n1 >= minX && n2 >= minY && sum >= minSum && mix >= minEnd) {
let isInserted = false
let array = [...indexArray]
indexArray = []
array.forEach((item: any, _index:number) => {
if(item) {
if(sum > item.sum &&  (n1 >= item.n1 && n2 >= item.n2)) {
if(!isInserted) {
const model = {index:i,n1,n2,sum}
indexArray.push(model)
isInserted = true
}
}else{
indexArray.push(item)
}
}
})
if(!isInserted){
indexArray.push({index:i,n1,n2,sum})
}
}
}
indexArray.forEach((item: any) => {
const { n1,n2,sum} = item
for(let j=0;j < queries.length;j++){
const x = queries[j][0]
const y = queries[j][1]
if(n1 >= x && n2 >= y && (result[j] == -1 || sum > result[j]  ) ) {
result[j] = sum
}
}
})
return result
};

解题思路

1、最大和查询,根据题意可知符合题意的最小和queries[i] = [xi, yi],应满足 sum >= xi + yi

2、需要过滤多解的数据,并取其最大值

3、先对符合条件的数值对进行存储,然后进行求值替换,提高复杂数据的运算速度

Leetcode題目:删除有序数组中的重复项 II

胖蔡阅读(348)

题目描述

给你一个有序数组 nums ,请你 原地 删除重复出现的元素,使得出现次数超过两次的元素只出现两次 ,返回删除后数组的新长度。

不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。

说明:

为什么返回数值是整数,但输出的答案是数组呢?

请注意,输入数组是以「引用」方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。

你可以想象内部操作如下:

// nums 是以“引用”方式传递的。也就是说,不对实参做任何拷贝
int len = removeDuplicates(nums);
// 在函数里修改输入数组对于调用者是可见的。
// 根据你的函数返回的长度, 它会打印出数组中 该长度范围内 的所有元素。
for (int i = 0; i < len; i++) {
    print(nums[i]);
}

示例 1:

输入:nums = [1,1,1,2,2,3]
输出:5, nums = [1,1,2,2,3]
解释:函数应返回新长度 length = 5, 并且原数组的前五个元素被修改为 1, 1, 2, 2, 3。 不需要考虑数组中超出新长度后面的元素。

示例 2:

输入:nums = [0,0,1,1,1,1,2,3,3]
输出:7, nums = [0,0,1,1,2,3,3]
解释:函数应返回新长度 length = 7, 并且原数组的前五个元素被修改为 0, 0, 1, 1, 2, 3, 3。不需要考虑数组中超出新长度后面的元素。

提示:

  • 1 <= nums.length <= 3 * 104
  • -104 <= nums[i] <= 104
  • nums 已按升序排列

实现

/**
* @param {number[]} nums
* @return {number}
*/
var removeDuplicates = function(nums) {
const len = nums.length
if(len < 3) return len
// [1,1,1,2,2,3] 顺序列表,只需要之前两个元素进行比较是否相同就行,
let target =2;
for(let i=2;i<len;i++){
// 前两个肯定不会重复
const current = nums[i] // 当前处理元素 2 - n
if(current != nums[target -2]) {
nums[target] = current
target++
}
}
return target
};

思路

1、剔除特殊数据,降低非必要的处理

2、根据要求,因不可出现两次以上,且为有序数组,则可以将i和i+2遍历对比即可

LeetCode题目:函数防抖

胖蔡阅读(310)

題目描述

请你编写一个函数,接收参数为另一个函数和一个以毫秒为单位的时间 t ,并返回该函数的 函数防抖 后的结果。

函数防抖 方法是一个函数,它的执行被延迟了 t 毫秒,如果在这个时间窗口内再次调用它,它的执行将被取消。你编写的防抖函数也应该接收传递的参数。

例如,假设 t = 50ms ,函数分别在 30ms 、 60ms 和 100ms 时调用。前两个函数调用将被取消,第三个函数调用将在 150ms 执行。如果改为 t = 35ms ,则第一个调用将被取消,第二个调用将在 95ms 执行,第三个调用将在 135ms 执行。

Debounce Schematic

上图展示了了防抖函数是如何转换事件的。其中,每个矩形表示 100ms,反弹时间为 400ms。每种颜色代表一组不同的输入。

请在不使用 lodash 的 _.debounce() 函数的前提下解决该问题。

实现

type F = (...args: number[]) => void
function debounce(fn: F, t: number): F {
let isRunned = false
let task: any = null
const a = (...args) => {
task =  setTimeout(()=>{
fn(...args)
isRunned = true
}, t)
}
return function(...args) {
if(task === null || isRunned) {
// 首次进入,启动任务
isRunned = false
a(...args)
}else if(!isRunned){
// 未执行完
clearTimeout(task)
a(...args)
}
}
};
/**
* const log = debounce(console.log, 100);
* log('Hello'); // cancelled
* log('Hello'); // cancelled
* log('Hello'); // Logged at t=100ms
*/

如上,通过clearTimeout辅助实现函数防抖。

手把手教你怎么正确快速的部署一个TS Node.js项目!

胖蔡阅读(492)

正确快速的部署一个TS Node.js项目!

创建一个TS Node.js应用

如果你已经熟悉创建TS

Node.js项目,可以直接跳到“部署发布应用”部分。

初始化Node.js项目

在我们团队,我们非常喜欢TS,并且在我们的所有新项目中都会使用TS,因此创建一个TS项目并不新鲜。

让我们从最基本的开始:

1、初始化一个Node.js项目

可以使用-y参数可以快速跳过一步一步的配置,安装express依赖,和用于TS开发的expresstypes类型文件,安装typescript作为开发依赖,操作如下:

mkdir myapp & cd myapp
npm init -y
npm install express @types/express --save
npm install typescript --save-dev

2、配置Typescript

创建一个typescript默认配置文件tsconfig.json

npm tsc --init

tsconfig.json文件中:

  • declaration: 用于指定是否在编译完成后生成相应的*.d.ts文件,默认为false
  • outDir: 定义TS编译后的目录,如果没有声明,默认编译后的文件位置将和ts源文件在同一位置

修改以下配置:

"compilerOptions":{
…
"outDir": "./dist",
"declaration": true,
}

创建项目入口文件,创建文件app.ts文件

import Express from 'express'
import cookieParser from 'cookie-parser';
import bodyParser from 'body-parser';
let app = Express();
let PORT = 3000;
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());
app.get('/',(req,res)=>{
res.send('hello world!');
});
app.set('port', process.env.PORT || 3000);
let server = app.listen(app.get('port'), function () {
debug('Express server listening…');
});

完成上述步骤后,我们的文件目录结构如下:

+node_modules
-package.json
-package-lock.json
-app.ts
-tsconfig.json

3、编译TS

我们下一步是构建和部署我们的TS Node.js应用,由于在生产环境中,我们不运行TS版本,而是运行编译后的JS。下面我们来编译项目。修改package.json文件增加以下命令

"scripts": {
"tsc":"tsc",
"start": "node dist/app.js"
},

$ npm run tsc

将会根据我们tsconfig.json的配置编译我们的项目,并输出到指定目录

$ npm run start

将会运行我们编译后的JS文件.然后在本地进行测试,通过浏览器访问http://localhost:3000/,访问成功,接下来我们部署和发布我们的应用

4、部署发布应用
这里我们主要采用两种方法将编译后的TS项目分发部署到各种环境中

  • npm依赖包的形式
  • docker容器方式4

NPM依赖包的形式

NPM生命周期钩子

一些特殊的生命周期钩子会在触发指定操作时被触发,这里我们将使用“prepare”钩子,该钩子会在执行npm publish命令发布到NPM之前被触发一次。所以我们可以这时编译的TS应用。

指定发布文件

通过 “files”字段我们可以去定义发布NPM包时应该包括哪些文件,如果省略该属性默认会为[“*”],会上传所有文件。

下面是修改后的部分package.json

…
"name": "myapp",
"version": "1.0.0",
"description": "",
"main": "dist/app.js",
"files": [
"dist",
"package.json",
"package-lock.json"
],
"scripts": {
"tsc":"tsc",
"prepare":"npm run tsc",
"start": "node dist/app.js"
},
…

在修改完package.json配置后,我们运行npm publish命令,将我们的应用发布到NPM上去

$ npm publish

发布成功后,可以看到npmjs上多了一个myapp

Docker容器方式

要将我们的TS Node.js应用作为容器发布,我们要在项目根目录中创建docker配置文件Dockerfile

下面我们一步步编写Dockerfile文件:

  • 拷贝编译后的文件到容器内
  • 拷贝package.jsonpackage-lock.json到容器内
  • 使用npm install安装依赖
  • 使用node build/app.js运行我们的应用

Node版本

From node:16.15.1
ARG NODE_ENV=production
ENV NODE_ENV $NODE_ENV
copy ./dist /dist
copy ./package.json /package.json
copy ./package-lock.json /package-lock.json
RUN NODE_ENV=$NODE_ENV npm install
EXPOSE 3000
CMD ["node","dist/app.js"]

现在我们可以在根目录中构建docker镜像,运行命令

$ docker build --tag myapp:test

接着我们运行容器,使用命令来运行我们的应用,查看程序是否成功运行在3000端口

$ docker run -p 3000:3000 -it myapp:test

通过浏览器访问http://localhost:3000/,测试

JS中常用数组操作方法

胖蔡阅读(569)

JavaScript中数组Array提供了多个数组的操作方法,便于我们更快、更方便的处理数据,如下内容采自网络,帮助记忆查询使用。

1、concat()

用于连接两个或多个数组。该方法不会改变现有的数组,仅会返回被连接数组的一个副本。

2、join()

用于把数组中的所有元素放入一个字符串。元素是通过指定的分隔符进行分隔的,默认使用’,’号分割,不改变原数组

3、push()

可向数组的末尾添加一个或多个元素,并返回新的长度。末尾添加,返回的是长度,改变原数组

4、pop()

用于删除并返回数组的最后一个元素。返回最后一个元素,改变原数组

5、shift()

用于把数组的第一个元素从其中删除,并返回第一个元素的值,改变原数组

6、unshift()

可向数组的开头添加一个或更多元素,并返回新的长度,改变原数组。

7、slice() 截取

返回一个新的数组,包含从 start end (不包括该元素)的 arrayObject 中的元素(含头不含尾)。返回选定的元素,不会修改原数组

8、splice() 删除

splice() 方法可删除从 index(从0开始) 处开始的零个或多个元素,并且用参数列表中声明的一个或多个值来替换那些被删除的元素。如果从 arrayObject 中删除了元素,返回的是含有被删除的元素的数组。会改变原数组

9、substr() 和 substring()

  • 相同点:如果只是写一个参数,两者的作用都一样:都是是截取字符串从当前下标直到字符串最后的字符串片段。都不会改变原字符串
  • 不同点:第二个参数
    substr(startIndex,lenth): 第二个参数是截取字符串的长度(从起始点截取某个长度的字符串);
    substring(startIndex, endIndex): 第二个参数是截取字符串最终的下标 (截取2个位置之间的字符串,‘含头不含尾’)。

10、sort 排序

按照 Unicode code 位置排序,默认升序,会改变原数组

11、reverse()

用于颠倒数组中元素的顺序。返回的是颠倒后的数组,会改变原数组

12、indexOf 和 lastIndexOf

都接受两个参数:查找的值、查找起始位置

  • 不存在,返回 -1 ;
  • 存在,返回位置。

indexOf 是从前往后查找, lastIndexOf 是从后往前查找。

13、every

对数组的每一项都运行给定的函数,每一项都返回 ture,则返回 true

14、some

对数组的每一项都运行给定的函数,任意一项都返回 ture,则返回 true

15、filter

对数组的每一项都运行给定的函数,返回 结果为 ture 的项组成的数组

16、map

对数组的每一项都运行给定的函数,返回每次函数调用的结果组成一个新数组。map不会改变原数组,但如果数组是对象数组就会改变

17、forEach 数组遍历

  • 删除数组中的指定值
//第一种用最常见的ForEach循环来对比元素找到之后将其删除:
var colors = ["red", "blue", "grey"];
colors.forEach(function(item, index, arr) {
if(item == "red") {
arr.splice(index, 1);
}
});
//第二种我们用循环中的filter方法
var colors = ["red", "blue", "grey"];
colors = colors.filter(function(item) {
return item != "red"
});
console.log(colors); //["blue", "grey"]
  • 删除数组中的某一项

JavaScript中如何删除数组某个元素,我们有需要了解splice这个方法,翻译为剪接,arr.splice(0,1)表示删除数组的第一个。arr.splice(1,2)表示删除从第二个开始,长度为2个的元素。

Array.prototype 也是个数组,所以它也有 length 属性

//1、首先可以给js的数组对象定义一个函数,用于查找指定的元素在数组中的位置,即索引
Array.prototype.indexOf = function(val) {
for (var i = 0; i < this.length; i++) {
if (this[i] == val) return i;
}
return -1;
};
//2、然后使用通过得到这个元素的索引,使用js数组自己固有的函数去删除这个元素:
Array.prototype.remove = function(val) {
var index = this.indexOf(val);
if (index > -1) {
this.splice(index, 1);
}
};

这样就构造了这样一个函数,比如我有一个数组:
var emp = ['abs','dsf','sdf','fd']
//假如我们要删除其中的 ‘fd’ ,就可以使用:
emp.remove('fd');

使用 reduce 方法实现 forEach、map、filter

// forEach
function forEachUseReduce(array, handler) {
array.reduce(function (pre, item, index) {
handler(item, index);
}, null);
}
// map
function mapUseReduce(array, handler) {
let result = [];
array.reduce(function (pre, item, index) {
let mapItem = handler(item, index);
result.push(mapItem);
}, null);
return result;
}
// filter
function filterUseReduce(array, handler) {
let result = [];
array.reduce(function (pre, item, index) {
if (handler(item, index)) {
result.push(item);
}
}, null);
return result;
}
//reduce的用法
arr.reduce(function(prev,cur,index,arr){
…
}, init);
  • prev 必需。累计器累计回调的返回值; 表示上一次调用回调时的返回值,或者初始值 init;
  • cur 必需。表示当前正在处理的数组元素;
  • index 可选。表示当前正在处理的数组元素的索引,若提供 init 值,则起始索引为- 0,否则起始索引为1
  • arr 可选。表示原数组;
  • init 可选。表示初始值。