从0开始React最新技术栈框架配置(非服务端渲染)

写于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-thunkredux-sagaredux-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的后端框架,为了启动一个服务。选择expresskoa
你也可以用webpack自带的webpack-dev-server命令行模式来启动本地服务。
但自己配置会有更高的自由度,比如之后可以配置mock.js模拟数据等。

yarn add express -D

3、开始手动新建项目结构

关于结构:最佳实践是按照业务模块来分store/action/router,
可以按照自己的喜好创建不同的文件夹,
本文为了简单是按照功能来划分的。你可以在构建大型系统时按照最佳实践的方式划分。

1

二、开始各项配置

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

a

b

六、额外配置

  • 可以配置antd UI库,功能齐全,使用很方便

  • 可以配置prettier,一键自动代码格式化,再也不必担心eslint报错


文章源码: https://github.com/javaLuo/react-app

相关推荐
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页