React + TypeScript实践

❗️准备知识 :

  • 熟悉 React

本文档参考 TypeScript 最新版本git

如何引入React

import * as React from 'react'

import * as ReactDOM from 'react-dom'
复制代码

这种引用方式被证实是最可靠的一种方式, 推荐使用github

而另一种引用方式:typescript

import React from 'react'

import ReactDOM from 'react-dom'
复制代码

须要添加额外的配置:"allowSyntheticDefaultImports": true小程序

函数式组件的声明方式

声明的几种方式数组

第一种:也是比较推荐的一种,使用 React.FunctionComponent,简写形式:React.FC:promise

// Great

type AppProps = {

  message: string

}



const App: React.FC<AppProps> = ({ message, children }) => (

  <div> {message} {children} </div>

)
复制代码

使用用React.FC声明函数组件和普通声明以及 PropsWithChildren 的区别是:babel

  • React.FC显式地定义了返回类型,其余方式是隐式推导的
  • React.FC对静态属性:displayName、propTypes、defaultProps提供了类型检查和自动补全

  • React.FC为children提供了隐式的类型(ReactElement | null),可是目前,提供的类型存在一些 issue(问题)

好比如下用法 React.FC 会报类型错误:

const App: React.FC = props => props.children



const App: React.FC = () => [1, 2, 3]



const App: React.FC = () => 'hello'
复制代码

解决方法:

const App: React.FC<{}> = props => props.children as any

const App: React.FC<{}> = () => [1, 2, 3] as any

const App: React.FC<{}> = () => 'hello' as any



// 或者



const App: React.FC<{}> = props => (props.children as unknown) as JSX.Element

const App: React.FC<{}> = () => ([1, 2, 3] as unknown) as JSX.Element

const App: React.FC<{}> = () => ('hello' as unknown) as JSX.Element
复制代码

在一般状况下,使用 React.FC 的方式声明最简单有效,推荐使用;若是出现类型不兼容问题,建议使用如下两种方式:

第二种:使用 PropsWithChildren,这种方式能够为你省去频繁定义 children 的类型,自动设置 children 类型为 ReactNode:

type AppProps = React.PropsWithChildren<{ message: string }>



const App = ({ message, children }: AppProps) => (

  <div> {message} {children} </div>

)
复制代码

第三种:直接声明:

type AppProps = {

  message: string

  children?: React.ReactNode

}



const App = ({ message, children }: AppProps) => (

  <div> {message} {children} </div>

)
复制代码

Hooks

useState<T>

大部分状况下,TS 会自动为你推导 state 的类型:

// `val`会推导为boolean类型, toggle接收boolean类型参数

const [val, toggle] = React.useState(false)



// obj会自动推导为类型: {name: string}

const [obj] = React.useState({ name: 'sj' })



// arr会自动推导为类型: string[]

const [arr] = React.useState(['One', 'Two'])
复制代码

使用推导类型做为接口/类型:

export default function App() {

  // user会自动推导为类型: {name: string}

  const [user] = React.useState({ name: 'sj', age: 32 })

  const showUser = React.useCallback((obj: typeof user) => {

    return `My name is ${obj.name}, My age is ${obj.age}`

  }, [])



  return <div className="App">用户: {showUser(user)}</div>

}
复制代码

可是,一些状态初始值为空时(null),须要显示地声明类型:

type User = {

  name: string

  age: number

}



const [user, setUser] = React.useState<User | null>(null)
复制代码

useRef<T>

当初始值为 null 时,有两种建立方式:

const ref1 = React.useRef<HTMLInputElement>(null)

const ref2 = React.useRef<HTMLInputElement | null>(null)
复制代码

这两种的区别在于

  • 第一种方式的 ref1.current 是只读的(read-only),而且能够传递给内置的 ref 属性,绑定 DOM 元素
  • 第二种方式的 ref2.current 是可变的(相似于声明类的成员变量)
const ref = React.useRef(0)



React.useEffect(() => {

  ref.current += 1

}, [])
复制代码

这两种方式在使用时,都须要对类型进行检查:

const onButtonClick = () => {

  ref1.current?.focus()



  ref2.current?.focus()

}
复制代码

在某种状况下,能够省去类型检查,经过添加 ! 断言不推荐

// Bad

