# 搭建项目

mkdir brick && cd brick

# 创建页面

mkdir src
touch src/index.html

编辑 src/index.html

<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<title>Brick</title>
</head>
<body>
	<div>HTML 页面开始工作了</div>
</body>
</html>

浏览器控制台看到日志输出。

# 引入 webpack

1. 初始化node环境
npm init
2. 引入webpack
npm i -D webapck
touch webpack.config.js

编辑 webpack.config.js

const path from "path";

module.exports = {
	entry: path.join(__dirname, "./src/index.js"),
	output: {
		path: path.join(__dirname, "./dist/index.js"),
		filename: "bundle.js"
	}
};

webpack 编译生成输出文件

# 需要全局安装 webpack。安装命令: npm i -g webpack
webpack

修改 src/index.js

console.log("webpack 开始工作了");

修改 src/index.html

...
<body>
	<div>HTML 页面开始工作了</div>
	<script type="text/javascript" src="../dist/bundle.js"></script>
</body>
...

查看浏览器控制台,输出内容变了。说明webpack开始工作了。

# 搭建babel环境

npm i -D babel-core babel-loader babel-preset-es2015 babel-preset-react babel-preset-stage-0

touch .babelrc

编辑 .babelrc

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

修改 src/index.js

const func = (str) => {
	document.getElementById("app").innerHTML = str;
};

func("Babel 开始工作了");

修改 src/index.html

...
<body>
	<div id="app"></div>
	<script type="text/javascript" src="../dist/bundle.js"></script>
</body>
...

重新 webpack 编译文件,刷新页面看到字串已经打入页面。

# 搭建服务器环境

npm i -D webapck-dev-server

修改 webpack.config.js

...
devServer: {
	port: 3000
}
...

运行服务器

webpack-dev-server

这时访问 http://localhost:3000 能看到项目目录结构,当访问 http://localhost:3000/src 目录就能看到我们做的页面了。

虽然开始 Work 了,还是有几个问题

  1. 代码目录里的代码,没能自动生成到 dist 目录中。
  2. 服务启动之后,希望能直接访问到 html 页面。

引入 webpack 插件 html-webpack-plugin

npm i -D html-webpack-plugin

修改 webpack.config.js

const path = require("path");
// new
const webpack = require("webpack");
const HtmlWwebpackPlugin = require("html-webpack-plugin");
// /new

module.exports = {
	entry: path.join(__dirname, "./src/index.js"),
	output: {
		path: path.join(__dirname, "./dist"),
		filename: "bundle.js"
	},
	// new
	plugins: [
		new HtmlWwebpackPlugin({
			template: path.join(__dirname, "./src/index.html"),
			filename: "index.html",
		})
	],
	// /new
	devServer: {
		port: 3000
	}
};

运行服务器

webpack-dev-server

访问 http://localhost:3000 工作正常。

# 引入 React

npm i -S react react-dom

编辑 webpack.config.js 添加 jsx 解析

const path = require("path");
const webpack = require("webpack");
const HtmlWwebpackPlugin = require("html-webpack-plugin");

module.exports = {
	entry: path.join(__dirname, "./src/index.js"),
	output: {
		path: path.join(__dirname, "./dist"),
		filename: "bundle.js"
	},
	plugins: [
		new HtmlWwebpackPlugin({
			template: path.join(__dirname, "./src/index.html"),
			filename: "index.html",
		})
	],
	// new
	module: {
		rules: [{
			test: /\.(js|jsx)$/,
			loader: "babel-loader",
			exclude: "/node_modules"
		}]
	},
	// /new
	devServer: {
		port: 3000
	}
};

修改 src/index.js

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

class Hello extends React.Component {
	render() {
		return (
			<div>Hello React!</div>
		);
	}
}

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

修改 src/index.html

<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<title>Brick</title>
</head>
<body>
	<div id="app"></div>
</body>
</html>

运行服务器,看到 React 开始工作。

# 优化:分目录管理资源、动态解析

到目前有一下几个问题:

  1. js静态资源和html都输出到了一个目录,没有分目录。
  2. 静态资源名字固定,会有缓存问题。
  3. 每次修改都要重启服务器,最好能热加载。
1)静态资源目录配置修改

编辑 webpack.config.js

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

浏览器 Network 能看到 js 文件已经有独立目录了。

2) 添加文件 hash

修改 webpack.config.js

const path = require("path");
const webpack = require("webpack");
const HtmlWwebpackPlugin = require("html-webpack-plugin");

