React上手 —— 進(jìn)階篇

代碼分割

使用Webpack或者Browserify這樣的打包工具,最終會生成一個bundle.js,會一次性把代碼都加載進(jìn)來,但是隨著項目的不斷擴(kuò)大, 一次性加載所有文件導(dǎo)致加載時間過長。為了避免搞出大體積的代碼包,在前期就思考該問題并對代碼包進(jìn)行分割是個不錯的選擇。代碼分割是由諸如 Webpack(代碼分割)和 Browserify(factor-bundle)這類打包器支持的一項技術(shù),能夠創(chuàng)建多個包并在運(yùn)行時動態(tài)加載。

import

import("./math").then(math => {
  console.log(math.add(16, 26));
});

如果你自己配置 Webpack,你可能要閱讀下 Webpack 關(guān)于代碼分割的指南。你的 Webpack 配置應(yīng)該類似于此

當(dāng)使用 Babel 時,你要確保 Babel 能夠解析動態(tài) import 語法而不是將其進(jìn)行轉(zhuǎn)換。對于這一要求你需要 babel-plugin-syntax-dynamic-import 插件。

React.lazy 函數(shù)能讓你像渲染常規(guī)組件一樣處理動態(tài)引入(的組件)。

const OtherComponent = React.lazy(() => import('./OtherComponent'));

function MyComponent() {
  return (
    <div>
      <OtherComponent />
    </div>
  );
}

Suspense

const OtherComponent = React.lazy(() => import('./OtherComponent'));

function MyComponent() {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        <OtherComponent />
      </Suspense>
    </div>
  );
}

如果還沒有加載完可以這么操作。

異常捕獲邊界(Error boundaries)

import MyErrorBoundary from './MyErrorBoundary';
const OtherComponent = React.lazy(() => import('./OtherComponent'));
const AnotherComponent = React.lazy(() => import('./AnotherComponent'));

const MyComponent = () => (
  <div>
    <MyErrorBoundary>
      <Suspense fallback={<div>Loading...</div>}>
        <section>
          <OtherComponent />
          <AnotherComponent />
        </section>
      </Suspense>
    </MyErrorBoundary>
  </div>
);

React.lazy 目前只支持默認(rèn)導(dǎo)出(default exports)。如果需要使用命名導(dǎo)出需要增加中間模塊:

// ManyComponents.js
export const MyComponent = /* ... */;
export const MyUnusedComponent = /* ... */;
// MyComponent.js
export { MyComponent as default } from "./ManyComponents.js";
// MyApp.js
import React, { lazy } from 'react';
const MyComponent = lazy(() => import("./MyComponent.js"));

Context

從基礎(chǔ)篇我們可以看見,數(shù)據(jù)都是自定向上,但是由于項目的不斷擴(kuò)大,組件的層級也不斷加深,有些數(shù)據(jù)是應(yīng)該被共享的而不應(yīng)該,一層層傳遞(維護(hù)成本太高),比如:主題顏色、用戶信息、定位地區(qū)等。
如何使用:

// Context 可以讓我們無須明確地傳遍每一個組件,就能將值深入傳遞進(jìn)組件樹。
// 為當(dāng)前的 theme 創(chuàng)建一個 context(“l(fā)ight”為默認(rèn)值)。
const ThemeContext = React.createContext('light');

class App extends React.Component {
  render() {
    // 使用一個 Provider 來將當(dāng)前的 theme 傳遞給以下的組件樹。
    // 無論多深,任何組件都能讀取這個值。
    // 在這個例子中,我們將 “dark” 作為當(dāng)前的值傳遞下去。
    return (
      <ThemeContext.Provider value="dark">
        <Toolbar />
      </ThemeContext.Provider>
    );
  }
}