function MyComponent() {

  const ref1 = React.useRef<HTMLDivElement>(null!)

  React.useEffect(() => {

    // 不须要作类型检查,须要人为保证ref1.current.focus必定存在

    doSomethingWith(ref1.current.focus())

  })

  return <div ref={ref1}> etc </div>

}
复制代码

useEffect

useEffect 须要注意回调函数的返回值只能是函数或者 undefined

function App() {

  // undefined做为回调函数的返回值

  React.useEffect(() => {

    // do something...

  }, [])



  // 返回值是一个函数

  React.useEffect(() => {

    // do something...

    return () => {}

  }, [])

}
复制代码

useMemo<T> / useCallback<T>

useMemouseCallback 均可以直接从它们返回的值中推断出它们的类型

useCallback 的参数必须制定类型,不然ts不会报错,默认指定为 any

const value = 10

// 自动推断返回值为 number

const result = React.useMemo(() => value * 2, [value])



// 自动推断 (value: number) => number

const multiply = React.useCallback((value: number) => value * multiplier, [

  multiplier,

])
复制代码

同时也支持传入泛型, useMemo 的泛型指定了返回值类型,useCallback 的泛型指定了参数类型

// 也能够显式的指定返回值类型,返回值不一致会报错

const result = React.useMemo<string>(() => 2, [])

// 类型“() => number”的参数不能赋给类型“() => string”的参数。



const handleChange = React.useCallback<

  React.ChangeEventHandler<HTMLInputElement>

>(evt => {

  console.log(evt.target.value)

}, [])
复制代码

自定义Hooks

须要注意,自定义 Hook 的返回值若是是数组类型,TS 会自动推导为 Union 类型,而咱们实际须要的是数组里里每一项的具体类型,须要手动添加 const 断言 进行处理:

function useLoading() {

  const [isLoading, setState] = React.useState(false)

  const load = (aPromise: Promise<any>) => {

    setState(true)

    return aPromise.then(() => setState(false))

  }



  // 实际须要: [boolean, typeof load] 类型

  // 而不是自动推导的:(boolean | typeof load)[]

  return [isLoading, load] as const

}
复制代码

若是使用 const 断言遇到问题,也能够直接定义返回类型:

export function useLoading(): [ boolean, (aPromise: Promise<any>) => Promise<any> ] {

  const [isLoading, setState] = React.useState(false)

  const load = (aPromise: Promise<any>) => {

    setState(true)

    return aPromise.then(() => setState(false))

  }

  return [isLoading, load]

}
复制代码

若是有大量的自定义 Hook 须要处理,这里有一个方便的工具方法能够处理 tuple 返回值:

function tuplify<T extends any[]>(...elements: T) {

  return elements

}



function useLoading() {

  const [isLoading, setState] = React.useState(false)

  const load = (aPromise: Promise<any>) => {

    setState(true)

    return aPromise.then(() => setState(false))

  }



  // (boolean | typeof load)[]

  return [isLoading, load]

}



function useTupleLoading() {

  const [isLoading, setState] = React.useState(false)

  const load = (aPromise: Promise<any>) => {

    setState(true)

    return aPromise.then(() => setState(false))

  }



  // [boolean, typeof load]

  return tuplify(isLoading, load)

}
复制代码

默认属性defaultProps

大部分文章都不推荐使用 defaultProps , 相关讨论能够**参考连接**

推荐方式:使用默认参数值来代替默认属性:

type GreetProps = { age?: number }

const Greet = ({ age = 21 }: GreetProps) => {

  /* ... */

}
复制代码

defaultProps类型

TypeScript3.0+ 在默认属性 的类型推导上有了极大的改进,虽然尚且存在一些边界case仍然存在问题不推荐使用,若是有须要使用的场景,可参照以下方式:

type IProps = {

  name: string

}

const defaultProps = {

  age: 25,

}



// 类型定义

type GreetProps = IProps & typeof defaultProps



const Greet = (props: GreetProps) => <div></div>

Greet.defaultProps = defaultProps

// 使用

const TestComponent = (props: React.ComponentProps<typeof Greet>) => {

  return <h1 />

}

const el = <TestComponent name="foo" />
复制代码

Types or Interfaces

在平常的react开发中 interfacetype 的使用场景十分相似

implementsextends 静态操做,不容许存在一种或另外一种实现的状况,因此不支持使用联合类型:

class Point {

  x: number = 2