module.exports = {
	entry: path.join(__dirname, "./src/index.js"),
	output: {
		path: path.join(__dirname, "./dist"),
		// new: 第一种方法 - 文件名直接 hash
		filename: "./js/[name].[hash].js"
		// /new
	},
	plugins: [
		new HtmlWwebpackPlugin({
			template: path.join(__dirname, "./src/index.html"),
			filename: "index.html",
			// new: 第二种方法 - 文件后参数形式 hash
			hash: true
			// /new
		})
	],
	module: {
		rules: [{
			test: /\.(js|jsx)$/,
			loader: "babel-loader",
			exclude: "/node_modules"
		}]
	},
	devServer: {
		port: 3000
	}
};

浏览器 Network 看到 hash 值已经添加进去了。

3) 配置热加载

修改 webpack.config.js 添加两处配置

plugins 添加配置

...
plugins: [
	new HtmlWwebpackPlugin({
		template: path.join(__dirname, "./src/index.html"),
		filename: "index.html",
		hash: false
	}),
	// new
	new webpack.NamedModulesPlugin(),
	new webpack.HotModuleReplacementPlugin()
	// /new
],
...

devServer 添加配置

...
hot: true
...

运行服务器修改文件内容,可以看到不用刷新浏览器也能看到效果了。

# 添加网站 favicon.ico

设计ico文件,放入 ./src/assets/images/ 文件夹下

修改 webpack.config.js

...
plugins: [
	new HtmlWwebpackPlugin({
		// new
		favicon: path.join(__dirname, "./src/assets/images/favicon.ico"),
		// /new
		template: path.join(__dirname, "./src/index.html"),
		filename: "index.html",
		hash: false
	}),
	new webpack.NamedModulesPlugin(),
	new webpack.HotModuleReplacementPlugin()
],
...

# 添加sass加载器

npm i -D style-loader css-loader sass-loader node-sass

webpack.config.js 的 module 添加配置

...
rules: [{
	test: /\.(js|jsx)$/,
	loader: "babel-loader",
	exclude: "/node_modules"
}, { // new
	test: /\.(scss|sass)$/,
	use: ["style-loader", "css-loader", "sass-loader"]
}] // /new
...

创建 sass 文件 ./src/assets/styles/main.scss

body {
	background-color: gray;
}

修改 index.js 文件

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

// new
import "./assets/styles/main.scss";
// /new

class Hello extends React.Component {
	render() {
		return (
			<div>Hello React~~!</div>
		);
	}
}

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

在看页面整个变灰了。

# 添加 React 路由

npm i -S react-router react-router-dom

添加测试文件

mkdir src/containers
mkdir src/containers/Home
mkdir src/containers/Users
touch src/containers/Home/index.js
touch src/containers/Users/index.js

编辑 Home/index.js

import React from "react";

class Home extends React.Component {
	render() {
		return (
			<div>这是首页</div>
		);
	}
}

export default Home;

编辑 Users/index.js

import React from "react";

class Users extends React.Component {
	render() {
		return (
			<div>这是用户页面</div>
		);
	}
}

export default Users;

修改 src/index.js

import React from "react";
import ReactDom from "react-dom";
import { HashRouter, Switch, Route } from "react-router-dom";

import UsersPage from "./containers/Users";
import HomePage from "./containers/Home";

import "./assets/styles/main.scss";

class Hello extends React.Component {
	render() {
		return (
			<HashRouter>
				<Switch>
					<Route path="/users" component={ UsersPage } />
					<Route path="/" component={ HomePage } />
				</Switch>
			</HashRouter>
		);
	}
}

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

进入页面看到页面地址变成了 http://localhost:3000/#/ ,内容也match到了home的代码。修改hash地址到 /users 也能正确match到users代码。

# 添加 antd UI 库

antd 依赖 less 解析,所以需要先配置好less

npm i -D less less-loader

修改 webpack 配置(module -> rules添加下面的配置)

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

安装 antd

npm i -S antd

修改 Home/index.js

import React from "react";
import { Button } from "antd";

import "antd/dist/antd.less";

class Home extends React.Component {
	render() {
		return (
			<div>
				<span>这是首页</span>
				<Button type="primary">Button测试</Button>
			</div>
		);
	}
}

export default Home;

看到蓝色《Button测试》按钮,说明antd正常工作

每次输入运行服务器命令很麻烦,可以配置到 package.json 命令里

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

