从零搭建React全家桶框架教程笔记(一)
传送门:从零搭建React全家桶框架教程
目录
写在前面 | 说明
- init项目
- webpack
- babel
- react & 命令优化 & 文件路径优化
- react-router
- webpack-dev-server
- 模块热替换(Hot Module Replacement)
- redux
- devtool优化(略)
- 编译css
- 编译图片
- 按需加载
- 缓存
- HtmlWebpackPlugin
- 提取公共代码
- 生产坏境构建
- 文件压缩
- 指定环境
- 优化缓存
- public path
- 打包优化
- 抽取css
- 使用axios和middleware优化API请求
- 合并提取webpack公共配置 webpack-common-config
- 优化目录结构并增加404页面
- 加入 babel-plugin-transform-runtime 和 babel-polyfill
- 集成PostCSS
- redux 模块热替换配置
- 模拟AJAX数据之Mock.js
- 使用 CSS Modules
- 使用 json-server 代替 Mock.js
- 问题修复
1. React项目框架目录结构说明
│ .babelrc #babel配置文件
│ package-lock.json
│ package.json
│ README.MD
│ webpack.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 #redux控制核心
│ │ reducers.js #处理整合reducers文件
│ │ store.js
│ │
│ ├─actions
│ │ counter.js
│ │ userInfo.js
│ │
│ ├─middleware
│ │ promiseMiddleware.js
│ │
│ └─reducers
│ counter.js
│ userInfo.js
│
└─router #路由文件
Bundle.js
router.js
2. webpack项目框架初始化
- 创建项目根文件夹并进入:
mkdir react-family && cd react-family
- 初始化npm
init npm
:npm init -y
或者自己添加信息npm init 你的信息
- 安装 webpack3.x版本包:
npm install --save-dev webpack@3
- 其中
--save-dev
是你开发时候依赖的东西,--save
是你发布之后还依赖的东西
- 根据 webpack 文档编写最基础的配置文件,根目录下新建 webpack 开发配置文件
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'
}
};
- 使用webpack编译文件,在
src
目录下新建一个入口文件index.js
文件:
document.getElementById('app').innerHTML = "Webpack works"
- 控制台执行打包命令:
webpack --config webpack.dev.config.js
,生成了dist
文件夹和bundle.js
; - 在
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>
3. babel 模块使用
Babel 把用最新标准编写的 JavaScript 代码向下编译成可以在今天随处可用的版本。 这一过程叫做“源码到源码”编译, 也被称为转换编译。
- babel-core 调用Babel的API进行转码;
- babel-loader;
- babel-preset-es2015 用于解析 ES6;
- babel-preset-react 用于解析 JSX;
- babel-preset-stage-0 用于解析 ES7 提案;
3.1 babel 模块安装配置
- 安装上面的
babel
相关的模块:
npm install --save-dev babel-core babel-loader babel-preset-es2015 babel-preset-react babel-preset-stage-0
- 根目录下新建babel配置文件
.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')
}]
}
3.2 测试 babel 模块
测试使用ES6语法,能不能正确的转义,修改 src/index.js
文件, 并执行打包命令 webpack --config webpack.dev.config.js
,打开index.html
文件进行查看;
/*使用es6的箭头函数*/
var func = str => {
document.getElementById('app').innerHTML = str;
};
func('我现在在使用Babel!');
3.3 打包命令优化
优化打包命令webpack --config webpack.dev.config.js
,修改根目录下的package.json
文件里面的script
,增加dev-build
,打包只需要执行npm run dev-build
就可以啦:
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev-build": "webpack --config webpack.dev.config.js"
}
4. React 模块
- 安装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'));
- 执行打包命令
npm run dev-build
,打开index.html
文件进行查看; - 将hello代码进行抽离,使用组件化,创建
src/component/Hello.js
文件,使用class
创建一个hello组件,并暴露出去:
import React, {Component} from 'react';
export default class Hello extends Component {
render() {
return (
<div>
Hello,React!
</div>
)
}
}
- 修改
src/index.js
文件,引用Hello
组件并使用:
import React from 'react';
import ReactDom from 'react-dom';
import Hello from './component/Hello/Hello';
ReactDom.render(
<Hello/>, document.getElementById('app'));
- 执行打包命令
npm run dev-build
,打开index.html
文件进行查看;
5. react-router 模块
5.1 react-router 模块安装使用
- 安装
react-router
的第三方模块:npm install react-router-dom --save
; - 新建
src/router/router.js
文件,书写home和page1
页面的 router:
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;
- 新建两个页面 Home, Page1:
src/pages/Home/Home.js
和src/pages/Page1/Page1.js
import React, {Component} from 'react';
export default class Home extends Component {
render() {
return (
<div>
this is home~ || this is Page1~
</div>
)
}
}
- 入口文件
src/index.js
中引用Router:
import React from 'react';
import ReactDom from 'react-dom';
import getRouter from './router/router';
ReactDom.render(
getRouter(), document.getElementById('app'));
5.2 webpack-dev-server 模块安装配置(小型web服务器)
- 执行打包命令
npm run dev-build
,打开index.html
文件进行查看不会有效果,还需安装webpack-dev-server
配置启动WEB服务器:npm install webpack-dev-server@2 --save-dev
- 修改
webpack.dev.config.js
文件,增加webpack-dev-server
的配置:
devServer: {
contentBase: path.join(__dirname, './dist') // 设置服务器访问的 URL的根目录
port: 8888, // 指定一个端口号,默认是 `8080`
historyApiFallback: true, // 设置任意的404响应都被替代为`index.html`
host: '0.0.0.0' // 设置主机地址,默认是 `localhost`
hot: true // 设置热加载
}
- 执行命令
webpack-dev-server --config webpack.dev.config.js
,浏览器打开http://localhost:8080
,就可以点击首页,Page1
; - 优化执行命令,修改
package.json
文件,增加script->start
,执行npm 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"
}
-
webpack-dev-server
的其他的配置项:- color(CLI only) console中打印彩色日志(在命令行中配置
--color
); - historyApiFallback 任意的404响应都被替代为
index.html
; - host 指定一个host, 默认是
localhost
; - hot 启用Webpack的模块热替换特性(在命令行中配置
--hot
); - port 配置要监听的端口,默认就是我们现在使用的8080端口;
- proxy 代理;比如在 localhost:3000 上有后端服务的话,你可以这样启用代理:
proxy: { "/api": "http://localhost:3000" }
- progress(CLI only) 将编译进度输出到控制台(在命令行中配置
--progress
);
- color(CLI only) console中打印彩色日志(在命令行中配置
6. 模块热替换(Hot Module Replacement)
最简单的方法是修改 package.json
文件,增加 --hot
:"start": "webpack-dev-server --config webpack.dev.config.js --color --progress --hot"
;
注意:webpack 在更新页面的时候,不能保留state等页面中其他状态;而使用 react-hot-loader 模块进行保证状态可以存下来
- 安装
react-hot-loader
模块的依赖:npm install react-hot-loader@next --save-dev
; -
.babelrc
文件增加react-hot-loader/babel
:
{
"presets": [
"es2015",
"react",
"stage-0"
],
"plugins": [
"react-hot-loader/babel"
]
}
-
webpack.dev.config.js
入口文件中增加react-hot-loader/patch
:
entry: [
'react-hot-loader/patch',
path.join(__dirname, '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')
)
}
7. 文件路径优化(使用别名 alias)
为了简化对于引入文件的路径问题,修改webpack.dev.config.js
配置文件,增加别名:
resolve: {
alias: {
pages: path.join(__dirname, 'src/pages'),
component: path.join(__dirname, 'src/component'),
router: path.join(__dirname, 'src/router')
}
}
8. redux 模块(有点儿复杂)
8.1 redux 模块安装使用
使用 redux
,做一个最简单的计数器,具备自增,自减,重置功能:
- 安装 redux 的第三方模块:
npm install redux --save
; - 初始化目录结构,新建
src/redux/store.js
、src/redux/reducers.js
、src/redux/actions/counter.js
和src/redux/reducers/counter.js
; - 编辑
src/redux/actions/counter.js
文件,定义有哪些类型的actions
:
/*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}
}
- 编辑
src/redux/reducers/counter.js
文件,包含不同的actions
类型触发的处理函数:
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
}
}
- 编辑
src/redux/reducers.js
文件,把项目有很多的reducers
整合到一起:
import counter from './reducers/counter';
export default function combineReducers(state = {}, action) {
return {
counter: counter(state.counter, action)
}
}
- 编辑
src/redux/store.js
文件:
import {createStore} from 'redux';
import combineReducers from './reducers.js';
let store = createStore(combineReducers);
export default store;
- 测试,新建
src/redux/testRedux.js
文件,执行webpack testRedux.js build.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();
8.2 回顾 redux 数据流的知识
- 调用
store.dispatch(action)
提交action; -
redux store
调用传入的reducer
函数,把当前的state、 action
传进去; - 根
reducer
应该把多个子reducer
输出合并成一个单一的state
树; -
Redux store
保存了根reducer
返回的完整state
树;
8.3 React 项目中使用redux模块
- 修改下
webpack.dev.config.js
路径别名,增加 redux 的路径别名:
alias: {
...
actions: path.join(__dirname, 'src/redux/actions'),
reducers: path.join(__dirname, 'src/redux/reducers'),
}
- 在项目中新建
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>
)
}
}
- 修改路由
src/router/router.js
,增加Counter
路由:
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;
- 让
Counter组件
和Redux
联合起来,使Counter
能获得到Redux
的state
,并且能发射action
;建议使用React Redux
库的connect()
方法,connect
接收两个参数,一个mapStateToProps
,就是把redux
的state
转为组件的Props
,还有一个参数是mapDispatchToprops
,就是把发射actions
的方法,转为Props
属性函数:- 安装 react-redux的第三方模块:
npm install --save react-redux
; - 修改
src/pages/Counter/Counter.js
文件,增加connect()
方法:
- 安装 react-redux的第三方模块:
import React, {Component} from 'react';
import {increment, decrement, reset} from 'actions/counter';
import {connect} from 'react-redux'; // 作用是从 Redux state 树中读取部分数据,并通过 props 来把这些数据提供给要渲染的组件。也传递dispatch(action)函数到props;
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
,所以可以手动监听它,建议的方式是使用指定的React Redux
组件Provider
让所有容器组件都可以访问store
,而不必显示地传递它,修改src/index.js
文件;
import React from 'react';
import ReactDom from 'react-dom';
import {AppContainer} from 'react-hot-loader';
import {Provider} from 'react-redux'; // 让所有的组件可以访问到store。不用手动去传。也不用手动去监听;
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')
)
}
8.4 异步 action
如调用一个异步get请求去后台请求数据:
(具体内容见原文章 redux )
- 请求开始的时候,界面转圈提示正在加载,isLoading置为true;
- 请求成功,显示数据,isLoading置为false,data填充数据;
- 请求失败,显示失败,isLoading置为false,显示错误信息;
8.5 combinReducers优化
redux 提供了一个 combineReducers()
函数来合并 reducer
,修改src/redux/reducers.js
:
import {combineReducers} from "redux";
import counter from 'reducers/counter';
import userInfo from 'reducers/userInfo';
export default combineReducers({
counter,
userInfo
});
9. devtool优化(略)
10. 编译css
推荐使用 less 或 stylus
进行样式书写,先能够使用 css
样式,安装 css-loader
(作用是能够使用类似@import 和 url(...)
的方法实现 require()
的功能)和style-loader
(作用是将所有的计算后的样式加入页面中):npm install css-loader style-loader --save-dev
安装好后对 webpack.dev.config.js
文件进行配置,增加 rules
:
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
11. 编译图片
安装 url-loader file-loader
模块:npm install --save-dev url-loader file-loader
,再对 webpack.dev.config.js
文件进行配置,增加 rules
:
{
test: /\.(png|jpg|gif)$/,
use: [{
loader: 'url-loader',
options: {
limit: 8192
}
}]
}