  y: number = 3

}



interface IShape {

  area(): number

}



type Perimeter = {

  perimeter(): number

}



type RectangleShape = (IShape | Perimeter) & Point



class Rectangle implements RectangleShape {

  // 类只能实现具备静态已知成员的对象类型或对象类型的交集。

  x = 2

  y = 3

  area() {

    return this.x + this.y

  }

}



interface ShapeOrPerimeter extends RectangleShape {}

// 接口只能扩展使用静态已知成员的对象类型或对象类型的交集
复制代码

使用Type仍是Interface?

有几种经常使用规则:

  • 在定义公共 API 时(好比编辑一个库)使用 interface,这样能够方便使用者继承接口
  • 在定义组件属性(Props)和状态(State)时,建议使用 type,由于 type的约束性更强

interfacetype 在ts中是两个不一样的概念,但在 React 大部分使用的 case 中,interfacetype 能够达到相同的功能效果,typeinterface 最大的区别是:

  • type 类型不能二次编辑,而 interface 能够随时扩展
interface Animal {

  name: string

}



// 能够继续在原有属性基础上,添加新属性:color

interface Animal {

  color: string

}



/********************************/



type Animal = {

  name: string

}



// type类型不支持属性扩展

// Error: Duplicate identifier 'Animal'

type Animal = {

  color: string

}
复制代码

获取未导出的Type

某些场景下咱们在引入第三方的库时会发现想要使用的组件并无导出咱们须要的组件参数类型或者返回值类型,这时候咱们能够经过 ComponentProps/ ReturnType 来获取到想要的类型。

// 获取参数类型

import { Button } from 'library' // 可是未导出props type

type ButtonProps = React.ComponentProps<typeof Button> // 获取props

type AlertButtonProps = Omit<ButtonProps, 'onClick'> // 去除onClick

const AlertButton: React.FC<AlertButtonProps> = props => (

  <Button onClick={() => alert('hello')} {...props} />

)
复制代码
// 获取返回值类型

function foo() {

  return { baz: 1 }

}



type FooReturn = ReturnType<typeof foo> // { baz: number }
复制代码

Props

一般咱们使用 type 来定义 Props,为了提升可维护性和代码可读性,在平常的开发过程当中咱们但愿能够添加清晰的注释。

如今有这样一个 type

type OtherProps = {

  name: string

  color: string

}
复制代码

在使用的过程当中,hover 对应类型会有以下展现

// type OtherProps = {

// name: string;

// color: string;

// }

const OtherHeading: React.FC<OtherProps> = ({ name, color }) => (

  <h1>My Website Heading</h1>

)
复制代码

增长相对详细的注释,使用时会更清晰,须要注意,注释须要使用 /**/ // 没法被 vscode 识别

// Great

/** * @param color color * @param children children * @param onClick onClick */

type Props = {

  /** color */

  color?: string

  /** children */

  children: React.ReactNode

  /** onClick */

  onClick: () => void

}



// type Props

// @param color — color

// @param children — children

// @param onClick — onClick

const Button: React.FC<Props> = ({ children, color = 'tomato', onClick }) => {

  return (

    <button style={{ backgroundColor: color }} onClick={onClick}> {children} </button>

  )

}
复制代码

经常使用Props ts类型

基础属性类型

type AppProps = {

  message: string

  count: number

  disabled: boolean

  /** array of a type! */

  names: string[]

  /** string literals to specify exact string values, with a union type to join them together */

  status: 'waiting' | 'success'

  /** 任意须要使用其属性的对象(不推荐使用,可是做为占位颇有用) */

  obj: object

  /** 做用和`object`几乎同样,和 `Object`彻底同样 */

  obj2: {}

  /** 列出对象所有数量的属性 (推荐使用) */

  obj3: {

    id: string

    title: string

  }

  /** array of objects! (common) */

  objArr: {

    id: string

    title: string

  }[]

  /** 任意数量属性的字典,具备相同类型*/

  dict1: {

    [key: string]: MyTypeHere

  }

  /** 做用和dict1彻底相同 */

  dict2: Record<string, MyTypeHere>

  /** 任意彻底不会调用的函数 */

  onSomething: Function

  /** 没有参数&返回值的函数 */

  onClick: () => void

  /** 携带参数的函数 */

  onChange: (id: number) => void

  /** 携带点击事件的函数 */

  onClick(event: React.MouseEvent<HTMLButtonElement>): void

  /** 可选的属性 */

  optional?: OptionalType

}
复制代码

