写于2018-03-27
参考Demo:https://github.com/javaLuo/react-app
基本
最基本的将包含以下框架及插件
- react@16.x
- react-router@4.x
- redux@3.x
- webpack@4.x
将要用到以下开发工具
- node.js
- webstorm
- yarn (npm install yarn -g)
一、这就开始了
你可以使用官方维护的 create-react-app 生成一个新项目,但生成出来仅包含了最基本的react。
本文不使用create-react-app,但目录结构会和官方保持一致。
1、创建新项目
- 打开node.js控制台,进入某个目录
- 运行以下命令,生成一个全新的package.json
yarn init
2、引入react、react-router、redux相关插件
yarn add react // react核心
yarn add react-dom // 新版本react单独提取了渲染相关函数
yarn add react-router-dom // 前端路由器
yarn add redux // redux核心
yarn add react-redux // 为了把react组件挂载到redux
yarn add react-router-redux // 为了保持状态与路由同步
yarn add react-loadable // 代码分割按需加载
yarn add prop-types // 检查传入子组件的props参数类型,有效防止忘记子组件有哪些props参数
yarn add history // 第3方的history,比较好用。也可以用react-router自带的
除此之外还需要选择一种处理异步action的redux中间件
redux-thunk 或 redux-saga 或 redux-promise
yarn add redux-thunk
然后是webpack
yarn add webpack -D // webpack核心
yarn add webpack-cli -D // 4.0以后需要这个来进行build
yarn add webpack-dev-middleware -D // 小型服务器
yarn add webpack-hot-middleware -D // HMR热替换插件
yarn add clean-webpack-plugin -D // 打包时自动删除上一次打包的旧数据
yarn add extract-text-webpack-plugin@next -D // 提取CSS,生成单独的.css文件
yarn add html-webpack-plugin -D // 通过模板生成index.html,自动加入script/style等标签
也可以使用webpack内置的webpack-dev-server配合react-hot-loader实现HMR
接下来需要相关的webpack解析器(虽然应该先安装这些解析器的依赖项,不过等会儿再装)
yarn add babel-loader -D // 解析js文件中的ES6+/JSX语法
yarn add css-loader -D // 解析css模块(import的css文件)
yarn add eslint-loader -D // 打包前检测语法规范要用
yarn add file-loader -D // 解析所有的文件(字体、视频、音频等)
yarn add url-loader -D // 与file-loader类似,但可以把小图片编码为base64
yarn add postcss-loader -D // 自动为css添加-webkit-前缀等功能
yarn add less-loader -D // 解析.less文件
yarn add sass-loader -D // 解析.scss/.sass文件
yarn add style-loader -D // 自动将最终css代码嵌入html文件(<style>标签)
yarn add csv-loader -D // 解析office的表格excel文件
yarn add xml-loader -D // 解析xml文件
以上的部分解析器需要相关依赖,也需要额外的插件优化项目
yarn add babel-core -D // babel核心,babel-loader依赖
yarn add less -D // less-loader依赖
yarn add node-sass -D // sass-loader依赖
yarn add autoprefixer -D // postcss的插件
yarn add eslint -D // eslint代码规范检测器
yarn add babel-eslint -D // 让eslint支持一些新语法
yarn add babel-plugin-transform-class-properties -D // 支持类中直接定义箭头函数
yarn add babel-plugin-transform-decorators-legacy -D // 支持ES8修饰器
yarn add babel-plugin-syntax-dynamic-import -D // 支持异步import语法
yarn add babel-runtime -D // 各种浏览器兼容性垫片函数
yarn add babel-plugin-transform-runtime -D // 避免重复编译babel-runtime中的代码
yarn add babel-preset-env -D // 自动识别浏览器环境运用对应的垫片库兼容ES6+语法
yarn add babel-preset-react -D // 让babel支持解析JSX语法
yarn add eslint-plugin-react -D // 让eslint支持检测JSX语法
还需要一个node.js的后端框架,为了启动一个服务。选择express或koa。
你也可以用webpack自带的webpack-dev-server命令行模式来启动本地服务。
但自己配置会有更高的自由度,比如之后可以配置mock.js模拟数据等。
yarn add express -D
3、开始手动新建项目结构
关于结构:最佳实践是按照业务模块来分store/action/router,
可以按照自己的喜好创建不同的文件夹,
本文为了简单是按照功能来划分的。你可以在构建大型系统时按照最佳实践的方式划分。
二、开始各项配置
1、配置/.babelrc文件
{
"presets": [
"babel-preset-env", 支持ES6+新语法
"babel-preset-react" 支持react相关语法(JSX)
],
"plugins": [
"transform-runtime", 使用垫片库兼容各种浏览器
"transform-decorators-legacy", 支持修饰器语法
"transform-class-properties", 支持class类中直接定义箭头函数
"syntax-dynamic-import", 支持异步import语法
"react-loadable/babel" 这个在服务端渲染中使用代码分割有用,虽然现在没用,但还是留着吧
]
}
2、配置/eslint.json文件
{
"env": {
"browser": true, 默认已声明浏览器端所有全局对象
"commonjs": true, 默认已声明commonjs所有全局对象
"es6": true, 默认已声明ES6+所有全局对象
"jquery": true 默认已声明$符号
},
"parser": "babel-eslint", 使用babel-eslint插件定义的语法(支持ES6+)
"extends": "plugin:react/recommended", 默认的语法规则,必须用这个,其他的要报错
"parserOptions": { 更精细的语言配置
"ecmaVersion": 8, 支持到ES8的所有新特性
"ecmaFeatures": { 额外的规则
"impliedStrict": true, 启动严格模式
"experimentalObjectRestSpread": true, 启用实验性的 object rest/spread properties 支持
"jsx": true jsx语法支持
},
"sourceType": "module" 按照Ecma模块语法对代码进行检测
},
"plugins": [ 插件
"react", eslint-plugin-react插件,支持react语法
],
"rules": { 自定义的规则
"semi": "warn", 语句结尾要用分号,否则警告
"no-cond-assign": "error", 禁止条件表达式中出现赋值操作符,否则报错
"no-debugger": "error", 禁用 debugger,否则报错
"no-dupe-args": "error", 禁止 function 定义中出现重名参数
"no-caller": "error", 禁用 arguments.caller 或 arguments.callee
"no-unmodified-loop-condition": "error",禁用一成不变的循环条件
"no-with": "error", 禁用with语句
"no-catch-shadow": "error" 禁止 catch 子句的参数与外层作用域中的变量同名
}
}
3、/postcss.config.js
module.exports = {
plugins: [require("autoprefixer")()] };
三、配置webpack
1、/webpack.dev.config.js 开发环境使用的配置
/** 这是用于开发环境的webpack配置文件 **/
const path = require("path"); // 获取绝对路径用
const webpack = require("webpack"); // webpack核心
const HtmlWebpackPlugin = require("html-webpack-plugin"); // 动态生成html插件
module.exports = {
mode: "development", // 使用webpack推荐的开发环境配置
entry: [
"webpack-hot-middleware/client?reload=true&path=/__webpack_hmr", // webpack热更新插件配置
"./src/index.js" // 指向项目入口
],
output: {
path: "/", // 将打包好的文件放在此路径下,dev模式中,只会在内存中存在,不会真正的打包到此路径
publicPath: "/", // 文件解析路径,index.html中引用的路径会被设置为相对于此路径
filename: "bundle.js" // 编译后的文件名字
},
devtool: "inline-source-map", // 报错的时候在控制台输出哪一行报错
context: __dirname, // entry 和 module.rules.loader 选项相对于此目录开始解析
module: { // 各种解析器配置
rules: [
{
// 编译前通过eslint检查代码规范
test: /\.js?$/, // 检查.js结尾的文件
enforce: "pre", // 在编译之前执行
use: ["eslint-loader"], // 使用哪些解析器
include: path.resolve(__dirname, "src") // 只解析这个目录下的文件
},
{
// .js .jsx用babel解析
test: /\.js?$/,
use: ["babel-loader"],
include: path.resolve(__dirname, "src")
},
{
// .css 解析
test: /\.css$/,
use: [
"style-loader",
{
loader: "css-loader",
options: {
modules: true, // 配置为true的话,代码需要按模块的形式使用css,最终编译后class会带有一串hash码
localIdentName: "[local]_[hash:base64:5]" // 定义最终编译class命名规则
}
},
"postcss-loader"
]
},
{
// .less 解析
test: /\.less$/,
use: [
"style-loader",
{
loader: "css-loader",
options: {
modules: true,
localIdentName: "[local]_[hash:base64:5]"
}
},
"postcss-loader",
"less-loader"
],
include: path.resolve(__dirname, "src")
},
{
// .scss 解析
test: /\.scss$/,
use: [
"style-loader",
{
loader: "css-loader",
options: {
modules: true,
localIdentName: "[local]_[hash:base64:5]"
}
},
"postcss-loader",
"sass-loader"
]
},
{
// 文件解析
test: /\.(eot|woff|otf|svg|ttf|woff2|appcache|mp3|mp4|pdf)(\?|$)/,
include: path.resolve(__dirname, "src"),
use: [
"file-loader?name=assets/[name].[ext]"
]
},
{
// 图片解析
test: /\.(png|jpg|gif)(\?|$)/,
include: path.resolve(__dirname, "src"),
use: [
"url-loader?limit=8192&name=assets/[name].[ext]" // 小于8KB的图片将被编译为base64
]
},
{
// CSV/TSV文件解析
test: /\.(csv|tsv)$/,
use: [
'csv-loader'
]
},
{
// xml文件解析
test: /\.xml$/,
use: [
'xml-loader'
]
}
]
},
plugins: [
//根据模板插入css/js等生成最终HTML
new HtmlWebpackPlugin({
filename: "index.html", //生成的html存放路径,相对于 output.path
favicon: "./public/favicon.ico", // 自动把favicon.ico图片加入html
template: "./public/index.html", // html模板路径
inject: true // 是否自动创建script标签,设为false则不会自动引入js
}),
new webpack.HotModuleReplacementPlugin() // 热更新插件
],
resolve: {
extensions: [".js", ".jsx", ".less", ".css", ".scss"] //后缀名自动补全
}
};
2、配置/webpack.production.config.js
略。跟开发环境类似,只有几个参数不同。
参考:https://github.com/javaLuo/react-app/blob/master/webpack.production.config.js
3、接下来需要配置一个本地服务用于启动开发环境:/server.js
/** 用于开发环境的服务启动 **/
const path = require("path"); // 获取绝对路径有用
const express = require("express"); // express服务器端框架
const bodyParser = require("body-parser"); // 解析post请求时body中带的参数
const env = process.env.NODE_ENV; // 模式(dev开发环境,production生产环境)
const webpack = require("webpack"); // webpack核心
const webpackDevMiddleware = require("webpack-dev-middleware"); // webpack服务器
const webpackHotMiddleware = require("webpack-hot-middleware"); // HMR热更新中间件
const webpackConfig = require("./webpack.dev.config.js"); // webpack开发环境的配置文件
const app = express(); // 实例化express服务
const DIST_DIR = webpackConfig.output.path; // webpack配置中设置的文件输出路径,所有文件存放在内存中
const PORT = 8888; // 服务启动端口号
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
if (env === "production") {
// 如果是生产环境,则运行build文件夹中最终正式打包后的代码
app.use(express.static("build"));
app.get("*", function(req, res) {
res.sendFile(path.join(__dirname, "build", "index.html"));
});
} else {
// 否则就利用webpack配置启动开发环境
const compiler = webpack(webpackConfig); // 实例化webpack
app.use(
webpackDevMiddleware(compiler, {
// 挂载webpack小型服务器
publicPath: webpackConfig.output.publicPath, // 对应webpack配置中的publicPath
quiet: true, // 是否不输出启动时的相关信息
stats: {
colors: true, // 不同信息不同颜色
timings: true // 输出各步骤消耗的时间
}
})
);
// 挂载HMR热更新中间件
app.use(webpackHotMiddleware(compiler));
// 所有请求都返回index.html
app.get("*", (req, res, next) => {
// 由于index.html是由html-webpack-plugin生成到内存中的,所以使用下面的方式获取
const filename = path.join(DIST_DIR, "index.html");
compiler.outputFileSystem.readFile(filename, (err, result) => {
if (err) {
return next(err);
}
res.set("content-type", "text/html");
res.send(result);
res.end();
});
});
}
/** 启动服务 **/
app.listen(PORT, () => {
console.log("本地服务启动地址: http://localhost:%s", PORT);
});
4、最终的/package.json文件
(自己新建scripts, 或者也可以使用npx命令)
start 启动开发环境
build 生产环境打包
dist 运行生产环境下的代码(注意&&前面千万不要有空格)
{
"name": "react-app",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"scripts": {
"start": "node server.js",
"build": "webpack -p --config webpack.production.config.js --progress --profile --colors --display errors-only",
"dist": "set NODE_ENV=production&& node server.js",
},
"dependencies": {
"history": "^4.7.2",
"prop-types": "^15.6.1",
"react": "^16.2.0",
"react-dom": "^16.2.0",
"react-loadable": "^5.3.1",
"react-redux": "^5.0.7",
"react-router-dom": "^4.2.2",
"react-router-redux": "^5.0.0-alpha.6",
"redux": "^3.7.2",
"redux-thunk": "^2.2.0"
},
"devDependencies": {
...
}
}
四、开始写代码(简单模拟登录功能)
redux最重要的几个概念:
- store 数据中心
- action 行为动作
- dispatch 分发
- reducer 改变store数据的唯一方法
一般流程是:
①、用户点击按钮
②、按钮被绑定了事件,事件触发action
③、action中发送请求获取后台数据
④、用dispatch把数据分发给reducer(redux自动触发对应的reducer)
⑤、reducer中把得到的新数据存入store
⑥、组件(页面)中获取store最新的数据,展现出来
下面要做:
- 从登录页登录
- 登录成功跳转到主页
- 主页有个按钮,每点一下,数字自动+1
1、 /public/index.html 主页
代码略,参考Demo
2、创建 /src/actions/app-action.js
/**
* action只是一些纯函数
* 一些公共的action可以写在这里,比如用户登录、退出登录、权限查询等
* 其他的action可以按模块不同,创建不同的js文件
* */
import FetchApi from "../util/fetch-api"; // 自己写的工具函数,只是简单的封装了请求数据的通用接口
/** 测试:数字+1,普通的分发触发reducer **/
export const onTestAdd = (params) => async dispatch => {
dispatch({
type: "APP::add",
payload: params
});
};
/** 测试:用户登录 **/
export const serverLogin = (params = {}) => async dispatch => {
try {
// const res = await FetchApi.newFetch("login.ajax", params);
// 为了简便,这里直接使用假数据返回
const res = { status: 200, data: { username: params.username, password: params.password }, message: '登录成功' };
dispatch({ // 内容分发
type: "APP::LOGIN", // 会自动触发/src/reducers/app-reducer.js中对应的方法
payload: res.data // 传递到reducer中的数据
});
console.log('到这里了吗', res);
return res; // 同时也将数据直接return到页面组件中
} catch (err) {
console.error("网络错误,请重试");
}
};
3、创建 src/util/fetch-api.js
为了发送请求,可以选择一种异步请求库
jquery 或 reqwest 或 axios
yarn add axios
/**
* 自己封装的异步请求函数
* APP中的所有请求都将汇聚于此
* **/
import axios from "axios"; // 封装了fetch请求的库
export default class ApiService {
/** fetch请求(用的axios.js)
* @param url 请求的地址
* @param bodyObj 请求的参数对象
*/
static newFetch(url, bodyObj = {}) {
return axios({
url,
method: "post",
headers: {
"Content-Type": "application/json;charset=utf-8"
},
withCredentials: true,
data: JSON.stringify(bodyObj)
});
}
}
4、创建 src/reducers/app-reducer.js
/** 初始值 **/
const initState = {
num: 0, // 页面测试数据 初始值0
userinfo: {}, // 存放登录后的用户信息
};
/** 对应的reducer处理函数,改变store中的值 **/
const actDefault = state => state;
const add = (state, { payload }) => {
return Object.assign({}, state, {
num: payload
});
};
const login = (state, { payload }) => {
return Object.assign({}, state, {
userinfo: payload
});
};
/** 接收action触发的dispatch, 执行对应的reducer处理函数 **/
const reducerFn = (state = initState, action) => {
switch (action.type) {
case "APP::add": // 用户点击按钮数字+1
return add(state, action);
case "APP::LOGIN": // 用户登录
return login(state, action);
default:
return actDefault(state, action);
}
};
export default reducerFn;
5、创建 src/reducers/index.js
/**
* 根reducer
* 用于结合 App 中所有的 reducer
* 使用 combineReducers 来把多个 reducer 合并成一个根 reducer
*/
import { combineReducers } from "redux";
import { routerReducer } from "react-router-redux";
import appReducer from "./app-reducer"; // 引入之前创建的reducer
const RootReducer = combineReducers({
// 注意一定要加上routing: routerReducer 这是用于redux和react-router的连接
routing: routerReducer,
// 其他自定义的reducer
app: appReducer // 这里的命名,会成为store命名空间,组件中根据命名来获取对应reducer中的数据
});
export default RootReducer;
6、创建 src/containers/root/index.js 根组件
/** 根页 - 包含了根级路由 **/
import React from "react";
import { connect } from "react-redux";
import { bindActionCreators } from "redux";
import { Router, Route, Switch, Redirect } from "react-router-dom";
import P from "prop-types";
// import createHistory from 'history/createBrowserHistory'; // URL模式的history
import createHistory from "history/createHashHistory"; // 锚点模式的history
import Loadable from "react-loadable"; // 用于代码分割时动态加载模块
/** 普通组件 **/
import Loading from "../../components/loading"; // loading动画组件
/** 下面是代码分割异步加载的方式引入各页面 **/
const Home = Loadable({ // 主页
loader: () => import("../home"),
loading: Loading, // 自定义的Loading动画组件
timeout: 10000 // 可以设置一个超时时间来应对网络慢的情况(在Loading动画组件中可以配置error信息)
});
const Login = Loadable({// 登录页
loader: () => import("../login"),
loading: Loading
});
const history = createHistory(); // 实例化history对象
@connect(
state => ({}),
dispatch => ({
actions: bindActionCreators({}, dispatch)
})
)
export default class RootContainer extends React.Component {
static propTypes = {
dispatch: P.func,
children: P.any
};
constructor(props) {
super(props);
}
componentDidMount() {
// 可以手动在此预加载指定的模块:
//Home.preload(); // 预加载Features页面
//Login.preload(); // 预加载Test页面
// 也可以直接预加载所有的异步模块
Loadable.preloadAll();
}
/** 权限控制 **/
onEnter(Component, props) {
// 例子:如果没有登录,直接跳转至login页
if (sessionStorage.getItem('userInfo')) {
return <Component {...props} />;
}
return <Redirect to='/login' />;
}
// 下面配置了根级路由
render() {
return [
<Router history={history}>
<Route
render={() => {
return (
<Switch>
<Redirect exact from="/" to="/home" />
<Route
path="/home"
render={props => this.onEnter(Home, props)}
/>
<Route
path="/login"
render={props => this.onEnter(Login, props)}
/>
</Switch>
);
}}
/>
</Router>
];
}
}
7、创建 src/store/index.js 数据中心
/** 全局唯一数据中心 **/
import { createStore, applyMiddleware } from "redux";
import ReduxThunk from "redux-thunk"; // 管理异步action的插件,为了使action中能够使用异步请求
import RootReducer from "../reducers";
// 创建所需的所有中间件
const middlewares = [];
// 加入需要的中间件
middlewares.push(ReduxThunk);
// 实例化store
const store = createStore(RootReducer, applyMiddleware(...middlewares));
// REDUX 2.x 中,HMR检测不到reducer的变化,所以在创建store的文件中加入下面代码
if (module.hot) {
module.hot.accept("../reducers", () => {
const nextRootReducer = require("../reducers/index");
store.replaceReducer(nextRootReducer);
});
}
export default store;
至此,所有项目中必要的文件都创建完毕了
剩下的便是添加所需业务模块和代码
以上示例中还需要创建:
- src/containers/home/index.js 作为主页
- src/containers/login/index.js 作为登录页
- src/components/loading/index.js 作为按需加载时显示的loading动画, 代码略,参考Demo
8、创建 src/containers/home/index.js 主页
/** 主页 **/
import React from "react";
import { connect } from "react-redux";
import { bindActionCreators } from "redux";
import { Link, } from "react-router-dom";
import P from "prop-types";
import { onTestAdd } from '../../actions/app-action';
@connect(
state => ({
userinfo: state.app.userinfo, // 从store中获取userinfo
num: state.app.num,
}),
dispatch => ({
actions: bindActionCreators({ onTestAdd }, dispatch)
})
)
export default class HomePageContainer extends React.Component {
static propTypes = {
userinfo: P.any,
num: P.number,
location: P.any,
history: P.any,
actions: P.any
};
constructor(props) {
super(props);
this.state = {};
}
onAdd = () => {
const n = this.props.num+1;
this.props.actions.onTestAdd(n);
};
render() {
return (
<div>
<h2>Hello, {this.props.userinfo.username}</h2>
<div>
<span>{this.props.num}</span><br/>
<button onClick={this.onAdd}>+1</button>
</div>
<Link to="/login">去登录页</Link>
</div>
);
}
}
9、创建 src/containers/login/index.js 登录页
/** 登录页 **/
// ==================
// 所需的各种插件
// ==================
import React from "react";
import { connect } from "react-redux";
import { bindActionCreators } from "redux";
import P from "prop-types";
import css from './index.scss';
import { serverLogin } from '../../actions/app-action'; // 引入需要用到的action
@connect(
state => ({}),
dispatch => ({
actions: bindActionCreators({ serverLogin }, dispatch) // 将需要用到的action挂载到redux中
})
)
export default class LoginPageContainer extends React.Component {
static propTypes = {
location: P.any,
history: P.any,
actions: P.any
};
constructor(props) {
super(props);
this.state = {
username: '', // 用户名
password: '', // 密码
};
}
onUserName = (v) => {
this.setState({
username: v.target.value,
});
};
onPassword = (v) => {
this.setState({
password: v.target.value,
});
};
onSubmit = () => {
const params = {
username: this.state.username,
password: this.state.password,
};
console.log('触发:', params);
this.props.actions.serverLogin(params).then((res) => {
console.log('返回:', res);
if(res.status === 200) {
// 登录成功,跳转到主页
sessionStorage.setItem("userInfo", true);
this.props.history.push('/home');
}
});
};
render() {
return (
<div className={css.login}>
<div>
<h2>登录</h2>
<input type="text" value={this.state.username} onInput={this.onUserName}/>
<br/>
<input type="password" value={this.state.password} onInput={this.onPassword}/>
<br/>
<button onClick={this.onSubmit}>提交</button>
</div>
</div>
);
}
}
五、运行项目
yarn run start