探React Hooks

前言

衆所周知,hooks在 React@16.8 中已經正式發佈了。而下週週會,咱們團隊有個同窗將會仔細介紹分享一下hooks。最近網上呢有很多hooks的文章,這難免激起了我本身的好奇心,想先行探探hooks到底好很差用。javascript

react hooks在其文檔的最開頭,就闡明瞭hooks的一個鮮明做用跟幾個動機(或者說hooks的好處)。html

明確的做用

它可讓你在不編寫 class 的狀況下使用 state 以及其餘的 React 特性。前端

意思很明瞭,就是拓展函數式組件的邊界。結果也很清晰,只要Class 組件能實現的,函數式組件+Hooks都能勝任。java

動機

文檔中列了三點:react

  1. 在組件之間複用狀態邏輯很難;
  2. 複雜組件變得難以理解;
  3. class讓開發人員與計算機都難理解;

關於這三點的詳細介紹文檔裏也有,還有中文的,我就很少說了。git

我對動機的理解

動機也便是hooks能帶來的好處。其中第三點,文檔中所說class的弊端對於我本人,仍是有點兒不痛不癢。this的問題,箭頭函數解決的差很少了;語法提案也到stage-3了;代碼壓縮什麼的,本身的資源代碼大小每每不是核心問題。github

如今說利用hooks能夠勝任class組件全部的能力。但你勝任歸你勝任,我寫class又有什麼不能夠。我繼承、高階騷的一逼,要啥hooks。編程

然而第一、2兩點仍是吸引了個人注意。狀態邏輯的複用,以前我主要採用高階組件+繼承,雖然也能解決,但hooks彷佛有更優雅的方案。複雜組件變得難以理解,這個也確實是日常中遇到的問題,一個組件寫着寫着狀態愈來愈多,抽成子組件吧props跟state又傳來傳去。三個月後,本身的代碼本身已經看不懂了。redux

那hooks真的就能更好的解決這些問題麼?文檔裏輕飄飄的幾句話,對於實際業務來講,確實沒有太多體感。因而我決定簡單寫幾個場景,探一探這hooks的活到底好很差。api

狀態邏輯的複用

這種場景其實挺常見。只要頁面中有須要複用的組件,且這個組件又有較爲複雜的狀態邏輯,就會有這樣的需求。舉個例子:中後臺系統常見的各類列表,表格內容各不相同,可是都要有分頁的行爲,因而分頁組件就須要去抽象。按照正常的寫法,咱們會怎麼作呢?

傳統流派

最開始,咱們可能不會想着通用,就寫一個列表+分頁的組件。以最簡單的分頁爲例,可能會以下寫(爲方便閱讀,不作太多異常處理):

import { Component } from 'react';
import { range } from 'lodash';

// 模擬列表數據請求
const fetchList = ({ page = 1, size = 10 }) =>
  new Promise(resolve => resolve(range((page - 1) * size, page * size)))

export default class ListWithPagination extends Component {
  state = {
    page: 1,
    data: [],
  }

  componentDidMount() {
    this.fetchListData(this.setState);
  }

	handlePageChange = newPage =>
  	this.setState({ page: newPage }, this.fetchListData)

  fetchListData = () => {
    const { page } = this.state;
    fetchList({ page }).then(data => this.setState({ data }));
  }
  
  render() {
    const { data, page } = this.state;
    return (
      <div> <ul className="list"> {data.map((item, key) => ( <li key={key}>{item}</li> ))} </ul> <div className="nav"> <button type="button" onClick={() => this.handlePageChange(page - 1)}> 上一頁 </button> <label>當前頁: {page}</label> <button type="button" onClick={() => this.handlePageChange(page + 1)}> 下一頁 </button> </div> </div>
    );
  }
}
複製代碼

而後咱們就會想,每一個地方都要有分頁,惟一不太同樣的僅是 列表渲染 跟數據請求api而已,那何不抽個高階組件呢?因而代碼變成了:

export default function ListHoc(ListComponent) {
  return class ListWithPagination extends Component {
    // ...同上述code,省略

    // 數據請求方法,從props中傳入
    fetchListData = () => {
      const { fetchApi } = this.props;
      const { page } = this.state
      return fetchApi({ page }).then(data => this.setState({ data }));
    }

    render() {
      const { data, page } = this.state;
      return (
        <div> <ListComponent data={data} /> <div className="nav">...省略</div> </div> ); } }; } 複製代碼