// 中間的組件再也不必指明往下傳遞 theme 了。
function Toolbar(props) {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

class ThemedButton extends React.Component {
  // 指定 contextType 讀取當(dāng)前的 theme context。
  // React 會往上找到最近的 theme Provider,然后使用它的值。
  // 在這個例子中,當(dāng)前的 theme 值為 “dark”。
  static contextType = ThemeContext;
  render() {
    return <Button theme={this.context} />;
  }
}

還有一種情況是,componentA 渲染 componentB ,componentB 渲染 componentC, componentC 渲染 componentD,而控制組件數(shù)據(jù)的是A,最終渲染的是D,這樣的情況不需要Context而用組合組件是更優(yōu)雅的方式:

function Page(props) {
  const user = props.user;
  const userLink = (
    <Link href={user.permalink}>
      <Avatar user={user} size={props.avatarSize} />
    </Link>
  );
  return <PageLayout userLink={userLink} />;
}

// 現(xiàn)在,我們有這樣的組件:
<Page user={user} avatarSize={avatarSize} />
// ... 渲染出 ...
<PageLayout userLink={...} />
// ... 渲染出 ...
<NavigationBar userLink={...} />
// ... 渲染出 ...
{props.userLink}

即在A里就定義好組件D,將組件D一層一層傳遞下去。
但是如果是很多組件,不同層級需要相同數(shù)據(jù)還是使用Context比較好。

錯誤邊界

注意
錯誤邊界無法捕獲以下場景中產(chǎn)生的錯誤:

  • 事件處理(了解更多
  • 異步代碼(例如 setTimeoutrequestAnimationFrame 回調(diào)函數(shù))
  • 服務(wù)端渲染
  • 它自身拋出來的錯誤(并非它的子組件)

請使用 static getDerivedStateFromError() 渲染備用 UI ,使用 componentDidCatch() 打印錯誤信息。

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    // Update state so the next render will show the fallback UI.
    return { hasError: true };
  }

  componentDidCatch(error, info) {
    // You can also log the error to an error reporting service
    logErrorToMyService(error, info);
  }

  render() {
    if (this.state.hasError) {
      // You can render any custom fallback UI
      return <h1>Something went wrong.</h1>;
    }

    return this.props.children; 
  }
}

然后你可以將它作為一個常規(guī)組件去使用:

<ErrorBoundary>
  <MyWidget />
</ErrorBoundary>

Refs 轉(zhuǎn)發(fā)

當(dāng)我們需要控制一個封裝的組件的焦點時,比如input或者button,我們需要那到這個組件的實例就是ref。來進(jìn)行操作,react提供了一個方法來來轉(zhuǎn)發(fā)ref。

const FancyButton = React.forwardRef((props, ref) => (
  <button ref={ref} className="FancyButton">
    {props.children}
  </button>
));

// 你可以直接獲取 DOM button 的 ref:
const ref = React.createRef();
<FancyButton ref={ref}>Click me!</FancyButton>;
  • 我們通過調(diào)用 React.createRef 創(chuàng)建了一個 React ref 并將其賦值給 ref 變量。
  • 我們通過指定 ref 為 JSX 屬性,將其向下傳遞給 <FancyButton ref={ref}>。
  • React 傳遞 ref 給 fowardRef 內(nèi)函數(shù) (props, ref) => ...,作為其第二個參數(shù)。
  • 我們向下轉(zhuǎn)發(fā)該 ref 參數(shù)到 <button ref={ref}>,將其指定為 JSX 屬性。
  • 當(dāng) ref 掛載完成,ref.current 將指向 <button> DOM 節(jié)點。
function logProps(WrappedComponent) {
  class LogProps extends React.Component {
    componentDidUpdate(prevProps) {
      console.log('old props:', prevProps);
      console.log('new props:', this.props);
    }

    render() {
      return <WrappedComponent {...this.props} />;
    }
  }

  return LogProps;
}

注意:refs 將不會透傳下去。這是因為 ref 不是 prop 屬性。就像 key 一樣,其被 React 進(jìn)行了特殊處理。如果你對 HOC 添加 ref,該 ref 將引用最外層的容器組件,而不是被包裹的組件。

Fragments

React 中的一個常見模式是一個組件返回多個元素。Fragments 允許你將子列表分組,而無需向 DOM 添加額外節(jié)點。

