代碼分割
使用Webpack或者Browserify這樣的打包工具,最終會生成一個bundle.js钻趋,會一次性把代碼都加載進來,但是隨著項目的不斷擴大, 一次性加載所有文件導(dǎo)致加載時間過長鞭铆。為了避免搞出大體積的代碼包,在前期就思考該問題并對代碼包進行分割是個不錯的選擇。代碼分割是由諸如 Webpack(代碼分割)和 Browserify(factor-bundle)這類打包器支持的一項技術(shù)车遂,能夠創(chuàng)建多個包并在運行時動態(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 語法而不是將其進行轉(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 目前只支持默認導(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ù)都是自定向上译断,但是由于項目的不斷擴大详囤,組件的層級也不斷加深,有些數(shù)據(jù)是應(yīng)該被共享的而不應(yīng)該镐作,一層層傳遞(維護成本太高)藏姐,比如:主題顏色、用戶信息该贾、定位地區(qū)等羔杨。
如何使用:
// Context 可以讓我們無須明確地傳遍每一個組件,就能將值深入傳遞進組件樹杨蛋。
// 為當(dāng)前的 theme 創(chuàng)建一個 context(“l(fā)ight”為默認值)兜材。
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)生的錯誤:
- 事件處理(了解更多)
- 異步代碼(例如
setTimeout
或requestAnimationFrame
回調(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除抛。來進行操作,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 進行了特殊處理返十。如果你對 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ā)布訂閱忙菠,甚至更多的組件需要一個相似的功能何鸡,如果每次我們都是在每個組件里寫的話維護成本太高,效率太低牛欢,我們希望我們只寫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() {
// ...負責(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ā)強制動畫仿荆。
- 集成第三方 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
文件中,有許多配置項用于配置編譯器吠昭。查看所有配置項的的詳細說明喊括,請參考此文檔。
- 首先矢棚,讓我們重新整理下項目目錄郑什,把所有的源代碼放入 src 目錄中。
- 其次蒲肋,我們將通過配置項告訴編譯器源碼和輸出的位置蘑拯。
// tsconfig.json
{
"compilerOptions": {
// ...
"rootDir": "src",
"outDir": "build"
// ...
},
}
類型定義
為了能夠顯示來自其他包的錯誤和提示劫拢,編譯器依賴于聲明文件。聲明文件提供有關(guān)庫的所有類型信息强胰。這樣舱沧,我們的項目就可以用上像 npm 這樣的平臺提供的三方 JavaScript 庫。
Bundled
DefinitelyTyped :DefinitelyTyped 是一個龐大的聲明倉庫偶洋,為沒有聲明文件的 JavaScript 庫提供類型定義熟吏。這些類型定義通過眾包的方式完成,并由微信和開源貢獻者一起管理玄窝。例如牵寺,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)格模式檢查黎休。但是,ComponentOne 和 ComponentTwo 以及它們的所有后代元素都將進行檢查玉凯。
StrictMode
目前有助于:
使用 PropTypes 進行類型檢查
PropTypes
提供一系列驗證器势腮,可用于確保組件接收到的數(shù)據(jù)類型是有效的。在本例中, 我們使用了 PropTypes.string
漫仆。當(dāng)傳入的 prop
值類型不正確時捎拯,JavaScript 控制臺將會顯示警告。出于性能方面的考慮盲厌,propTypes
僅在開發(fā)模式下進行檢查署照。