這麼一來,將來再寫列表時,使用高階組件包裹一下,再把數據請求方法 以props傳入,就能達到一個複用狀態邏輯與分頁組件的效果了。

就在咱們得意之際,又來了一個新需求,說有一個列表的分頁導航,須要在 列表上面,而不是 列表下面,換成程序語言意思就是Dom的結構與樣式有變動。唔.....仔細想一想有幾種方案:

  • 傳遞一個props叫「theme」,控制不一樣的順序跟樣式....乍一看還行,但若是將來兩種列表風格愈來愈遠,這個高階組件會愈來愈重....不行不行。
  • 再寫一個相似的高階組件,dom結構不同,但其餘如出一轍。唔,代碼重複度這麼高,真low,不行不行。
  • 再寫一個組件,繼承這個這個高階組件,重寫render。好像還能夠,就是這個繼承關係略略有點兒奇怪,應該是兄弟關係,而不是繼承關係。固然我能夠再抽象一層包含狀態邏輯處理的通用Component,兩種列表形式的高階組件都是繼承它,而不是繼承 React.Component。可是即便如此,經過繼承來複寫render的方式,沒法清晰感知組件到底有哪些狀態值,尤爲在狀態較多,邏輯較爲複雜的狀況下。這樣往後維護,或者拓展render時,就舉步維艱。

這也不行,那也很差。那用hooks來作又能作成哪樣呢?

Hooks流派

注:爲了簡化,下文中的 effect 都指代 side effect。

嘗試改造

首先,咱們把最開始那個 ListWithPagination 以hooks改寫,那就成了:

import { useState, useEffect } from 'react';
import { range } from 'lodash';

// 模擬列表數據請求
const fetchList = ({ page = 1, size = 10 }) =>
	new Promise(resolve => resolve(range((page - 1) * size, page * size)));

export default function List() {
  const [page, setPage] = useState(1); // 初始頁碼爲: 1
  const [list, setList] = useState([]); // 初始列表數據爲空數組: []

  useEffect(() => {
    fetchList({ page }).then(setList);
  }, [page]); // 當page變動時,觸發effect

  const prevPage = () => setPage(currentPage => currentPage - 1);
  const nextPage = () => setPage(currentPage => currentPage + 1);

  return (
    <div> <ul> {list.map((item, key) => ( <li key={key}>{item}</li> ))} </ul> <div> <button type="button" onClick={prevPage}> 上一頁 </button> <label>當前頁: {page}</label> <button type="button" onClick={nextPage}> 下一頁 </button> </div> </div>
  );
}

複製代碼

爲防止部分同窗不理解,我再簡單介紹下 useState 與 useEffect。

  • useState: 執行後,返回一個數組,第一個值爲狀態值,第二個值爲更新此狀態值的對應方法。useState函數入參爲state初始值。
  • useEffect:執行反作用操做。第一個參數爲反作用方法,第二個參數是一個數組,填寫反作用依賴項。當依賴項變了時,反作用方法纔會執行。若爲空數組,則只執行一次。如不填寫,則每次render都會觸發。

若是對此仍是不理解,建議先看下相關文檔。若是關於反作用不理解,能夠到文章最後再看。在咱們當下的場景中,知道異步請求數據並更新組件內部狀態值就屬於反作用的一種便可。

知道基本概念之後,咱們看上述的代碼,其實也大體能理解其機制。

  1. 組件初始化也即第一次render後,會觸發一次effect,請求第一頁數據後,更新列表數據 list ,進而又觸發第二次render。
  2. 在第二次render中,useState會獲取當前的 list 值,而不是初始值,進而頁面渲染新的列表。至於react如何作到能數據的匹配,文檔裏有簡單介紹
  3. 在後續的用戶點擊行爲中,觸發了setPage,進而更新了 page ,因爲它的變動觸發了effect,effect執行後又更新 list ,觸發新的render,渲染最新的列表。

在瞭解機制之後,咱們就要開始作正經事了。上述傳統流派中,經過高階組件抽象公共邏輯。如今咱們經過hooks改造了最初的class組件。下一步應該抽離狀態邏輯。相似剛剛高階組件的結果,咱們指望將分頁的行爲抽離,那太簡單了,把處理狀態的相關代碼封裝成函數,抽離出組件,再傳遞一下數據請求api就好:

