(Webpack 4.0+, React 16.0.0+, Babel 7+)

作者: 趙瑋龍

寫在開頭: 在懷著激動和忐忑的心情寫出團隊第一篇文章時, 這個興奮感一方面來自團隊組建以來這是我們首次對外部開啟一扇窗, 另一方面我們也會持續聽取意見,維持一個交流的心態。

自 React 在 master 分支2017。09。27更新了16。0。0以來, 到至今為止發過多個版本(雖然 fiber 演算法帶來的非同步載入還沒有開放穩定版本 API, 但是不遠啦。。。)

但是除去這個我們翹首以盼的改變外, 也同樣有很多我們值得一提的東西。

結合 Webpack 4。0, Babel 7 我們會在這裡實現一個基本滿足日常開發需求的前端腳手架

(有亮點哦!! 我們自己實現了我們自己的 react-loadable 和 react-redux 的功能借助新特性)

我們先從編譯檔案開始

我們看看 Babel 7 和 Webpack 4 給我的編譯和構建帶來那些便利。

以往的。babelrc都離不開``babel-preset-es20**``包括``stage-*``等級的配置, 在新的版本里作者覺得這些過於繁瑣, 乾脆直接支援最新版本好啦

(可以看看他們的調研和理由)

於是我們的 。babelrc 就變成這樣啦:

{

“presets”

“@babel/preset-env”

,{

“modules”

false

// 依然是對於webpack的tree-shaking相容做法

}],

“@babel/preset-react”

“@babel/preset-stage-0”

],

“plugins”

“@babel/plugin-syntax-dynamic-import”

],

}

很容易發現 react 還是需要單獨配置的 stage-0 只有0級的規範啦, 支援新的原生 api 還是需要 syntax-dynamic-import 這個存在。還有個問題可能你也注意到了, 所有 Babel 7 的 Packages 都是這麼寫的(@babel/x), 原因在

(這裡)

也有。

再來說說Webpack 4的一些改變

首先說說最大改變可能也是 parcel 出現0配置給本身配置就比較繁瑣的 webpack 更多壓力了,

這回官方破釜沉舟的也推出0配置選項。

使用方式提供 cli 模式, 當然你也可以在配置檔案中宣告, 我們後面會指出

webpack ——mode production

webpack ——mode development

那麼這個預設模式裡會包含以往哪些配置選項

官網是這麼解釋的development 環境包含:

瀏覽器 debugging 的工具(預設設定了 devtool)

更快的編譯環境週期(設定 cache)

執行過程中有用的報錯資訊

production 環境包含:

檔案輸出大小壓縮( ugliy 處理)

更快的打包時間

設定全域性環境變數 production

不暴露原始碼和檔案路徑

容易使用的 output 資源(會有很多類似於 hosting 內部程式碼編譯後最佳化預設使用)

(兩種模式甚至於還幫你預設設定了入口 entry 和 output 路徑, 但是為了配置的易讀性和可配置性我們還是留給我們自己設定比較好)

還有一個重要的改變是官方廢棄掉了 CommonsChunkPlugin 這個外掛

原因有如下:

官方認為首先這個 api 不容易理解並且不好用

並且提取公共檔案中含有大量的冗餘程式碼

在做非同步載入的時候這個檔案必須每次都首先載入

(這麼看來廢棄也確實理所應當啦!)

取而代之的是現在預設就支援的 code-splitting(只要你採用動態載入的 api => import())

webpack 會預設幫你做程式碼拆分並且非同步載入, 並且不受上面提到mode模式的限制(意味著 mode 為 none 也是可以 work 的, 這就是所謂的拆包即用了吧!)

寫法如下:

const

Contract

=

asyncRoute

(()

=>

import

‘。/pages/contract’

),

{

loading

Loading

})

上面的寫法看起來有點怪, 正常的寫法直接應該是 import 返回一個 promise:

import

/* webpackChunkName: “lodash” */