render() {
  return (
    <React.Fragment>
      <ChildA />
      <ChildB />
      <ChildC />
    </React.Fragment>
  );
}

最終不會渲染Fragment,只有children。

高階組件

高階組件(HOC)是 React 中用于復(fù)用組件邏輯的一種高級技巧。HOC 自身不是 React API 的一部分,它是一種基于 React 的組合特性而形成的設(shè)計模式。

HOC 是純函數(shù),沒有副作用。

比如A組件需要發(fā)布訂閱,組件B需要發(fā)布訂閱,甚至更多的組件需要一個相似的功能,如果每次我們都是在每個組件里寫的話維護(hù)成本太高,效率太低,我們希望我們只寫base組件,而用高級組件給包裹一下就能都擁有這個邏輯:

我們可以編寫一個創(chuàng)建組件的函數(shù)(高級函數(shù)),比如 CommentList 和 BlogPost,訂閱 DataSource。該函數(shù)將接受一個子組件作為它的其中一個參數(shù),該子組件將訂閱數(shù)據(jù)作為 prop。讓我們調(diào)用函數(shù) withSubscription:

const CommentListWithSubscription = withSubscription(
  CommentList,
  (DataSource) => DataSource.getComments()
);

const BlogPostWithSubscription = withSubscription(
  BlogPost,
  (DataSource, props) => DataSource.getBlogPost(props.id)
);

當(dāng)渲染 CommentListWithSubscription 和 BlogPostWithSubscription 時, CommentList 和 BlogPost 將傳遞一個 data prop,其中包含從 DataSource 檢索到的最新數(shù)據(jù):

// 此函數(shù)接收一個組件...
function withSubscription(WrappedComponent, selectData) {
  // ...并返回另一個組件...
  return class extends React.Component {
    constructor(props) {
      super(props);
      this.handleChange = this.handleChange.bind(this);
      this.state = {
        data: selectData(DataSource, props)
      };
    }

    componentDidMount() {
      // ...負(fù)責(zé)訂閱相關(guān)的操作...
      DataSource.addChangeListener(this.handleChange);
    }

    componentWillUnmount() {
      DataSource.removeChangeListener(this.handleChange);
    }

    handleChange() {
      this.setState({
        data: selectData(DataSource, this.props)
      });
    }

    render() {
      // ... 并使用新數(shù)據(jù)渲染被包裝的組件!
      // 請注意,我們可能還會傳遞其他屬性
      return <WrappedComponent data={this.state.data} {...this.props} />;
    }
  };
}

Portals

Portal 提供了一種將子節(jié)點渲染到存在于父組件以外的 DOM 節(jié)點的優(yōu)秀的方案。
第一個參數(shù)(child)是任何可渲染的 React 子元素,例如一個元素,字符串或 fragment。第二個參數(shù)(container)是一個 DOM 元素。
這包含事件冒泡。一個從 portal 內(nèi)部觸發(fā)的事件會一直冒泡至包含 React 樹的祖先,即便這些元素并不是 DOM 樹 中的祖先。假設(shè)存在如下 HTML 結(jié)構(gòu):

<html>
  <body>
    <div id="app-root"></div>
    <div id="modal-root"></div>
  </body>
</html>

在 #app-root 里的 Parent 組件能夠捕獲到未被捕獲的從兄弟節(jié)點 #modal-root 冒泡上來的事件。

// 在 DOM 中有兩個容器是兄弟級 (siblings)
const appRoot = document.getElementById('app-root');
const modalRoot = document.getElementById('modal-root');

class Modal extends React.Component {
  constructor(props) {
    super(props);
    this.el = document.createElement('div');
  }

  componentDidMount() {
    // 在 Modal 的所有子元素被掛載后,
    // 這個 portal 元素會被嵌入到 DOM 樹中,
    // 這意味著子元素將被掛載到一個分離的 DOM 節(jié)點中。
    // 如果要求子組件在掛載時可以立刻接入 DOM 樹,
    // 例如衡量一個 DOM 節(jié)點,
    // 或者在后代節(jié)點中使用 ‘a(chǎn)utoFocus’,
    // 則需添加 state 到 Modal 中,
    // 僅當(dāng) Modal 被插入 DOM 樹中才能渲染子元素。
    modalRoot.appendChild(this.el);
  }