// 傳遞獲取數據api,返回 [當前列表,分頁數據,分頁行爲]
const usePagination = (fetchApi) => {
  const [page, setPage] = useState(1); // 初始頁碼爲: 1
  const [list, setList] = useState([]); // 初始列表數據爲空數組: []

  useEffect(() => {
    fetchApi({ page }).then(setList);
  }, [page]); // 當page變動時,觸發effect

  const prevPage = () => setPage(currentPage => currentPage - 1);
  const nextPage = () => setPage(currentPage => currentPage + 1);

  return [list, { page }, { prevPage, nextPage }];
};

export default function List() {
  const [list, { page }, { prevPage, nextPage }] = usePagination(fetchList);
  return (
    <div>...省略</div>
  );
}

複製代碼

若是你但願分頁的dom結構也想複用,那就再抽個函數便好。

function renderCommonList({ ListComponent, fetchApi }) {
  const [list, { page }, { prevPage, nextPage }] = usePagination(fetchApi);
  return (
    <div> <ListComponent list={list} /> <div> <button type="button" onClick={prevPage}> 上一頁 </button> <label>當前頁: {page}</label> <button type="button" onClick={nextPage}> 下一頁 </button> </div> </div> ); } export default function List() { function ListComponent({ list }) { return ( <ul> {list.map((item, key) => ( <li key={key}>{item}</li> ))} </ul> ); } return renderCommonList({ ListComponent, fetchApi: fetchList, }); } 複製代碼

若是你但願有一個新的分頁結構與樣式,那就重寫一個結構,並引用 usePagination 。總之,最核心的狀態處理邏輯已經被咱們抽離出來,由於無關this,因而它與組件無關、與dom也能夠無關。愛插哪插哪,誰愛用誰用。百花叢中過,片葉不沾身。

這麼一來,數據層與dom更加的分離,react組件更加的退化成一層UI層,進而更易閱讀、維護、拓展。

場景深刻

不過不能開心的太早。作事若是淺嘗則止,每每後續會遇到深坑。就以剛剛的需求來講,有些特殊邏輯還未考察到。假如說,咱們的分頁請求會失敗,而頁碼已經更新,這該怎麼辦?通常來講有幾個思路:

  1. 請求失敗之後回滾頁碼。但實現不優雅,且頁碼跳來跳去,放棄。
  2. 數據請求成功之後再更新頁碼。比較適合移動端滾動加載的狀況。
  3. 不回滾頁碼,列表頁提示異常,點擊觸發重試。比較適合上述中分頁列表的狀況。

那咱們就按方案3,暴露一個error的狀態,提供一個刷新頁面的方法。咱們忽然意識到一個問題,如何刷新頁面數據呢?咱們的effect依賴於page變動,而刷新頁面不變動page,effect便不會觸發。想一下,也有兩個思路:

  1. 再加一個關於刷新的狀態值,刷新頁面數據的方法,每次執行都會爲其+1,觸發effect。不過這樣會致使組件無緣無故加個狀態值。
  2. 依賴項改成一個對象,page爲對象中一個屬性,往後也方便拓展。因爲對象沒法對比的特性,每次setState都會觸發effect。不過有可能會致使數據無心義的重複獲取,好比快速點擊同一個頁碼時,觸發了兩次數據獲取。

綜合考慮來講,我採起第二個方案。由於effect強依賴於入參的變動也不合理,畢竟這是一個有反作用的方法。相同的分頁入參下,服務端也有可能返回不一樣的結果。數據重複獲取的問題,能夠手動加入防抖等手段優化。具體代碼以下:

const usePagination = (fetchApi) => {
  const [query, setQuery] = useState({ page: 1, size: 15 }); // 初始頁碼爲: 1
  const [isError, setIsError] = useState(false); // 初始狀態爲false
  const [list, setList] = useState([]); // 初始列表數據爲空數組: []
  
  useEffect(() => {
    setIsError(false);
    fetchApi(query)
    .then(setList)
    .catch(() => setIsError(true));
  }, [query]); // 當頁面查詢參數變動時,觸發effect
  
  const { page, size } = query;
  const prevPage = () => setQuery({ size, page: page - 1 });
  const nextPage = () => setQuery({ size, page: page + 1 });
  const refreshPage = () => setQuery({ ...query });
  
	// 若是數據過多,數組解構麻煩,也能夠選擇返回對象
  return [list, query, { prevPage, nextPage, refreshPage }, isError];
};
複製代碼

