如何搭建一个REACT全家桶框架

前端技术发展太快,有些库的版本一直在升级,如今网上的搭建教程不是不全面,就是版本过低,本人综合一些教程和本身的理解,整理了一下,方便你们快速入手react框架。本教程针对刚入门和技术栈转型人员。注:(本教程写于2019-3-29,请注意版本)!!!javascript

你们阅读的时候,如发现问题,可提出,我会及时更新(本人较懒,有些命令没有打出来,请仔细阅读,避免遗漏!!!)css

前言

本人也是半路加入react大军的一员,因为半路加入,对总体框架了解较少,使用现成DVA框架,始终是只知其一;不知其二。日常遇到问题,老是须要找资料去解决,也有问题最后难以解决,为了方便本身理解整个react相关的技术,也避免后来像我同样的人继续踩坑,能够根据这个教程有个比较全面的了解。(高手勿拍)!!!html

项目简介

1.技术栈目前是最新的前端

  • node 8.11.1
  • react 16.8.6
  • react-router-dom 5.0.0
  • redux 4.0.1
  • webpack 4.28.2

2.包管理工具java

经常使用的有npm yarn等,本人这里使用yarn,使用npm的小伙伴注意下命令区别node

直接开始

初始化项目

  1. 先建立一个目录并进入
mkdir react-cli && cd react-cli
复制代码
  1. 初始化项目,填写项目信息(可一路回车)
npm init
复制代码

安装webpack

yarn global add webpack -D 
yarn global add webpack-cli -D 
复制代码
  • yarn使用add添加包,-D等于--save-dev -S等于--save
  • -D和-S二者区别:-D是你开发时候依赖的东西,--S 是你发布以后还依赖的东西
  • -g是全局安装,方便咱们后面使用webpack命令(全局安装后依然不能使用的小伙伴,检查下本身的环境变量PATH)

安装好后新建build目录放一个webpack基础的开发配置webpack.dev.config.jsreact

mkdir build && cd build && echo. > webpack.dev.config.js
复制代码

配置内容很简单,配置入口和输出webpack

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'
    }
};
复制代码

而后根据咱们配置的入口文件的地址,建立../src/index.js文件(请注意src目录和build目录同级)ios

mkdir src && cd src && echo. > index.js
复制代码

而后写入一行内容git

document.getElementById('app').innerHTML = "Hello React";
复制代码

如今在根目录下执行webpack打包命令

webpack --config ./build/webpack.dev.config.js
复制代码

咱们能够看到生成了dist目录和bundle.js。(消除警告看后面mode配置) 接下来咱们在dist目录下新建一个index.html来引用这个打包好的文件

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
<div id="app"></div>
<script type="text/javascript" src="./bundle.js" charset="utf-8"></script>
</body>
</html>
复制代码

而后双击打开index.html,咱们就看到浏览器输出

Hello React
复制代码

这样咱们一个基本的打包功能就作好了!!!

mode

刚才打包成功可是带有一个警告,意思是webpack4须要咱们指定mode的类型来区分开发环境和生产环境,他会帮咱们自动执行相应的功能,mode能够写到启动命令里--mode=production or development,也能够写到配置文件里,这里咱们将 webpack.dev.config.js里面添加mode属性。

/*入口*/
    entry: path.join(__dirname, '../src/index.js'),
    mode:'development',
复制代码

在执行打包命令,警告就消失了。

babel

Babel 把用最新标准编写的 JavaScript 代码向下编译成能够在今天随处可用的版本。 
这一过程叫作“源码到源码”编译, 也被称为转换编译。(本教程使用的babel版本是7,请注意包名和配置与6的不一样)
复制代码
  • @babel/core 调用Babel的API进行转码
  • @babel/preset-env 用于解析 ES6
  • @babel/preset-react 用于解析 JSX
  • babel-loader 加载器
yarn add @babel/core @babel/preset-env @babel/preset-react babel-loader -D
复制代码

而后在根目录下新建一个babel配置文件

babel.config.js

const babelConfig = {
   presets: ["@babel/preset-react", "@babel/preset-env"],
    plugins: []
}

module.exports = babelConfig;
复制代码

修改webpack.dev.config.js,增长babel-loader!

/*src目录下面的以.js结尾的文件,要使用babel解析*/
/*cacheDirectory是用来缓存编译结果,下次编译加速*/
module: {
    rules: [{
        test: /\.js$/,
        use: ['babel-loader?cacheDirectory=true'],
        include: path.join(__dirname, '../src')
    }]
}
复制代码

如今咱们简单测试下,是否能正确转义ES6~