‘lodash’

)。

then

_

=>

{

var

element

=

document

createElement

‘div’

element

innerHTML

=

_

join

([

‘Hello’

‘webpack’

],

‘ ’

return

element

})。

catch

error

=>

‘An error occurred while loading the component’

但是我們返回的是個 React 的 component, 所以需要做一些處理, 並且在非同步載入的時候因為是發起一次網路請求你可能還會需要一個友好地loading介面(非同步載入的具體細粒度也需要你自己確定, 比較常見的是根據頁面 route 去請求自己的 container 然後載入頁面裡的相應 component)

這裡我們自己封裝了這個 asyncRoute, 它的作用除去返回給我們一個正常的 component 之外, 我們還可以給他傳遞一個 loading,用來處理 loading 介面和請求過程中捕獲的 error 資訊, 如果我們需要支援 ssr, 還需要給個特殊標記用以做不同的處理, 廢話不多說上程式碼如何實現這個 asyncRoute

// 這裡是它的用法

// e。x author: zhaoweilong

// const someRouteContainer = asyncRoute(() => import(‘。。/componet’), {

// loading: loading。。。

// })

//

// function Loading(props) {

// if (props。error) {

// return

Error!

// } else {

// return

Loading。。。

// }

// }

const

asyncRoute

=

getComponent

opts

=>

{

return

class

AsyncRoute

extends

React

Component

{

static

Component

=

null

state

=

{

Component

AsyncRoute

Component

error

null

}

componentWillMount

()

{

if

this

state

Component

{

getComponent

()

then

module

=>

module

default

||

module

then

Component

=>

{

AsyncRoute

Component

=

Component

this

setState

({

Component

})

})

catch

error

=>

{

this

setState

({

error

})

})

}

}

render

()

{

const

{

Component

error

}

=

this

state

const

loading

=

opts

loading

if

loading

&&

Component

{

return

React

createElement

loading

{

error

})

}

else

if

Component

{

return

<

Component

{。。。

this

props

}

/>

}

return

null

}

}

}

(上面的寫法不包含ssr的處理, ssr還要你把這些 component 提前載入好 preload)

說了這麼多。。。還沒說如果我們真正的webpack的配置檔案長什麼樣子:

const

path

=

require

‘path’

const

HtmlWebpackPlugin

=

require

‘html-webpack-plugin’

const

port

=

process

env

PORT

||

3000

module

exports

=

{

target

‘web’

entry

{

bundle

‘。/src/index。js’

],

},

output

{

path

path

resolve

__dirname

‘dist’

),

filename

‘[name]。js’

publicPath

‘/’

},

module

{

rules

{

test

/\。js$/

use

‘babel-loader’

exclude

/node_modules/

],

},

],

},

mode

‘development’

devtool

‘cheap-module-source-map’

//這裡需要替換掉預設的devtool設定eval為了相容後面我們提到的react 的ErrorBoundary

plugins

new

HtmlWebpackPlugin

{

filename

‘。/src/index。html’

}

),

}

可以看到我們只用了 HtmlWebpackPlugin 來動態載入編譯過後的檔案, entry 和 output 也是因為需要定製化和方便維護性, 我們自己定義配置檔案極其簡單,那麼你可能會好奇開發環境簡單, 那麼生產環境呢?

const

webpack

=

require

‘webpack’

const

devConfig

=

require

‘。/webpack。config’

const

ASSET_PATH

=

process

env

ASSET_PATH

||

‘/static/’

module

exports

=

Object

assign

devConfig

{

entry

{

bundle

‘。/src/index。js’

},

output

Object

assign

devConfig

output

{

filename

‘[name]。[chunkhash]。js’

publicPath

ASSET_PATH

}),

module

{

rules

。。。

devConfig

module

rules

},

mode

‘production’

devtool

‘none’

})

它好像更加簡單啦, 我們只需要對 output 做一些我們需要的定製化, 完全沒有外掛選項