可是若是按照方案2呢?「數據請求成功之後再更新頁碼」。在移動端的長列表滾動加載時,頁面並不透出頁碼,滾動加載失敗時,toast提示失敗,再滾動依舊加載剛剛失敗的那一頁。然而在咱們的 usePagination 中,數據請求的effect必須是經過query變動來觸發的,沒法實現請求結束之後再更改頁碼。若是是經過方案1「請求失敗之後回滾頁碼」,那因爲回滾了頁面,又會觸發一次effect請求,這也不是咱們想看到的。

其實這是鑽了牛角尖,這自己已是不一樣的場景了。在移動端的滾動加載中,是否加載並不是是由「頁碼變動」控制,而是由「是否滾動到底部」控制。因而代碼應該是:

// 滾動到底部時,執行
const useBottom = (action, dependencies) => {
  function doInBottom() {
    // isScrollToBottom 的代碼不貼了
    return isScrollToBottom() && action();
  }
  useEffect(() => {
    window.addEventListener('scroll', doInBottom);
    // 組件卸載或函數下一次執行時,會先執行上一次函數內部return的方法
    return () => {
      window.removeEventListener('scroll', doInBottom);
    };
  }, dependencies);
};

const usePagination = (fetchApi) => {
  // 由於每次是請求下一頁數據,因此如今初始頁碼爲: 0
  const [query, setQuery] = useState({ page: 0, size: 50 });
  const [list, setList] = useState([]); // 初始列表數據爲空數組: []

  const fetchAndSetQuery = () => {
    // 每次請求下一頁數據
    const newQuery = {
      ...query,
      page: query.page + 1,
    };
    fetchApi(newQuery)
    .then((newList) => {
      // 成功後插入新列表數據,並更新最新分頁參數
      setList([...list, ...newList]);
      setQuery(newQuery);
    })
    .catch(() => window.console.log('加載失敗,請重試'));
  };
  // 首次mount後觸發數據請求
  useEffect(fetchAndSetQuery, []);
  // 滾動到底部觸發數據請求
  useBottom(fetchAndSetQuery);

  return [list];
};
複製代碼

其中在 useBottom 內的effect函數中,返回了一個解綁滾動事件的函數,在組件卸載或者下一次effect觸發時,會先執行此函數進行解綁行爲。在傳統的class組件中,咱們通常是在unmount階段去解綁事件。若是反作用依賴了props或state,在update階段可能也須要清除老effect,執行新effect。如此一來,處理統一邏輯的函數就被分散在多個地方,致使組件複雜度的上升。

另外眼尖的同窗會發現,爲何useBottom內部的useEffect的依賴項,在咱們這個場景中不設置呢?滾動事件,不是應該mount的時候初始化就行了嗎?按以前的理解,應該是寫一個空數組[],這樣滾動事件只綁定一次。然而若是咱們真的這樣寫: useBottom(fetchAndSetQuery, []) 的話,就會發現一個大bug。 fetchAndSetQuery 中的query與list 永遠都是初始化時的數據,也便是 { page: 0, size: 50 } 與 [] 。結果就是每次滾動到底部,加載的仍是第一頁數據,渲染的也仍是第一頁數據([...[], ...第一頁數據])。

Why!!!

因而我又閱讀了一次uesEffect的相關文檔,揣摩了一番,終於大體領悟。

useState與useEffect的正確使用姿式

state永遠都是新的值

這一點同咱們過去class組件中的state是徹底不同的。在class組件中,state一直是掛載在當前實例下,保持着同一個引用。而在函數式組件中,根本沒有this。無論你的state是一個基本數據類型(如string、number),仍是一個引用數據類型(如object),只要是經過useState獲取的state,每一次render,都是新的值。 useState返回的狀態更新方法,只是讓下一次render時的state能獲取到當前最新的值。而不是保持一個引用、更新那個引用值。(這一段若是看不懂,就多看幾遍,若是還看不懂,請評論區溫柔的指出,我想一想再怎麼通俗的去解釋)

讀懂這個概念,並把這個概念做爲hooks使用的第一準則後,咱們就能清晰的明白,爲何上述代碼中,若是useBottom中的useEffect的依賴項設爲空數組,則內部的state,也即query與list,永遠都是初始值。由於設爲空數組後,其內部的 useEffect 中的滾動監聽函數 內執行的 fetchAndSetQuery函數,其內部的query與list,也一直是第一次render時 useState 返回的值。