修改 src/index.js

/*使用es6的箭头函数*/
    var func = str => {
        document.getElementById('app').innerHTML = str;
    };
    func('我如今在使用Babel!');
复制代码

再执行打包命令

webpack --config ./build/webpack.dev.config.js
复制代码

如今刷新dist下面的index.html就会看到浏览器输出

我如今在使用Babel!
复制代码

有兴趣的能够打开打包好的bundle.js,最下面会发现ES6箭头函数被转换为普通的function函数

react

接下来是咱们的重点内容,接入react

yarn add react react-dom -S
复制代码

注:这里使用 -S 来保证生产环境的依赖

修改 src/index.js使用react

import React from 'react';
import ReactDom from 'react-dom';

ReactDom.render(
    <div>Hello React!</div>, document.getElementById('app'));
复制代码

执行打包命令

webpack --config ./build/webpack.dev.config.js
复制代码

刷新index.html 看效果。

接下来咱们使用react的组件化思想作一下封装,src下新建components目录,而后新建一个Hello目录,里面建立一个index.js,写入:

import React, { PureComponent } from 'react';

export default class Hello extends PureComponent  {
    render() {
        return (
            <div>
                Hello,组件化-React!
            </div>
        )
    }
}
复制代码

而后让咱们修改src/index.js,引用Hello组件!

import React from 'react';
import ReactDom from 'react-dom';
import Hello from './components/Hello';

ReactDom.render(
    <Hello/>, document.getElementById('app'));
复制代码

注:import 模块化导入会默认选择目录下的index文件,因此直接写成'./components/Hello'

在根目录执行打包命令

webpack --config ./build/webpack.dev.config.js
复制代码

打开index.html看效果咯~

命令优化

每次打包都输入很长的打包命令,很麻烦,咱们对此优化一下。

修改package.json里面的script对象,增长build属性,写入咱们的打包命令。

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

如今咱们打包只须要执行npm run build就能够啦!(除了start是内置命令,其余新增的命令都须要用run去运行)

react-router

如今咱们接入react的路由react-router

yarn add react-router-dom -S
复制代码

接下来为了使用路由,咱们建两个页面来作路由切换的内容。首先在src下新建一个pages目录,而后pages目录下分别建立home和page目录,里面分别建立一个index.js。

src/pages/home/index.js
复制代码
import React, {PureComponent} from 'react';

export default class Home extends PureComponent {
    render() {
        return (
            <div>
                this is home~
            </div>
        )
    }
}
复制代码
src/pages/page/index.js
复制代码
import React, {PureComponent} from 'react';

export default class Page extends PureComponent {
    render() {
        return (
            <div>
                this is Page~
            </div>
        )
    }
}
复制代码

两个页面就写好了,而后建立咱们的菜单导航组件

components/Nav/index.js
复制代码
import React from 'react';
import { Link } from 'react-router-dom';

export default () => {
    return (
        <div>
            <ul>
                <li><Link to="/">首页</Link></li>
                <li><Link to="/page">Page</Link></li>
            </ul>
        </div>
    )
}
复制代码

注:使用Link组件改变当前路由

而后咱们在src下面新建router.js,写入咱们的路由,并把它们跟页面关联起来

import React from 'react';

import { Route, Switch } from 'react-router-dom';

// 引入页面
import Home from './pages/home';
import Page from './pages/page';

// 路由
const getRouter = () => (
    <Switch>
        <Route exact path="/" component={Home}/>
        <Route path="/page" component={Page}/>
    </Switch>
);

export default getRouter;
复制代码

页面和菜单和路由都写好了,咱们把它们关联起来。在src/index.js中

import React from 'react';
import ReactDom from 'react-dom';
import {BrowserRouter as Router} from 'react-router-dom';
import Nav from './components/Nav';
import getRouter from './router';

ReactDom.render(
    <Router>
        <Nav/>
        {getRouter()}
    </Router>,
    document.getElementById('app')
)

复制代码

如今执行npm run build打包后就能够看到内容了,可是点击菜单并无反应,这是正常的。由于咱们目前使用的依然是本地磁盘路径,并非ip+端口的形式,接下来咱们引入webpack-dev-server来启动一个简单的服务器。

yarn global add webpack-dev-server -D
复制代码

修改webpack.dev.config.js,增长webpack-dev-server的配置。

// webpack-dev-server
devServer: {
    contentBase: path.join(__dirname, '../dist'), 
    compress: true,  // gzip压缩
    host: '0.0.0.0', // 容许ip访问
    hot:true, // 热更新
    historyApiFallback:true, // 解决启动后刷新404
    port: 8000 // 端口
},
复制代码

