从零搭建react全家桶脚手架

  从零搭建react全家桶脚手架

Posted by WWJ on May 10, 2018

从零搭建react全家桶脚手架

init项目

  1. 创建文件夹并进入 mkdir react-family && cd react-family
  2. 初始化项目 npm init按照提示输入信息

webpack

  1. 安装webpack npm install webpack --save-dev --save-dev只在开发环境中依赖的东西 --save发布之后还依赖的东西
  2. 配置webpack.dev.config.js文件
    const path = require('path')
    module.exports = {
     //入口文件,刚才初始化项目填写信息时有写,因人而异
     entry:path.join(__dirname, '/src/index.js'),
     //输出到dist文件夹,输出的名字叫bundle.js
     output:{
         path:path.join(__dirname, './dist'),
         filename:'bundle.js'
     }
    }
    
  3. 学会使用webpack编译文件 新建入口文件mkdir src && touch ./src/index.js 在index.js中添加内容document.getElementById("app").innerHTML = "webpack works" 现在执行命令webpack --config webpack.dev.config.js 可以看到在项目根目录下面生产了dist文件以及bundle.js文件
  4. 测试 dist文件夹下面新建一个index.html文件 touch ./dist/index.html并在文件里面写入以下内容
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>wentworth</title>
</head>
<body>
    <div id="app"></div>
    <script src="./bundle.js"></script>
</body>
</html>

优化命令

修改package.json里面的script,增加start

"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev-build": "webpack --config webpack.dev.config.js"
  }

babel

Babel把最新标准编写的Javascript代码向下编译成可以在今天随处可用的版本。这一过程叫做“源码到源码”的编译,也被称为转换编译 通俗地讲,就是我们可以用ES6,ES7来编写代码,Babel会把他们统统转为ES5

  • babel-core 调用Babel的API进行转码
  • babel-loader 用来解析文件
  • babel-preset-es2015 用于解析ES6
  • babel-preset-react 用来解析JSX
  • babel-preset-stage-0 用于解析ES7的提案

npm install --save-dev babel-core babel-loader babel-preset-es2015 babel-preset-react babel-preset-stage-0

  1. 新建babel配置文件.babelrc touch .babelrc
    {
     "presets": [
         "es2015",
         "react",
         "stage-0"
     ],
     "plugins": []
    }
    
  2. 修改webpack.dev.config.js,增加babel-loader
    //src下面以.js结尾的文件,要是用babel解析
    //cacheDirectory=true用来缓存编译结果,下次编译加速
    module:{
     rules:[{
         test:/\.js$/,
         use:['babel-loader?cacheDirectory=true'],
         include:path.join(__dirname, 'src')
     }]
    }
    
  3. 修改src/index.js
    const func = str => {
      document.getElementById('app').innerHTML = str;
    };
    func('我现在在使用Babel!');
    
  4. 执行命令npm run dev-build

react

npm install --save react react-dom

  1. 修改src/index.js,使用react
    import React from 'react'
    import ReactDom from 'react-dom'
    ReactDom.render(
     <div>Hello React</div>,
     document.getElementById("app")
    )
    export default Home
    

    执行命令 npm run dev-build查看结果

  2. 实现组件化
    cd src
    mkdir component
    cd component
    mkdir Home
    cd Home
    touch Home.js
    
  3. 按照React语法,写一个Home组件
    import React, {Component} from 'react'
    class Home extends Component {
     render() {
         return(
             <div className="wwj-home">
                 Hello React
             </div>
         )
     }
    }
    export default Home
    
  4. 修改src/index.js引用Home组件
    import React from 'react'
    import ReactDom from 'react-dom'
    import Home from './component/Home/home'
    ReactDom.render(
     <Home />,
     document.getElementById("app")
    )
    
  5. 执行命令npm run dev-build查看效果

React-Router