而若是不是空數組,每次render後,useBottom 中的滾動監聽函數,會從新解綁舊函數,綁定新函數。新的函數帶來的是 最新一次render時,useState 返回的最新狀態值,故而實現正確的邏輯。

正確認識依賴項

因而咱們更能深入的認識到,爲何useEffect的依賴項設置如此重要。其實並不是是設置依賴項後,依賴變動會觸發effect。而是effect本應該每次render都觸發,但由於effect內部依賴了外部數據,外部數據不變則內部effect執行無心義。所以只有當外部數據變動時,effect纔會從新觸發。

因此科學的來講,只要內部使用了某個外部變量,函數也好、變量也好,都應該填寫到依賴配置中。因此咱們上述編寫的 useBottom 與使用方法其實並不嚴謹,咱們再review一遍:

const useBottom = (action, dependencies) => {
  function doInBottom() {
    // isScrollToBottom 的代碼不貼了
    return isScrollToBottom() && action();
  }
  useEffect(() => {
    window.addEventListener('scroll', doInBottom);
    // useEffect內部return的方法,會在下一次render時執行
    return () => {
      window.removeEventListener('scroll', doInBottom);
    };
  }, dependencies);
};

const usePagination = (fetchApi) => {
  // ...
  useBottom(fetchAndSetQuery);
  // ...
};
複製代碼

咱們能夠明確知道兩點不對的:

  1. 在這個場景中,useEffect明確依賴了doInBottom ,所以,useEffect的依賴項至少應該填寫 doInBottom 。固然,咱們也選擇把 doInBootom 寫到useEffect內部中,這樣這個函數就成了內部引用,而不是外部依賴。
  2. action 是一個未知的函數,其內部可能包含了外部依賴,咱們傳遞的 dependencies 應該是知足action 的明確依賴的,而不是本身瞎想究竟是不填仍是空數組。固然,更粗暴的方法是,直接把 action 做爲依賴項。

因此最終科學的代碼應該是:

const useBottom = (action) => {
  useEffect(() => {
    function doInBottom() {
      return isScrollToBottom() && action();
    }
    window.addEventListener('scroll', doInBottom);
    return () => {
      window.removeEventListener('scroll', doInBottom);
    };
  }, [action]);
};

const usePagination = (fetchApi) => {
  // ...
  useBottom(fetchAndSetQuery);
  // ...
};

複製代碼

偏要勉強

仍是有些同窗,不喜歡這個依賴項,嫌傳來傳去的太麻煩,那有沒有辦法不傳?仍是有一些辦法的。

首先,useState 返回的setState能夠接受一個函數,函數的入參便是當前最新的狀態值。在剛剛滾動加載的例子中,就能夠避免了 list 成爲反作用的依賴。不過 query 依舊沒辦法,由於請求數據須要最新狀態值。但若是咱們每一頁數據的數量是固定的,咱們能夠把頁碼狀態封裝在請求方法裏,如:

// 利用閉包維持分頁狀態
const fetchNextPage = ({ initPage, size }) => {
  let page = initPage - 1;
  return () => fetchList({ page: page + 1, size }).then((rs) => {
    page += 1;
    return rs;
  });
};
複製代碼

而後咱們的 useBottom 能夠真的無論關心依賴了,只須要第一次render時綁定滾動事件便可,代碼以下:

const useBottom = (action, dependencies) => {
  useEffect(() => {
    function doInBottom() {
      return isScrollToBottom() && action();
    }
    window.addEventListener('scroll', doInBottom);
    return () => {
      window.removeEventListener('scroll', doInBottom);
    };
  }, dependencies);
};

const usePagination = (fetchApi) => {
  const [list, setList] = useState([]); // 初始列表數據爲空數組: []

  const fetchData = () => {
    fetchApi()
    .then((newList) => {
      setList(oldList => [...oldList, ...newList]);
    })
    .catch(() => window.console.log('加載失敗,請重試'));
  };
  useEffect(fetchData, []);
  useBottom(fetchData, []);

  return [list];
};

export default function List() {
  const [list] = usePagination(fetchNextPage({ initPage: 1, size: 50 }));
  return (...略);
}
複製代碼

其實useState返回的setState還有一個小弊端。若是頁面狀態較多,在某些異步行爲(請求、定時器等)的回調中的setState是不會合並更新的(具體可自行研究react狀態更新事務機制)。那分散的setState會帶來屢次render,這必然不是咱們想看到的。

