从零开始搭建 React 项目

Posted by Sir0xb on 2018-02-07 21:08:25 +0800

# 搭建项目

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 -