npm install --save react-router-dom

  1. 新建moduleapp.js文件 避免在src/index.js文件中引入大量的组件,可用一个App容器来引入项目中所有的组件,App.js可放在 /src/module文件夹下面
    cd src
    mkdir module
    cd module
    touch app.js
    
    import React from 'react'
    import {
      BrowserRouter as Router,
      Route,
      Switch,
    } from 'react-router-dom'
    import Home from '../module/home/home'
    import Music from '../module/music/music'
    import Video from '../module/video/video'
    const App = () => (
     <Router>
         <Switch>
             <Route path="/" exact component={Home} />
             <Route path="/music" exact component={Music} />
             <Router path="/video" exact component={Video} />
         </Switch>
     </Router>
    )
    export default App
    
  2. 新建对应的组件文件夹和文件
    cd src/module
    mkdir music
    cd music && touch music.js
    mkdir home
    cd home && touch home.js
    mkdir video
    cd video && touch video.js
    

    home.js

    import React, {Component} from 'react'
    class Home extends Component {
     render() {
         return(
             <div className="wwj-home">
                 Hello React
             </div>
         )
     }
    }
    export default Home
    

    music.js

    import React, {Component} from 'react'
    class Music extends Component {
     render() {
         return(
             <div className="wwj-music">
                 Hello Music
             </div>
         )
     }
    }
    export default Music
    

    video.js

    import React, {Component} from 'react'
    class Video extends Component {
     render() {
         return(
             <div className="wwj-video">
                 Hello Video
             </div>
         )
     }
    }
    export default Video
    
  3. 执行命令npm run dev-build 打开index.html文件时没有反应,这是正常的,我们之前用的是绝对路径访问index.html,不是我们想象中的http://localhost:8080,那么,这时我们需要一个简单的WEB服务器,指向index.html

webpack-dev-server

简单来说,webpack-dev-server就是一个小型的静态文件服务器,使用它,可以为webpack打包生成的资源文件提供Web服务

npm install --save-dev webpack-dev-server -g需要全局安装 修改webpack.dev.config.js,增加devServer配置

devServer:{
    contentBase: path.join(__dirname, './dist'),//url根目录,默认指向项目根目录
    historyApiFallback: true,//当使用HTML5 History API 时,任意的404响应都可能需要被替代为 index.html
    open:true//自动打开浏览器
    port:8080//在指定的端口打开
}

webpack-dev-server的其他配置

  • color console中打印彩色日志
  • proxy代理 比如说在localhost:3000端口上有服务的话,可以这么写
    proxy: {
      "/api": "http://localhost:3000"
    }
    
  • progress 将编译进度输出到控制台。 根据这几个配置,package.json文件中的scripts
    "start": "webpack-dev-server --config webpack.dev.config.js --color --progress"
    

    执行npm run start(可能会报错, 无报错此步可以略过) 这个时候需要在webpack.dev.config.js的配置中增加mode: "development",,代码如下

    module.exports = {
      entry:path.join(__dirname, 'src/index.js'),
      mode: "development",
      output:{
          path:path.join(__dirname, './dist'),
          filename:'bundle.js'
      }
    }
    

    下一次启动就没问题了

模块热替换(Hot Module Replacement)

到目前为止,当我们修改代码的时候,浏览器会自动刷新,但是,我们并不希望每次修改了代码浏览器都会自动刷新,而是只刷新修改的那部分 接下来我们要这么修改package.json增加--hot

"start": "webpack-dev-server --config webpack.dev.config.js --color --progress --hot"

src/index.js增加module.hot.accept(),模块更新的时候,通知index.js文件

import React from 'react'
import ReactDom from 'react-dom'
import App from './module/app'
if(module.hot) {
    module.hot.accept()
}
const rootNode = document.getElementById('app')
ReactDom.render(
    <App />,
    rootNode
)

现在启动npm run start,修改home.js,打开浏览器,会看到浏览器在没有刷新的情况下页面内容也更新了 HRM配置其实有两种方式,一种是CLI,也就是我们现在用的这种,还有一种是Node.js API方式,功能上都是一样的 But,上面的配置对react不是很友好哦 例如下面的demo,当模块热替换的时候,state会重置,这不是我么想要的 修改home.js,增加计数state src/module/home/home.js

import React, {Component} from 'react'

class Home extends Component {
    state = {
        count:0
    }
    _handleClick = () => {
        this.setState({
            count:++this.state.count
        })
    }
    render() {
        return(
            <div className="ww-home">
                {this.state.count}
                <button onClick={this._handleClick}>自增</button>
            </div>
        )
    }
}
export default Home