经常使用React属性类型

export declare interface AppBetterProps {

  children: React.ReactNode // 通常状况下推荐使用,支持全部类型 Great

  functionChildren: (name: string) => React.ReactNode

  style?: React.CSSProperties // 传递style对象

  onChange?: React.FormEventHandler<HTMLInputElement>

}



export declare interface AppProps {

  children1: JSX.Element // 差, 不支持数组

  children2: JSX.Element | JSX.Element[] // 通常, 不支持字符串

  children3: React.ReactChildren // 忽略命名,不是一个合适的类型,工具类类型

  children4: React.ReactChild[] // 很好

  children: React.ReactNode // 最佳,支持全部类型 推荐使用

  functionChildren: (name: string) => React.ReactNode // recommended function as a child render prop type

  style?: React.CSSProperties // 传递style对象

  onChange?: React.FormEventHandler<HTMLInputElement> // 表单事件, 泛型参数是event.target的类型

}
复制代码

Forms and Events

onChange

change 事件,有两个定义参数类型的方法。

第一种方法使用推断的方法签名(例如:React.FormEvent <HTMLInputElement> :void

import * as React from 'react'



type changeFn = (e: React.FormEvent<HTMLInputElement>) => void



const App: React.FC = () => {

  const [state, setState] = React.useState('')



  const onChange: changeFn = e => {

    setState(e.currentTarget.value)

  }



  return (

    <div> <input type="text" value={state} onChange={onChange} /> </div>

  )

}
复制代码

第二种方法强制使用 @types / react 提供的委托类型,两种方法都可。

import * as React from 'react'



const App: React.FC = () => {

  const [state, setState] = React.useState('')



  const onChange: React.ChangeEventHandler<HTMLInputElement> = e => {

    setState(e.currentTarget.value)

  }

  return (

    <div> <input type="text" value={state} onChange={onChange} /> </div>

  )

}
复制代码

onSubmit

若是不太关心事件的类型,能够直接使用 React.SyntheticEvent,若是目标表单有想要访问的自定义命名输入,可使用类型扩展

import * as React from 'react'



const App: React.FC = () => {

  const onSubmit = (e: React.SyntheticEvent) => {

    e.preventDefault()

    const target = e.target as typeof e.target & {

      password: { value: string }

    } // 类型扩展

    const password = target.password.value

  }

  return (

    <form onSubmit={onSubmit}> <div> <label> Password: <input type="password" name="password" /> </label> </div> <div> <input type="submit" value="Log in" /> </div> </form>

  )

}
复制代码

Operators

经常使用的操做符,经常使用于类型判断

  • typeof and instanceof: 用于类型区分
  • keyof: 获取object的key

  • O[K]: 属性查找

  • [K in O]: 映射类型

  • + or - or readonly or ?: 加法、减法、只读和可选修饰符

  • x ? Y : Z: 用于泛型类型、类型别名、函数参数类型的条件类型

  • !: 可空类型的空断言

  • as: 类型断言

  • is: 函数返回类型的类型保护

Tips

使用查找类型访问组件属性类型

经过查找类型减小 type 的非必要导出,若是须要提供复杂的 type,应当提取到做为公共 API 导出的文件中。

如今咱们有一个 Counter 组件,须要 name 这个必传参数:

// counter.tsx

import * as React from 'react'

export type Props = {

  name: string

}

const Counter: React.FC<Props> = props => {

  return <></>

}

export default Counter
复制代码

在其余引用它的组件中咱们有两种方式获取到 Counter 的参数类型

第一种是经过 typeof 操做符(推荐

// Great

import Counter from './d-tips1'

type PropsNew = React.ComponentProps<typeof Counter> & {

  age: number

}



const App: React.FC<PropsNew> = props => {

  return <Counter {...props} />

}

export default App
复制代码

第二种是经过在原组件进行导出

import Counter, { Props } from './d-tips1'



type PropsNew = Props & {

  age: number

}



const App: React.FC<PropsNew> = props => {

  return (

    <> <Counter {...props} /> </>

  )

}

export default App
复制代码

不要在type或interface中使用函数声明

保持一致性,类型/接口的全部成员都经过相同的语法定义。

--strictFunctionTypes 在比较函数类型时强制执行更严格的类型检查,但第一种声明方式下严格检查不生效。

✅

interface ICounter {

  start: (value: number) => string

}



❌

interface ICounter1 {

  start(value: number): string

}



🌰

interface Animal {}

interface Dog extends Animal {

  wow: () => void

}

interface Comparer<T> {

  compare: (a: T, b: T) => number

}

declare let animalComparer: Comparer<Animal>

declare let dogComparer: Comparer<Dog>

animalComparer = dogComparer // Error

dogComparer = animalComparer // Ok



interface Comparer1<T> {

  compare(a: T, b: T): number

}

declare let animalComparer1: Comparer1<Animal>

declare let dogComparer1: Comparer1<Dog>

animalComparer1 = dogComparer // Ok

dogComparer1 = animalComparer // Ok
复制代码

事件处理

咱们在进行事件注册时常常会在事件处理函数中使用 event 事件对象,例如当使用鼠标事件时咱们经过 clientXclientY 去获取指针的坐标。

你们可能会想到直接把 event 设置为 any 类型,可是这样就失去了咱们对代码进行静态检查的意义。

function handleEvent(event: any) {

  console.log(event.clientY)

}
复制代码

试想下当咱们注册一个 Touch 事件,而后错误的经过事件处理函数中的 event 对象去获取其 clientY 属性的值,在这里咱们已经将 event 设置为 any 类型,致使 TypeScript 在编译时并不会提示咱们错误, 当咱们经过 event.clientY 访问时就有问题了,由于 Touch 事件的 event 对象并无 clientY 这个属性。

经过 interfaceevent 对象进行类型声明编写的话又十分浪费时间,幸运的是 React 的声明文件提供了 Event 对象的类型声明。

Event 事件对象类型

  • ClipboardEvent<T = Element> 剪切板事件对象
  • DragEvent<T =Element> 拖拽事件对象

  • ChangeEvent<T = Element> Change事件对象

  • KeyboardEvent<T = Element> 键盘事件对象

  • MouseEvent<T = Element> 鼠标事件对象

  • TouchEvent<T = Element> 触摸事件对象

  • WheelEvent<T = Element> 滚轮时间对象

  • AnimationEvent<T = Element> 动画事件对象

  • TransitionEvent<T = Element> 过渡事件对象

事件处理函数类型

当咱们定义事件处理函数时有没有更方便定义其函数类型的方式呢?答案是使用 React 声明文件所提供的 EventHandler 类型别名,经过不一样事件的 EventHandler 的类型别名来定义事件处理函数的类型

type EventHandler<E extends React.SyntheticEvent<any>> = {

  bivarianceHack(event: E): void

}['bivarianceHack']

type ReactEventHandler<T = Element> = EventHandler<React.SyntheticEvent<T>>

type ClipboardEventHandler<T = Element> = EventHandler<React.ClipboardEvent<T>>

type DragEventHandler<T = Element> = EventHandler<React.DragEvent<T>>

type FocusEventHandler<T = Element> = EventHandler<React.FocusEvent<T>>

type FormEventHandler<T = Element> = EventHandler<React.FormEvent<T>>

type ChangeEventHandler<T = Element> = EventHandler<React.ChangeEvent<T>>

type KeyboardEventHandler<T = Element> = EventHandler<React.KeyboardEvent<T>>

type MouseEventHandler<T = Element> = EventHandler<React.MouseEvent<T>>

type TouchEventHandler<T = Element> = EventHandler<React.TouchEvent<T>>

type PointerEventHandler<T = Element> = EventHandler<React.PointerEvent<T>>

type UIEventHandler<T = Element> = EventHandler<React.UIEvent<T>>

type WheelEventHandler<T = Element> = EventHandler<React.WheelEvent<T>>

type AnimationEventHandler<T = Element> = EventHandler<React.AnimationEvent<T>>

type TransitionEventHandler<T = Element> = EventHandler<

  React.TransitionEvent<T>

>
复制代码

bivarianceHack 为事件处理函数的类型定义,函数接收一个 event 对象,而且其类型为接收到的泛型变量 E 的类型, 返回值为 void

关于为什么是用bivarianceHack而不是(event: E): void,这与strictfunctionTypes选项下的功能兼容性有关。(event: E): void,若是该参数是派生类型,则不能将其传递给参数是基类的函数。

class Animal {

  private x: undefined

}

class Dog extends Animal {

  private d: undefined

}



type EventHandler<E extends Animal> = (event: E) => void

let z: EventHandler<Animal> = (o: Dog) => {} // fails under strictFunctionTyes



type BivariantEventHandler<E extends Animal> = {

  bivarianceHack(event: E): void

}['bivarianceHack']

let y: BivariantEventHandler<Animal> = (o: Dog) => {}
复制代码

Promise 类型

在作异步操做时咱们常用 async 函数,函数调用时会 return 一个 Promise 对象,可使用 then 方法添加回调函数。Promise<T> 是一个泛型类型,T 泛型变量用于肯定 then 方法时接收的第一个回调函数的参数类型。

type IResponse<T> = {

  message: string

  result: T

  success: boolean

}



async function getResponse(): Promise<IResponse<number[]>> {

  return {

    message: '获取成功',

    result: [1, 2, 3],

    success: true,

  }

}



getResponse().then(response => {

  console.log(response.result)

})
复制代码

首先声明 IResponse 的泛型接口用于定义 response 的类型,经过 T 泛型变量来肯定 result 的类型。而后声明了一个 异步函数 getResponse 而且将函数返回值的类型定义为 Promise<IResponse<number[]>> 。最后调用 getResponse 方法会返回一个 promise 类型,经过 then 调用,此时 then 方法接收的第一个回调函数的参数 response 的类型为,{ message: string, result: number[], success: boolean}

泛型参数的组件

下面这个组件的name属性都是指定了传参格式,若是想不指定,而是想经过传入参数的类型去推导实际类型,这就要用到泛型。

const TestB = ({ name, name2 }: { name: string; name2?: string }) => {

  return (

    <div className="test-b"> TestB--{name} {name2} </div>

  )

}
复制代码

若是须要外部传入参数类型,只需 ->

type Props<T> = {

  name: T

  name2?: T

}

const TestC: <T>(props: Props<T>) => React.ReactElement = ({ name, name2 }) => {

  return (

    <div className="test-b"> TestB--{name} {name2} </div>

  )

}



const TestD = () => {

  return (

    <div> <TestC<string> name="123" /> </div> ) } 复制代码

何时使用泛型

当你的函数,接口或者类:

  • 须要做用到不少类型的时候,举个🌰

当咱们须要一个 id 函数,函数的参数能够是任何值,返回值就是将参数原样返回,而且其只能接受一个参数,在 js 时代咱们会很轻易地甩出一行

const id = arg => arg
复制代码

因为其能够接受任意值,也就是说咱们的函数的入参和返回值都应该能够是任意类型,若是不使用泛型,咱们只能重复的进行定义

type idBoolean = (arg: boolean) => boolean

type idNumber = (arg: number) => number

type idString = (arg: string) => string

// ...
复制代码

若是使用泛型,咱们只须要

function id<T>(arg: T): T {

  return arg

}



// 或



const id1: <T>(arg: T) => T = arg => {

  return arg

}
复制代码
  • 须要被用到不少地方的时候,好比经常使用的工具泛型 Partial

功能是将类型的属性变成可选, 注意这是浅 Partial

type Partial<T> = { [P in keyof T]?: T[P] }
复制代码

若是须要深 Partial 咱们能够经过泛型递归来实现

type DeepPartial<T> = T extends Function

  ? T

  : T extends object

  ? { [P in keyof T]?: DeepPartial<T[P]> }

  : T



type PartialedWindow = DeepPartial<Window>
复制代码

字节跳动懂车帝团队招聘

咱们是字节跳动旗下懂车帝产品线,目前业务上正处于高速发展阶段,懂车帝自2017年8月正式诞生,仅三年时间已是汽车互联网行业第二。

如今前端团队主流的技术栈是React、Typescript,主要负责懂车帝App、M站、PC站、懂车帝小程序产品矩阵、商业化海量业务,商业数据产品等。咱们在类客户端、多宿主、技术建站、中后台系统、全栈、富交互等多种应用场景都有大量技术实践,致力于技术驱动业务发展,探索全部可能性。

加入懂车帝,一块儿打造汽车领域最专业最开放的前端团队!

简历直达dcar_fe@bytedance.com

邮件标题:应聘+城市+岗位名称

相关文章
相关标签/搜索