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

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


做者: 趙瑋龍javascript

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

自React在master分支2017.09.27更新了16.0.0以來,到至今爲止發過多個版本(雖然fiber算法帶來的異步加載尚未開放穩定版本API,可是不遠啦...)前端

可是除去這個咱們翹首以盼的改變外,也一樣有不少咱們值得一提的東西。java

結合Webpack 4.0,Babel 7咱們會在這裏實現一個基本知足平常開發需求的前端腳手架node

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


咱們先從編譯文件開始咱們看看Babel 7和Webpack 4給個人編譯和構建帶來那些便利。

以往的.babelrc都離不開babel-preset-es20**包括stage-*等級的配置,在新的版本里做者以爲這些過於繁瑣,乾脆直接支持最新版本好啦(能夠看看他們的調研和理由)。因而咱們的.babelrc就變成這樣啦react

{
    "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

webpack --mode production webpack --mode development 那麼這個默認模式裏會包含以往哪些配置選項 官網是這麼解釋的: development環境包含git

  1. 瀏覽器debugging的工具(默認設置了devtool)
  2. 更快的編譯環境週期(設置cache)
  3. 運行過程當中有用的報錯信息 production環境包含 1.文件輸出大小壓縮(ugliy處理) 2.更快的打包時間 3.設置全局環境變量production 4.不暴露源碼和文件路徑 5.容易使用的output資源(會有不少相似於hosting內部代碼編譯後優化默認使用)

(兩種模式甚至於還幫你默認設置了入口entry和output路徑,可是爲了配置的易讀性和可配置性咱們仍是留給咱們本身設置比較好。)github

還有一個重要的改變是官方廢棄掉了CommonsChunkPlugin這個插件 緣由有以下: 1.官方認爲首先這個api不容易理解而且很差用 2.而且提取公共文件中含有大量的冗餘代碼 3.在作異步加載的時候這個文件必須每次都首先加載 (這麼看來廢棄也確實理所應當啦!)web

取而代之的是如今默認就支持的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的寫法以參數的形式傳遞給你真正須要用到的子組件。是否是感受他甚至都不那麼的全局概念了呢?

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

若是你想嘗試能夠訪問這裏,歡迎點贊!!

做爲最後的總結

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

相關文章
相關標籤/搜索