文件路径优化

在我们之前写的代码中,我们引用文件的时候用的都是相对路径 比如在src/module/app.js中引用home.js时,我们用的就是

import Home from '../module/home/home'

webpack提供了一个别名配置,无论文件在什么位置下,都可以这么引用

import Home from 'mudule/home/home'

修改webpck.dev.config.js文件,增加别名配置

resolve: {
    alias: {
        module: path.join(__dirname, 'src/module'),
        component: path.join(__dirname, 'src/component'),
    }
},

修改src/module/app.js

import Home from 'module/home/home'
import Music from 'module/music/music'
import Video from 'module/video/video'

Redux

接下来我么要集成讲redux了,需要对redux有个比较全面的认识,可以去看中文文档 开始撸代码,我们就做一个比较简单的例子,实现计数的自增、自减和重置

  1. npm install --save redux
  2. src/module/home/文件夹下面新建文件home.action-creator.jshome.action-type.js home.action-type.js
    //每个action3种都有3状态,REQUEST、SUCCESS、ERROR
    export const createActionSet = actionName => ({
     REQUEST: `${actionName}_REQUEST`,
     SUCCESS: `${actionName}_SUCCESS`,
     ERROR: `${actionName}_ERROR`
    })
    export const INCREMENT = createActionSet('INCREMENT')
    export const DECREMENT = createActionSet('DECREMENT')
    export const RESET = createActionSet('RESET')
    

    home.action-creator.js

    import * as type from './home.action-type'
    export const homeActionCreator = type => err => {
     return {
         type,
         payload:err
     }
    }
    export const increMentReq = () => {
     return {
         type: type.INCREMENT.REQUEST,
     }
    }
    export const increMentSuccess = () => {
     return {
         type: type.INCREMENT.SUCCESS,
     }
    }
    export const increMentError =  homeActionCreator(type.INCREMENT.ERROR)
    export const decreMentReq = () => {
     return {
         type: type.DECREMENT.REQUEST,
     }
    }
    export const decreMentSuccess = () => {
     return {
         type: type.DECREMENT.SUCCESS,
     }
    }
    export const decreMentError =  homeActionCreator(type.DECREMENT.ERROR)
    export const resetReq = () => {
     return {
         type: type.RESET.REQUEST,
     }
    }
    export const resetSuccess = () => {
     return {
         type: type.RESET.SUCCESS,
     }
    }
    export const resetError =  homeActionCreator(type.RESET.ERROR)
    

    home.reducer.js

    import * as type from './home.action-type'
    const initState = {
     error: [],
     isloading: false,
     count: 0
    }
    const home = (state = initState, action) => {
     switch (action.type) {
         case type.INCREMENT.REQUEST:
             return {
                 ...state,
                 isloading: true,
             }
         case type.INCREMENT.SUCCESS:
             return {
                 ...state,
                 isloading: false,
                 count: state.count+1
             }
         case type.INCREMENT.ERROR:
             return {
                 ...state,
                 isloading: false,
                 error: state.error.concat(action.payload)
             }
         case type.DECREMENT.REQUEST:
             return {
                 ...state,
                 isloading: true,
             }
         case type.DECREMENT.SUCCESS:
             return {
                 ...state,
                 isloading: false,
                 count: state.count - 1
             }
         case type.DECREMENT.ERROR:
             return {
                 ...state,
                 isloading: false,
                 error: state.error.concat(action.payload)
             }
         case type.RESET.REQUEST:
             return {
                 ...state,
                 isloading: true,
             }
         case type.RESET.SUCCESS:
             return {
                 ...state,
                 isloading: false,
                 count: 0
             }
         case type.RESET.ERROR:
             return {
                 ...state,
                 isloading: false,
                 error: state.error.concat(action.payload)
             }
         default:
             return state
     }
    }
    export default home
    

    src/reducer.index.js

    import { combineReducers } from 'redux'
    import { home } from 'module/home/home.reducer'
    const rootReducer = combineReducers({
     home
    })
    export default rootReducer
    

    到这里,我们必须理解下面一句话 reducer就是纯函数,接收stateaction,然后返回一个新的state 接下来我们要创建一个store store是用来干什么的? 我们用action来描述发生了什么,用reducer来根据action更新state 那么我们如何提交action,提交的时候如何触发reducerstore就是把他们联系在一起的对象,store有以下职责:

    • 维持应用的state
    • 提供getState()方法获取state
    • 提供dispatch方法触发reducer更新state
    • 通过subscribe(listener) 注册监听器

