(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),原因在blog也有。
再來說說Webpack 4的一些改變
首先說說最大改變可能也是parcel出現0配置給本身配置就比較繁瑣的webpack更多壓力了 這回官方破釜沉舟的也推出0配置選項。 使用方式提供cli模式,當然你也可以在配置檔案中宣告,我們後面會指出
webpack --mode production
webpack --mode development
那麼這個預設模式裡會包含以往哪些配置選項
官網是這麼解釋的:
development環境包含
- 瀏覽器debugging的工具(預設設定了devtool)
- 更快的編譯環境週期(設定cache)
- 執行過程中有用的報錯資訊 production環境包含 1.檔案輸出大小壓縮(ugliy處理) 2.更快的打包時間 3.設定全域性環境變數production 4.不暴露原始碼和檔案路徑 5.容易使用的output資源(會有很多類似於hosting內部程式碼編譯後優化預設使用)
(兩種模式甚至於還幫你預設設定了入口entry和output路徑,但是為了配置的易讀性和可配置性我們還是留給我們自己設定比較好。)
還有一個重要的改變是官方廢棄掉了CommonsChunkPlugin這個外掛 原因有如下: 1.官方認為首先這個api不容易理解並且不好用 2.並且提取公共檔案中含有大量的冗餘程式碼 3.在做非同步載入的時候這個檔案必須每次都首先載入 (這麼看來廢棄也確實理所應當啦!)
取而代之的是現在預設就支援的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>loading...</Loading>
// })
// <Route exact path='/router' componet={someRouteContainer} />
// function Loading(props) {
// if (props.error) {
// return <div>Error!</div>;
// } else {
// return <div>Loading...</div>;
// }
// }
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之後檔案是什麼樣子的:
可以看到我們除去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的一種思路
當有報錯的時候你會發現在詳情中有一個報錯元件的呼叫棧,方便你去定位錯誤,當然報錯的樣式你可以自己定義這裡過於醜陋請忽略!!!
//以前
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>
)
複製程式碼
你可能會質疑,等等。。。我們的<Provider store={store}/>
呢?
來啦來啦,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的寫法以引數的形式傳遞給你真正需要用到的子元件。是不是感覺他甚至都不那麼的全域性概念了呢?
介紹了這麼多酷酷的東西,好像我們的新架構也出具模樣啦,嘿嘿!