GithubHelp home page GithubHelp logo

react-cli's Introduction

搭建react 全家桶框架

运行环境

1.技术栈

node                    8.11.1
react                   16.8.6
react-router-dom        5.0.0
redux                   4.0.1
webpack                 4.28.2
@babel/core             7.10.5
@babel/preset-env       7.10.4
@babel/preset-react     7.10.4
babel-loader            8.1.0

2.包管理工具

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

构建项目

初始化项目

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

创建webpack打包环境

yarn add webpack -D 
yarn add webpack-cli -D 
  • yarn使用add添加包,-D等于--save-dev -S等于--save
  • -D-S两者区别:-D是你开发时候依赖的东西,-S 是你发布之后还依赖的东西

安装好后新建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';

package.json文件scripts节点中添加可执行的打包命令脚本

{
  "scripts": {
    "build": "webpack --config ./build/webpack.dev.config.js"
  }
}

现在在根目录下执行配置的打包命令

yarn build

我们可以看到生成了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
  • 环境

刚才打包成功但是带有一个警告,意思是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 加载器

安装babel

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!');

执行打包命令

yarn build

现在刷新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'));

执行打包命令后,刷新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

在根目录执行打包命令,刷新index.html查看运行效果

使用react-router路由

对接react的路由react-router

yarn add react-router-dom -S

接下来为了使用路由,我们建两个页面来做路由切换的内容。首先在src下新建一个pages目录,然后pages目录下分别创建homepage目录,里面分别创建一个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')
)

现在执行yarn 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",

执行yarn start命令后打开 http://localhost:8000 即可看到内容,并可以切换路由了!

配置代理

devServer下有个proxy属性可以帮助我们设置代理,代理后台接口和前端在一个域名下

修改webpack.dev.config.js

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

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

devtool优化

当启动报错或者像打断点的时候,会发现打包后的代码无从下手。所以我们使用devtool方便调试。在webpack.dev.config.js里面添加

devtool: 'inline-source-map'

然后就可以在srouce里面能看到我们写的代码,也能打断点调试哦~

文件路由优化

正常我们引用组件或者页面的时候,一般都是已../的形式去使用。若是文件层级过深,会导致../../../的情况,不好维护和读懂,为此webpack提供了alias 别名配置。

看这里:切记名称不可声明成你引入的其他包名。别名的会覆盖你的包名,导致你无法引用其他包。栗子:reduxreact等 首先在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目录,里面分别创建两个目录,actionsreducers,分别存放我们的actionreducer

安装reduxreact-redux

yarn add redux -S
yarn add react-redux  -S

在目录下redux/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}
}

在目录下redux/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.dev.config.js配置里添加actionsreducers的别名。

'@actions': path.join(__dirname, '../src/redux/actions'),
'@reducers': path.join(__dirname, '../src/redux/reducers')

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

特殊说明:你可能有很多reducertype一定要是全局唯一的,一般通过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提供了一个方法connectconnect主要有两个参数,一个mapStateToProps,就是把reduxstate,转为组件的Props,还有一个参数是mapDispatchToprops,把发射actions的方法,转为Props属性函数。

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

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。不用手动去传。也不用手动去监听。 接着我们启动一下,yarn start,然后就可以再浏览器中看到我们的计数器功能了。

我们开发中会有很多的reducerredux提供了一个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的工作流:

  • 调用store.dispatch(action)提交action。
  • redux store调用传入的reducer函数。把当前的stateaction传进去。
  • reducer 应该把多个子 reducer 输出合并成一个单一的 state 树。
  • 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.dev.config.jscontentBase配置,并在根目录下新建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-pluginwebpack会自动将打包好的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增加

rules: [{
    test: /\.js$/,
    use: ['babel-loader?cacheDirectory=true'],
    include: path.join(__dirname, '../src')
},{
    test: /\.css$/,
    use: ['style-loader', 'css-loader']
}]

yarn start 启动后查看page路由就可以看到样式生效了。

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

  • style-loader将所有的计算后的样式加入页面中; 二者组合在一起使你能够把样式表嵌入webpack打包后的JS文件中。

集成PostCSS优化

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

安装postcss-loader postcss-cssnext

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>

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

/** 修改之前 */
css-loader?modules

/** 之后 */
{
    loader:'css-loader',
    options: {
        modules: {
            localIdentName: '[local]--[hash:base64:5]',
        }
    }
}

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

编译图片优化

安装图片的加载器

yarn add url-loader file-loader -D

然后在src下新建images目录,并放一个图片a.png

接着在webpack.dev.config.jsrules中配置,同时添加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.png'

<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文件里面,现在我们将它提取出来。 使用webpackmini-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']
}
plugins: [
    ...,
    new MiniCssExtractPlugin({ // 压缩css
        filename: '[name].[contenthash].css',
        chunkFilename: '[id].[contenthash].css'
    })
]

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

缓存

刚才我们output输出的时候写入了hashchunkhashcontenthash,那他们到底有什么用呢?

  • hash是跟整个项目的构建相关,只要项目里有文件更改,整个项目构建的hash值都会更改,并且全部文件都共用相同的hash
  • chunkhashhash不一样,它根据不同的入口文件(Entry)进行依赖文件解析、构建对应的chunk,生成对应的哈希值。
  • contenthash是针对文件内容级别的,只有你自己模块的内容变了,那么hash值才改变,所以我们可以通过contenthash解决上诉问题

生产坏境构建

开发环境(development)和生产环境(production)的构建目标差异很大。

在开发环境中,我们需要具有实时重新加载 或 热模块替换能力的 source maplocalhost 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"

再把webpack.prod.config.js文件中的devtool的值改成none

接下来我们为打包多做一些优化。

文件压缩

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

现在只需要配置mode即可自动使用开发环境的一些配置,包括JS压缩等等

mode:'production',

打包后体积大幅度变小

公共块提取

这表示将选择哪些块进行优化。当提供一个字符串,有效值为allasyncinitial。提供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

安装optimize-css-assets-webpack-plugin插件

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插件。

安装clean-webpack-plugin

yarn add clean-webpack-plugin -D

然后配置打包文件

const { CleanWebpackPlugin } = require('clean-webpack-plugin');
plugins: [
    ...
    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,它却整个的引入了
  • 二是会污染全局
  • 接下来我们做一下优化,添加

``shell script yarn add @babel/plugin-transform-runtime -D yarn add [email protected] -D yarn add @babel/plugin-proposal-class-properties -D

yarn add @babel/runtime-corejs2 -S


添加完后配置`package.json`,添加`browserslist`,来声明生效浏览器
```json
"browserslist": [
    "> 1%",
    "last 2 versions"
  ],

在修改我们的babel.config.js配置文件

const babelConfig = {
    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']
}

module.exports = babelConfig;

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

然后分别创建userInforeduceractionpage

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添加到全局唯一的statestore里面去,

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支持异步函数。

store.js 如下

import {createStore,applyMiddleware} from 'redux';
import counter  from '@reducers/counter';
import {combineReducers} from 'redux';
import userInfo  from '@reducers/userInfo';
import thunkMiddleware from 'redux-thunk';

let store = createStore(combineReducers({counter, userInfo}), applyMiddleware(thunkMiddleware));

export default store;

然后我们在重新启动一下,会发现获取到了数据。

部署

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

cd 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。我们将它改为 修改webpack.prod.config.js中的

publicPath : '/dist/',

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

react-cli's People

Watchers

 avatar  avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.