注:contentBase通常不配,主要是容许访问指定目录下面的文件,这里使用到了dist下面的index.html

而后在package.json里新建启动命令

"start": "webpack-dev-server --config ./build/webpack.dev.config.js",
复制代码

执行npm start命令后打开 http://localhost:8000 便可看到内容,并能够切换路由了!

proxy代理

devServer下有个proxy属性能够设置咱们的代理

devServer: {
       ...
        proxy: { // 配置服务代理
            '/api': {
                 target: 'http://localhost:8000',
                 pathRewrite: {'^/api' : ''},  //可转换
                 changeOrigin:true
            }
        },
        port: 8000 // 端口
    },
复制代码

在 localhost:8000 上有后端服务的话,你能够这样启用代理。请求到 /api/users 如今会被代理到请求 http://localhost:8000/users。(注意这里的第二个属性,它将'/api'替换成了'')。changeOrigin: true能够帮咱们解决跨域的问题。

devtool优化

当启动报错或者像打断点的时候,会发现打包后的代码无从下手。咱们在webpack里面添加

devtool: 'inline-source-map'
复制代码

而后就能够在srouce里面能看到咱们写的代码,也能打断点调试哦~

文件路径优化

正常咱们引用组件或者页面的时候,通常都是已../的形式去使用。如果文件层级过深,会致使../../../的状况,很差维护和读懂,为此webpack提供了alias 别名配置。

看这里:切记名称不可声明成你引入的其余包名。别名的会覆盖你的包名,致使你没法引用其余包。栗子:redux、react等

首先在webpack.dev.config.js里面加入

resolve: {
    alias: {
        pages: path.join(__dirname, '../src/pages'),
        components: path.join(__dirname, '../src/components'),
        router: path.join(__dirname, '../src/router')
    }
}
复制代码

而后咱们的router.js里面引入组件就能够改成

// 引入页面
import Home from './pages/home';
import Page from './pages/page';

// 引入页面
import Home from 'pages/home';
import Page from 'pages/page';
复制代码

此功能层级越复杂越好用。

redux

接下来咱们要集成redux,咱们先不讲理论,直接用redux作一个最多见的例子,计数器。首先咱们在src下建立一个redux目录,里面分别建立两个目录,actions和reducers,分别存放咱们的action和reducer。

首先引入redux
yarn add redux -S
复制代码

目录下actions下counter.js

/*action*/

export const INCREMENT = "counter/INCREMENT";
export const DECREMENT = "counter/DECREMENT";
export const RESET = "counter/RESET";

export function increment() {
    return {type: INCREMENT}
}

export function decrement() {
    return {type: DECREMENT}
}

export function reset() {
    return {type: RESET}
}
复制代码

目录下reducers下counter.js

import {INCREMENT, DECREMENT, RESET} from '../actions/counter';

/*
* 初始化state
 */

const initState = {
    count: 0
};
/*
* reducer
 */
export default function reducer(state = initState, action) {
    switch (action.type) {
        case INCREMENT:
            return {
                count: state.count + 1
            };
        case DECREMENT:
            return {
                count: state.count - 1
            };
        case RESET:
            return {count: 0};
        default:
            return state
    }
}
复制代码

在webpack配置里添加actions和reducers的别名。

actions: path.join(__dirname, '../src/redux/actions'),
reducers: path.join(__dirname, '../src/redux/reducers')
复制代码

到这里要说一下,action建立函数,主要是返回一个action类,action类有个type属性,来决定执行哪个reducer。reducer是一个纯函数(只接受和返回参数,不引入其余变量或作其余功能),主要接受旧的state和action,根据action的type来判断执行,而后返回一个新的state。

特殊说明:你可能有不少reducer,type必定要是全局惟一的,通常经过prefix来修饰实现。栗子:counter/INCREMENT里的counter就是他全部type的前缀。
复制代码

接下来我么要在redux目录下建立一个store.js。

import {createStore} from 'redux';
import counter  from 'reducers/counter';

let store = createStore(counter);

export default store;
复制代码

store的具体功能介绍:

  • 维持应用的 state;
  • 提供 getState() 方法获取 state;
  • 提供 dispatch(action) 触发reducers方法更新 state;
  • 经过 subscribe(listener) 注册监听器;
  • 经过 subscribe(listener) 返回的函数注销监听器。

接着咱们建立一个counter页面来使用redux数据。在pages目录下建立一个counter目录和index.js。 页面中引用咱们的actions来执行reducer改变数据。