看看我們 build 之後檔案是什麼樣子的:

那些激動人心的 React, Webpack, Babel 的新特性對於我們開發體驗帶來哪些提升

可以看到我們除去 bundle 的入口檔案之外多了0,1,2三個檔案這裡面分別提取了 react 和 index 以及非同步載入的一個路由 contract 相應 js 檔案。

搞定配置之後, 來看看激動人心的React新特性以及一些應用

我們著重介紹4個特性並且實戰3個特性

增加 ErrorBoundary 元件 catch 元件錯誤

廢棄 componentWillReceiveProps 更換為 static getDerivedStateFromProps

增加 render props 寫法

新的 context API

我們先介紹下第一個改動,

這裡 React 覺得之前的開發報錯機制過於不人性化了, 所以允許我們在元件外層包裹元件 ErrorBoundary, 而這個自定義的元件會有一個自己的生命週期 componentDidCatch 用來補貨錯誤, 我們廢話不多說來看看程式碼:

import

React

from

‘react’

import

styled

from

‘styled-components’

const

StyledBoundaryBox

=

styled

div

`

background: rgba(0,0,0,0。4);

position: fixed;

top: 0;

left: 0;

right: 0;

bottom: 0;

width: 100%;

height: 100%;

z-index: 2;

`

const

Title

=

styled

h2

`

position: relative;

padding: 0 10px;

font-size: 17px;

color: #0070c9;

z-index: 1991;

`

const

Details

=

styled

details

`

position: relative;

padding: 0 10px;

color: #bb1d1d;

z-index: 1991;

`

class

ErrorBoundary

extends

React

Component

{

state

=

{

hasError

false

error

null

errorInfo

null

}

componentDidCatch

error

info

{

this

setState

({

hasError

true

error

error

errorInfo

info

})

}

render

()

{

if

this

state

hasError

{

return

<

StyledBoundaryBox

>

<

Title

>

頁面可能存在錯誤

!<

/Title>

<

Details

>

{

this

state

error

&&

this

state

error

toString

()}

<

br

/>

{

this

state

errorInfo

componentStack

}

<

/Details>

<

/StyledBoundaryBox>

}

return

this

props

children

}

}

export

default

ErrorBoundary

把它包裹在你想 catch 的元件外層。我直接放到了最外層。

當然你可以按照Dan的做法分別catch頁面相應的部分

其實你會發現這個元件非常類似於我們 js 中的 try{}catch{} 程式碼塊, 其實確實是 React 希望這樣的開發體驗更佳接近於原生 js 的一種思路。

當有報錯的時候你會發現在詳情中有一個報錯元件的呼叫棧, 方便你去定位錯誤, 當然報錯的樣式你可以自己定義這裡過於醜陋請忽略!!!

那些激動人心的 React, Webpack, Babel 的新特性對於我們開發體驗帶來哪些提升

再來看看這個新的生命週期

首先把原先的生命週期從例項屬性更改為class的靜態屬性, 我們能想到最直觀對於我們開發者的影響是我們沒發直接呼叫 this 了。我們先看下這個 function 長什麼樣子(

如果你想詳細瞭解這個提案

//以前

class

ExampleComponent

extends

React

Component

{

state

=

{

derivedData

computeDerivedState

this

props

};

componentWillReceiveProps

nextProps

{

if

this

props

someValue

!==

nextProps

someValue

{

this

setState

({

derivedData

computeDerivedState

nextProps

});

}

}

}

//以後

class

ExampleComponent

extends

React

Component

{

state

=

{};

static

getDerivedStateFromProps

nextProps

prevState

{

if

prevState

someMirroredValue

!==

nextProps

someValue

{

return

{

derivedData

computeDerivedState

nextProps

),

someMirroredValue

nextProps

someValue

};

}

return

null

}

}

}

我們發現首先我們不需要在改變的時候 this。setState 了, 而是 return 有改變的部分(這裡就是 setState 的作用), 如果沒有 return null 其他的屬性會依舊保持原來的狀態。

