react 环境 全家桶

 
Owner

brickspert commented on 2 Sep 2017  

从零搭建React全家桶框架教程

源码地址:https://github.com/brickspert/react-family
提问反馈:blog

大家阅读的时候,照着目录来阅读哦,有些章节不在文章里面。要点链接的~

目录

  1. 写在前面
  2. 说明
  3. init项目
  4. webpack
  5. babel
  6. react
  7. 命令优化
  8. react-router
  9. webpack-dev-server
  10. 模块热替换(Hot Module Replacement)
  11. 文件路径优化
  12. redux
  13. devtool优化
  14. 编译css
  15. 编译图片
  16. 按需加载
  17. 缓存
  18. HtmlWebpackPlugin
  19. 提取公共代码
  20. 生产坏境构建
  21. 文件压缩
  22. 指定环境
  23. 优化缓存
  24. public path
  25. 打包优化
  26. 抽取css
  27. 使用axiosmiddleware优化API请求
  28. 合并提取webpack公共配置 webpack-common-config(2017-09-04)
  29. 优化目录结构并增加404页面(2017-09-04)
  30. 加入 babel-plugin-transform-runtime 和 babel-polyfill(2017-09-17)
  31. 集成PostCSS(2017-09-17)
  32. redux 模块热替换配置(2017-09-26)
  33. 模拟AJAX数据之Mock.js(2017-11-21)
  34. 使用 CSS Modules(2017-12-13)
  35. 使用 json-server 代替 Mock.js(2017-12-18)
  36. 问题修复

写在前面

当我第一次跟着项目做react项目的时候,由于半截加入的,对框架了解甚少,只能跟着别人的样板写。对整个框架没有一点了解。

做项目,总是要解决各种问题的,所以每个地方都需要去了解,但是对整个框架没有一个整体的了解,实在是不行。

期间,我也跟着别人的搭建框架的教程一步一步的走,但是经常因为自己太菜,走不下去。在经过各种蹂躏之后,对整个框架也有一个大概的了解,
我就想把他写下来,让后来的菜鸟能跟着我的教程对react全家桶有一个全面的认识。

我的这个教程,从新建根文件夹开始,到成型的框架,每个文件为什么要建立?建立了干什么?每个依赖都是干什么的?一步一步写下来,供大家学习。

当然,这个框架我以后会一直维护的,也希望大家能一起来完善这个框架,如果您有任何建议,欢迎在这里留言,欢迎fork源码react-family

我基于该框架react-family又做了一个兼容IE8的版本,教程在这里react-family框架兼容IE8教程

说明

  1. 每个命令行块都是以根目录为基础的。例如下面命令行块,都是基于根目录的。
cd src/pages
mkdir Home
  1. 技术栈均是目前最新的。
  • react 15.6.1
  • react-router-dom 4.2.2
  • redux 3.7.2
  • webpack 3.5.5
  1. 目录说明
│  .babelrc                          #babel配置文件
│  package-lock.jsonpackage.jsonREADME.MDwebpack.config.js                 #webpack生产配置文件
│  webpack.dev.config.js             #webpack开发配置文件
│  
├─dist
├─public                             #公共资源文件
└─src                                #项目源码
    │  index.html                    #index.html模板
    │  index.js                      #入口文件
    │  
    ├─component                      #组建库
    │  └─Hello
    │          Hello.js
    │          
    ├─pages                          #页面目录
    │  ├─Counter
    │  │      Counter.js
    │  │      
    │  ├─Home
    │  │      Home.js
    │  │      
    │  ├─Page1
    │  │  │  Page1.css                #页面样式
    │  │  │  Page1.js
    │  │  │  
    │  │  └─images                    #页面图片
    │  │          brickpsert.jpg
    │  │          
    │  └─UserInfo
    │          UserInfo.js
    │          
    ├─redux
    │  │  reducers.js
    │  │  store.js
    │  │  
    │  ├─actions
    │  │      counter.js
    │  │      userInfo.js
    │  │      
    │  ├─middleware
    │  │      promiseMiddleware.js
    │  │      
    │  └─reducers
    │          counter.jsuserInfo.js
    │          
    └─router                        #路由文件
            Bundle.js
            router.js
            

init项目

  1. 创建文件夹并进入

    mkdir react-family && cd react-family

  2. init npm

    npm init 按照提示填写项目基本信息

webpack

  1. 安装 webpack

    npm install --save-dev webpack

    Q: 什么时候用--save-dev,什么时候用--save

    A: --save-dev 是你开发时候依赖的东西,--save 是你发布之后还依赖的东西。看这里

  2. 根据webpack文档编写最基础的配置文件

    新建webpack开发配置文件 touch webpack.dev.config.js

    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

    src/index.js 添加内容

    document.getElementById('app').innerHTML = "Webpack works"

    现在我们执行命令 webpack --config webpack.dev.config.js

    webpack如果没有全局安装,这里会报错哦。命令建议全局安装,如果编译有问题看这里 #1 (comment)

    我们可以看到生成了dist文件夹和bundle.js

  4. 现在我们测试下~

    dist文件夹下面新建一个index.html

    touch ./dist/index.html

    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,可以看到Webpack works!

    webpack

    现在回头看下,我们做了什么或者说webpack做了什么。

    把入口文件 index.js 经过处理之后,生成 bundle.js。就这么简单。

babel

Babel 把用最新标准编写的 JavaScript 代码向下编译成可以在今天随处可用的版本。 这一过程叫做“源码到源码”编译, 也被称为转换编译。

通俗的说,就是我们可以用ES6, ES7等来编写代码,Babel会把他们统统转为ES5。

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

新建babel配置文件.babelrc

touch .babelrc

.babelrc

 {
   "presets": [
     "es2015",
     "react",
     "stage-0"
   ],
   "plugins": []
 }

修改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 webpack.dev.config.js

浏览器打开index.html,我们看到正确输出了我现在在使用Babel!

babel

然后我们打开打包后的bundle.js,翻页到最下面,可以看到箭头函数被转换成普通函数了!

babel-bundle.png

Q: babel-preset-state-0,babel-preset-state-1,babel-preset-state-2,babel-preset-state-3有什么区别?

A: 每一级包含上一级的功能,比如 state-0包含state-1的功能,以此类推。state-0功能最全。具体可以看这篇文章:babel配置-各阶段的stage的区别

参考地址:

  1. https://segmentfault.com/a/1190000008159877

  2. http://www.ruanyifeng.com/blog/2016/01/babel.html

react

npm install --save react react-dom

修改 src/index.js使用react

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

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

执行打包命令webpack --config webpack.dev.config.js

打开index.html 看效果。

我们简单做下改进,把Hello React放到组件里面。体现组件化~

cd src
mkdir component
cd component
mkdir Hello
cd Hello
touch Hello.js

按照React语法,写一个Hello组件

import React, {Component} from 'react';

export default class Hello extends Component {
    render() {
        return (
            <div>
                Hello,React!
            </div>
        )
    }
}

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

src/index.js

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

ReactDom.render(
    <Hello/>, document.getElementById('app'));

根目录执行打包命令

webpack --config webpack.dev.config.js

打开index.html看效果咯~

命令优化

Q:每次打包都得在根目录执行这么一长串命令webpack --config webpack.dev.config.js,能不打这么长吗?

A:修改package.json里面的script,增加dev-build

package.json

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

现在我们打包只需要执行npm run dev-build就可以啦!

参考地址:

http://www.ruanyifeng.com/blog/2016/10/npm_scripts.html

react-router

npm install --save react-router-dom

新建router文件夹和组件

cd src
mkdir router && touch router/router.js

按照react-router文档编辑一个最基本的router.js。包含两个页面homepage1

src/router/router.js

import React from 'react';

import {BrowserRouter as Router, Route, Switch, Link} from 'react-router-dom';

import Home from '../pages/Home/Home';
import Page1 from '../pages/Page1/Page1';


const getRouter = () => (
    <Router>
        <div>
            <ul>
                <li><Link to="/">首页</Link></li>
                <li><Link to="/page1">Page1</Link></li>
            </ul>
            <Switch>
                <Route exact path="/" component={Home}/>
                <Route path="/page1" component={Page1}/>
            </Switch>
        </div>
    </Router>
);

export default getRouter;

新建页面文件夹

cd src
mkdir pages

新建两个页面 Home,Page1

cd src/pages
mkdir Home && touch Home/Home.js
mkdir Page1 && touch Page1/Page1.js

填充内容:

src/pages/Home/Home.js

import React, {Component} from 'react';

export default class Home extends Component {
    render() {
        return (
            <div>
                this is home~
            </div>
        )
    }
}

Page1.js

import React, {Component} from 'react';

export default class Page1 extends Component {
    render() {
        return (
            <div>
                this is Page1~
            </div>
        )
    }
}

现在路由和页面建好了,我们在入口文件src/index.js引用Router。

修改src/index.js

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

import getRouter from './router/router';

ReactDom.render(
    getRouter(), document.getElementById('app'));

现在执行打包命令npm run dev-build。打开index.html查看效果啦!

那么问题来了~我们发现点击‘首页’和‘Page1’没有反应。不要惊慌,这是正常的。

我们之前一直用这个路径访问index.html,类似这样:file:///F:/react/react-family/dist/index.html
这种路径了,不是我们想象中的路由那样的路径http://localhost:3000~我们需要配置一个简单的WEB服务器,指向
index.html~有下面两种方法来实现

  1. NginxApacheIIS等配置启动一个简单的的WEB服务器。
  2. 使用webpack-dev-server来配置启动WEB服务器。

下一节,我们来使用第二种方法启动服务器。这一节的DEMO,先放这里。

参考地址

  1. http://www.jianshu.com/p/e3adc9b5f75c

  2. http://reacttraining.cn/web/guides/quick-start

webpack-dev-server

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

npm install webpack-dev-server --save-dev

2017.11.16补充:这里webpack-dev-server需要全局安装,要不后面用的时候要写相对路径。需要再执行这个 npm install webpack-dev-server -g

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

webpack.dev.config.js

    devServer: {
        contentBase: path.join(__dirname, './dist')
    }

现在执行

webpack-dev-server --config webpack.dev.config.js

浏览器打开http://localhost:8080,OK,现在我们可以点击首页,Page1了,
看URL地址变化啦!我们看到react-router已经成功了哦。

Q: --content-base是什么?

A:URL的根目录。如果不设定的话,默认指向项目根目录。

重要提示:webpack-dev-server编译后的文件,都存储在内存中,我们并不能看见的。你可以删除之前遗留的文件dist/bundle.js
仍然能正常打开网站!

每次执行webpack-dev-server --config webpack.dev.config.js,要打很长的命令,我们修改package.json,增加script->start:

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

下次执行npm start就可以了。

既然用到了webpack-dev-server,我们就看看它的其他的配置项
看了之后,发现有几个我们可以用的。

  • color(CLI only) console中打印彩色日志
  • historyApiFallback 任意的404响应都被替代为index.html。有什么用呢?你现在运行
    npm start,然后打开浏览器,访问http://localhost:8080,然后点击Page1到链接http://localhost:8080/page1
    然后刷新页面试试。是不是发现刷新后404了。为什么?dist文件夹里面并没有page1.html,当然会404了,所以我们需要配置
    historyApiFallback,让所有的404定位到index.html
  • host 指定一个host,默认是localhost。如果你希望服务器外部可以访问,指定如下:host: "0.0.0.0"。比如你用手机通过IP访问。
  • hot 启用Webpack的模块热替换特性。关于热模块替换,我下一小节专门讲解一下。
  • port 配置要监听的端口。默认就是我们现在使用的8080端口。
  • proxy 代理。比如在 localhost:3000 上有后端服务的话,你可以这样启用代理:
    proxy: {
      "/api": "http://localhost:3000"
    }
  • progress(CLI only) 将编译进度输出到控制台。

根据这几个配置,修改下我们的webpack-dev-server的配置~

webpack.dev.config.js

    devServer: {
        port: 8080,
        contentBase: path.join(__dirname, './dist'),
        historyApiFallback: true,
        host: '0.0.0.0'
    }

CLI ONLY的需要在命令行中配置

package.json

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

现在我们执行npm start 看看效果。是不是看到打包的时候有百分比进度?在http://localhost:8080/page1页面刷新是不是没问题了?
用手机通过局域网IP是否可以访问到网站?

参考地址:

  1. https://segmentfault.com/a/1190000006670084
  2. https://webpack.js.org/guides/development/#using-webpack-dev-server

模块热替换(Hot Module Replacement)