import React, {PureComponent} from 'react';
import { connect } from 'react-redux';
import { increment, decrement, reset } from 'actions/counter';

class Counter extends PureComponent {
    render() {
        return (
            <div>
                <div>当前计数为{this.props.count}</div>
                <button onClick={() => this.props.increment()}>自增
                </button>
                <button onClick={() => this.props.decrement()}>自减
                </button>
                <button onClick={() => this.props.reset()}>重置
                </button>
            </div>
        )
    }
}
export default connect((state) => state, dispatch => ({
    increment: () => {
        dispatch(increment())
    },
    decrement: () => {
        dispatch(decrement())
    },
    reset: () => {
        dispatch(reset())
    }
}))(Counter);
复制代码

connect是什么呢?react-redux提供了一个方法connect。connect主要有两个参数,一个mapStateToProps,就是把redux的state,转为组件的Props,还有一个参数是mapDispatchToprops,把发射actions的方法,转为Props属性函数。

而后咱们引入react-redux:

yarn add react-redux  -S
复制代码

接着咱们添加计数器的菜单和路由来展现咱们的计数器功能。

Nav组件

<li><Link to="/counter">Counter</Link></li>
复制代码
router.js
import Counter from 'pages/counter';
---
<Route path="/counter" component={Counter}/>
复制代码

最后在src/index.js中使用store功能

import {Provider} from 'react-redux';
import store from './redux/store';

ReactDom.render(
    <Provider store={store}>
        <Router>
            <Nav/>
            {getRouter()}
        </Router>
    </Provider>,
    document.getElementById('app')
)
复制代码

Provider组件是让全部的组件能够访问到store。不用手动去传。也不用手动去监听。 接着咱们启动一下,npm start,而后就能够再浏览器中看到咱们的计数器功能了。

咱们开发中会有不少的reducer,redux提供了一个combineReducers函数来合并reducer,使用起来很是简单。在store.js中引入combineReducers并使用它。

import {combineReducers} from "redux";

let store = createStore(combineReducers({counter}));
复制代码

而后咱们在counter页面组件中,使用connect注入的state改成counter便可(state完整树中选择你须要的数据集合)。

export default connect(({counter}) => counter, dispatch => ({
    increment: () => {
        dispatch(increment())
    },
    decrement: () => {
        dispatch(decrement())
    },
    reset: () => {
        dispatch(reset())
    }
}))(Counter);
复制代码

梳理一下redux的工做流:

  1. 调用store.dispatch(action)提交action。
  2. redux store调用传入的reducer函数。把当前的state和action传进去。
  3. 根 reducer 应该把多个子 reducer 输出合并成一个单一的 state 树。
  4. Redux store 保存了根 reducer 返回的完整 state 树。

HtmlWebpackPlugin优化

以前咱们一直经过webpack里面的

contentBase: path.join(__dirname, '../dist'),
复制代码

配置获取dist/index.html来访问。须要写死引入的JS,比较麻烦。这个插件,每次会自动把js插入到你的模板index.html里面去。

yarn add html-webpack-plugin -D
复制代码

而后注释webpack的contentBase配置,并在根目录下新建public目录,将dist下的index.html移动到public下,而后删除bundle.js的引用

接着在webpack.dev.config.js里面加入html-webpack-plugin的配置。

const HtmlWebpackPlugin = require('html-webpack-plugin');

plugins: [
    new HtmlWebpackPlugin({
        filename: 'index.html',
        template: path.join(__dirname, '../public/index.html')
    })
]
复制代码

接下来,咱们每次启动都会使用这个html-webpack-plugin,webpack会自动将打包好的JS注入到这个index.html模板里面。

编译css优化

首先引入css的loader

yarn add css-loader style-loader -D
复制代码

而后在咱们以前的pages/page目录下添加index.css文件,写入一行css

.page-box {
    border: 1px solid red;
    display: flex;
}
复制代码

而后咱们在page/index.js中引入并使用

import './index.css';

<div class="page-box">
    this is Page~
</div>
复制代码

最后咱们让webpack支持加载css,在webpack.dev.config.js rules增长

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

npm start 启动后查看page路由就能够看到样式生效了。

  • css-loader使你可以使用相似@import 和 url(...)的方法实现 require()的功能;

  • style-loader将全部的计算后的样式加入页面中; 两者组合在一块儿使你可以把样式表嵌入webpack打包后的JS文件中。

集成PostCSS优化

刚才的样式咱们加了个display:flex;样式,每每咱们在写CSS的时候须要加浏览器前缀。但是手动添加太过于麻烦,PostCSS提供了Autoprefixer这个插件来帮咱们完成这个工做。