它還有一個作用是之前 cwrp() 沒有的, cwrp() 只在元件 props update 時候更新。

但是新的 gdsfp() 確在首次掛在 inital mount 的時候也會走, 你可能會覺得很奇怪我以前明明習慣使用

(this。props 和nextProps)

做判斷為何現在非要放到 state 裡去判斷呢, 我們可以從這個 api 的名字看出從 state 取得 props 也就是希望你能存一份 props 到 state 如果你需要做對比直接比之前存的和之後可能改變的 nextprops 就好啦, 後面無論是 dispatch(someAction) 還有 return{} 都可以。但是問題是如果我採用 redux 我還要存一份改變的資料在 state 而不是都在全域性的 store 中嗎? 這個地方還真是一個非常敏感並且很大的話題(因為它關係到 React 本身發展未來和相對以來這些 redux 包括 react-redux 的未來)

如果你感興趣你可以看下包括redux作者Dan和幾位核心成員的討論,很具有啟發性

, 當 api 穩定後我們後續文章也會來討論下來它的可能性。如果你持續關注我們!!!

下面我們來說下 render props 這個更新可是讓我個人很興奮的

因為它直接影響到我們在的程式設計體驗

這個概念你可以在官網詳細檢視

其實這個概念之前在react-router4中就有體現如果你還記得類似這種寫法:

<

Route

exact

path

=

‘/’

render

=

{()

=>

<

Pstyled

>

歡迎光臨

!<

/Pstyled>}

/>

如果這時候你還在用Mixins那貌似我們之間就有點gap了。之前我們談到HOC的實現一般都會想到高階元件, 但是本身它卻有一些弊端(我們來看一下):

(藉助官方一個例子)

import

React

from

‘react’

import

ReactDOM

from

‘react-dom’

const

withMouse

=

Component

=>

{

return

class

extends

React

Component

{

state

=

{

x

0

y

0

}

handleMouseMove

=

event

=>

{

this

setState

({

x

event

clientX

y

event

clientY

})

}

render

()

{

return

<

div

style

=

{{

height

‘100%’

}}

onMouseMove

=

{

this

handleMouseMove

}

>

<

Component

{。。。

this

props

}

mouse

=

{

this

state

}

/>

<

/div>

}

}

}

const

App

=

React

createClass

({

render

()

{

// Instead of maintaining our own state,

// we get the mouse position as a prop!

const

{

x

y

}

=

this

props

mouse

return

<

div

style

=

{{

height

‘100%’

}}

>

<

h1

>

The

mouse

position

is

({

x

},

{

y

})

<

/h1>

<

/div>

}

})

const

AppWithMouse

=

withMouse

App

ReactDOM

render

<

AppWithMouse

/>

document

getElementById

‘app’

))

問題一 是你不知道 hoc 中到底傳遞給你什麼改變了你的props, 如果他還是第三方的。那更是黑盒問題。

問題二 命名衝突, 因為你總會有個函式名這裡叫做 withMouse

那我們看看 render props 如果解決這兩個問題呢?

import

React

from

‘react’

import

ReactDOM

from

‘react-dom’

import

PropTypes

from

‘prop-types’

// 我們可以用普通的component來實現hoc

class

Mouse

extends

React

Component

{

static

propTypes

=

{

render

PropTypes

func

isRequired

}

state

=

{

x

0

y

0

}

handleMouseMove

=

event

=>

{

this

setState

({

x

event

clientX

y

event

clientY

})

}

render

()

{

return

<

div

style

=

{{

height

‘100%’

}}

onMouseMove

=

{

this

handleMouseMove

}

>

{

this

props

render

this

state

)}

<

/div>

}

}

const

App

=

React

createClass

({

render

()

{

return

<

div

style

=

{{

height

‘100%’

}}

>

<

Mouse

render

=

{({

x

y

})

=>

// 這裡面的傳遞很清晰

<

h1

>

The

mouse

position

is

({

x

},

{

y

})

<

/h1>

)}

/>

<

/div>

}

})