  componentWillUnmount() {
    modalRoot.removeChild(this.el);
  }

  render() {
    return ReactDOM.createPortal(
      this.props.children,
      this.el,
    );
  }
}

class Parent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {clicks: 0};
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    // 當(dāng)子元素里的按鈕被點擊時,
    // 這個將會被觸發(fā)更新父元素的 state,
    // 即使這個按鈕在 DOM 中不是直接關(guān)聯(lián)的后代
    this.setState(state => ({
      clicks: state.clicks + 1
    }));
  }

  render() {
    return (
      <div onClick={this.handleClick}>
        <p>Number of clicks: {this.state.clicks}</p>
        <p>
          Open up the browser DevTools
          to observe that the button
          is not a child of the div
          with the onClick handler.
        </p>
        <Modal>
          <Child />
        </Modal>
      </div>
    );
  }
}

function Child() {
  // 這個按鈕的點擊事件會冒泡到父元素
  // 因為這里沒有定義 'onClick' 屬性
  return (
    <div className="modal">
      <button>Click</button>
    </div>
  );
}

ReactDOM.render(<Parent />, appRoot);

Refs and the DOM

Refs 提供了一種方式,允許我們訪問 DOM 節(jié)點或在 render 方法中創(chuàng)建的 React 元素。
何時使用 Refs:

  • 下面是幾個適合使用 refs 的情況:
  • 管理焦點,文本選擇或媒體播放。
  • 觸發(fā)強(qiáng)制動畫。
  • 集成第三方 DOM 庫。

創(chuàng)建refs:

16.3以后:

class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.myRef = React.createRef();
  }
  render() {
    return <div ref={this.myRef} />;
  }
}

16.3以前:回調(diào)創(chuàng)建

class CustomTextInput extends React.Component {
  constructor(props) {
    super(props);

    this.textInput = null;

    this.setTextInputRef = element => {
      this.textInput = element;
    };

    this.focusTextInput = () => {
      // 使用原生 DOM API 使 text 輸入框獲得焦點
      if (this.textInput) this.textInput.focus();
    };
  }

  componentDidMount() {
    // 組件掛載后,讓文本框自動獲得焦點
    this.focusTextInput();
  }

  render() {
    // 使用 `ref` 的回調(diào)函數(shù)將 text 輸入框 DOM 節(jié)點的引用存儲到 React
    // 實例上(比如 this.textInput)
    return (
      <div>
        <input
          type="text"
          ref={this.setTextInputRef}
        />
        <input
          type="button"
          value="Focus the text input"
          onClick={this.focusTextInput}
        />
      </div>
    );
  }
}

在 componentDidMount 或 componentDidUpdate 觸發(fā)前,React 會保證 refs 一定是最新的。
你可以在組件間傳遞回調(diào)形式的 refs,就像你可以傳遞通過 React.createRef() 創(chuàng)建的對象 refs 一樣。

function CustomTextInput(props) {
  return (
    <div>
      <input ref={props.inputRef} />
    </div>
  );
}

class Parent extends React.Component {
  render() {
    return (
      <CustomTextInput
        inputRef={el => this.inputElement = el}
      />
    );
  }
}

訪問Refs

  • 原生元素:接受底層DOM作為current屬性
  • class組件:接受組件實例作為current屬性
  • 函數(shù)組件:不能在函數(shù)組件上創(chuàng)建refs

Render Props

術(shù)語 “render prop” 是指一種在 React 組件之間使用一個值為函數(shù)的 prop 共享代碼的簡單技術(shù)
解決什么問題需要這個Render Props呢?
比如,我有一個鼠標(biāo)組件,他會記錄每次用戶的鼠標(biāo)位置:

// <Mouse> 組件封裝了我們需要的行為...
class Mouse extends React.Component {
  constructor(props) {
    super(props);
    this.handleMouseMove = this.handleMouseMove.bind(this);
    this.state = { x: 0, y: 0 };
  }