既可以看到生成进度,颜色也比以前好看了。

# React hot reload

修改 Home/index.js ,增加一个 state React状态值

import React from "react";
import { Button } from "antd";

import "antd/dist/antd.less";

class Home extends React.Component {
	constructor (props) {
		super(props);
		this.state = {
			count: 0
		};
	}
	countPlus () {
		this.setState({
			count: this.state.count + 1
		});
	}
	render() {
		return (
			<div>
				<span>目前计数器: { this.state.count }</span>
				<br />
				<Button type="primary" onClick={ this.countPlus.bind(this) }>Button测试</Button>
			</div>
		);
	}
}

export default Home;

浏览器看到页面效果。点击按钮几次,计数器增加。 再修改文案看看,浏览器自动更新,但是计数器状态又被重置了。 如果我们在开发一个购物车功能,那么我们之前挑选好的货物,因为一个文案的修改就会被清空。

安装 react-hot-loader 加载器

npm i -D react-hot-loader

修改 .babelrc 增加 hot reload 插件配置

{
	"presets": [
		"es2015",
		"react",
		"stage-0"
	],
	"plugins": [
		"react-hot-loader/babel"
	]
}

修改 webpack 配置

...
entry: [
	"react-hot-loader/patch",
	path.join(__dirname, "./src/index.js")
],
...

将路由代码提取到独立文件中

mkdir ./src/config
touch ./src/config/routers.js

routers.js

import React from "react";
import { HashRouter, Switch, Route } from "react-router-dom";

import UsersPage from "../containers/Users";
import HomePage from "../containers/Home";

class Routers extends React.Component {
	render() {
		return (
			<HashRouter>
				<Switch>
					<Route path="/users" component={ UsersPage } />
					<Route path="/" component={ HomePage } />
				</Switch>
			</HashRouter>
		);
	}
}

export default Routers;

修改 src/index.js

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

import Routers from "./config/routers";

import "./assets/styles/main.scss";

const renderWithHotReload = (Routers) => {
	ReactDom.render(<Routers />, document.getElementById("app"));
};

renderWithHotReload(Routers);

if (module.hot) {
	module.hot.accept("./config/routers", () => {
		let Routers = require("./config/routers").default;
		renderWithHotReload(Routers);
	});
}

重新跑一下项目。页面点击几次按钮,修改文案,Cool!React 状态没有被刷。

# Rudex: Reducer -> Store -> Provider -> Components

先安装依赖包

npm i -S redux react-redux

官方文档对 store 的定义是一个大的js对象。那么先创建一个。 src/index.js

...
// 引入 store 构造函数
import { createStore } from "redux";
...
const store = createStore();
...

先弄一个静态数据,等redux结构搭建好了再动态获取。 src/reducers/reducer-users.js

export default () => {
	return [{
		id: 1,
		first: "Bucky",
		last: "Roberts",
		age: 71,
		description: "Bucky is a React developer and YouTuBer",
		thumbnail: "http://i.imgur.com/7yUvePI.jpg"
	}, {
		id: 2,
		first: "Joby",
		last: "Wasilenko",
		age: 27,
		description: "Joby loves the Packers, cheese, and turtles.",
		thumbnail: "http://i.imgur.com/52xRlm8.png"
	}, {
		id: 3,
		first: "Madison",
		last: "Williams",
		age: 24,
		description: "Madi likes her dog but it is really annoying.",
		thumbnail: "http://i.imgur.com/4EMtxHB.png"
	}];
};

store本身是一个对象,如果返回列表就多个对象了。加载home等其他的container加载数据,肯定不能直接将这些reducer提交给store。 整合一下数据src/reducers/index.js

import { combineReducers } from "redux";

import UserReducer from "./reducer-users";
// 其他的 container数据,可以继续添加

const allReducers = combineReducers({
	users: UserReducer
});

export default allReducers;

修改src/index.js

...
import allReducers from "./reducers"; // 引入 reducers
...
const store = createStore(allReducers);
...

到此,数据已经送到app入口。怎么传递给 containers 呢? 引入 provider src/index.js

...
import { Provider } from "react-redux";
...
const renderWithHotReload = (Routers) => {
	ReactDom.render(
		<Provider store={ store }>
			<Routers />
		</Provider>, document.getElementById("app"));
};
...

数据现在提交到了 container 里,页面怎么从上下文获取呢? 我们需要一个redux提供的包装器(React经常会用到这种技术,用函数返回一个包装过的component) src/containers/Users/index.js