到目前,当我们修改代码的时候,浏览器会自动刷新,不信你可以去试试。(如果你的不会刷新,看看这个调整文本编辑器

我相信看这个教程的人,应该用过别人的框架。我们在修改代码的时候,浏览器不会刷新,只会更新自己修改的那一块。我们也要实现这个效果。

我们看下webpack模块热替换教程。

我们接下来要这么修改

package.json 增加 --hot

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

src/index.js 增加module.hot.accept(),如下。当模块更新的时候,通知index.js

src/index.js

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

import getRouter from './router/router';

if (module.hot) {
    module.hot.accept();
}

ReactDom.render(
    getRouter(), document.getElementById('app'));

现在我们执行npm start,打开浏览器,修改Home.js,看是不是不刷新页面的情况下,内容更新了?惊不惊喜?意不意外?

做模块热替换,我们只改了几行代码,非常简单的。纸老虎一个~

现在我需要说明下我们命令行使用的--hot,可以通过配置webpack.dev.config.js来替换,
向文档上那样,修改下面三处。但我们还是用--hot吧。下面的方式我们知道一下就行,我们不用。同样的效果。

const webpack = require('webpack');

devServer: {
    hot: true
}

plugins:[
     new webpack.HotModuleReplacementPlugin()
]

HRM配置其实有两种方式,一种CLI方式,一种Node.js API方式。我们用到的就是CLI方式,比较简单。
Node.js API方式,就是建一个server.js等等,网上大部分教程都是这种方式,这里不做讲解了。

你以为模块热替换到这里就结束了?nonono~

上面的配置对react模块的支持不是很好哦。

例如下面的demo,当模块热替换的时候,state会重置,这不是我们想要的。

修改Home.js,增加计数state

src/pages/Home/Home.js

import React, {Component} from 'react';

export default class Home extends Component {
    constructor(props) {
        super(props);
        this.state = {
            count: 0
        }
    }

    _handleClick() {
        this.setState({
            count: ++this.state.count
        });
    }

    render() {
        return (
            <div>
                this is home~<br/>
                当前计数:{this.state.count}<br/>
                <button onClick={() => this._handleClick()}>自增</button>
            </div>
        )
    }
}

你可以测试一下,当我们修改代码的时候,webpack在更新页面的时候,也把count初始为0了。

为了在react模块更新的同时,能保留state等页面中其他状态,我们需要引入react-hot-loader~

Q: 请问webpack-dev-serverreact-hot-loader两者的热替换有什么区别?

A: 区别在于webpack-dev-server自己的--hot模式只能即时刷新页面,但状态保存不住。因为React有一些自己语法(JSX)是HotModuleReplacementPlugin搞不定的。
react-hot-loader--hot基础上做了额外的处理,来保证状态可以存下来。(来自segmentfault

下面我们来加入react-hot-loader v3,

安装依赖

npm install react-hot-loader@next --save-dev

根据文档
我们要做如下几个修改~

  1. .babelrc 增加 react-hot-loader/babel

.babelrc

{
  "presets": [
    "es2015",
    "react",
    "stage-0"
  ],
  "plugins": [
    "react-hot-loader/babel"
  ]
}
  1. webpack.dev.config.js入口增加react-hot-loader/patch

webpack.dev.config.js

    entry: [
        'react-hot-loader/patch',
        path.join(__dirname, 'src/index.js')
    ]
  1. src/index.js修改如下

src/index.js

import React from 'react';
import ReactDom from 'react-dom';
import {AppContainer} from 'react-hot-loader';

import getRouter from './router/router';

/*初始化*/
renderWithHotReload(getRouter());

/*热更新*/
if (module.hot) {
    module.hot.accept('./router/router', () => {
        const getRouter = require('./router/router').default;
        renderWithHotReload(getRouter());
    });
}

function renderWithHotReload(RootElement) {
    ReactDom.render(
        <AppContainer>
            {RootElement}
        </AppContainer>,
        document.getElementById('app')
    )
}

现在,执行npm start,试试。是不是修改页面的时候,state不更新了?

参考文章:

  1. gaearon/react-hot-loader#243

文件路径优化

做到这里,我们简单休息下。做下优化~

在之前写的代码中,我们引用组件,或者页面时候,写的是相对路径~

比如src/router/router.js里面,引用Home.js的时候就用的相对路径

import Home from '../pages/Home/Home';

webpack提供了一个别名配置,就是我们无论在哪个路径下,引用都可以这样

import Home from 'pages/Home/Home';

下面我们来配置下,修改webpack.dev.config.js,增加别名~

webpack.dev.config.js

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

然后我们把之前使用的绝对路径统统改掉。

src/router/router.js

import Home from 'pages/Home/Home';
import Page1 from 'pages/Page1/Page1';

src/index.js

import getRouter from 'router/router';

我们这里约定,下面,我们会默认配置需要的别名路径,不再做重复的讲述哦。

redux

接下来,我们就要就要就要集成redux了。

要对redux有一个大概的认识,可以阅读阮一峰前辈的Redux 入门教程(一):基本用法

如果要对redux有一个非常详细的认识,我推荐阅读中文文档,写的非常好。读了这个教程,有一个非常深刻的感觉,redux并没有任何魔法。

不要被各种关于 reducers, middleware, store 的演讲所蒙蔽 ---- Redux 实际是非常简单的。

当然,我这篇文章是写给新手的,如果看不懂上面的文章,或者不想看,没关系。先会用,多用用就知道原理了。

开始整代码!我们就做一个最简单的计数器。自增,自减,重置。

先安装redux npm install --save redux

初始化目录结构

cd src
mkdir redux
cd redux
mkdir actions
mkdir reducers
touch reducers.js
touch store.js
touch actions/counter.js
touch reducers/counter.js

先来写action创建函数。通过action创建函数,可以创建action~
src/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}
}

再来写reducer,reducer是一个纯函数,接收action和旧的state,生成新的state.

src/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
    }
}

一个项目有很多的reducers,我们要把他们整合到一起

src/redux/reducers.js

import counter from './reducers/counter';

export default function combineReducers(state = {}, action) {
    return {
        counter: counter(state.counter, action)
    }
}

到这里,我们必须再理解下一句话。

reducer就是纯函数,接收state 和 action,然后返回一个新的 state

看看上面的代码,无论是combineReducers函数也好,还是reducer函数也好,都是接收stateaction
返回更新后的state。区别就是combineReducers函数是处理整棵树,reducer函数是处理树的某一点。

接下来,我们要创建一个store

前面我们可以使用 action 来描述“发生了什么”,使用action创建函数来返回action

还可以使用 reducers 来根据 action 更新 state 。

那我们如何提交action?提交的时候,怎么才能触发reducers呢?

store 就是把它们联系到一起的对象。store 有以下职责:

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

src/redux/store.js

import {createStore} from 'redux';
import combineReducers from './reducers.js';

let store = createStore(combineReducers);

export default store;

到现在为止,我们已经可以使用redux了~

下面我们就简单的测试下

cd src
cd redux
touch testRedux.js

src/redux/testRedux.js

import {increment, decrement, reset} from './actions/counter';

import store from './store';

// 打印初始状态
console.log(store.getState());

// 每次 state 更新时,打印日志
// 注意 subscribe() 返回一个函数用来注销监听器
let unsubscribe = store.subscribe(() =>
    console.log(store.getState())
);

// 发起一系列 action
store.dispatch(increment());
store.dispatch(decrement());
store.dispatch(reset());

// 停止监听 state 更新
unsubscribe();

当前文件夹执行命令

webpack testRedux.js build.js

node build.js

是不是看到输出了state变化?

{ counter: { count: 0 } }
{ counter: { count: 1 } }
{ counter: { count: 0 } }
{ counter: { count: 0 } }

做这个测试,就是为了告诉大家,reduxreact没关系,虽说他俩能合作。

到这里,我建议你再理下redux的数据流,看看这里

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

就是酱紫~~

这会webpack.dev.config.js路径别名增加一下,后面好写了。

webpack.dev.config.js

        alias: {
            ...
            actions: path.join(__dirname, 'src/redux/actions'),
            reducers: path.join(__dirname, 'src/redux/reducers'),
            redux: path.join(__dirname, 'src/redux')
        }

把前面的相对路径都改改。

下面我们开始搭配react使用。

写一个Counter页面

cd src/pages
mkdir Counter
touch Counter/Counter.js

src/pages/Counter/Counter.js

import React, {Component} from 'react';

export default class Counter extends Component {
    render() {
        return (
            <div>
                <div>当前计数为(显示redux计数)</div>
                <button onClick={() => {
                    console.log('调用自增函数');
                }}>自增
                </button>
                <button onClick={() => {
                    console.log('调用自减函数');
                }}>自减
                </button>
                <button onClick={() => {
                    console.log('调用重置函数');
                }}>重置
                </button>
            </div>
        )
    }
}

修改路由,增加Counter

src/router/router.js

import React from 'react';

import {BrowserRouter as Router, Route, Switch, Link} from 'react-router-dom';

import Home from 'pages/Home/Home';
import Page1 from 'pages/Page1/Page1';
import Counter from 'pages/Counter/Counter';

const getRouter = () => (
    <Router>
        <div>
            <ul>
                <li><Link to="/">首页</Link></li>
                <li><Link to="/page1">Page1</Link></li>
                <li><Link to="/counter">Counter</Link></li>
            </ul>
            <Switch>
                <Route exact path="/" component={Home}/>
                <Route path="/page1" component={Page1}/>
                <Route path="/counter" component={Counter}/>
            </Switch>
        </div>
    </Router>
);

export default getRouter;

npm start看看效果。

下一步,我们让Counter组件和Redux联合起来。使Counter能获得到Reduxstate,并且能发射action

当然我们可以使用刚才测试testRedux的方法,手动监听~手动引入store~但是这肯定很麻烦哦。

react-redux提供了一个方法connect

容器组件就是使用 store.subscribe() 从 Redux state 树中读取部分数据,并通过 props 来把这些数据提供给要渲染的组件。你可以手工来开发容器组件,但建议使用 React Redux 库的 connect() 方法来生成,这个方法做了性能优化来避免很多不必要的重复渲染。

connect接收两个参数,一个mapStateToProps,就是把reduxstate,转为组件的Props,还有一个参数是mapDispatchToprops,
就是把发射actions的方法,转为Props属性函数。

先来安装react-redux

npm install --save react-redux

src/pages/Counter/Counter.js

import React, {Component} from 'react';
import {increment, decrement, reset} from 'actions/counter';

import {connect} from 'react-redux';

class Counter extends Component {
    render() {
        return (
            <div>
                <div>当前计数为{this.props.counter.count}</div>
                <button onClick={() => this.props.increment()}>自增
                </button>
                <button onClick={() => this.props.decrement()}>自减
                </button>
                <button onClick={() => this.props.reset()}>重置
                </button>
            </div>
        )
    }
}

const mapStateToProps = (state) => {
    return {
        counter: state.counter
    }
};

const mapDispatchToProps = (dispatch) => {
    return {
        increment: () => {
            dispatch(increment())
        },
        decrement: () => {
            dispatch(decrement())
        },
        reset: () => {
            dispatch(reset())
        }
    }
};

export default connect(mapStateToProps, mapDispatchToProps)(Counter);

下面我们要传入store

所有容器组件都可以访问 Redux store,所以可以手动监听它。一种方式是把它以 props 的形式传入到所有容器组件中。但这太麻烦了,因为必须要用 store 把展示组件包裹一层,仅仅是因为恰好在组件树中渲染了一个容器组件。

建议的方式是使用指定的 React Redux 组件 来 魔法般的 让所有容器组件都可以访问 store,而不必显示地传递它。只需要在渲染根组件时使用即可。

src/index.js

import React from 'react';
import ReactDom from 'react-dom';
import {AppContainer} from 'react-hot-loader';
import {Provider} from 'react-redux';
import store from './redux/store';

import getRouter from 'router/router';

/*初始化*/
renderWithHotReload(getRouter());

/*热更新*/
if (module.hot) {
    module.hot.accept('./router/router', () => {
        const getRouter = require('router/router').default;
        renderWithHotReload(getRouter());
    });
}

function renderWithHotReload(RootElement) {
    ReactDom.render(
        <AppContainer>
            <Provider store={store}>
                {RootElement}
            </Provider>
        </AppContainer>,
        document.getElementById('app')
    )
}

到这里我们就可以执行npm start,打开localhost:8080/counter看效果了。

但是你发现npm start一直报错

ERROR in ./node_modules/react-redux/es/connect/mapDispatchToProps.js
Module not found: Error: Can't resolve 'redux' in 'F:\Project\react\react-family\node_modules\react-redux\es\connect'

ERROR in ./src/redux/store.js
Module not found: Error: Can't resolve 'redux' in 'F:\Project\react\react-family\src\redux'

WTF?这个错误困扰了半天。我说下为什么造成这个错误。我们引用redux的时候这样用的

import {createStore} from 'redux'

然而,我们在webapck.dev.config.js里面这样配置了

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

然后webapck编译的时候碰到redux都去src/redux去找了。但是找不到啊。所以我们把webpack.dev.config.js里面redux这一行删除了,就好了。
并且把使用我们自己使用redux文件夹的地方改成相对路径哦。

现在你可以npm start去看效果了。