src文件夹下面新建store.index.js src/store.index.js

import {createStore} from 'redux'
import rootReducer from './reducer.index'
const store = createStore(
    rootReducer
)
export default store

React-Redux

另外我们还需要安装一个东西react-redux react-reduxreactredux联系起来

  1. 安装react-redux npm install --save react-redux
  2. 修改src/module/home/home.js
    import React, {Component} from 'react'
    import {connect} from 'react-redux'
    import * as homeAction from './home.action-creator'
    class Home extends Component {
     _increMent = () => {
         const {dispatch} = this.props
         dispatch(homeAction.increMentReq())
         dispatch(homeAction.increMentSuccess())
     }
     _decreMent = () => {
         const {dispatch} = this.props
         dispatch(homeAction.decreMentReq())
         dispatch(homeAction.decreMentSuccess())
     }
     _reset = () => {
         const {dispatch} = this.props
         dispatch(homeAction.resetReq())
         dispatch(homeAction.resetSuccess())
     }
     render() {
         const {home:{count}} = this.props
         return(
             <div className="ww-home">
                 {count}
                 <button onClick={this._increMent}>自增</button>
                 <button onClick={this._decreMent}>自减</button>
                 <button onClick={this._reset}>重置</button>
             </div>
         )
     }
    }
    const mapStateToProps = ({home}) => {
     return {
         home
     }
    }
    export default connect(mapStateToProps)(Home)
    

Redux-Saga

发起异步action

  1. 安装redux-saga npm install --save redux-saga
  2. src/module/home文件夹下新建home.saga.js
    import {take, put, fork, select, cancel, all} from 'redux-saga/effects'
    import * as type from './home.action-type'
    import * as action from './home.action-creator'
    function* handleIncrement() {
     while(true) {
         try {
             yield take(type.INCREMENT.REQUEST)
             yield put(action.increMentSuccess())
         } catch (error) {
             console.log(error)
         }
     }
    }
    function* handleDecrement() {
     while(true) {
         try {
             yield take(type.DECREMENT.REQUEST)
             yield put(action.decreMentSuccess())
         } catch (error) {
             console.log(error)
         }
     }
    }
    function* handleReset() {
     while(true) {
         try {
             yield take(type.RESET.REQUEST)
             yield put(action.resetSuccess())
         } catch (error) {
             console.log(error)
         }
     }
    }
    export default function* homeSaga() {
     yield all([
         fork(handleIncrement),
         fork(handleDecrement),
         fork(handleReset)
     ])
    }
    
  3. src文件夹下新建saga.index.js
    import {all} from 'redux-saga/effects'
    import homeSaga from 'module/home/home.saga'
    export default function* rootSaga() {
     yield all([
         homeSaga()
     ])
    }
    
  4. 修改store.index.js文件
    import { createStore, applyMiddleware } from 'redux'
    import createSagaMiddleware from 'redux-saga'
    import rootReducer from './reducer.index'
    import rootSaga from './saga.index'
    const sagaMiddleware = createSagaMiddleware()
    const store = createStore(
     rootReducer,
     applyMiddleware(
         sagaMiddleware
     )
    )
    sagaMiddleware.run(rootSaga)
    export default store
    
  5. 修改scr/module/home/home.js
    import React, {Component} from 'react'
    import {connect} from 'react-redux'
    import * as homeAction from './home.action-creator'
    class Home extends Component {
     _increMent = () => {
         const {dispatch} = this.props
         dispatch(homeAction.increMentReq())
         dispatch(homeAction.increMentSuccess())
     }
     _decreMent = () => {
         const {dispatch} = this.props
         dispatch(homeAction.decreMentReq())
     }
     _reset = () => {
         const {dispatch} = this.props
         dispatch(homeAction.resetReq())
     }
     render() {
         const {home:{count}} = this.props
         return(
             <div className="ww-home">
                 {count}
                 <button onClick={this._increMent}>自增</button>
                 <button onClick={this._decreMent}>自减</button>
                 <button onClick={this._reset}>重置</button>
             </div>
         )
     }
    }
    const mapStateToProps = ({home}) => {
     return {
         home
     }
    }
    export default connect(mapStateToProps)(Home)
    
  6. 启动项目npm run start 这个时候可能在控制台会出现下图错误 Alt text 这个时候是因为你还没有安装babel-polyfill,安装babel-ployfill npm install --save babel-polyfill,并在src/index.js引入
    import 'babel-polyfill'
    

    重新刷页面错误解决