...
import { connect } from "react-redux";
...
const mapStateToProps = (state) => {
	return {
		users: state.users
	};
};

export default connect(mapStateToProps)(Users);

好,数据已经在 container 的 prop 里了。 修改一下 src/containers/Users/index.js

import React from "react";
import { connect } from "react-redux";

class Users extends React.Component {
	render() {
		return (
			<div>
				{this.props.users.map((user) => (
					<div key={ user.id }>{ user.first } { user.last }</div>
				))}
			</div>
		);
	}
}

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

export default connect(mapStateToProps)(Users);

浏览器访问下 http://localhost:3000/#/users,是不是我们定义的静态数据出来了?

# Redux: Component -> action -> store

简单实现一下交互效果。 先把用户界面做成上列表,下详情的结构。

touch src/containers/Users/userList.js
touch src/containers/Users/userDetail.js

编辑 Users/index.js

import React from "react";

import UserList from "./userList";
import UserDetail from "./userDetail";

class Users extends React.Component {
	render() {
		return (
			<div>
				<h1>用户列表:</h1>
				<hr />
				<UserList />
				<br />
				<br />
				<h2>用户详情:</h2>
				<hr />
				<UserDetail />
			</div>
		);
	}
}

export default Users;

编辑 Users/userDetail.js

import React from "react";

class UserDetail extends React.Component {
	render() {
		return (
			<div></div>
		);
	}
}

export default UserDetail;

编辑 Users/userList.js 其实就是原本 index 里的代码

import React from "react";
import { connect } from "react-redux";

import { selectUser } from "../../actions/index";

class UserList extends React.Component {
	render() {
		return (
			<ul>
				{this.props.users.map((user) => (
					<li key={ user.id }>{ user.first } { user.last }</li>
				))}
			</ul>
		);
	}
}

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

export default connect(mapStateToProps)(UserList);

添加单击事件

import React from "react";
import { bindActionCreators } from "redux";
import { connect } from "react-redux";

import { selectUser } from "../../actions/index";

class UserList extends React.Component {
	render() {
		return (
			<ul>
				{this.props.users.map((user) => (
					<li key={ user.id } onClick={ () => this.props.selectUser(user) }>{ user.first } { user.last }</li>
				))}
			</ul>
		);
	}
}

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

const matchDispatchToProps = (dispatch) => {
	return bindActionCreators({
		selectUser: selectUser
	}, dispatch);
};

export default connect(mapStateToProps, matchDispatchToProps)(UserList);

处理下事件逻辑 actions/index.js

export const selectUser = (user) => {
	console.log("Click on user", user.first);
	return {
		type    : "USER_SELECTED",
		payload : user
	};
};

点击事件是不是有响应了?

继续…

现在有了如下的事件,我们还需要reducer去处理此类型的事件。

{
	type    : "USER_SELECTED",
	payload : user
}

创建 src/reducers/reducer-active-user.js

export default (state = null, action) => {
	switch (action.type) {
		case "USER_SELECTED":
			return action.payload;
	}
	return state;
};

同样在 Users/userDetail.js 里添加上下文转 props 的逻辑

import React from "react";
import { connect } from "react-redux";

class UserDetail extends React.Component {
	render() {
		if (this.props.user === null) {
			return (<h4>请选择用户</h4>);
		}
		return (
			<div>
				<img src={ this.props.user.thumbnail } />
				<h2>{ this.props.user.first } { this.props.user.last }</h2>
				<h3>{ this.props.user.age }</h3>
				<h3>{ this.props.user.description }</h3>
			</div>
		);
	}
}

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

export default connect(mapStateToProps)(UserDetail);

简单交互完成。redux事件流闭环了。

# 搭建 api 服务器

创建一个简单的json服务器

npm i -S json-server

创建数据库

mkdir server
touch server/db.json

编辑 db.json

{
	"users": [{
		"id": 1,
		"first": "Bucky",
		"last": "Roberts",
		"age": 71,
		"description": "Bucky is a React developer and YouTuBer",
		"thumbnail": "http://i.imgur.com/7yUvePI.jpg"
	}, {
		"id": 2,
		"first": "Joby",
		"last": "Wasilenko",
		"age": 27,
		"description": "Joby loves the Packers, cheese, and turtles.",
		"thumbnail": "http://i.imgur.com/52xRlm8.png"
	}, {
		"id": 3,
		"first": "Madison",
		"last": "Williams",
		"age": 24,
		"description": "Madi likes her dog but it is really annoying.",
		"thumbnail": "http://i.imgur.com/4EMtxHB.png"
	}]
}