  handleMouseMove(event) {
    this.setState({
      x: event.clientX,
      y: event.clientY
    });
  }

  render() {
    return (
      <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>

        {/* ...但我們?nèi)绾武秩?<p> 以外的東西? */}
        <p>The current mouse position is ({this.state.x}, {this.state.y})</p>
      </div>
    );
  }
}

class MouseTracker extends React.Component {
  render() {
    return (
      <div>
        <h1>移動鼠標(biāo)!</h1>
        <Mouse />
      </div>
    );
  }
}

現(xiàn)在我們需要實現(xiàn),鼠標(biāo)移動的時候有一只貓跟著鼠標(biāo),又或者其他組件會跟隨鼠標(biāo),那么他們都需要鼠標(biāo)的x,y。 如果僅僅只是一只貓跟著鼠標(biāo),那么還好,我們把鼠標(biāo)和貓的代碼寫在一起就行了:

class Cat extends React.Component {
  render() {
    const mouse = this.props.mouse;
    return (
      <img src="/cat.jpg" style={{ position: 'absolute', left: mouse.x, top: mouse.y }} />
    );
  }
}

class MouseWithCat extends React.Component {
  constructor(props) {
    super(props);
    this.handleMouseMove = this.handleMouseMove.bind(this);
    this.state = { x: 0, y: 0 };
  }

  handleMouseMove(event) {
    this.setState({
      x: event.clientX,
      y: event.clientY
    });
  }

  render() {
    return (
      <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>

        {/*
          我們可以在這里換掉 <p> 的 <Cat>   ......
          但是接著我們需要創(chuàng)建一個單獨的 <MouseWithSomethingElse>
          每次我們需要使用它時,<MouseWithCat> 是不是真的可以重復(fù)使用.
        */}
        <Cat mouse={this.state} />
      </div>
    );
  }
}

class MouseTracker extends React.Component {
  render() {
    return (
      <div>
        <h1>移動鼠標(biāo)!</h1>
        <MouseWithCat />
      </div>
    );
  }
}

但是問題就在于有很多組件都需要x,y這倆值,如果我們都這么寫毫無復(fù)用性可言,這個時候就出現(xiàn)了這個技術(shù),render props:

class Cat extends React.Component {
  render() {
    const mouse = this.props.mouse;
    return (
      <img src="/cat.jpg" style={{ position: 'absolute', left: mouse.x, top: mouse.y }} />
    );
  }
}

class Mouse extends React.Component {
  constructor(props) {
    super(props);
    this.handleMouseMove = this.handleMouseMove.bind(this);
    this.state = { x: 0, y: 0 };
  }

  handleMouseMove(event) {
    this.setState({
      x: event.clientX,
      y: event.clientY
    });
  }

  render() {
    return (
      <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>

        {/*
          Instead of providing a static representation of what <Mouse> renders,
          use the `render` prop to dynamically determine what to render.
        */}
        {this.props.render(this.state)}
      </div>
    );
  }
}

class MouseTracker extends React.Component {
  render() {
    return (
      <div>
        <h1>移動鼠標(biāo)!</h1>
        <Mouse render={mouse => (
          <Cat mouse={mouse} />
        )}/>
      </div>
    );
  }
}

這樣就實現(xiàn)了x,y的共享。
除此之外,我們并不一定要用render來命名,我們也可以用其他屬性名,甚至是children:

render() {
    return (
      <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>

        {/*
          Instead of providing a static representation of what <Mouse> renders,
          use the `render` prop to dynamically determine what to render.
        */}
        {this.props.children(this.state)}
      </div>
    );
  }
<div>
        <h1>移動鼠標(biāo)!</h1>
        <Mouse>
          {mouse => (
          <Cat mouse={mouse} />
        )}
        </Mouse>
</div>

Typescript

在 Create React App 中使用 TypeScript

npx create-react-app my-app --typescript

如需將 TypeScript 添加到現(xiàn)有的 Create React App 項目中,請參考此文檔.

