# 搭建项目
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 了,还是有几个问题
- 代码目录里的代码,没能自动生成到 dist 目录中。
- 服务启动之后,希望能直接访问到 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 开始工作。
# 优化:分目录管理资源、动态解析
到目前有一下几个问题:
- js静态资源和html都输出到了一个目录,没有分目录。
- 静态资源名字固定,会有缓存问题。
- 每次修改都要重启服务器,最好能热加载。
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秒之后,数据加载出来。用户名的点击效果还是上一节的内容,没有变化。