(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
以往的.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"
],
}
复制代码
首先说说最大改变可能也是parcel出现0配置给自己配置就比较繁琐的webpack更多压力了 这回官方破釜沉舟的也推出0配置选项。 使用方式提供cli模式,固然你也能够在配置文件中声明,咱们后面会指出webpack
webpack --mode production
webpack --mode development
那么这个默认模式里会包含以往哪些配置选项 官网是这么解释的: development环境包含git
(两种模式甚至于还帮你默认设置了入口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(/* 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以为以前的开发报错机制过于不人性化了,因此容许咱们在组件外层包裹组件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稳定后咱们后续文章也会来讨论下来它的可能性。若是你持续关注咱们!!!
其实这个概念以前在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')) 复制代码
那咱们看看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)
}
}
复制代码
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遇到一些问题,详细的缘由都在这里啦
介绍了这么多酷酷的东西,好像咱们的新架构也出具模样啦,嘿嘿!