添加 TypeScript 到現(xiàn)有項目中

  • 安裝typescript
npm install --save-dev typescript

恭喜!你已將最新版本的 TypeScript 安裝到項目中。安裝 TypeScript 后我們就可以使用 tsc 命令。在配置編譯器之前,讓我們將 tsc 添加到 package.json 中的 “scripts” 部分:

"scripts": {
    "build": "tsc",
    // ...
  },
  • 配置 TypeScript 編譯器
npx tsc --init

tsconfig.json 文件中,有許多配置項用于配置編譯器。查看所有配置項的的詳細(xì)說明,請參考此文檔。

  • 首先,讓我們重新整理下項目目錄,把所有的源代碼放入 src 目錄中。
  • 其次,我們將通過配置項告訴編譯器源碼和輸出的位置。
// tsconfig.json

{
  "compilerOptions": {
    // ...
    "rootDir": "src",
    "outDir": "build"
    // ...
  },
}

類型定義

為了能夠顯示來自其他包的錯誤和提示,編譯器依賴于聲明文件。聲明文件提供有關(guān)庫的所有類型信息。這樣,我們的項目就可以用上像 npm 這樣的平臺提供的三方 JavaScript 庫。

Bundled
DefinitelyTyped :DefinitelyTyped 是一個龐大的聲明倉庫,為沒有聲明文件的 JavaScript 庫提供類型定義。這些類型定義通過眾包的方式完成,并由微信和開源貢獻(xiàn)者一起管理。例如,React 庫并沒有自己的聲明文件。但我們可以從 DefinitelyTyped 獲取它的聲明文件。只要執(zhí)行以下命令。

# yarn
yarn add --dev @types/react

# npm
npm i --save-dev @types/react

你現(xiàn)在已做好編碼準(zhǔn)備了!我們建議你查看以下資源來了解有關(guān) TypeScript 的更多知識:

嚴(yán)格模式

import React from 'react';

function ExampleApplication() {
  return (
    <div>
      <Header />
      <React.StrictMode>
        <div>
          <ComponentOne />
          <ComponentTwo />
        </div>
      </React.StrictMode>
      <Footer />
    </div>
  );
}

在上述的示例中,不會對 Header 和 Footer 組件運(yùn)行嚴(yán)格模式檢查。但是,ComponentOne 和 ComponentTwo 以及它們的所有后代元素都將進(jìn)行檢查。
StrictMode 目前有助于:

使用 PropTypes 進(jìn)行類型檢查

PropTypes 提供一系列驗證器,可用于確保組件接收到的數(shù)據(jù)類型是有效的。在本例中, 我們使用了 PropTypes.string。當(dāng)傳入的 prop 值類型不正確時,JavaScript 控制臺將會顯示警告。出于性能方面的考慮,propTypes 僅在開發(fā)模式下進(jìn)行檢查。

參考文獻(xiàn)

https://react.docschina.org/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 深入JSX date:20170412筆記原文其實JSX是React.createElement(componen...
    gaoer1938閱讀 8,185評論 2 35
  • 歡迎訪問我的最佳實踐網(wǎng)站 一個動態(tài)導(dǎo)入加載組件的高階組件. 示例 用戶反饋: "我現(xiàn)在非常癡迷于: create-...
    lxg1986閱讀 26,655評論 0 24
  • 作為一個合格的開發(fā)者,不要只滿足于編寫了可以運(yùn)行的代碼。而要了解代碼背后的工作原理;不要只滿足于自己的程序...
    六個周閱讀 8,681評論 1 33
  • 離開那片霞紅之地,你將繼續(xù)去編織你青春里的某一段時光。在等待你的綠皮小火車到來之時,我們休憩的地方,聽“青春又醉倒...
    小芥閱讀 737評論 8 7
  • 你渴了, 我是離你最近的一杯茶。 你干渴的唇是我的克星, 我終于被你一飲而盡。 你卻沒有解渴, 我只剩一只空空的杯...
    相對兩無厭不止敬亭山閱讀 433評論 0 4

友情鏈接更多精彩內(nèi)容