这里我们再缕下(可以读React 实践心得:react-redux 之 connect 方法详解

  1. Provider组件是让所有的组件可以访问到store。不用手动去传。也不用手动去监听。

  2. connect函数作用是从 Redux state 树中读取部分数据,并通过 props 来把这些数据提供给要渲染的组件。也传递dispatch(action)函数到props

接下来,我们要说异步action

参考地址: http://cn.redux.js.org/docs/advanced/AsyncActions.html

想象一下我们调用一个异步get请求去后台请求数据:

  1. 请求开始的时候,界面转圈提示正在加载。isLoading置为true
  2. 请求成功,显示数据。isLoading置为false,data填充数据。
  3. 请求失败,显示失败。isLoading置为false,显示错误信息。

下面,我们以向后台请求用户基本信息为例。

  1. 我们先创建一个user.json,等会请求用,相当于后台的API接口。
cd dist
mkdir api
cd api
touch user.json

dist/api/user.json

{
  "name": "brickspert",
  "intro": "please give me a star"
}
  1. 创建必须的action创建函数。
cd src/redux/actions
touch userInfo.js

src/redux/actions/userInfo.js

export const GET_USER_INFO_REQUEST = "userInfo/GET_USER_INFO_REQUEST";
export const GET_USER_INFO_SUCCESS = "userInfo/GET_USER_INFO_SUCCESS";
export const GET_USER_INFO_FAIL = "userInfo/GET_USER_INFO_FAIL";

function getUserInfoRequest() {
    return {
        type: GET_USER_INFO_REQUEST
    }
}

function getUserInfoSuccess(userInfo) {
    return {
        type: GET_USER_INFO_SUCCESS,
        userInfo: userInfo
    }
}

function getUserInfoFail() {
    return {
        type: GET_USER_INFO_FAIL
    }
}

我们创建了请求中,请求成功,请求失败三个action创建函数。

  1. 创建reducer

再强调下,reducer是根据stateaction生成新state纯函数

cd src/redux/reducers
touch userInfo.js

src/redux/reducers/userInfo.js

import {GET_USER_INFO_REQUEST, GET_USER_INFO_SUCCESS, GET_USER_INFO_FAIL} from 'actions/userInfo';


const initState = {
    isLoading: false,
    userInfo: {},
    errorMsg: ''
};

export default function reducer(state = initState, action) {
    switch (action.type) {
        case GET_USER_INFO_REQUEST:
            return {
                ...state,
                isLoading: true,
                userInfo: {},
                errorMsg: ''
            };
        case GET_USER_INFO_SUCCESS:
            return {
                ...state,
                isLoading: false,
                userInfo: action.userInfo,
                errorMsg: ''
            };
        case GET_USER_INFO_FAIL:
            return {
                ...state,
                isLoading: false,
                userInfo: {},
                errorMsg: '请求错误'
            };
        default:
            return state;
    }
}

这里的...state语法,是和别人的Object.assign()起同一个作用,合并新旧state。我们这里是没效果的,但是我建议都写上这个哦

组合reducer

src/redux/reducers.js

import counter from 'reducers/counter';
import userInfo from 'reducers/userInfo';

export default function combineReducers(state = {}, action) {
    return {
        counter: counter(state.counter, action),
        userInfo: userInfo(state.userInfo, action)
    }
}
  1. 现在有了action,有了reducer,我们就需要调用把action里面的三个action函数和网络请求结合起来。

    • 请求中 dispatch getUserInfoRequest
    • 请求成功 dispatch getUserInfoSuccess
    • 请求失败 dispatch getUserInfoFail

src/redux/actions/userInfo.js增加

export function getUserInfo() {
    return function (dispatch) {
        dispatch(getUserInfoRequest());

        return fetch('http://localhost:8080/api/user.json')
            .then((response => {
                return response.json()
            }))
            .then((json) => {
                    dispatch(getUserInfoSuccess(json))
                }
            ).catch(
                () => {
                    dispatch(getUserInfoFail());
                }
            )
    }
}

我们这里发现,别的action创建函数都是返回action对象:

{type: xxxx}

但是我们现在的这个action创建函数 getUserInfo则是返回函数了。

为了让action创建函数除了返回action对象外,还可以返回函数,我们需要引用redux-thunk

npm install --save redux-thunk

这里涉及到redux中间件middleware,我后面会讲到的。你也可以读这里Middleware

简单的说,中间件就是action在到达reducer,先经过中间件处理。我们之前知道reducer能处理的action只有这样的{type:xxx},所以我们使用中间件来处理
函数形式的action,把他们转为标准的actionreducer。这是redux-thunk的作用。
使用redux-thunk中间件

我们来引入redux-thunk中间件

src/redux/store.js

import {createStore, applyMiddleware} from 'redux';
import thunkMiddleware from 'redux-thunk';
import combineReducers from './reducers.js';

let store = createStore(combineReducers, applyMiddleware(thunkMiddleware));

export default store;

到这里,redux这边OK了,我们来写个组件验证下。

cd src/pages
mkdir UserInfo
cd UserInfo
touch UserInfo.js

src/pages/UserInfo/UserInfo.js

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

class UserInfo extends Component {

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

export default connect((state) => ({userInfo: state.userInfo}), {getUserInfo})(UserInfo);

这里你可能发现connect参数写法不一样了,mapStateToProps函数用了es6简写,mapDispatchToProps用了react-redux提供的简单写法。

增加路由
src/router/router.js

import React from 'react';

import {BrowserRouter as Router, Route, Switch, Link} from 'react-router-dom';

import Home from 'pages/Home/Home';
import Page1 from 'pages/Page1/Page1';
import Counter from 'pages/Counter/Counter';
import UserInfo from 'pages/UserInfo/UserInfo';

const getRouter = () => (
    <Router>
        <div>
            <ul>
                <li><Link to="/">首页</Link></li>
                <li><Link to="/page1">Page1</Link></li>
                <li><Link to="/counter">Counter</Link></li>
                <li><Link to="/userinfo">UserInfo</Link></li>
            </ul>
            <Switch>
                <Route exact path="/" component={Home}/>
                <Route path="/page1" component={Page1}/>
                <Route path="/counter" component={Counter}/>
                <Route path="/userinfo" component={UserInfo}/>
            </Switch>
        </div>
    </Router>
);

export default getRouter;

现在你可以执行npm start去看效果啦!

redux

到这里redux集成基本告一段落了,后面我们还会有一些优化。

combinReducers优化

redux提供了一个combineReducers函数来合并reducer,不用我们自己合并哦。写起来简单,但是意思和我们
自己写的combinReducers也是一样的。

src/redux/reducers.js

import {combineReducers} from "redux";

import counter from 'reducers/counter';
import userInfo from 'reducers/userInfo';


export default combineReducers({
    counter,
    userInfo
});

devtool优化

现在我们发现一个问题,代码哪里写错了,浏览器报错只报在build.js第几行。

错误图片

这让我们分析错误无从下手。看这里

我们增加webpack配置devtool

src/webpack.dev.config.js增加

devtool: 'inline-source-map'

这次看错误信息是不是提示的很详细了?

错误图片

同时,我们在srouce里面能看到我们写的代码,也能打断点调试哦~

错误图片

编译css

先说这里为什么不用scss,因为Windows使用node-sass,需要先安装 Microsoft Windows SDK for Windows 7 and .NET Framework 4
我怕有些人copy这份代码后,没注意,运行不起来。所以这里不用scss了,如果需要,自行编译哦。

npm install css-loader style-loader --save-dev

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

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

webpack.dev.config.js rules增加

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

我们用Page1页面来测试下

cd src/pages/Page1
touch Page1.css

src/pages/Page1/Page1.css

.page-box {
    border: 1px solid red;
}

src/pages/Page1/Page1.js

import React, {Component} from 'react';

import './Page1.css';

export default class Page1 extends Component {
    render() {
        return (
            <div className="page-box">
                this is page1~
            </div>
        )
    }
}

好了,现在npm start去看效果吧。

编译图片

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请求。

我们来用Page1 测试下

cd src/pages/Page1
mkdir images

images文件夹放一个图片。

修改代码,引用图片

src/pages/Page1/Page1.js

import React, {Component} from 'react';

import './Page1.css';

import image from './images/brickpsert.jpg';

export default class Page1 extends Component {
    render() {
        return (
            <div className="page-box">
                this is page1~
                <img src={image}/>
            </div>
        )
    }
}

可以去看看效果啦。

按需加载

为什么要实现按需加载?

我们现在看到,打包完后,所有页面只生成了一个build.js,当我们首屏加载的时候,就会很慢。因为他也下载了别的页面的js了哦。

如果每个页面都打包了自己单独的JS,在进入自己页面的时候才加载对应的js,那首屏加载就会快很多哦。

在 react-router 2.0时代, 按需加载需要用到的最关键的一个函数,就是require.ensure(),它是按需加载能够实现的核心。

在4.0版本,官方放弃了这种处理按需加载的方式,选择了一个更加简洁的处理方式。

传送门

根据官方示例,我们开搞

  1. npm install bundle-loader --save-dev

  2. 新建bundle.js

cd src/router
touch Bundle.js

src/router/Bundle.js

import React, {Component} from 'react'

class Bundle extends Component {
    state = {
        // short for "module" but that's a keyword in js, so "mod"
        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() {
        return this.props.children(this.state.mod)
    }
}

export default Bundle;
  1. 改造路由器

src/router/router.js

import React from 'react';

import {BrowserRouter as Router, Route, Switch, Link} from 'react-router-dom';

import Bundle from './Bundle';

import Home from 'bundle-loader?lazy&name=home!pages/Home/Home';
import Page1 from 'bundle-loader?lazy&name=page1!pages/Page1/Page1';
import Counter from 'bundle-loader?lazy&name=counter!pages/Counter/Counter';
import UserInfo from 'bundle-loader?lazy&name=userInfo!pages/UserInfo/UserInfo';

const Loading = function () {
    return <div>Loading...</div>
};

const createComponent = (component) => (props) => (
    <Bundle load={component}>
        {
            (Component) => Component ? <Component {...props} /> : <Loading/>
        }
    </Bundle>
);

const getRouter = () => (
    <Router>
        <div>
            <ul>
                <li><Link to="/">首页</Link></li>
                <li><Link to="/page1">Page1</Link></li>
                <li><Link to="/counter">Counter</Link></li>
                <li><Link to="/userinfo">UserInfo</Link></li>
            </ul>
            <Switch>
                <Route exact path="/" component={createComponent(Home)}/>
                <Route path="/page1" component={createComponent(Page1)}/>
                <Route path="/counter" component={createComponent(Counter)}/>
                <Route path="/userinfo" component={createComponent(UserInfo)}/>
            </Switch>
        </div>
    </Router>
);

export default getRouter;

现在你可以npm start,打开浏览器,看是不是进入新的页面,都会加载自己的JS的~

但是你可能发现,名字都是0.bundle.js这样子的,这分不清楚是哪个页面的js呀!

我们修改下webpack.dev.config.js,加个chunkFilenamechunkFilename是除了entry定义的入口js之外的js~

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

现在你运行发现名字变成home.js,这样的了。棒棒哒!

那么问题来了home是在哪里设置的?webpack怎么知道他叫home

其实在这里我们定义了,router.js里面

import Home from 'bundle-loader?lazy&name=home!pages/Home/Home';

看到没。这里有个name=home。嘿嘿。

参考地址:

  1. http://www.jianshu.com/p/8dd98a7028e0
  2. https://github.com/ReactTraining/react-router/blob/master/packages/react-router-dom/docs/guides/code-splitting.md
  3. https://segmentfault.com/a/1190000007949841
  4. http://react-china.org/t/webpack-react-router/10123
  5. https://juejin.im/post/58f9717e44d9040069d06cd6

缓存

想象一下这个场景~

我们网站上线了,用户第一次访问首页,下载了home.js,第二次访问又下载了home.js~

这肯定不行呀,所以我们一般都会做一个缓存,用户下载一次home.js后,第二次就不下载了。

有一天,我们更新了home.js,但是用户不知道呀,用户还是使用本地旧的home.js。出问题了~

怎么解决?每次代码更新后,打包生成的名字不一样。比如第一次叫home.a.js,第二次叫home.b.js

文档看这里

我们照着文档来

webpack.dev.config.js

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

每次打包都用增加hash~

现在我们试试,是不是修改了文件,打包后相应的文件名字就变啦?

package

但是你可能发现了,网页打开报错了~因为你dist/index.html里面引用js名字还是bundle.js老名字啊,改成新的名字就可以啦。

啊~那岂不是我每次编译打包,都得去改一下js名字?欲知后事如何,且看下节分享。

HtmlWebpackPlugin

这个插件,每次会自动把js插入到你的模板index.html里面去。

npm install html-webpack-plugin --save-dev

新建模板index.html

cd src
touch index.html

src/index.html

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
<div id="app"></div>
</body>
</html>

修改webpack.dev.config.js,增加plugin

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

    plugins: [new HtmlWebpackPlugin({
        filename: 'index.html',
        template: path.join(__dirname, 'src/index.html')
    })],

npm start运行项目,看看是不是能正常访问啦。~

说明一下:npm start打包后的文件存在内存中,你看不到的。~ 你可以把遗留dist/index.html删除掉了。

提取公共代码

想象一下,我们的主文件,原来的bundle.js里面是不是包含了react,redux,react-router等等
这些代码??这些代码基本上不会改变的。但是,他们合并在bundle.js里面,每次项目发布,重新请求bundle.js的时候,相当于重新请求了
react等这些公共库。浪费了~

我们把react这些不会改变的公共库提取出来,用户缓存下来。从此以后,用户再也不用下载这些库了,无论是否发布项目。嘻嘻。

webpack文档给了教程,看这里

webpack.dev.config.js

    var webpack = require('webpack');

    entry: {
        app: [
            'react-hot-loader/patch',
            path.join(__dirname, 'src/index.js')
        ],
        vendor: ['react', 'react-router-dom', 'redux', 'react-dom', 'react-redux']
    }
    
        /*plugins*/
        new webpack.optimize.CommonsChunkPlugin({
            name: 'vendor'
        })

react等库生成打包到vendor.hash.js里面去。

但是你现在可能发现编译生成的文件app.[hash].jsvendor.[hash].js生成的hash一样的,这里是个问题,因为呀,你每次修改代码,都会导致vendor.[hash].js名字改变,那我们提取出来的意义也就没了。其实文档上写的很清楚,

   output: {
        path: path.join(__dirname, './dist'),
        filename: '[name].[hash].js', //这里应该用chunkhash替换hash
        chunkFilename: '[name].[chunkhash].js'
    }

但是无奈,如果用chunkhash,会报错。和webpack-dev-server --hot不兼容,具体看这里

现在我们在配置开发版配置文件,就向webpack-dev-server妥协,因为我们要用他。问题先放这里,等会我们配置正式版webpack.config.js的时候要解决这个问题。

生产坏境构建

开发环境(development)和生产环境(production)的构建目标差异很大。在开发环境中,我们需要具有强大的、具有实时重新加载(live reloading)或热模块替换(hot module replacement)能力的 source map 和 localhost server。而在生产环境中,我们的目标则转向于关注更小的 bundle,更轻量的 source map,以及更优化的资源,以改善加载时间。由于要遵循逻辑分离,我们通常建议为每个环境编写彼此独立的 webpack 配置。

文档看这里

我们要开始做了~

touch webpack.config.js

webpack.dev.config.js的基础上先做以下几个修改~

  1. 先删除webpack-dev-server相关的东西~
  2. devtool的值改成cheap-module-source-map
  3. 刚才说的hash改成chunkhash

webpack.config.js

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

module.exports = {
    devtool: 'cheap-module-source-map',
    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].[chunkhash].js',
        chunkFilename: '[name].[chunkhash].js'
    },
    module: {
        rules: [{
            test: /\.js$/,
            use: ['babel-loader'],
            include: path.join(__dirname, 'src')
        }, {
            test: /\.css$/,
            use: ['style-loader', 'css-loader']
        }, {
            test: /\.(png|jpg|gif)$/,
            use: [{
                loader: 'url-loader',
                options: {
                    limit: 8192
                }
            }]
        }]
    },
    plugins: [
        new HtmlWebpackPlugin({
            filename: 'index.html',
            template: path.join(__dirname, 'src/index.html')
        }),
        new webpack.optimize.CommonsChunkPlugin({
            name: 'vendor'
        })
    ],

    resolve: {
        alias: {
            pages: path.join(__dirname, 'src/pages'),
            component: path.join(__dirname, 'src/component'),
            router: path.join(__dirname, 'src/router'),
            actions: path.join(__dirname, 'src/redux/actions'),
            reducers: path.join(__dirname, 'src/redux/reducers')
        }
    }
};

package.json增加打包脚本

"build":"webpack --config webpack.config.js"

然后执行npm run build~看看dist文件夹是不是生成了我们发布要用的所有文件哦?

接下来我们还是要优化正式版配置文件~

文件压缩

webpack使用UglifyJSPlugin来压缩生成的文件。

npm i --save-dev uglifyjs-webpack-plugin

webpack.config.js

const UglifyJSPlugin = require('uglifyjs-webpack-plugin')

module.exports = {
  plugins: [
    new UglifyJSPlugin()
  ]
}

npm run build发现打包文件大小减小了好多。

uglify

指定环境

许多 library 将通过与 process.env.NODE_ENV 环境变量关联,以决定 library 中应该引用哪些内容。例如,当不处于生产环境中时,某些 library 为了使调试变得容易,可能会添加额外的日志记录(log)和测试(test)。其实,当使用 process.env.NODE_ENV === 'production' 时,一些 library 可能针对具体用户的环境进行代码优化,从而删除或添加一些重要代码。我们可以使用 webpack 内置的 DefinePlugin 为所有的依赖定义这个变量:

webpack.config.js

module.exports = {
  plugins: [
       new webpack.DefinePlugin({
          'process.env': {
              'NODE_ENV': JSON.stringify('production')
           }
       })
  ]
}

npm run build后发现vendor.[hash].js又变小了。

uglify

优化缓存

刚才我们把[name].[hash].js变成[name].[chunkhash].js后,npm run build后,
发现app.xxx.jsvendor.xxx.js不一样了哦。

但是现在又有一个问题了。

你随便修改代码一处,例如Home.js,随便改变个字,你发现home.xxx.js名字变化的同时,
vendor.xxx.js名字也变了。这不行啊。这和没拆分不是一样一样了吗?我们本意是vendor.xxx.js
名字永久不变,一直缓存在用户本地的。~

官方文档推荐了一个插件HashedModuleIdsPlugin

    plugins: [
        new webpack.HashedModuleIdsPlugin()
    ]

现在你打包,修改代码再试试,是不是名字不变啦?错了,现在打包,我发现名字还是变了,经过比对文档,我发现还要加一个runtime代码抽取,

new webpack.optimize.CommonsChunkPlugin({
    name: 'runtime'
})

加上这句话就好了~为什么呢?看下解释

注意,引入顺序在这里很重要。CommonsChunkPlugin 的 'vendor' 实例,必须在 'runtime' 实例之前引入。

public path

想象一个场景,我们的静态文件放在了单独的静态服务器上去了,那我们打包的时候,如何让静态文件的链接定位到静态服务器呢?

看文档Public Path

webpack.config.js output 中增加一个publicPath,我们当前用/,相对于当前路径,如果你要改成别的url,就改这里就好了。

    output: {
        publicPath : '/'
    }

打包优化

你现在打开dist,是不是发现好多好多文件,每次打包后的文件在这里混合了?我们希望每次打包前自动清理下dist文件。

npm install clean-webpack-plugin --save-dev

webpack.config.js

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


plugins: [
    new CleanWebpackPlugin(['dist'])
]

现在npm run build试试,是不是之前的都清空了。当然我们之前的api文件夹也被清空了,不过没关系哦~本来就是测试用的。

抽取css

目前我们的css是直接打包进js里面的,我们希望能单独生成css文件。

我们使用extract-text-webpack-plugin来实现。

npm install --save-dev extract-text-webpack-plugin

webpack.config.js

const ExtractTextPlugin = require("extract-text-webpack-plugin");

module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ExtractTextPlugin.extract({
          fallback: "style-loader",
          use: "css-loader"
        })
      }
    ]
  },
  plugins: [
     new ExtractTextPlugin({
         filename: '[name].[contenthash:5].css',
         allChunks: true
     })
  ]
}