解決辦法就是 useReducer ,其執行後返回 [state, dispatch] ,基本相似redux中的reducer。其中state是複雜狀態的合集,dispatch觸發reducer後,返回一個全新的狀態值。具體用法能夠見文檔。其中主要記住兩點:

  1. dispatch自己是穩定的,不會隨屢次render而致使變化,且dispatch觸發的reducer函數,其入參的state始終是當下最新值。因此如果新狀態的設置依賴於舊狀態值,經過dispatch來更新,也能夠避免effect依賴外部state。
  2. useReducer 返回的state(並不是reducer函數中的入參state),依舊遵循useState那套邏輯,每次render中獲取的都是全新值而非同一個引用。

既然有了useReducer,那有沒有 useRedux 呢?抱歉,並無。不過 Redux 目前已有issue在討論其hooks的實現了。也有外國網友作了一個簡版的 useRedux,實現機制也很是簡單,本身也能維護。若是有全局狀態管理的需求,也能夠作一下代碼的搬運工。

相信在19年,將會有不少基於hooks的工具甚至是hooks庫的出現。經過對狀態邏輯的抽象、更方便的狀態管理、更科學的函數組合與拆分,最開始所說的動機第二點「難以理解的複雜組件」在未來可能真的能夠更好的避免。

總結

探到這裏,我我的對hooks已經基本有個數了。它脫離了我傳統的class組件開發方式,對state的定義也不一樣於組件中的this.state,對effect的概念與處理須要更加清晰明瞭。

使用hooks的明顯好處是能夠更好的抽象包含狀態的邏輯,隱藏的一些功能是基於hooks的各類花式輪子。固然其「很差的地方」是有一個明顯的認知與學習成本,若是寫的很差,更容易出現性能問題。總體而言,這雖然比不上幾年前 直接操做dom 躍遷到** 數據驅動DOM** 這樣的革命性變動,但確實是react內部明顯的革命性成就。

不知道各位看完之後,將來是傾向於 函數式組件+hooks 仍是傾向於 class組件。能夠在評論區進行一下小投票。就我我的而言,我站hooks。

關於React反作用

有些同窗可能會對「反作用」這個概念不理解。我簡單的說一下個人見解。不少人都看過一個React公式

UI = F(props)

翻譯成普通話就是:一個組件最終的dom結構與樣式是由父級傳遞的props決定的。

瞭解過函數式編程的同窗,應該知道過一個概念,叫「純函數」。意思是固定的輸入必然有固定的輸出,它不依賴任何外部因素,也不會對外部環境產生影響。

react但願本身的組件渲染也是個純函數,因此有了純函數組件。然而真正的業務場景是有各類狀態的,實際影響UI的還有內部的state。(其實還有context,暫時先不討論)。

UI = F(props, state, context)

這個state可能會由於各類緣由產生變化,從而致使組件的渲染結果不一致。相同的入參(props)下,每次render都有可能返回不一樣的UI。所以任何致使此現象的行爲都是反作用(side effects)。好比用戶點擊下一頁,致使頁碼與列表發生變化,這就是反作用。一樣的props,不點擊時是第一頁數據,點擊一下後,變成了第二頁的數據or請求失敗的頁面or其餘UI交互。

固然state是明面上影響了UI,暗地裏,可能還有其餘因素會影響UI。好比組件內運用了緩存,致使每次渲染可能都不同,這也是反作用。


關於咱們:

咱們是螞蟻保險體驗技術團隊,來自螞蟻金服保險事業羣。咱們是一個年輕的團隊(沒有歷史技術棧包袱),目前平均年齡92年(去除一個最高分8x年-團隊leader,去除一個最低分97年-實習小老弟)。咱們支持了阿里集團幾乎全部的保險業務。18年咱們產出的相互寶轟動保險界,19年咱們更有多個重量級項目籌備動員中。現伴隨着事業羣的高速發展,團隊也在迅速擴張,歡迎各位前端高手加入咱們~

咱們但願你是:技術上基礎紮實、某領域深刻(Node/互動營銷/數據可視化等);學習上善於沉澱、持續學習;性格上樂觀開朗、活潑外向。

若有興趣加入咱們,歡迎發送簡歷至郵箱:fengxiang.zfx@antfin.com


本文做者:螞蟻保險-體驗技術組-阿相

掘金地址:相學長

相關文章
相關標籤/搜索