阅读 8241

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

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

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

前言

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

项目简介

1.技术栈目前是最新的

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

2.包管理工具

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

直接开始

初始化项目

  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.js

mkdir build && cd build && echo. > 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'
    }
};
复制代码

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

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

然后写入一行内容

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-corejs2、@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可以让我们愉快的使用浏览器不兼容的es6、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地址(这么大应该看得到吧)

关注下面的标签,发现更多相似文章
评论