npm run build后发现单独生成了css文件哦

使用 axiosmiddleware优化API请求

先安装下axios

npm install --save axios

我们之前项目的一次API请求是这样写的哦~

action创建函数是这样的。比我们现在写的fetch简单多了。

export function getUserInfo() {
    return {
        types: [GET_USER_INFO_REQUEST, GET_USER_INFO_SUCCESS, GET_USER_INFO_FAIL],
        promise: client => client.get(`http://localhost:8080/api/user.json`)
        afterSuccess:(dispatch,getState,response)=>{
            /*请求成功后执行的函数*/
        },
        otherData:otherData
    }
}

然后在dispatch(getUserInfo())后,通过redux中间件来处理请求逻辑。

中间件的教程看这里

我们想想中间件的逻辑

  1. 请求前dispatch REQUEST请求。
  2. 成功后dispatch SUCCESS请求,如果定义了afterSuccess()函数,调用它。
  3. 失败后dispatch FAIL请求。

来写一个

cd src/redux
mkdir middleware
cd middleware
touch promiseMiddleware.js

src/redux/middleware/promiseMiddleware.js

import axios from 'axios';

export default  store => next => action => {
    const {dispatch, getState} = store;
    /*如果dispatch来的是一个function,此处不做处理,直接进入下一级*/
    if (typeof action === 'function') {
        action(dispatch, getState);
        return;
    }
    /*解析action*/
    const {
        promise,
        types,
        afterSuccess,
        ...rest
    } = action;

    /*没有promise,证明不是想要发送ajax请求的,就直接进入下一步啦!*/
    if (!action.promise) {
        return next(action);
    }

    /*解析types*/
    const [REQUEST,
        SUCCESS,
        FAILURE] = types;

    /*开始请求的时候,发一个action*/
    next({
        ...rest,
        type: REQUEST
    });
    /*定义请求成功时的方法*/
    const onFulfilled = result => {
        next({
            ...rest,
            result,
            type: SUCCESS
        });
        if (afterSuccess) {
            afterSuccess(dispatch, getState, result);
        }
    };
    /*定义请求失败时的方法*/
    const onRejected = error => {
        next({
            ...rest,
            error,
            type: FAILURE
        });
    };

    return promise(axios).then(onFulfilled, onRejected).catch(error => {
        console.error('MIDDLEWARE ERROR:', error);
        onRejected(error)
    })
}

修改src/redux/store.js来应用这个中间件

import {createStore, applyMiddleware} from 'redux';
import combineReducers from './reducers.js';

import promiseMiddleware from './middleware/promiseMiddleware'

let store = createStore(combineReducers, applyMiddleware(promiseMiddleware));

export default store;

修改src/redux/actions/userInfo.js

export const GET_USER_INFO_REQUEST = "userInfo/GET_USER_INFO_REQUEST";
export const GET_USER_INFO_SUCCESS = "userInfo/GET_USER_INFO_SUCCESS";
export const GET_USER_INFO_FAIL = "userInfo/GET_USER_INFO_FAIL";

export function getUserInfo() {
    return {
        types: [GET_USER_INFO_REQUEST, GET_USER_INFO_SUCCESS, GET_USER_INFO_FAIL],
        promise: client => client.get(`http://localhost:8080/api/user.json`)
    }
}

是不是简单清新很多啦?

修改src/redux/reducers/userInfo.js

        case GET_USER_INFO_SUCCESS:
            return {
                ...state,
                isLoading: false,
                userInfo: action.result.data,
                errorMsg: ''
            };

action.userInfo修改成了action.result.data。你看中间件,请求成功,会给action增加一个result字段来存储响应结果哦~不用手动传了。

npm start看看我们的网络请求是不是正常哦。

调整文本编辑器

使用自动编译代码时,可能会在保存文件时遇到一些问题。某些编辑器具有“安全写入”功能,可能会影响重新编译。

要在一些常见的编辑器中禁用此功能,请查看以下列表:

  • Sublime Text 3 - 在用户首选项(user preferences)中添加 atomic_save: "false"。
  • IntelliJ - 在首选项(preferences)中使用搜索,查找到 "safe write" 并且禁用它。
  • Vim - 在设置(settings)中增加 :set backupcopy=yes。
  • WebStorm - 在 Preferences > Appearance & Behavior > System Settings 中取消选中 Use "safe write"。
 
@brickspert
 
Owner

brickspert commented on 4 Sep 2017  

合并提取 webpack公共配置

想象一个场景,现在我想给webpack增加一个css modules依赖,你会发现,WTF?我即要修改webpack.dev.config.js,又要修改webpack.config.js~

这肯定不行啊。所以我们要把公共的配置文件提取出来。提取到webpack.common.config.js里面~

webpack.dev.config.jswebpack.config.js写自己的特殊的配置。

这里我们需要用到webpack-merge来合并公共配置和单独的配置。

这样说一下,应该看代码就能看懂了。下次公共配置直接就写在webpack.common.config.js里面啦。

这里偷偷说下,我修改了CleanWebpackPlugin的参数,不让他每次构建都删除api文件夹了。要不每次都得复制进去。麻烦~

npm install --save-dev webpack-merge

touch webpack.common.config.js

webpack.common.config.js

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

commonConfig = {
    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].[chunkhash].js',
        chunkFilename: '[name].[chunkhash].js',
        publicPath: "/"
    },
    module: {
        rules: [{
            test: /\.js$/,
            use: ['babel-loader?cacheDirectory=true'],
            include: path.join(__dirname, 'src')
        }, {
            test: /\.(png|jpg|gif)$/,
            use: [{
                loader: 'url-loader',
                options: {
                    limit: 8192
                }
            }]
        }]
    },
    plugins: [
        new HtmlWebpackPlugin({
            filename: 'index.html',
            template: path.join(__dirname, 'src/index.html')
        }),
        new webpack.HashedModuleIdsPlugin(),
        new webpack.optimize.CommonsChunkPlugin({
            name: 'vendor'
        }),
        new webpack.optimize.CommonsChunkPlugin({
            name: 'runtime'
        })
    ],

    resolve: {
        alias: {
            pages: path.join(__dirname, 'src/pages'),
            components: path.join(__dirname, 'src/components'),
            router: path.join(__dirname, 'src/router'),
            actions: path.join(__dirname, 'src/redux/actions'),
            reducers: path.join(__dirname, 'src/redux/reducers')
        }
    }
};

module.exports = commonConfig;

webpack.dev.config.js

const merge = require('webpack-merge');
const path = require('path');

const commonConfig = require('./webpack.common.config.js');

const devConfig = {
    devtool: 'inline-source-map',
    entry: {
        app: [
            'react-hot-loader/patch',
            path.join(__dirname, 'src/index.js')
        ]
    },
    output: {
        /*这里本来应该是[chunkhash]的,但是由于[chunkhash]和react-hot-loader不兼容。只能妥协*/
        filename: '[name].[hash].js'
    },
    module: {
        rules: [{
            test: /\.css$/,
            use: ["style-loader", "css-loader"]
        }]
    },
    devServer: {
        contentBase: path.join(__dirname, './dist'),
        historyApiFallback: true,
        host: '0.0.0.0',
    }
};

module.exports = merge({
    customizeArray(a, b, key) {
        /*entry.app不合并,全替换*/
        if (key === 'entry.app') {
            return b;
        }
        return undefined;
    }
})(commonConfig, devConfig);

webpack.config.js

const merge = require('webpack-merge');

const webpack = require('webpack');
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const ExtractTextPlugin = require("extract-text-webpack-plugin");

const commonConfig = require('./webpack.common.config.js');

const publicConfig = {
    devtool: 'cheap-module-source-map',
    module: {
        rules: [{
            test: /\.css$/,
            use: ExtractTextPlugin.extract({
                fallback: "style-loader",
                use: "css-loader"
            })
        }]
    },
    plugins: [
        new CleanWebpackPlugin(['dist/*.*']),
        new UglifyJSPlugin(),
        new webpack.DefinePlugin({
            'process.env': {
                'NODE_ENV': JSON.stringify('production')
            }
        }),
        new ExtractTextPlugin({
            filename: '[name].[contenthash:5].css',
            allChunks: true
        })
    ]

};

module.exports = merge(commonConfig, publicConfig);
@STMU1320
 

STMU1320 commented on 4 Sep 2017

博主在打包这里没有讲清楚,webpack没有全局安装的话 webpack --config webpack.dev.config.js 命令会出错。要么全局安装,要么运行命令./node_modules/.bin/webpack --config webpack.dev.config.js

 
@brickspert
 
Owner

brickspert commented on 4 Sep 2017

@STMU1320 确实是,sorry。没注意~~ 谢谢谢谢。

 
@brickspert
 
Owner

brickspert commented on 4 Sep 2017

优化目录结构并增加404页面

现在我们优化下目录结构,把routernav分开,新建根组件App

  1. component改名为components,因为是复数。。。注意修改引用的地方哦。
  2. 新建根组件components/App/APP.js
import React, {Component} from 'react';

import Nav from 'components/Nav/Nav';
import getRouter from 'router/router';

export default class App extends Component {
    render() {
        return (
            <div>
                <Nav/>
                {getRouter()}
            </div>
        )
    }
}
  1. 新建components/Nav/Nav组件,把router/router.js里面的nav提出来。
  2. 新建components/Loading/Loading组件,把router/router.js里面的Loading提出来。
  3. 入口文件src/index.js修改
import React from 'react';
import ReactDom from 'react-dom';
import {AppContainer} from 'react-hot-loader';
import {Provider} from 'react-redux';
import store from './redux/store';
import {BrowserRouter as Router} from 'react-router-dom';
import App from 'components/App/App';

renderWithHotReload(App);

if (module.hot) {
    module.hot.accept('components/App/App', () => {
        const NextApp = require('components/App/App').default;
        renderWithHotReload(NextApp);
    });
}

function renderWithHotReload(RootElement) {
    ReactDom.render(
        <AppContainer>
            <Provider store={store}>
                <Router>
                    <RootElement/>
                </Router>
            </Provider>
        </AppContainer>,
        document.getElementById('app')
    )
}
  1. 新建pages/NotFound/NotFound组件。
  2. 修改router/router.js,增加404
import NotFound from 'bundle-loader?lazy&name=notFound!pages/NotFound/NotFound';

<Route component={createComponent(NotFound)}/>
@surpass-wei
 

surpass-wei commented on 6 Sep 2017

命令优化 处,应该是执行 npm run dev-build 而不是 npm start-build 吧

 
@brickspert
 
Owner

brickspert commented on 6 Sep 2017

@surpass-wei 哈哈哈哈哈。谢谢谢谢。。已经改了。 之前我的npm start命令叫npm run dev,之后感觉不好,我就批量把npm run dev替换成npm start了。误影响到这里了。尴尬~ 谢谢你。