首先引入相关包

yarn add postcss-loader postcss-cssnext -D
复制代码

postcss-cssnext 容许你使用将来的 CSS 特性(包括 autoprefixer)。

而后配置webpack.dev.config.js

rules: [{
    test: /\.(css)$/,
    use: ["style-loader", "css-loader", "postcss-loader"]
}]
复制代码

而后在根目录下新建postcss.config.js

module.exports = {
    plugins: {
        'postcss-cssnext': {}
    }
};
复制代码

如今你运行代码,而后写个css,去浏览器审查元素,看看,属性是否是生成了浏览器前缀!。以下:

编译前
.page-box {
    border: 1px solid red;
    display: flex;
}

编译后
.page-box {
    border: 1px solid red;
    display: -webkit-box;
    display: -ms-flexbox;
    display: flex;
}
复制代码

CSS Modules优化

CSS的规则都是全局的,任何一个组件的样式规则,都对整个页面有效。产生局部做用域的惟一方法,就是使用一个独一无二的class的名字,不会与其余选择器重名。这就是 CSS Modules 的作法。

咱们在webpack.dev.config.js中启用modules

use: ['style-loader', 'css-loader?modules', 'postcss-loader']
复制代码

接着咱们在引入css的时候,可使用对象.属性的形式。(这里有中划线,使用[属性名]的方式)

import style from './index.css';

<div className={style["page-box"]}>
    this is Page~
</div>
复制代码

这个时候打开控制台,你会发现className变成了一个哈希字符串。而后咱们能够美化一下,使用cssmodules的同时,也能看清楚原先是哪一个样式。修改css-loader

以前
css-loader?modules

以后
{
    loader:'css-loader',
    options: {
        modules: true,
        localIdentName: '[local]--[hash:base64:5]'
    }
}
复制代码

重启webpack后打开控制台,发现class样式变成了class="page-box--1wbxe",是否是很好用。

编译图片优化

首先引入图片的加载器

yarn add url-loader file-loader -D
复制代码

而后在src下新建images目录,并放一个图片a.jpg。

接着在webpack.dev.config.js的rules中配置,同时添加images别名。

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

images: path.join(__dirname, '../src/images'),
复制代码

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

而后咱们继续在刚才的page页面,引入图片并使用它。

import pic from 'images/a.jpg'

<div className={style["page-box"]}>
    this is Page~
    <img src={pic}/>
</div>
复制代码

重启webpack后查看到图片。

按需加载

咱们如今启动后看到他每次都加载一个bundle.js文件。当咱们首屏加载的时候,就会很慢。由于他也下载其余的东西,因此咱们须要一个东西区分咱们须要加载什么。目前大体分为按路由和按组件。咱们这里使用经常使用的按路由加载。react-router4.0以上提供了react-loadable。

首先引入react-loadable

yarn add react-loadable -D
复制代码

而后改写咱们的router.js

以前
import Home from 'pages/home';
import Page from 'pages/page';
import Counter from 'pages/counter';

以后
import loadable from 'react-loadable';
import Loading from 'components/Loading';

const Home = loadable({
    loader: () => import('pages/Home'),
    loading: Loading,
    timeout: 10000, // 10 seconds
})
const Page = loadable({
    loader: () => import('pages/page'),
    loading: Loading,
    timeout: 10000, // 10 seconds
})
const Counter = loadable({
    loader: () => import('pages/Counter'),
    loading: Loading,
    timeout: 10000, // 10 seconds
})
复制代码

loadable须要一个loading组件,咱们在components下新增一个Loading组件

import React from 'react';

export default () => {
    return <div>Loading...</div>
};
复制代码

这个时候启动会发现报错不支持动态导入,那么咱们须要babel支持动态导入。 首先引入

yarn add @babel/plugin-syntax-dynamic-import -D
复制代码

而后配置babel.config.js文件

plugins: ["@babel/plugin-syntax-dynamic-import"]
复制代码

再启动就会发现source下不仅有bundle.js一个文件了。并且每次点击路由菜单,都会新加载该菜单的文件,真正的作到了按需加载。

添加404路由

pages目录下新建一个notfound目录和404页面组件

import React, {PureComponent} from 'react';

class NotFound extends PureComponent {
    render() {
        return (
            <div>
                404
            </div>
        )
    }
}
export default NotFound;
复制代码

router.js中添加404路由

const NotFound = loadable({
    loader: () => import('pages/notfound'),
    loading: Loading,
    timeout: 10000, // 10 seconds
})