devtool优化

我们发现一个问题,不管代码错在哪里,浏览器只是报错误在bundle.js第几行 这时需要在webpack.dev.config.js增加配置

devtool: 'inline-source-map'

编译CSS

npm install --save-dev css-loader style-loader webpack.dev.config.js增加rules

{
	test: /\.css$/,
	use: ['style-loader', 'css-loader']
}

编译图片

npm install --save-dev url-loader file-loader webpack.dev.config.js增加rules

{
	test: /\.(png|jpg|gif)$/,
    use: [{
        loader: 'url-loader',
        options: {
            limit: 8192
        }
    }]
}

options limit 8192意思是,小于等于8K的图片会被转成base64编码,直接插入HTML中,减少HTTP请求。

按需加载

为什么要实现按需加载? 我们现在看到,打包完后,所有页面只生成了一个build.js,当我们首屏加载的时候,就会很慢。因为他也下载了别的页面的js了哦。 如果每个页面都打包了自己单独的JS,在进入自己页面的时候才加载对应的js,那首屏加载就会快很多哦。

  1. npm install --save-dev bundle-loader
  2. src/component文件夹下新建asyncRoute.js
    import React, { Component } from 'react'
    class AsyncRoute extends Component {
     state = {
         mod: null
     }
     componentWillMount() {
         this.load(this.props)
     }
     componentWillReceiveProps(nextProps) {
         if (nextProps.load !== this.props.load) {
             this.load(nextProps)
         }
     }
     load(props) {
         this.setState({
             mod: null
         })
         props.load((mod) => {
             this.setState({
                 // handle both es imports and cjs
                 mod: mod.default ? mod.default : mod
             })
         })
     }
     render() {
         if (!this.state.mod)
             return false
         return this.props.children(this.state.mod)
     }
    }
    export default AsyncRoute
    
  3. 修改src/module/app.js文件
    import React, { Component } from 'react'
    import {
     BrowserRouter as Router,
     Route,
     Switch,
    } from 'react-router-dom'
    import Home from 'bundle-loader?lazy&name=home!module/home/home'
    import Music from 'bundle-loader?lazy&name=music!module/music/music'
    import Video from 'bundle-loader?lazy&name=video!module/video/video'
    import AsyncRoute from 'component/asyncRoute'
    const Loading = () => {
     return <div>Loading...</div>
    }
    const createComponent = (component) => (props) => (
     <AsyncRoute load={component}>
         {
             (Component) => Component ? <Component {...props} /> : <Loading />
         }
     </AsyncRoute>
    );
    const App = () => (
     <Router>
         <Switch>
             <Route path="/" exact component={createComponent(Home)} />
             <Route path="/music" exact component={createComponent(Music)} />
             <Router path="/video" exact component={createComponent(Video)} />
         </Switch>
     </Router>
    )
    export default App
    

缓存

修改webpack.dev.config.js

output: {
    path: path.join(__dirname, './dist'),
    filename: '[name].[hash].js',
    chunkFilename: '[name].[chunkhash].js'
}

HtmlWebpackPlugin

npm install html-webpack-plugin --save-dev 修改webpack.dev.config.js

var HtmlWebpackPlugin = require('html-webpack-plugin');
plugins: [new HtmlWebpackPlugin({
    filename: 'index.html',
    template: path.join(__dirname, 'src/index.html')
})],
}