@yeliping
 

yeliping commented on 7 Sep 2017

一般运行我建议,npm run dev, 发布测试版本包交npm run testbuild,正式版本叫npm run build。这样挺好

@brickspert
 
Owner

brickspert commented on 7 Sep 2017

@yeliping 我看别人写的,基本一般运行都是npm start。我就想统一一下。嘿嘿。

@yeliping
 

yeliping commented on 7 Sep 2017

我公司的vue框架,不是我搭的。在npm run testbuild和npm run build有区别的,区别就在于,打包了测试和正式环境,那些接口地址就不一样。免得混淆,测试地址上了正式地址的错误。楼主你那个打包压缩没弄错zip这种格式吧

@brickspert
 
Owner

brickspert commented on 7 Sep 2017

@yeliping 打包压缩不是打包压缩成zip呀。。。没有这一步。 压缩是指代码压缩~

@yeliping
 

yeliping commented on 7 Sep 2017

我知道是指代码压缩呀,弄错zip这些可以再配置的

@brickspert
 
Owner

brickspert commented on 7 Sep 2017

@yeliping 恩恩。下一步 我可以做这个。嘿嘿。

@yeliping
 

yeliping commented on 7 Sep 2017  

`'use strict';

var exports = {
get baseUrl() {
var test = “bbbb",
prod = ”aaaa“;

    return this.isTest ? test : prod;
},
// 是否是测试环境
get isTest() {
  return $envType.type === 0 || $envType.type === 1;
},
// 是否是pc环境
get isPcTest() {
  return $envType.type === 0;
}

};

module.exports = exports;` 这个文件就是用来配置测试环境或者正式环境。主要是以前有个新同事把测试发到正式,导致出现大问题.所以他们弄了这样的配置

@brickspert
 
Owner

brickspert commented on 7 Sep 2017

@yeliping 我们开发时就没个问题的。可以把/api的请求路径拦截跳转,配置在nginx等服务器上。这样就不存在 测试和正式域名搞错了~

@brickspert
 
Owner

brickspert commented on 7 Sep 2017

@yeliping 当然因项目而异啦。

@ranjin
 

ranjin commented on 7 Sep 2017  

webpack配置服务器那里。发现就算改成了全局变量 之后打开localhost:8080之后打不开。之后搜下了。发现还需要配置端口。
可以在webpack.config.js文件里面 devServer下面加上 port:8080
devServer: { contentBase: path.join(__dirname, './dist'), compress: true, port: 8080 }
或者直接通过node_modules/.bin/webpack-dev-server 这个指令 发现是可以将端口默认配置成8080的。

入手第二天。还在学习。谢谢楼主的这一大篇资源。

@brickspert
 
Owner

brickspert commented on 7 Sep 2017

@ranjin webpack命令是打包成静态文件命令~,打包完不是访问localhost:8080(因为你没有启动服务器监听8080端口哦)的,是直接打开dist/index.html的,直接打开,直接打开,直接打开~用浏览器直接打开。类似F://react-family/dist/index.html这种。
webpack-dev-server是启动一个服务器,默认监听8080端口哦。

祝你好运。

 
@ranjin
 

ranjin commented on 7 Sep 2017

也就是说 如果配置的是webpack-dev-server的话 在devServer配置里面是并不需要加上指定端口的是吧。-----------
先mark了。谢谢大神的讲解。还在学习ing~

@brickspert
 
Owner

brickspert commented on 7 Sep 2017

@ranjin 可以加,也可以不加。 如果不加的话 就默认8080哦。

@surpass-wei
 

surpass-wei commented on 7 Sep 2017

向后台请求用户基本信息为例的例子中,一会是userInfo.js一会是getUserInfo.js(应当是同一个文件),可能需要修改下~

@brickspert
 
Owner

brickspert commented on 7 Sep 2017

@surpass-wei 确实是写错了。已经修正。谢谢你。

@ranjin
 

ranjin commented on 8 Sep 2017

看了一半了,不过概念性的东西实在太多了。
博主用了多少天差不多入门了呀。

@brickspert
 
Owner

brickspert commented on 8 Sep 2017

@ranjin 从我接触react到现在,也有快一年了吧。。。慢慢来,你先学会用别人搭好的现成的框架。熟悉怎么用之后,再来看怎么搭框架。

@ranjin
 

ranjin commented on 8 Sep 2017

嗯嗯。现在就是在看您这整个框架的。然后再慢慢来学习一些知识点的。
顺便问下。react用的是js语法还是其它的呀。之前没接触过。

@brickspert
 
Owner

brickspert commented on 8 Sep 2017

@ranjin JSX

@yeliping
 

yeliping commented on 8 Sep 2017

嘿嘿,看了博主文章自己弄了一个,删了一些跟我项目不是很必要的东西,自己改了一点,不过总体还是挺好的。谢谢博主啦。redux我还是去补补,因为之前没怎么接触过。以前一直用angular1.x和vue、jq做项目

@ranjin
 

ranjin commented on 8 Sep 2017

@yeliping 我也感觉redux好绕啊。

@brickspert
 
Owner

brickspert commented on 8 Sep 2017

@ranjin 是的,我最开始也不理解。 先学会用,用着用着就懂原理了。~你要明白,redux是独立的,和react没有一点关系。只是能用配合使用而已。

@Ccheng2729111
 

Ccheng2729111 commented on 13 Sep 2017

写的真的很不错,花了几个小时跟着走了一遍。以前项目都是脚手架create-react-app直接搭的。
现在一步一步搭起来学到了不少东西,特别是一些项目优化的问题得到了解决。
以前webpack插件了解的不多,只知道用,现在感觉好多了。
对新手或许看起来有些迷糊,概念多,但对实际上手写项目的人来说很精髓。:)

@Ccheng2729111
 

Ccheng2729111 commented on 13 Sep 2017

不过感觉react-redux那块还可以详尽一点,多创建一个container,纯显示组件就放component里就行。逻辑相关的放container。
还有无状态组件可以用箭头函数来创建。

这样相信新手就可以少走弯路了:》

@brickspert
 
Owner

brickspert commented on 13 Sep 2017

@Ccheng2729111 确实是,redux如果没用过,很难一下就讲清楚的。当时写的时候也考虑了很久~~还是没太讲明白。。。 我现在在准备写一篇从零开发一个redux。相信新手看了会豁然开朗 嘿嘿。

@brickspert brickspert added the react label on 15 Sep 2017

@AlenWang
 

AlenWang commented on 15 Sep 2017

本来是想看一下react入门,忽然最后关注点在webpack配置上。然而我们项目是vue2+webpack,看了之后对webpack配置理解更深刻了些,写的很赞了!

@brickspert
 
Owner

brickspert commented on 15 Sep 2017

@AlenWang 谢谢认可。

@brickspert
 
Owner

brickspert commented on 17 Sep 2017

加入 babel-plugin-transform-runtime 和 babel-polyfill

  1. 先来说说babel-plugin-transform-runtime

    在转换 ES2015 语法为 ECMAScript 5 的语法时,babel 会需要一些辅助函数,例如 _extend。babel 默认会将这些辅助函数内联到每一个 js 文件里,这样文件多的时候,项目就会很大。

    所以 babel 提供了 transform-runtime 来将这些辅助函数“搬”到一个单独的模块 babel-runtime 中,这样做能减小项目文件的大小。

    npm install --save-dev babel-plugin-transform-runtime

    修改.babelrc配置文件,增加配置

    .babelrc

         "plugins": [
           "transform-runtime"
         ]
  2. 再来看babel-polyfill

    Q: 为什么要集成babel-polyfill?

    A:

    Babel默认只转换新的JavaScript句法(syntax),而不转换新的API,比如Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise等全局对象,以及一些定义在全局对象上的方法(比如Object.assign)都不会转码。
    举例来说,ES6在Array对象上新增了Array.from方法。Babel就不会转码这个方法。如果想让这个方法运行,必须使用babel-polyfill,为当前环境提供一个垫片。

    网上很多人说,集成了transform-runtime就不用babel-polyfill了,其实不然,看看官方怎么说的:

    NOTE: Instance methods such as "foobar".includes("foo") will not work since that would require modification of existing built-ins (Use babel-polyfill for that).

    所以,我们还是需要babel-polyfill哦。

    npm install --save-dev babel-polyfill

    修改webpack两个配置文件。

    webpack.common.config.js

         app: [
             "babel-polyfill",
             path.join(__dirname, 'src/index.js')
         ]

    webpack.dev.config.js

         app: [
             'babel-polyfill',
             'react-hot-loader/patch',
             path.join(__dirname, 'src/index.js')
         ]

参考地址:

  1. http://www.ruanyifeng.com/blog/2016/01/babel.html

  2. lmk123/blog#45

  3. https://github.com/thejameskyle/babel-handbook/blob/master/translations/zh-Hans/README.md

@brickspert
 
Owner

brickspert commented on 17 Sep 2017

集成PostCSS

官方文档看这里

Q: 这是啥?为什么要用它?

他有很多很多的插件,我们举几个例子~

Autoprefixer这个插件,可以自动给css属性加浏览器前缀。

/*编译前*/
.container{
    display: flex;
}
/*编译后*/
.container{
    display: -webkit-box;
    display: -webkit-flex;
    display: -ms-flexbox;
    display: flex;
}

postcss-cssnext 允许你使用未来的 CSS 特性(包括 autoprefixer)

当然,它有很多很多的插件可以用,你可以去官网详细了解。我们今天只用postcss-cssnext。(它包含了autoprefixer)

npm install --save-dev  postcss-loader
npm install --save-dev  postcss-cssnext

修改webpack配置文件,增加postcss-loader

webpack.dev.config.js

        rules: [{
            test: /\.(css|scss)$/,
            use: ["style-loader", "css-loader", "postcss-loader"]
        }]

webpack.config.js

        rules: [{
            test: /\.css$/,
            use: ExtractTextPlugin.extract({
                fallback: "style-loader",
                use: ["css-loader", "postcss-loader"]
            })
        }]

根目录增加postcss配置文件。

touch postcss.config.js

postcss.config.js

module.exports = {
    plugins: {
        'postcss-cssnext': {}
    }
};

现在你运行代码,然后写个css,去浏览器审查元素,看看,属性是不是生成了浏览器前缀?

@alexguo88
 

alexguo88 commented on 19 Sep 2017

万分感谢,学习框架,显得弄清楚构建工具的使用,以前一直迷迷糊糊的,按你的步骤一步一步作下来,清晰多了,很好的文章!!!

@brickspert
 
Owner

brickspert commented on 19 Sep 2017

@alexguo88 感谢你的认可。谢谢~~以后会有更多好文的、

@ZHENGGEGE
 

ZHENGGEGE commented on 19 Sep 2017

很细致!一直用到webpack,却没有研究过,文章很棒

@zhouxinghui
 

zhouxinghui commented on 20 Sep 2017  

不开心 为什么部署到tomcat中报错误,我把dist文件放到tomcat 中 找不到css文件,js文件,文件明明都有,请问是为什么啊 @brickspert

@brickspert
 
Owner

brickspert commented on 20 Sep 2017

@zhouxinghui 你用的一级域名还是二级域名?

@zhouxinghui
 

zhouxinghui commented on 20 Sep 2017

没有用域名,我是说直接部署在本地的tomcat中运行一下,老板还没叫我部署到域名服务器上 @brickspert

@brickspert
 
Owner

brickspert commented on 20 Sep 2017  

@zhouxinghui 我意思是 你部署到tomcat后,使用" http://localhost:80/index.html " 这种域名访问的?还是 " http://localhost:80/xxx/index.html " 这种域名访问的?

第二种配置确实访问不到静态文件的。如果你非要用第二种域名访问,要修改webpack.common.config.js->output->publicPath属性的哦。并且路由那边也是要改的呢。

@zhouxinghui
 

zhouxinghui commented on 20 Sep 2017  

我是用第二种,那怎么用第一种呢 ,不想修改那么多配置@brickspert

@brickspert
 
Owner

brickspert commented on 20 Sep 2017

@zhouxinghui tomcalt配置域名 http://localhost:80 直接指向xxx目录就可以了~~你现在肯定是指向xxx目录再外面的一层。

@zhouxinghui
 

zhouxinghui commented on 20 Sep 2017  

我在tomcat 中的服务器配置中的标签下加了这段Context docBase="dist" path="" reloadable="true"
就可以用了,是这样改的吗 @brickspert

@brickspert
 
Owner

brickspert commented on 20 Sep 2017

@zhouxinghui 差不多,我看docBase="dist"这个应该没问题

 
@zhouxinghui
 

zhouxinghui commented on 20 Sep 2017

遇到一个问题 在tomcat中部署运行 重复刷新页面会报错误 只有主页重复刷新不报错误,其它页面都会报错误。 @brickspert

@brickspert
 
Owner

brickspert commented on 20 Sep 2017

@zhouxinghui 这是正常的,你要配置tomcat,让所有的页面请求都返回index.html

除了静态文件和api请求哦~

@zhouxinghui
 

zhouxinghui commented on 20 Sep 2017

这个怎么配啊 @brickspert

@brickspert
 
Owner

brickspert commented on 20 Sep 2017

@zhouxinghui 给你个关键字 “tomcat配置单页面应用”,你去查下吧。tomcat我没怎么用过唉~

@zhouxinghui
 

zhouxinghui commented on 20 Sep 2017

我去查了 我没有找到 不知道是不是我查找的方法不对 还是没有 @brickspert

@brickspert
 
Owner

brickspert commented on 20 Sep 2017

@zhouxinghui 你看看这个吧 这里

他这里有apache和nginx的配置(大概意思可以参考,别原模原样复制哦)。

总的思想就是,所有页面请求,重定向到index.html。 tomcat真没用过唉。

@zhouxinghui
 

zhouxinghui commented on 20 Sep 2017

我是在Windows下,没有他说的那个文件啊 tomcat我的是直接解压用的 @brickspert

@zhouxinghui
 

zhouxinghui commented on 20 Sep 2017  

在 tomcat 上面简单的做法,在 web.xml 中加入
<error-page> <error-code>404</error-code> <location>/index.html</location> </error-page>@brickspert 用这个方法解决了 谢谢耐心帮我解答 非常感谢!~