<Switch>
    <Route exact path="/" component={Home}/>
    <Route path="/page" component={Page}/>
    <Route path="/counter" component={Counter}/>
    <Route component={NotFound}/>
</Switch>
复制代码

这个时候输入一个不存在的路由,就会发现页面组件展示为404。

提取公共代码

咱们打包的文件里面包含了react,redux,react-router等等这些代码,每次发布都要从新加载,其实不必,咱们能够将他们单独提取出来。在webpack.dev.config.js中配置入口:

entry: {
    app:[
        path.join(__dirname, '../src/index.js')
    ],
    vendor: ['react', 'react-router-dom', 'redux', 'react-dom', 'react-redux']
},
output: {
    path: path.join(__dirname, '../dist'),
    filename: '[name].[hash].js',
    chunkFilename: '[name].[chunkhash].js'
},
复制代码

提取css文件

咱们看到source下只有js文件,可是实际上咱们是有一个css文件的,它被打包进入了js文件里面,如今咱们将它提取出来。 使用webpack的mini-css-extract-plugin插件。

yarn add mini-css-extract-plugin -D
复制代码

而后在webpack中配置

const MiniCssExtractPlugin = require("mini-css-extract-plugin");

{
    test: /\.css$/,
    use: [{loader: MiniCssExtractPlugin.loader}, {
        loader:'css-loader',
        options: {
            modules: true,
            localIdentName: '[local]--[hash:base64:5]'
        }
    }, 'postcss-loader']
 }
 
 new MiniCssExtractPlugin({ // 压缩css
    filename: "[name].[contenthash].css",
    chunkFilename: "[id].[contenthash].css"
})
复制代码

而后在重启,会发现source中多了一个css文件,那么证实咱们提取成功了

缓存

刚才咱们output输出的时候写入了hash、chunkhash和contenthash,那他们到底有什么用呢?

  • hash是跟整个项目的构建相关,只要项目里有文件更改,整个项目构建的hash值都会更改,而且所有文件都共用相同的hash值
  • chunkhash和hash不同,它根据不一样的入口文件(Entry)进行依赖文件解析、构建对应的chunk,生成对应的哈希值。
  • contenthash是针对文件内容级别的,只有你本身模块的内容变了,那么hash值才改变,因此咱们能够经过contenthash解决上诉问题

生产坏境构建

开发环境(development)和生产环境(production)的构建目标差别很大。 在开发环境中,咱们须要具备实时从新加载 或 热模块替换能力的 source map 和 localhost server。 在生产环境中,咱们的目标则转向于关注更小的 bundle,更轻量的 source map,以及更优化的资源,以改善加载时间。

build目录下新建webpack.prod.config.js,复制原有配置作修改。首先删除webpack.dev.config.js中的MiniCssExtractPlugin,而后删除webpack.prod.config.js中的devServer,而后修改打包命令。

"build": "webpack --config ./build/webpack.prod.config.js"
复制代码

再把devtool的值改为none。

devtool: 'none',
复制代码

接下来咱们为打包多作一些优化。

文件压缩

之前webpack使用uglifyjs-webpack-plugin来压缩文件,使咱们打包出来的文件体积更小。

如今只须要配置mode便可自动使用开发环境的一些配置,包括JS压缩等等

mode:'production',
复制代码

打包后体积大幅度变小。

公共块提取

这表示将选择哪些块进行优化。当提供一个字符串,有效值为all,async和initial。提供all能够特别强大,由于这意味着即便在异步和非异步块之间也能够共享块。

optimization: {
    splitChunks: {
      chunks: 'all'
    }
}
复制代码

从新打包,你会发现打包体积变小。

css压缩

咱们发现使用了生产环境的mode配置之后,JS是压缩了,可是css并无压缩。这里咱们使用optimize-css-assets-webpack-plugin插件来压缩css。如下是官网建议

虽然webpack 5可能内置了CSS minimizer,可是你须要携带本身的webpack 4。要缩小输出,请使用像optimize-css-assets-webpack-plugin这样的插件。设置optimization.minimizer会覆盖webpack提供的默认值,所以请务必同时指定JS minimalizer:
复制代码

首先引入

yarn add optimize-css-assets-webpack-plugin -D
复制代码

添加打包配置webpack.prod.config.js

const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');

plugins: [
    ...
    new OptimizeCssAssetsPlugin()
],
复制代码

从新打包,你会发现单独提取出来的CSS也压缩了。

打包清空

咱们发现每次打包,只要改动后都会增长文件,怎么自动清空以前的打包内容呢?webpack提供了clean-webpack-plugin插件。 首先引入