ReactDOM

render

<

App

/>

document

getElementById

‘app’

))

是不是覺得無論從傳值到最後的使用都那麼的簡潔如初!!! (最重要的是 this。props。children 也可以用來當函式哦!)

那麼接下來重頭戲啦, 如何用它實現 react-redux, 首先我們都知道 connect()() 就是一個典型的 HOC

下面是我們的實現:

import

PropTypes

from

‘prop-types’

import

React

{

Component

}

from

‘react’

const

dummyState

=

{}

class

ConnectConsumer

extends

Component

{

static

propTypes

=

{

context

PropTypes

shape

({

dispatch

PropTypes

func

isRequired

getState

PropTypes

func

isRequired

subscribe

PropTypes

func

isRequired

}),

children

PropTypes

func

isRequired

}

componentDidMount

()

{

const

{

context

}

=

this

props

this

unsubscribe

=

context

subscribe

(()

=>

{

this

setState

dummyState

})

}

componentWillUnmount

()

{

this

unsubscribe

()

}

render

()

{

const

{

context

}

=

this

props

const

passProps

=

this

props

return

this

props

children

context

getState

(),

context

dispatch

}

}

是不是很酷那他怎麼用呢? 我們傳遞了 state, dispatch 那它的用法和之前傳遞的方式就類似了而且可能更加直觀。

const

ConnectContract

=

()

=>

<

Connect

>

{(

state

dispatch

passProps

=>

{

//這裡無論是select還是你想用reselect都沒問題的因為這就是一個function,Do ever you want

const

{

addStars

{

num

}

}

=

state

const

props

=

{

num

onAddStar

(。。。

args

=>

dispatch

addStar

(。。。

args

)),

onReduceStart

(。。。

args

=>

dispatch

reduceStar

(。。。

args

)),

}

return

<

Contract

{。。。

props

}

/>

}}

<

/Connect>

你可能會質疑, 等等。。。我們的 呢?

來啦來啦, React 16。3。0 新的 context api 我們來試水下

import

React

{

createContext

Children

}

from

‘react’

export

const

StoreContext

=

createContext

({

store

{},

})

export

const

ProviderComponent

=

({

children

store

})

=>

<

StoreContext

Provider

value

=

{

store

}

>

{

Children

only

children

)}

<

/StoreContext。Provider>

``

``

import

{

StoreContext

}

from

‘。/provider’

const

Connect

=

({

children

})

=>

<

StoreContext

Consumer

>

{(

context

=>

<

ConnectConsumer

context

=

{

context

}

>

{

children

}

<

/ConnectConsumer>

)}

<

/StoreContext。Consumer>

這就是新的 api 你可能會發現呼叫方法該了 createContext 生成物件兩個屬性分別是一個 react component 一個叫做 provider 一個叫做 consumer, 你可能好奇為什麼要這麼改, 這裡就不得不提到之前的 context 遇到一些問題,

詳細的原因都在這裡啦

我這裡就不多嘴啦, 但是主要原因我還是要說一下原來的傳遞方式會被 shouldComponentUpdate blocks context changes 會被這個生命週期阻斷更新, 但是新的方法就不會因為你會在你需要的時候 consumer 並且透過我們之前說的 render props 的寫法以引數的形式傳遞給你真正需要用到的子元件。是不是感覺他甚至都不那麼的全域性概念了呢?

介紹了這麼多酷酷的東西, 好像我們的新架構也出具模樣啦, 嘿嘿

如果你想嘗試跑一下可以訪問這裡, 歡迎點贊!!

作為最後的總結我們是滴滴 AMC 事業部的前端團隊, 以後會有更多有趣的分享哦, 歡迎關注專欄! 順便劇透下下篇會是 redux 相關主題! (有任何問題麻煩留言交流哦! )