@zhouxinghui
 

zhouxinghui commented on 20 Sep 2017

不过这样做还是有一个错误 那就是刷新页面的时候 还是会显示找不到页面 但是绑定了主页 所以主页会一直显示~ @brickspert

@brickspert
 
Owner

brickspert commented on 20 Sep 2017

@zhouxinghui 理解不了你的意思了~~~ 是都返回主页的,但是js会控制显示的页面的呢。。 怎么你的不行哩?

@zhouxinghui
 

zhouxinghui commented on 20 Sep 2017

不知道啊 楼主你在tomcat中试一试 估计也是一样的 要不我把配好的文件发给你 你看看 我用的是你github上的源码 @brickspert

@brickspert
 
Owner

brickspert commented on 20 Sep 2017

@zhouxinghui 我晚上下个tomcat 试试。没咋用过。

@zhouxinghui
 

zhouxinghui commented on 20 Sep 2017

嗯嗯 好的 那就麻烦你了 @brickspert

@brickspert
 
Owner

brickspert commented on 20 Sep 2017

@zhouxinghui

  1. tomcat/conf/server.xml 增加
<Host name="localhost" appBase="F:\Project\react\react-family\dist" unpackWARs="true" autoDeploy="true"></Host>
  1. 目标目录结构 dist/ROOT

28 19 enb5 stzs4ulch

  1. dist/ROOT/WEB-INF 下面新建一个web.xml
<web-app>
<error-page>
<error-code>404</error-code>
<location>/index.html</location>
</error-page>
</web-app>

亲测可用。

 
@zhouxinghui
 

zhouxinghui commented on 21 Sep 2017

Chrome浏览器 按F12 可以看到错误 不是页面上报错误 进入开发模式 刷新页面会报404 @brickspert

@brickspert
 
Owner

brickspert commented on 21 Sep 2017

@zhouxinghui 截个图发上来看看~

@zhouxinghui
 

zhouxinghui commented on 21 Sep 2017  

@brickspert
 
Owner

brickspert commented on 21 Sep 2017

@zhouxinghui 我用我的方法测试了,没问题的。应该是你404页面配置的有问题。你按照我的来配应该就可以了、

@zhouxinghui
 

zhouxinghui commented on 21 Sep 2017

我们的目录结构不一样啊 我把你的源码 dist文件夹拷贝到 Tomcat服务器中的webapps文件夹目录下 静态代码发布到Tomcat都是放在这个目录下的 @brickspert

@zhouxinghui
 

zhouxinghui commented on 21 Sep 2017

要是在生产环境 要怎么部署啊 @brickspert 上面你介绍的时候说拷贝dist文件夹 到服务器 我觉得不够清楚@brickspert

@brickspert
 
Owner

brickspert commented on 21 Sep 2017

@zhouxinghui

  1. 无论放在哪个目录,原理都一样的。难道你放C盘,我放F盘配置就不一样了吗?原理都是一模一样的。你不能指望我再按你的目录结构原模原样再给你发一遍配置文件吧。。其实都是一样的。

  2. 生产坏境部署可以部署很多坏境 nginx, apache, iis, tomcat等等。就是普通的静态文件部署方案就可以了。(服务器配置需要考虑安全,gzip压缩等等很多东西)

@zhouxinghui
 

zhouxinghui commented on 21 Sep 2017

我按你的那个配了 还是这样报错 @brickspert
qq

@brickspert
 
Owner

brickspert commented on 21 Sep 2017

@zhouxinghui 这个我真的不知道了唉。我这边确实不报错的。tomcat文档看看吧,肯定是tomcat配置的问题~~。~~

@zhouxinghui
 

zhouxinghui commented on 21 Sep 2017

好吧 那这个项目怎么和后端的连接起来啊 @brickspert

@Pegggy
 

Pegggy commented on 22 Sep 2017

redux 教程那里, Counter 组件我照博主的代码打出错啦。

 class Counter extends Component{
  render(){
    return(
      <div>
        <div>显示当前计数为{this.props.counter}</div> //这里 props 应该就到 counter,不然不显示数字
        <button onClick = {()=>this.props.increment()}>自增</button>
        <button onClick = {()=>this.props.decrement()}>自减</button>
        <button onClick = {()=>this.props.reset()}>重置</button>
      </div>
    )
  }
}

const mapStateToProps = (state) =>{
  return {
    counter: state.count,//这里应该是  state.count 不是 counter,不然编译完会报错
  }
}
@brickspert
 
Owner

brickspert commented on 22 Sep 2017

@Pegggy 亲,应该是你搞错了吧,亲。

看这里,我们在这里定义的是state.counter哦,你是不是这个文件写错了~

src/redux/reducers.js

import counter from './reducers/counter';

export default function combineReducers(state = {}, action) {
    return {
        counter: counter(state.counter, action)
    }
}
@yichonglai
 

yichonglai commented on 24 Sep 2017

vendor文件没经过babel处理???

@brickspert
 
Owner

brickspert commented on 25 Sep 2017

@yichonglai 我是这样理解的。babel只处理src文件夹里面的代码(我们配置的),用npm install安装的依赖,人家自己都处理好了。不用我们处理。不知道我的理解对不对唉~我再去查查。

@yichonglai
 

yichonglai commented on 25 Sep 2017

\node_modules\webpack-dev-server\client
嗯呢 这个文件里的js文件都还是es6语法(已经打包进vendor),ie11以下都不兼容

@brickspert
 
Owner

brickspert commented on 25 Sep 2017

@yichonglai 不应该啊,webpack-dev-server不会打包进vendor的呀~~ 截个图看看。

@yichonglai
 

yichonglai commented on 25 Sep 2017

image
image
这些const我改为var就可以了哦!(这是属于webpack-dev-server里的代码)

@yichonglai
 

yichonglai commented on 25 Sep 2017

按理不会进vendor
image

@brickspert
 
Owner

brickspert commented on 25 Sep 2017

@yichonglai 等我研究下 然后给你回复哦~

@vonxq vonxq referenced this issue in vonxq/vonxq.github.io on 26 Sep 2017

 Open
react #1
@brickspert
 
Owner

brickspert commented on 26 Sep 2017  

redux 模块热替换配置

今天突然发现,当修改reducer代码的时候,页面会整个刷新,而不是局部刷新唉。

这不行,就去查了webpack文档,果然是要配置的。看这里

代码修改起来也简单,增加一段监听reducers变化,并替换的代码。

src/redux/store.js

if (module.hot) {
    module.hot.accept("./reducers", () => {
        const nextCombineReducers = require("./reducers").default;
        store.replaceReducer(nextCombineReducers);
    });
}

哦了~

@xinkuan-jack
 

xinkuan-jack commented on 28 Sep 2017

无法兼容ie,好心塞

@brickspert
 
Owner

brickspert commented on 28 Sep 2017

@xinkuan-jack 这里我写了这个框架如何兼容ie8的~~ 看这里 #5

@xinkuan-jack
 

xinkuan-jack commented on 28 Sep 2017

@brickspert 不考虑ie8了,ie9+能运行就ok,问题还是 @yichonglai 显示的那个问题,转码规则制定了,打包后文件还是es6

@brickspert
 
Owner

brickspert commented on 28 Sep 2017

@xinkuan-jack 你是说开发(npm start)的时候兼容ie9 还是 生产坏境(npm run build)之后兼容ie9呢?

@brickspert
 
Owner

brickspert commented on 28 Sep 2017

@xinkuan-jack 我测试了,打包之后是兼容ie9+的,你开发的时候不要用ie9开发啊~

@brickspert
 
Owner

brickspert commented on 13 Oct 2017

@tingyuxuan2302 报错说的是,你配置文件写的不对,你配置一个error项,但是不应该有这个配置的,是错误滴~

@sanshuiwang
 

sanshuiwang commented on 18 Oct 2017

笔者,留个QQ或者微信,有问题好请教嘛!

@sanshuiwang
 

sanshuiwang commented on 19 Oct 2017

我想抽取scss还是像抽取css一样吗?

@brickspert
 
Owner

brickspert commented on 19 Oct 2017

@sanshuiwang 没理解你的意思。 scss和css是一样的哦。

@firstsmarts
 

firstsmarts commented on 30 Oct 2017

谢谢分享 获益良多

@zhangtianxiao
 

zhangtianxiao commented on 30 Oct 2017

辛苦了,

@xuezhenxiang
 

xuezhenxiang commented on 3 Nov 2017

谢谢分享!

@xintianyou
 

xintianyou commented on 8 Nov 2017

谢谢提主的分享,相当精彩!

@xintianyou
 

xintianyou commented on 8 Nov 2017

上面的QQ群加不进

@brickspert
 
Owner

brickspert commented on 8 Nov 2017

@xintianyou 可以加了

@xintianyou
 

xintianyou commented on 8 Nov 2017

@brickspert 谢谢

@JaydenLD
 

JaydenLD commented on 8 Nov 2017

就是有个地方不知道怎么改哪里,就是 redux: path.join(__dirname, 'src/redux')注释掉以后,我点击counter就报错了
image

@brickspert
 
Owner

brickspert commented on 9 Nov 2017  

@JaydenLD 可以把报错信息贴出来。是不是你有文件引用了这样的路径
import "redux/xxxx/xxxx"~

@JaydenLD
 

JaydenLD commented on 9 Nov 2017

我点击Counter以后
image
我只有在store里
image
这样改了也还是报错

@brickspert
 
Owner

brickspert commented on 9 Nov 2017

@JaydenLD src/index.js是不是这里没改~~~

/*你是不是"redux/store"*/
import store from './redux/store';
@JaydenLD
 

JaydenLD commented on 9 Nov 2017

这里改了就报错
Uploading image.png…

@brickspert
 
Owner

brickspert commented on 9 Nov 2017

@JaydenLD 哈~这里你写错了,应该这么写的,这里。
你要分清楚,哪个资源是从redux文件夹引用的,哪个是从nodemodules里面的文件夹引用的。
import {createStore} from 'redux'

@JaydenLD
 

JaydenLD commented on 9 Nov 2017

Uploading image.png…
这样写还是报错

@brickspert
 
Owner

brickspert commented on 9 Nov 2017

@JaydenLD 你把我源码下载下来,对比一下吧~ 我这么看也看不出的

@JaydenLD
 

JaydenLD commented on 9 Nov 2017

好的,还是谢谢啦

@FrankHuPerficient

博主厉害了。写得很清晰。谢谢分享。

@lm5645
 

lm5645 commented on 10 Nov 2017

打包优化的命令是错的:现在npm run bundle试试,是不是之前的都清空了。当然我们之前的api文件夹也被清空了,不过没关系哦~本来就是测试用的。build

@JaydenLD
 

JaydenLD commented on 10 Nov 2017

昨天下午已经按照这个重新来了一遍,是因为当时我install的依赖没有下载进去,json文件里没有,所以路径改了也会报错

@brickspert
 
Owner

brickspert commented on 10 Nov 2017

@lm5645 谢谢。已经改正了。thank you~

@brickspert
 
Owner

brickspert commented on 10 Nov 2017  

@JaydenLD 可以用了就好~祝你好运

@git-issue
 

git-issue commented on 10 Nov 2017

写得很不错,谢谢楼主分享

@hezhii hezhii referenced this issue in brickspert/react-family on 14 Nov 2017

 Open
使用 babel-preset-env #1
@nanxiaodi
 

nanxiaodi commented on 15 Nov 2017

打包之后,用webstorm打开页面,发现报错,
image

@robinLiu1989
 

robinLiu1989 commented on 15 Nov 2017

@nanxiaodi,我遇到了和你同样的问题
image

@brickspert
 
Owner

brickspert commented on 16 Nov 2017

@nanxiaodi @robinLiu1989
hello~
首先,不应该用webstorm来启动页面。正确的方式是用nginx/apache等来作为服务器。

  1. 因为我们自己配了路由,首页的路径应该是"http://localhost:xxxx" 根目录。
  2. 我们的js等资源文件,都是相对于根路径的,就比如你现在启动的页面,要去加载一个js文件,会去http://localhost:63342/xxx.js这里去找的,肯定找不到了。
@nanxiaodi
 

nanxiaodi commented on 16 Nov 2017

用服务器发布就好了,我找了一下原因,第一是,出口配置不弄成绝对路径,publice:'./'变成相对路径就可以访问资源了,然后是子页面的路由设置,是BrowserRouter好像必须要服务器支持,改成hashrouter就可以在本地运行了

@robinLiu1989
 

robinLiu1989 commented on 16 Nov 2017

@brickspert 在本地开了一个服务器,确实可以,谢谢楼主~

@brickspert brickspert closed this on 16 Nov 2017

@ZGahou
 

ZGahou commented on 16 Nov 2017

image
跳到某个路由,刚进去是可以的,然后刷新就变成这样

@brickspert
 
Owner

brickspert commented on 16 Nov 2017

@ZGahou 你肯定知道,我们这是单页面应用。我们只有一个文件Index.html~
你需要做的是,在服务器那边配置,所有的404都返回Index.html就好啦~

@ZGahou
 

ZGahou commented on 16 Nov 2017

browserRouter会重新请求服务器,webpack-dev-server怎么配置能让404返回index.html

@brickspert
 
Owner

brickspert commented on 16 Nov 2017

@ZGahou 原来你在开发模式呀,其实你往后看,后面就有写到开发模式如何处理的。
#1 (comment)
historyApiFallback

@ZGahou
 

ZGahou commented on 16 Nov 2017

之前没发现这个的用处,没加上,现在可以了,多谢楼主

@brickspert
 
Owner

brickspert commented on 16 Nov 2017

@ZGahou 这样最好了,完全理解这个字段了。下次碰到肯定记住了。嘿嘿。碰到问题,解决问题后才能记住。
我估计你开始就加上了这个字段,没有碰到问题。下次碰到肯定还是不会。哈哈哈?

@ZGahou
 

ZGahou commented on 16 Nov 2017

是啊,现在记住了devServer里有这么个配置可以起到这个作用

@FrankHuPerficient

@TrustTheBoy 将运行命令写在package.json中就可以了

@brickspert brickspert reopened this on 16 Nov 2017

@brickspert
 
Owner

brickspert commented on 17 Nov 2017