yarn add clean-webpack-plugin -D
复制代码

而后配置打包文件

const CleanWebpackPlugin = require('clean-webpack-plugin');

new CleanWebpackPlugin(), // 每次打包前清空
复制代码

public path

publicPath 配置选项在各类场景中都很是有用。你能够经过它来指定应用程序中全部资源的基础路径。在打包配置中添加

output: {
    publicPath : '/'
}
复制代码

加入 @babel/polyfill、@babel/plugin-transform-runtime、core-js、@babel/runtime-corejs二、@babel/plugin-proposal-class-properties

yarn add @babel/polyfill -S
复制代码

将如下行添加到您的webpack配置文件的入口中:

/*入口*/
entry: {
    app:[
        "@babel/polyfill",
        path.join(__dirname, '../src/index.js')
    ],
    vendor: ['react', 'react-router-dom', 'redux', 'react-dom', 'react-redux']
},
复制代码

@babel/polyfill可让咱们愉快的使用浏览器不兼容的es六、es7的API。可是他有几个缺点:

  • 一是咱们只是用了几个API,它却整个的引入了
  • 二是会污染全局

接下来咱们作一下优化,添加

yarn add @babel/plugin-transform-runtime -D
yarn add core-js@2.6.5 -D
yarn add @babel/plugin-proposal-class-properties -D

yarn add @babel/runtime-corejs2 -S
复制代码

添加完后配置page.json,添加browserslist,来声明生效浏览器

"browserslist": [
    "> 1%",
    "last 2 versions"
  ],
复制代码

在修改咱们的babel配置文件

{
    presets: [["@babel/preset-env",{
        useBuiltIns: "entry",
        corejs: 2
    }], "@babel/preset-react"],
    plugins: ["@babel/plugin-syntax-dynamic-import",'@babel/plugin-transform-runtime','@babel/plugin-proposal-class-properties']
}
复制代码

useBuiltIns是关键属性,它会根据 browserlist 是否转换新语法与 polyfill 新 AP业务代码使用到的新 API 按需进行 polyfill

  • false : 不启用polyfill, 若是 import '@babel/polyfill', 会无视 browserlist 将全部的 polyfill 加载进来
  • entry : 启用,须要手动 import '@babel/polyfill', 这样会根据 browserlist 过滤出 须要的 polyfill
  • usage : 不须要手动import '@babel/polyfill'(加上也无妨,构造时会去掉), 且会根据 browserlist +

注:经测试usage没法支持IE,推荐使用entry,虽然会大几十K。

@babel/plugin-transform-runtime和@babel/runtime-corejs2,前者是开发时候使用,后者是生产环境使用。主要功能:避免屡次编译出helper函数:Babel转移后的代码想要实现和原来代码同样的功能须要借助一些帮助函数。还能够解决@babel/polyfill提供的类或者实例方法污染全局做用域的状况。

@babel/plugin-proposal-class-properties是我以前漏掉了,若是你要在class里面写箭头函数或者装饰器什么的,须要它的支持。

数据请求axios和Mock

咱们如今作先后端彻底分离的应用,前端写前端的,服务端写服务端的,他们经过API接口链接。 然而每每服务端接口写的好慢,前端无法调试,只能等待。这个时候咱们就须要咱们的mock.js来本身提供数据。 Mock.js会自动拦截的咱们的ajax请求,而且提供各类随机生成数据。(必定要注释开始配置的代理,不然没法请求到咱们的mock数据)

首先安装mockjs

yarn add mockjs -D
复制代码

而后在根目录下新建mock目录,建立mock.js

import Mock from 'mockjs';
 
Mock.mock('/api/user', {
    'name': '@cname',
    'intro': '@word(20)'
});
复制代码

上面代码的意思就是,拦截/api/user,返回随机的一个中文名字,一个20个字母的字符串。 而后在咱们的src/index.js中引入它。

import '../mock/mock.js';
复制代码

接口和数据都准备好了,接下来咱们写一个请求获取数据并展现。

首先引入axios

yarn add axios -S
复制代码

而后分别建立userInfo的reducer、action和page

redux/actions/userInfo.js以下

import axios from 'axios';

export const GET_USER_INFO = "userInfo/GET_USER_INFO";

export function getUserInfo() {
    return dispatch=>{
        axios.post('/api/user').then((res)=>{
            let data = JSON.parse(res.request.responseText);
            dispatch({
                type: GET_USER_INFO,
                payload:data
            });
        })
    }
}
复制代码
redux/reducers/userInfo.js以下