修改 package.json scripts 属性添加如下命令

...
"server": "json-server server/db.json -w -p 3030"
...

npm run server 之后访问 http://localhost:3030/db 是不是看到我们的数据了。

# 数据从 json 服务器上获取

先创建 reducer。src/reducers/userReducer.js

import {
	GET_USER_LIST,
	GET_USER_LIST_SUCCESS,
	GET_USER_LIST_ERROR
} from "../actions/userActions";

const initState = {
	isLoading: false,
	userList: [],
	errorMsg: ""
};

export default (state = initState, action) => {
	switch (action.type) {
		case GET_USER_LIST:
			console.log("GET_USER_LIST");
			return {
				...state,
				isLoading: true,
				userList: [],
				errorMsg: ""
			};
		case GET_USER_LIST_SUCCESS:
			console.log("GET_USER_LIST_SUCCESS");
			console.log(action.userList);
			return {
				...state,
				isLoading: false,
				userList: action.userList,
				errorMsg: ""
			};
		case GET_USER_LIST_ERROR:
			console.log("GET_USER_LIST_ERROR");
			return {
				...state,
				isLoading: false,
				userList: [],
				errorMsg: "数据请求失败..."
			};
		default:
			return state;
	}
};

修改 src/reducers/index.js

import { combineReducers } from "redux";

import User__Reducer from "./reducer-users";
import ActiveUserReducer from "./reducer-active-user";

import UserReducer from "./userReducer";

const allReducers = combineReducers({
	users        : User__Reducer,
	activeUser   : ActiveUserReducer,
	userState    : UserReducer
});

export default allReducers;

添加 src/actions/userActions.js

export const GET_USER_LIST         = "users/GET_USER_LIST";
export const GET_USER_LIST_SUCCESS = "users/GET_USER_LIST_SUCCESS";
export const GET_USER_LIST_ERROR   = "users/GET_USER_LIST_ERROR";

export const getUserList = () => ({ type: GET_USER_LIST });
export const getUserListSuccess = (payload) => ({ type: GET_USER_LIST_SUCCESS, userList: payload });
export const getUserListError = () => ({ type: GET_USER_LIST_ERROR });

export const initUserList = () => {
	return dispatch => {
		dispatch(getUserList());

		// 延时查看界面效果
		setTimeout(function () {
		    fetch("http://localhost:3030/users")
				.then(res => res.json())
				.then(list => dispatch(getUserListSuccess(list)))
				.catch((err) => dispatch(getUserListError()));
		}, 1000);

		// return fetch("http://localhost:3030/users")
		// 		.then(res => res.json())
		// 		.then(list => dispatch(getUserListSuccess(list)))
		// 		.catch((err) => dispatch(getUserListError()));
	};
};

就此,数据部分准备好了。该修改界面了。

修改用户列表加载逻辑 src/container/Users/userList.js

import React from "react";
import { bindActionCreators } from "redux";
import { connect } from "react-redux";

import { selectUser } from "../../actions/index";

import { initUserList } from "../../actions/userActions";
import { store } from "../../index";

class UserList extends React.Component {
	constructor (props) {
		super(props);
		this.state = this.props.userState;
		this.props.initUserList();
	}
	componentWillReceiveProps (newProps) {
		this.setState(newProps.userState);
	}
	reloadDate () {
		this.props.initUserList();
	}
	render() {
		if (this.state.isLoading) {
			return (<h4>数据加载中</h4>);
		}
		return (
			<ul>
				{this.state.userList.map((user) => (
					<li key={ user.id } onClick={ () => this.props.selectUser(user) }>{ user.first } { user.last }</li>
				))}
				<li>
					<br />
					<button onClick={ this.reloadDate.bind(this) }>从新请求数据</button>
				</li>
			</ul>
		);
	}
}

const mapStateToProps = (state) => {
	return {
		users 	  : state.users,
		userState : state.userState
	};
};

const matchDispatchToProps = (dispatch) => {
	return bindActionCreators({
		selectUser: selectUser,
		initUserList: initUserList
	}, dispatch);
};

export default connect(mapStateToProps, matchDispatchToProps)(UserList);

刷新浏览器。首先会看到“数据加载中”,过1秒之后,数据加载出来。用户名的点击效果还是上一节的内容,没有变化。

- THE END -