@TrustTheBoy 你看报错信息,<Router>下面只能有一个子节点,你路由这边写错了。可以代码对比一下看看~

@TrustTheBoy
 

TrustTheBoy commented on 17 Nov 2017  

@brickspert 您好,感谢你的帮助,我的Router确实是一个子节点,我也对照了你的源码

@brickspert
 
Owner

brickspert commented on 17 Nov 2017

@TrustTheBoy src/router/router.js发出来看看

@brickspert
 
Owner

brickspert commented on 17 Nov 2017

@TrustTheBoy 我看着也没啥问题。。尴尬。你下载我的源码对比下吧。看不出来哪里错了~~

如果最后还不行,晚上我 可以帮你调试下。

@TrustTheBoy
 

TrustTheBoy commented on 17 Nov 2017  

@brickspert 是不是和react && react-dom的版本有关呢?我用的全是新版本

@brickspert
 
Owner

brickspert commented on 17 Nov 2017

@TrustTheBoy 应该不是的~~不过你可以尝试的。

@TrustTheBoy
 

TrustTheBoy commented on 17 Nov 2017

@brickspert 您好,想问下,如果按" http://localhost:80/xxx/index.html " 这种域名访问的,publicPath这里需要如何配置呢?还有路由需要怎么改动呢?

@brickspert
 
Owner

brickspert commented on 17 Nov 2017

@TrustTheBoy

  1. 静态文件路径修改: webpack.common.config.js->output-> publicPath改成/xxx/,这样静态文件加载的路径会相对这个路径。
  2. 路由修改:看这里,加个basename为'/xxx/'就可以。
@brickspert
 
Owner

brickspert commented on 17 Nov 2017

@TrustTheBoy hello~

  1. 访问首页的时候,不要带后面的index.html
  2. 服务器那边配置,所有的404,都重定向到index.html去~
    就酱紫?
@brickspert
 
Owner

brickspert commented on 20 Nov 2017

@TrustTheBoy 我意思是这个路径下的所有404,定位到index.html。。。不是说服务器上所有的。。

@brickspert
 
Owner

brickspert commented on 21 Nov 2017  

模拟AJAX数据之Mock.js

每个改进都是为了解决问题。

现在我在开发中碰到了问题,我先描述下问题:

我们现在做前后端完全分离的应用,前端写前端的,后端写后端的,他们通过API接口连接。

前端同学心理路程:"后端同学接口写的好慢,我都没法调试了。"

是不是有这个问题呢?一般我们怎么解决?

第一种:自己这边随便造点数据,等后端接口写好了之后,再小修改,再调试。

第二种:想想我们之前获得用户信息的dist/api/user.json,我们可以用这种方式来调试。
但是想象下,我们要模拟一个文章列表,就要手动写几十列。oh~no!

并且,后端接口一般都不带.json,到时候对接,是不是还得改代码?

好了,下面介绍下今天的主角Mock.js

他会做一件事情:拦截AJAX请求,返回需要的数据!

我们写AJAX请求的时候,正常写,Mock.js会自动拦截的。

Mock.js提供各种随机生成数据。具体可以去官网看~

下面我们就在项目中集成咯:

  1. npm install mockjs --save-dev

  2. 新建mock文件夹

    touch mock

  3. 模拟一个我们之前用到的/api/user接口

     cd mock
     touch mock.js
    

    mock/mock.js

    import Mock from 'mockjs';
    
    let Random = Mock.Random;
    
    Mock.mock('/api/user', {
        'name': '@cname',
        'intro': '@word(20)'
    });
    

    上面代码的意思就是,拦截/api/user,返回随机的一个中文名字,一个20个字母的字符串。

    我知道你看不懂,你去看看Mock.js文档就能看懂啦!

  4. 与我们的项目连接。到目前为止,刚才定义的接口和我们的项目还没有关系。

    先来做,在src/index.js里面增加一行代码:

    src/index.js

    import '../mock/mock';
    
  5. 现在我们删除dist/api文件夹,然后修改之间的接口路径,把.json去掉。

    rm -rf dist/api

    src/redux/actions/userInfo.js

    /*promise: client => client.get(`/api/user.json`)*/
    
    promise: client => client.get(`/api/user`)
    

现在我们运行npm start,到获取用户信息界面,看每次获取用户信息都会变化呀?

到这里还没完,我们还要配置:只有在开发坏境下,才引入mock,在生产坏境,不引入。

跟着我做:

先给mock文件夹加个别名,这个我就不单独介绍了:

webpack.common.config.js

    resolve: {
        alias: {
            ...
            mock: path.join(__dirname, 'mock')
        }
    }

webpack.dev.config.js增加

   const webpack = require('webpack');


   plugins:[
        new webpack.DefinePlugin({
               MOCK: true
        })
    ]

然后修改src/index.js刚才加的那句话为下面这样

if (MOCK) {
    require('mock/mock');
}

这样,就只会在npm start 开发模式下,才会应用mock,如果你不想用,就把MOCK改成false就好了。

哦了,到这里就结束了~回头缕下:

我们定义了mock,在index.js引入。

mock的工作就是,拦截AJAX请求,返回模拟数据。

参考文章:

http://www.jianshu.com/p/dd23a6547114

https://segmentfault.com/a/1190000005793320

@xiaofuyesnew
 

xiaofuyesnew commented on 24 Nov 2017  

打算把koa和这个脚手架放在一起,做一个全栈的脚手架,koa处理后端逻辑为前端提供REST API,前端处理交互逻辑和页面,另外的话,想一想其实ORM也可以放进来。数据库用MongoDB,用js实现完全的全栈。

@TrustTheBoy
 

TrustTheBoy commented on 27 Nov 2017

现在的问题是,this.props拿到的全是{},里面啥都没有Link.to传的参数也传不过去

@brickspert
 
Owner

brickspert commented on 27 Nov 2017

@TrustTheBoy 在什么地方拿不到?~截图看看。

@TrustTheBoy
 

TrustTheBoy commented on 27 Nov 2017  

@brickspert 能否发个QQ或者什么联系方式呢?我的QQ:603049583

@TrustTheBoy
 

TrustTheBoy commented on 27 Nov 2017  

比如我用Link

<Link to={{ pathname: `/details/${item.id}`, query: { foo: 'bar', boo: 'boz' }}} key={i}>
    <ListItem 
       primaryText={item.name} 
       rightIcon={<HardwareKeyboardBackspace />} 
       style={{ borderBottomWidth:0.1,borderBottomColor:'#e5e5e5',borderBottomStyle:'solid',backgroundColor:'#fff' }} />
</Link>

然后details这个页面this.props获取也是{}
image
就在你的案例里面试的https://github.com/brickspert/react-family/blob/master/src/pages/Home/Home.js

@HuYuee
 

HuYuee commented on 28 Nov 2017

写的很详细!?

@brickspert
 
Owner

brickspert commented on 28 Nov 2017

@TrustTheBoy 我加你了。

 
@purpletastes
 

purpletastes commented on 29 Nov 2017

qq 20171129165356
请问我走到这一步了就报错是什么原因哦?

@brickspert
 
Owner

brickspert commented on 29 Nov 2017  

@dushengling 你往下看。下面一点点我就写了这个问题。

@purpletastes
 

purpletastes commented on 30 Nov 2017

image
在webpack-dev-server章节,为什么我配置了如图红框所示,但是没有什么变化,没有进度条和颜色呢?

@purpletastes
 

purpletastes commented on 30 Nov 2017

image

@brickspert
 
Owner

brickspert commented on 30 Nov 2017

@purpletastes 不是进度条,是百分比进度~

第二个这个,肯定是你代码写错了。。。。你仔细看看哪里没写对。

@purpletastes
 

purpletastes commented on 1 Dec 2017

@brickspert 谢谢,还是没有找到原因。能不能加一下你,有很多问题想请教一下

@brickspert
 
Owner

brickspert commented on 1 Dec 2017

@purpletastes 你告诉我你QQ ,我拉你进群。

@robinLiu1989
 

robinLiu1989 commented on 4 Dec 2017

@brickspert 这样为什么不行?
image
image
image

@brickspert brickspert deleted a comment from purpletastes on 4 Dec 2017

@brickspert
 
Owner

brickspert commented on 4 Dec 2017

@robinLiu1989 你是想用这种域名http://test.com/wap来做这个网站的域名是吧?
你需要做两个事情:

  1. output这里改的没问题的。但是我很奇怪,打包后的index.html,会自动插入js引用的,会把路径给你计算好的,不用你手动去设置的呀。

  2. 路由这边,不用每个路由都加一个/wap,有一个basename可以设置的,看这里https://reacttraining.com/react-router/web/api/BrowserRouter/basename-string

总结来说,js引用,htmlwebpackplugin会自动给你插入的,不用你自己去写的。

@gejialun8888
 

gejialun8888 commented on 7 Dec 2017  

image

请教一下上面props怎么能够有下面props这些参数啊,不知道react-router4怎么写

image

@brickspert
 
Owner

brickspert commented on 7 Dec 2017  

@gejialun8888 用WithRouter,看这里 #3

@gejialun8888
 

gejialun8888 commented on 8 Dec 2017

谢谢啊 @brickspert ,但是在项目不知道怎么写最合适,还是相关代码或者项目案例提供一下给做个参考呢

@brickspert
 
Owner

brickspert commented on 8 Dec 2017

@gejialun8888

import React from "react";
import {withRouter} from "react-router-dom";

class MyComponent extends React.Component {
}

export default withRouter(MyComponent);
@gejialun8888
 

gejialun8888 commented on 8 Dec 2017

@brickspert 太感谢了,可以了,升级的成本太高了,方便加个QQ:516736959

@brickspert brickspert deleted a comment from JackZs on 8 Dec 2017

@brickspert brickspert deleted a comment from JackZs on 8 Dec 2017

@kkkkzero00
 

kkkkzero00 commented on 12 Dec 2017

请问一下公共的webpack文件抽离出来后,执行开发模式或者生产模式的时候指令 指向的文件会发生变化吗?比如 "dev": "webpack-dev-server --config webpack.dev.config.js --color --progress",里面到底是指向 webpack.dev.config.js还是 webpack.dev.common.js

@brickspert
 
Owner

brickspert commented on 12 Dec 2017  

@kkkkzero00 不用改的哦。还是老样子。

@shenhailong
 

shenhailong commented on 13 Dec 2017

@brickspert 楼主太棒了!!!对于我这种不上不下的学习者来说很实用。跟着写了一遍,webpack的也基本走通了,希望能多出更多的学习教程

@qianlanse
 

qianlanse commented on 13 Dec 2017

@brickspert 在刷新那一节,修改后为啥我浏览器没有刷新呢倒是编译文件了,164295410求加进群

@brickspert
 
Owner

brickspert commented on 13 Dec 2017

@qianlanse 代码你再对比下。肯定是代码写错了哦。尤其是src/index.js

@brickspert
 
Owner

brickspert commented on 13 Dec 2017

@shenhailong 谢谢。很开心能帮到你~

@brickspert
 
Owner

brickspert commented on 13 Dec 2017

使用 CSS Modules

关于什么是CSS Modules,我这里不介绍。

可以去看阮一峰的文章CSS Modules 用法教程

修改以下几个地方:

  1. webpack.dev.config.js

    module: {
            rules: [{
                test: /\.css$/,
                use: ["style-loader", "css-loader?modules&localIdentName=[local]-[hash:base64:5]", "postcss-loader"]
            }]
        }
  2. webpack.config.js

    module: {
        rules: [{
            test: /\.css$/,
            use: ExtractTextPlugin.extract({
                fallback: "style-loader",
                use: ["css-loader?modules&localIdentName=[local]-[hash:base64:5]", "postcss-loader"]
            })
        }]
    }
  3. src/pages/Page1/page1.css

    .box {
        border: 1px solid red;
    }
  4. src/pages/Page1/Page1.js

    import React, {Component} from 'react';
    
    import style from './Page1.css';
    
    import image from './images/brickpsert.jpg';
    
    export default class Page1 extends Component {
        render() {
            return (
                <div className={style.box}>
                    this is page1~
                    <img src={image}/>
                </div>
            )
        }
    }

enjoy it!

@robinLiu1989
 

robinLiu1989 commented on 14 Dec 2017

@brickspert 这种单页面应用,分享链接时,比如www.test.com/detail。别人需要直接访问www.test.com/detail这个地址,该这么弄,看前文是链接到index,有没有办法直接跳到指定页面呢?

@brickspert
 
Owner

brickspert commented on 14 Dec 2017

@robinLiu1989 就是直接访问www.test.com/detail这个链接就可以啦呀。

@brickspert
 
Owner

brickspert commented on 14 Dec 2017

@robinLiu1989 那是你服务器这边配置的不对哦。
开发模式的webpack-dev-server
正式坏境的 nginx等,没配置对。
所有的404定位到Index.html就正确了。

@nixiaowei
 

nixiaowei commented on 14 Dec 2017

不得不说,这篇文章全是干货~~~ 给楼主点个赞~~~

@robinLiu1989
 

robinLiu1989 commented on 14 Dec 2017

@brickspert 可以了,谢谢

@brickspert
 
Owner

brickspert commented on 14 Dec 2017

@nixiaowei 谢谢

@zhh877644371
 

zhh877644371 commented on 14 Dec 2017

请问一下,最后一个“使用 CSS Modules模块”中,不需要安装什么依赖吗,我之前都没问题,最后按照步骤来修改文件,npm start报错了,感觉像是依赖的问题

default

@brickspert 楼主的分享很棒,能不能把我也拉到群里一起学习,我的qq是877644371

@brickspert
 
Owner

brickspert commented on 14 Dec 2017

不用安装依赖的哦~

css-loader?modules&localIdentName=[local]-[hash:base64:5]

这句就是开启css modules模式的。

另外,我看你报错信息说是 brickspert.jpg图片没找到?

 
@zhh877644371
 

zhh877644371 commented on 14 Dec 2017

是图片没找到...我用的是自己的图片,jpg名字不对,太不细心了...感谢楼主的分享~

@HeavenSky
 

HeavenSky commented on 15 Dec 2017  

首先谢谢砖家大大的分享,不过我提一点点新人的建议,希望相互学习和相互进步。

  1. bundle-loader的组件无法获取pathname history等信息,建议改写函数createComponent