import { GET_USER_INFO } from 'actions/userInfo';


const initState = {
    userInfo: {}
};

export default function reducer(state = initState, action) {
    switch (action.type) {
        case GET_USER_INFO:
            return {
                ...state,
                userInfo: action.payload,
            };
        default:
            return state;
    }
}
复制代码
pages/userInfo/index.js以下

import React, {PureComponent} from 'react';
import {connect} from 'react-redux';
import {getUserInfo} from "actions/userInfo";

class UserInfo extends PureComponent {

    render() {
        const { userInfo={} } = this.props.userInfo;
        return (
            <div>
                {
                    <div>
                        <p>用户信息:</p>
                        <p>用户名:{userInfo.name}</p>
                        <p>介绍:{userInfo.intro}</p>
                    </div>
                }
                <button onClick={() => this.props.getUserInfo()}>请求用户信息</button>
            </div>
        )
    }
}

export default connect((userInfo) => userInfo, {getUserInfo})(UserInfo);
复制代码

而后将咱们的userInfo添加到全局惟一的state,store里面去,

store.js

import userInfo  from 'reducers/userInfo';

let store = createStore(combineReducers({counter, userInfo}));
复制代码

最后在添加新的路由和菜单便可

router.js

const UserInfo = loadable({
    loader: () => import('pages/UserInfo'),
    loading: Loading,
    timeout: 10000, // 10 seconds
})

<Route path="/userinfo" component={UserInfo}/>
复制代码
components/Nav/index.js

<li><Link to="/userinfo">UserInfo</Link></li>
复制代码

运行,点击请求获取信息按钮,发现报错:Actions must be plain objects. Use custom middleware for async actions.这句话标识actions必须是个action对象,若是想要使用异步必须借助中间件。

redux-thunk中间件

咱们先引入它

yarn add redux-thunk -S
复制代码

而后咱们使用redux提供的applyMiddleware方法来启动redux-thunk中间件,使actions支持异步函数。

import {createStore, applyMiddleware} from 'redux';
import thunkMiddleware from 'redux-thunk';

let store = createStore(combineReducers({counter, userInfo}), applyMiddleware(thunkMiddleware));
复制代码

而后咱们在从新启动一下,会发现获取到了数据。

部署

为了测试咱们打包出来的文件是否可行,这里简单搭一个小型的express服务。首先根目录下新建一个server目录,在该目录下执行如下命令。

npm init 

yarn add nodemon express -D
复制代码
  • express 是一个比较容易上手的node框架
  • nodemon 是一个node开发辅助工具,能够无需重启更新nodejs的代码,很是好用。 安装好依赖后,咱们添加咱们的express.js文件来写node服务
var express = require('express');
var path = require('path');
var app = express();

app.get('/dist*', function (req, res) {
   res.sendFile( path.join(__dirname , "../" + req.url));
})
app.use(function (req, res) {
	res.sendFile(path.join( __dirname , "../dist/" + "index.html" ));
}) 
 
var server = app.listen(8081, function () {
  var host = server.address().address
  var port = server.address().port
  console.log("应用实例,访问地址为 http://%s:%s", host, port)
})
复制代码

node的代码我就不细说了,你们能够网上找找教程。这里主要是启动了一个端口为8081的服务,而后作了两个拦截,第一个拦截是全部访问dist*这个地址的,将它转到咱们的dist下面打包的文件上。第二个拦截是拦截全部错误的地址,将它转发到咱们的index.html上,这个能够解决刷新404的问题。

在server目录package.json文件中添加启动命令并执行。

"test": "nodemon ./express.js"
复制代码
npm run test
复制代码

启动后访问http://localhost:8081会发现不少模块引入404,不用慌,这里涉及到以前讲到的一个知识点--publicPath。咱们将它改成

publicPath : '/dist/',
复制代码

在打包一次,就会发现一切正常了,咱们node服务好了,打包出来的代码也能正常使用。

结尾

到这里,本搭建一个react全家桶的教程就结束了。第一次写,有些地方总结的不太好。话很少说,放一些资料供你们参考。

特别说明

本人也是万千前端业务仔的一员,有些问题问到了个人知识盲区或者没时间回复,请见谅,感谢!!!

另外本教程主要是针对新人和其余技术栈转react的新朋友做参考,可以对react框架有个相对全面的了解。其余的优化和支持就不在这里添加了。

建议本教程只作参考学习,并不能做为一个优质的项目可用开发框架。

代码本人会再测试一遍,下周会上传到github。

git地址(这么大应该看获得吧)

相关文章
相关标签/搜索