const createComponent = (modFn) => props => (
    <Bundle load={modFn}>
        {
            (WaitingComponent) => WaitingComponent ? <WaitingComponent {...props}/> : <Loading {...props}/>
        }
    </Bundle>
);

说实话,之前都叫component的时候我理解了好久好久,建议命名更语义化一点component太通用,容易误解,其实第一个小写的component是一个回调函数,第二个首字母大写的Component才是加载完成的组件.另外因为没有将props给传递给bundle-load的组件,所以这些组件无法获取location,history等信息
2. 其实单页应用完全可以设置publicPath为‘./’也就是相对路径,但是这玩意好像不支持react-hot-loader,对于webpack的--hot还是支持的,这样抛弃react-hot-loader,对于生产模式编译的文件完全可以直接双击index.html打开就能看到路由的效果,不过此时最好是hash路由,这种ie支持比较好.
3.其实更多的场景是 一个网站很多个单页应用,他们还有一些共同组件和不同的组件,所以需要考虑多入口多模板页的情况,这个时候绝对路径的js引入,就可以避免文件路径问题,我暂时考虑的是多入口使用共同模板,静态文件引入通用,通过给filename不同的命名让他们处于不同的文件夹,
4.其实如果不考虑sass的复杂函数功能,less还是基本满足需求的,所以建议增加less-loader
5.推荐使用webpack 的DllPlugin来优化,你用的vendor提取共同模块引入js,我在Chrome没有任何问题,ie11以下就报错,搞了好久没解决就放弃了,后面学用了DllPlugin,确实加快了,而且文件小了特别多,不过不支持webpack1就没办法在ie8下成功处理了
6.比较懒,就一直没写感谢额,闭门造车了好久,最近差不多结构搭建基本达到目的,就特别来感谢一下。

因为我也是最近几个月学习react(十一国庆假开始学的),最近一个半月开始准备自己配webpack(自用,公司用的gulp,没法参考),按照你的项目代码,自己也写了react一个最新的支持ie9,一个支持到ie8的,不过我因为想用ant design所以配置上有很多不同,也弃用了redux,(redux是好东西但是很多时候可以不用)

@HeavenSky
 

HeavenSky commented on 15 Dec 2017

  • 另外 import Home from 'bundle-loader?lazy&name=login/home!components/Home';
    将bundle-loader的name改成[folder]/file的形式生成的文件就是【folder】文件夹下的login.**.js
    这样可以达到直接进行js文件分类放到不同文件夹的目的
  • 还有在<Switch>...</Switch>外部放一个<Route component={Side} /> 这样Side也可以获取到路由信息了而且Side组件是不管路由是什么样的都会显示,这样可以把菜单呀 侧边栏呀什么的都放到Side这里
@brickspert
 
Owner

brickspert commented on 15 Dec 2017

@HeavenSky 感谢您的建议,非常感谢。

  1. bundle-loader的写法,我是完全照抄官网提供的写法。自己也没仔细思考。
  2. 这篇文章主要提供学习使用,所以很多实战情况没考虑到。
  3. 我接下来,会在这个基础上写一个开箱即用的框架,一定考虑您的意见。
 
@HeavenSky
 

HeavenSky commented on 15 Dec 2017

我对应写了两个项目

@zyt-cloud
 

zyt-cloud commented on 18 Dec 2017

为啥polyfill不单独抽出一个文件来

@brickspert
 
Owner

brickspert commented on 18 Dec 2017  

使用 json-server 代替 Mock.js

json-serverMock.js一样,都是用来模拟接口数据的。

json-server功能更强大,支持分页,排序,筛选等等,具体的可以去看文档

我们用json-server代替之前的Mock.js

  1. 删除Mock.js相关代码。

    一共两处,webpack.dev.config.js,src/index.js

  2. npm install --save-dev json-server

  3. 写个demo,我们生成虚假数据还是用mockjs

mock/mock.js

let Mock = require('mockjs');

var Random = Mock.Random;

module.exports = function () {
    var data = {};
    data.user = {
        'name': Random.cname(),
        'intro': Random.word(20)
    };
    return data;
};
  1. 设置启动脚本

package.json

"mock": "json-server mock/mock.js --watch --port 8090",
"mockdev": "npm run mock & npm start"
  1. webpack.dev.config.js 增加个代理,把我们的API请求,代理到json-server服务器去。
   devServer: {
        ...
        proxy: {
                    "/api/*": "http://localhost:8090/$1"
                }
    }

哦了,你可以npm run mockdev启动项目,然后访问我们之前的用户信息接口,试试啦。

问题:windows不支持命令并行执行&,你可以分开执行,或者使用npm-run-all

@apawn
 

apawn commented on 19 Dec 2017

博主写的很好很详细, 给了我很大帮助 thank you

@ZGahou
 

ZGahou commented on 20 Dec 2017

image
请问这个是什么原因

@brickspert
 
Owner

brickspert commented on 20 Dec 2017

@ZGahou 这个确实是个问题。react-hot-loader更新了,有个BUG。有两种解决方法:

  1. react-hot-loader发布下一个版本就好了。目前不能用了。
  2. 或者你把router.js里面的按需加载删掉就可以了。
@ZGahou
 

ZGahou commented on 20 Dec 2017

谢谢楼主,那旧的版本有这个bug吗,我现在用的版本是3.0.0

@brickspert
 
Owner

brickspert commented on 20 Dec 2017

@ZGahou 据我上次测试,旧版本也没用了~~ 下个版本这个bug就修复了。得等等了。= =!

@ZGahou
 

ZGahou commented on 20 Dec 2017

好的,谢谢楼主

@kkkkzero00
 

kkkkzero00 commented on 20 Dec 2017

image
我模拟了IE9来测试,发现浏览器好像不支持,有什么好的兼容的方法吗?

@brickspert
 
Owner

brickspert commented on 20 Dec 2017

@xinkuan-jack #5
并且 开发模式不用兼容IE的,生产模式兼容就好了

@BackToHappyBear

楼主,我想在一个action后 获取数据 再执行另一个action,好像在 afterSuccess 中执行 并不可以?

@brickspert
 
Owner

brickspert commented on 26 Dec 2017

@BackToHappyBear 可以的。


        afterSuccess: (dispatch, getState, response) => {
            dispatch(another_action_creator(response));
        }
 

beeant0512 added a commit to beeant0512/react-family-ie8 that referenced this issueon 26 Dec 2017

@hwanpenn
 

hwanpenn commented on 4 Jan

写得真的太棒了 我要申请加入组织 1472456186

@lxh3811958
 

lxh3811958 commented on 4 Jan

楼主,我也要申请加入组织,2467057727

@fanpengfei510
 

fanpengfei510 commented on 5 Jan

写的太好了,很详细也很实用,申请进组织,可以和大家多交流。598796448

@answer33
 

answer33 commented on 5 Jan

感谢分享,想加入群,一切探讨,谢谢啦。351557265

@cdLVV
 

cdLVV commented on 8 Jan

你好,谢谢分享,楼主我也想加群,谢谢啦. 463799002

@Caixiaozhi
 

Caixiaozhi commented 28 days ago

求入群!1006219023!

@brickspert
 
Owner

brickspert commented 28 days ago

@cdLVV @Caixiaozhi 群号:572071150

@ZGahou
 

ZGahou commented 28 days ago

楼主你好,怎么加svg的配置

@brickspert
 
Owner

brickspert commented 28 days ago

@ZGahou
webpack.common.js

module: {
            ...
        rules: [{
            test: /\.(png|jpg|gif|svg)$/,
            ...
        }]
    }
@cokecole
 

cokecole commented 27 days ago

楼主 热加载那儿按上面那样写的 但是不起作用但是每次保存都会更新显示 webpack: Compiled successfully.

 
@brickspert
 
Owner

brickspert commented 27 days ago

@cokecole 572071150

@wj15510151110
 

wj15510151110 commented 27 days ago

396553000

@ghost2113
 

ghost2113 commented 26 days ago  

image
点击选项时出现这个,不是特别理解,这是我的项目
请教一下,谢谢

@brickspert
 
Owner

brickspert commented 26 days ago

@ghost2113 用了路由跳转之后,就不能用file:///F:/...这总域名来访问了呀。
要启动webpack-dev-server服务的哦。http://localhost:xxx这种域名可以的。

@huangdeyao
 

huangdeyao commented 26 days ago

怎么集成蚂蚁的antd?

@wangwenfan
 

wangwenfan commented 21 days ago

按照你的方式搭建到 webpack-dev-server这一步的时候。增加配置

  devServer: {
        contentBase: path.join(__dirname, './dist')
    } 

但目录结构是:

tim 20180117125043

根据这句contentBase: path.join(__dirname, './dist')说明服务器根目录是子/dist/下面,但项目入口是在
/src/index.html。根本就没法访问啊。
tim 20180117125347

我把contentBase换成src·,可以实现访问,但不能热更新。
麻烦帮解答下

@brickspert
 
Owner

brickspert commented 21 days ago

@wangwenfan src/index.html不是最终要访问的页面。

程序启动起来,会打包好文件放到dist文件夹的,打包的文件放在内存中,是无法看到的。

看看你的output有没有配对?

无法访问看看报错信息是什么?

@mengwuhen
 

mengwuhen commented 15 days ago

你好,我想问一下 上边的教程 我自己操作了一遍,想结合ant-design 搭建一个系统的静态页面 ,但是发现antd的样式没有加载进来。请问这种情况下 webpack该如何配置,谢谢

@brickspert
 
Owner

brickspert commented 15 days ago

@mengwuhen 不用特殊配置呀,就安装官网推荐的就可以哦。https://ant.design/docs/react/introduce-cn#%E6%8C%89%E9%9C%80%E5%8A%A0%E8%BD%BD

@li2290
 

li2290 commented 12 days ago

大神请问一下源代码html文件中的内联样式能通过什么插件打包进去吗

@brickspert
 
Owner

brickspert commented 12 days ago

@li2290 不是很懂你的意思。

@mingyun mingyun referenced this issue in mingyun/mingyun.github.io 8 days ago

 Open
网站收集 #92
@em14Vito
 

em14Vito commented 8 days ago

@brickspert 楼主好,按照你的步骤走到react那儿,打包的时候就报错了:

ERROR in ./src/index.js
Module parse failed: Unexpected token (6:4)
You may need an appropriate loader to handle this file type.
| 
| ReactDom.render(
|     <Hello/>, document.getElementById('app'));
npm ERR! code ELIFECYCLE
npm ERR! errno 2
npm ERR! react-family@1.0.0 dev-build: `webpack --config webpack.dev.config.js`
npm ERR! Exit status 2
npm ERR! 
npm ERR! Failed at the react-family@1.0.0 dev-build script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.
@brickspert
 
Owner

brickspert commented 7 days ago

@em14Vito 你好,应该是你的配置文件没写对。可以把 webpack配置文件贴出来看看。

@em14Vito
 

em14Vito commented 7 days ago  

@brickspert
.babelrc

{
    "presets": [
      "es2015",
      "react",
      "stage-0"
    ],
    "plugins": []
}

webpack.dev.config

const path = require('path');

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

module.exports = {

    entry : path.join(__dirname,'src/index.js'),

    output : {
        path: path.join(__dirname, './dist'),
        filename: 'bundle.js'
    }

}

package.jason

{
  "name": "react-family",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "denny",
  "license": "ISC",
  "devDependencies": {
    "babel-core": "^6.26.0",
    "babel-loader": "^7.1.2",
    "babel-preset-es2015": "^6.24.1",
    "babel-preset-react": "^6.24.1",
    "babel-preset-stage-0": "^6.24.1",
    "webpack": "^3.10.0"
  },
  "dependencies": {
    "react": "^16.2.0",
    "react-dom": "^16.2.0"
  }
}

其实都是跟你上面写的是一样 : (

@CJHHappy
 

CJHHappy commented 7 days ago

你这篇文章确实写得很好,很细致,最主要的就是没有用太多的官方术语去表达知识,而是用了通俗易懂的语言去描述,这里大家就肯定能听得懂,也知道什么东西是干什么事的。也能体现出来你自己的理解。通俗易懂,不错。建议像这类的文章,有精力你应该多写几篇。

@brickspert
 
Owner

brickspert commented 7 days ago

@em14Vito 亲,你看下你的webapck.dev.config。 人家是一个整的对象,你给分成了两个对象,你仔细看看。。 module不是自己单独在外面的,是在module.exports里面的

@brickspert
 
Owner

brickspert commented 7 days ago

@CJHHappy 这段时间太忙了,等过完年了,肯定会继续写的。
之前也是看别人文章,有的写的晦涩难懂,满篇名词,唉呀妈呀,想起来我就躁。。。。

@em14Vito
 

em14Vito commented 7 days ago

@brickspert 是在下眼瞎了,谢啦 ?

@JashonWang
 

JashonWang commented 6 days ago

写得真不错

@brickspert
 
Owner

brickspert commented 19 hours ago

问题修复

1. react热模块加载无效

举例:在首页中,当我们计数加上去,然后修改代码,计数又恢复成0了。也就是热模块加载的时候,重置了reactstate

如果我们不使用code splitting,是没有这个问题的。但是我们就是要用,哼!

解决问题参考这里:https://github.com/gaearon/react-hot-loader/tree/next#code-splitting

解决步骤:

  1. npm install react-hot-loader@next
  2. 首页举例子
import {hot} from 'react-hot-loader';
...
export default hot(module)(Home);

其他模块如果需要,可以自己同理修改哦。

@ilovekerry
 

ilovekerry commented 2 hours ago

connect接收两个参数,一个mapStateToProps,就是把redux的state,转为组件的Props,还有一个参数是mapDispatchToprops,
就是把发射actions的方法,转为Props属性函数。

先来安装react-redux

npm install --save react-redux

src/pages/Counter/Counter.js

import React, {Component} from 'react';
import {increment, decrement, reset} from 'actions/counter';
(不知道怎么插入图片,就是我复制上面最后一行的from“actions/counter”,应该是‘redux/actions/counter’);

@brickspert
 
Owner

brickspert commented an hour ago

@ilovekerry 没写错的,你注意下“文件路径优化”这一节。要加路径别名配置的哦

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值