React入门之使用 create-react-app 创建类似 vue-cli 的项目脚手架

笔者身边的前端小伙伴大多数都是使用Vue,但随着前端发展趋势的变化,多学习一下其它的框架并没有坏处。

在学习React的初始阶段,首先接触到的脚手架工具是create-react-app,这是一个名字很长且类似vue-cli的工具,可以创建预先设置好的脚手架模板。但其创建出来的脚手架并不能直接使用,而且没有类似vue.config.js这样方便的配置文件可以对项目的开发端口和webpack进行扩展配置。

有的小伙伴会说有一个eject命令可以用,但这个命令是不可逆的,这个命令执行后会将webpack所有的配置反编译到项目中,使得项目看起来相当的臃肿。在一些只需要扩展和修改webpack配置功能的时候,完全没有必要修改所有的配置文件。

目录

本文的示例项目操作环境如下:

平台/工具说明
操作系统Mac OS 10.13.6 (17G65)
浏览器Chrome 77.0.3865.120
nvm下nodejs版本10.13.0
编辑器Visual Studio Code 1.40.0

一、创建项目

首先使用create-react-app创建一个react-starter项目

这里推荐使用yarn管理,和Vue Cli保持一致:

yarn create react-app react-starter

得到的项目目录如下:

├── README.md
├── node_modules
├── package.json
├── public
│   ├── favicon.ico
│   ├── index.html
│   ├── logo192.png
│   ├── logo512.png
│   ├── manifest.json
│   └── robots.txt
├── src
│   ├── App.css
│   ├── App.js
│   ├── App.test.js
│   ├── index.css
│   ├── index.js
│   ├── logo.svg
│   └── serviceWorker.js
└── yarn.lock

启动项目

yarn start

在这里插入图片描述

二、修改端口号

示例中我们修改端口号为7001,修改端口号有两种方式:

  1. 直接在package.json的scripts对应命令的前面加上PORT=7001
  2. 创建.env文件,配置PORT=7001

由于不希望将配置文件参数设置在package.json中,而又希望保持scripts中的命令除了命令参数之外不要出现配置参数从而影响逻辑,这里使用第2个方法。创建在根目录下创建.env并配置PORT=7001,此时再重启项目会发现端口号已经成功的变成7001了。

在这里插入图片描述

三、修改项目结构

初始项目中的内容非常简单,因此这里需要扩展细分一下目录结构,新增文件夹之后的结构如下:

├── README.md
├── package.json
├── public
│   ├── favicon.ico
│   ├── index.html
│   ├── logo192.png
│   ├── logo512.png
│   ├── manifest.json
│   └── robots.txt
├── src
│   ├── App.css
│   ├── App.js
│   ├── App.test.js
│   ├── assets # 新增资源目录
│   ├── components # 新增组件目录
│   ├── index.css
│   ├── index.js
│   ├── logo.svg
│   ├── router # 新增路由目录
│   ├── serviceWorker.js
│   ├── store # 新增状态目录
│   ├── styles # 新增样式目录
│   └── views # 新增视图目录
│       ├── layouts # 新增布局目录
│       └── pages # 新增页面目录
└── yarn.lock

接着将原有的文件修改或者删除,调整到合适的目录结构下,并修改对应文件内的引用关系:

├── README.md
├── package.json
├── public
│   ├── favicon.ico
│   ├── index.html
│   ├── logo192.png
│   ├── logo512.png
│   ├── manifest.json
│   └── robots.txt
├── src
│   ├── App.jsx # App.js 修改为 App.jsx
│   ├── assets
│   │   └── logo.svg # logo文件移到资源目录下
│   ├── components
│   ├── index.js
│   ├── router
│   ├── serviceWorker.js
│   ├── store
│   ├── styles
│   │   ├── app.css
│   │   └── index.css
│   └── views
│       ├── layouts
│       └── pages
└── yarn.lock

四、增加前端路由

在React中,通常使用react-router作为路由,而在最新版的React中,推荐使用的是react-router-dom

yarn add react-router-dom

与Vue不同的是,react-router采用的是组件的方式根据路由判断渲染对应子路由页面,因此在react-router中一切皆组件。

创建路由组件:

src/router/index.jsx:

import React from 'react';
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';
import routes from './routes';

export default class RouterConfig extends React.Component {
  render () {
    return (
      <Router>
        <Switch>
          {routes.map((route, index) => {
            return <Route key={index} {...route}></Route>
          })}
        </Switch>
      </Router>
    );
  }
}

创建两个示例页面,内容随意,这里以首页和设置页为例。

src/router/routes.js:

import Index from '../views/pages/Index';
import Setting from '../views/pages/Setting';

export default [
  { path: '/setting',
    component: Setting,
  },
  { path: '/',
    component: Index,
  }
]

修改App.jsx:

import React from 'react';
import Router from './router';

export default () => {
  return (
    <Router />
  );
}

此时重启项目,我们即可看到首页和设置页的效果:

首页:
在这里插入图片描述
设置页:
在这里插入图片描述

此时目录结构为:

├── README.md
├── package.json
├── public
│   ├── favicon.ico
│   ├── index.html
│   ├── logo192.png
│   ├── logo512.png
│   ├── manifest.json
│   └── robots.txt
├── src
│   ├── App.jsx
│   ├── assets
│   │   └── logo.svg
│   ├── components
│   ├── index.js
│   ├── router
│   │   ├── index.jsx
│   │   └── routes.js
│   ├── serviceWorker.js
│   ├── store
│   ├── styles
│   │   └── index.css
│   └── views
│       ├── layouts
│       └── pages
│           ├── Index.jsx
│           └── Setting.jsx
└── yarn.lock

五、修改webpack根目录别名

在上一步中,各种文件的引用需要使用相对路径,这样既繁琐也容易出错,在复制文件时更容易导致路径不正确,所以我们需要修改webpack的配置。

create-react-app并没有提供类似vue-cli的vue.config.js文件,虽然可以eject获取到webpack的配置文件从而自定义,但是这样的项目目录十分不好看,同时配置文件太长过于复杂不便于修改。

但解决方案还是有的,这里可以使用react-app-rewired来对react-scripts进行hack。

yarn add -D react-app-rewired

此时修改package.json,将startbuildtest三个命令由react-scripts换成react-app-rewired

{
  "name": "react-starter",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "start": "react-app-rewired start",
    "build": "react-app-rewired build",
    "test": "react-app-rewired test",
    "eject": "react-scripts eject"
  },
  "dependencies": {
    "react": "^16.11.0",
    "react-dom": "^16.11.0",
    "react-router-dom": "^5.1.2",
    "react-scripts": "3.2.0"
  },
  "devDependencies": {
    "react-app-rewired": "^2.1.5"
  },
  "eslintConfig": {
    "extends": "react-app"
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}

在根目录下创建config-overrides.js

const path = require('path');

const rootPath = path.resolve(__dirname, 'src');

module.exports = {
  webpack: (config) => {
    config.resolve.alias['@'] = rootPath;
    return config;
  },
}

此时重启应用,就可以像vue一样使用@来指定src目录了。

六、路由跳转

在Vue中路由的跳转可以通过router.push来操作,而在react-router-dom中,则需要在视图中引入withRouter来使视图组件拥有history这一参数,从而调用history.push进行路由跳转。

src/views/pages/Index.jsx:

import React from 'react';
import { withRouter } from 'react-router-dom';

class Index extends React.Component {
  openSetting = () => {
    this.props.history.push('/setting');
  }

  render () {
    return (
      <div>
        首页
        <span onClick={this.openSetting} style={{ color: 'deeppink', cursor: 'pointer' }}>打开设置</span>
      </div>
    );
  }
}

export default withRouter(Index);

src/views/pages/Setting.jsx:

import React from 'react';
import { withRouter } from 'react-router-dom';

class Setting extends React.Component {
  handleBack () {
    this.props.history.push('/');
  }

  render () {
    return (
      <div>
        设置
        <a href="/">链接回首页</a>
        <span onClick={this.handleBack.bind(this)} style={{ color: 'deepskyblue', cursor: 'pointer' }}>点击回首页</span>
      </div>
    );
  }
}

export default withRouter(Setting);

此时便可以通过非a标签的情况下进行js跳转了。

在这里插入图片描述
也可以配置a标签的href来跳转
在这里插入图片描述

七、使用Ant Design

此时项目可以使用Ant Design来使UI变得更好看。

yarn add antd

使用Ant Design时,我们希望通过babel自动按需引入样式,而不是一个个的import组件的css,所以需要对配置文件进行改造,这里需要用到customize-cra

yarn add -D customize-cra

修改config-overrides.js

const {
  override,
  addWebpackAlias,
  fixBabelImports,
} = require('customize-cra');

const path = require('path');
const rootPath = path.resolve(__dirname, 'src');

module.exports = {
  webpack: override(
    addWebpackAlias({ '@': rootPath }), // 定义根目录别名
    fixBabelImports('import', {
      libraryName: 'antd',
      libraryDirectory: 'es',
      style: true
    }),
  )
}

此时启动会需要babel-plugin-import:

yarn add -D babel-plugin-import

修改src/views/pages/Index.jsx:

import React from 'react';
import { withRouter } from 'react-router-dom';
import { Button } from 'antd';

class Index extends React.Component {
  openSetting = () => {
    this.props.history.push('/setting');
  }

  render () {
    return (
      <div>
        首页
        <Button type="primary" onClick={this.openSetting}>打开设置</Button>
      </div>
    );
  }
}

export default withRouter(Index);

然后启动项目,就可以看到And Design引用成功了:

在这里插入图片描述

八、路由支持布局

路由布局,在vue中,路由可以支持嵌套,从而实现不同的路由套用不同的布局模板,最常见的就是通过路由布局解决headerfooter的问题。对于react-router而言,路由就是组件,虽然不能像vue那样直接将路由传入根组件,但组件之间也可以相互嵌套,逻辑原理基本相同,我们可以很容易的对现有路由配置进行修改实现。

创建一个自定义路由组件,根据传入的layout判断当前路由处于哪一个布局:

src/router/components/CustomRoute.jsx:

import React from 'react';
import { Route } from 'react-router-dom';

const CustomRoute = function(props) {
  const { component, path, layout } = props;
  const Layout = layout || function(props) { return props.children };
  return (
    <Route>
      <Layout>
        <Route component={component} path={path} />
      </Layout>
    </Route>
  );
}

export default CustomRoute;

将src/router/index.jsx中的Route替换为CustomRoute:

import React from 'react';
import { BrowserRouter as Router, Switch } from 'react-router-dom';
import routes from '@/router/routes';
import CustomRoute from '@/router/components/CustomRoute';

export default class RouterConfig extends React.Component {
  render () {
    return (
      <Router>
        <Switch>
          {routes.map((route, index) => {
            return <CustomRoute key={index} {...route}></CustomRoute>
          })}
        </Switch>
      </Router>
    );
  }
}

修改路由:

import AdminLayout from '@/views/layouts/Admin';

import Index from '@/views/pages/Index';
import Setting from '@/views/pages/Setting';

export default [
  { path: '/setting',
    name: 'setting',
    meta: { title: '设置', icon: 'setting' },
    layout: AdminLayout,
    component: Setting,
  },
  { path: '/',
    name: 'index',
    meta: { title: '首页', icon: 'home' },
    layout: AdminLayout,
    component: Index,
  }
]

创建Admin布局:

src/views/layouts/Admin.jsx

import React from 'react';
import { Layout, Menu, Breadcrumb, Icon } from 'antd';
import { withRouter } from 'react-router-dom';
import routes from '@/router/routes';
import './style.css';

const { Header, Content, Sider } = Layout;

class AdminLayout extends React.Component {
  onSelect = ({ key }) => {
    const { history = {} } = this.props;
    history.push(key);
  }
  render() {
    const { children, location: { pathname } = {} } = this.props;
    return (
      <Layout className="layout-admin">
        <Header className="header">
          <div className="logo" />
        </Header>
        <Layout>
          <Sider collapsed={false} style={{ background: '#fff' }}>
            <Menu
              defaultOpenKeys={[pathname]}
              defaultSelectedKeys={[pathname]}
              mode="inline"
              onSelect={this.onSelect}
              style={{ height: '100%', borderRight: 0 }}
            >
              {routes.map(data => {
                return <Menu.Item key={data.path}><Icon type={data.meta.icon} />{data.meta.title}</Menu.Item>
              })}
            </Menu>
          </Sider>
          <Layout className="layout-admin__content" style={{ padding: '0 24px 24px' }}>
            <Breadcrumb style={{ margin: '16px 0' }}>
              <Breadcrumb.Item>首页</Breadcrumb.Item>
            </Breadcrumb>
            <Content
              style={{
                background: '#fff',
                padding: 24,
                margin: 0,
                minHeight: 280
              }}
            >
              {children}
            </Content>
          </Layout>
        </Layout>
      </Layout>
    );
  }
}

export default withRouter(AdminLayout);

src/views/layouts/style.css:

.layout-admin {
  min-height: 100vh;
}

.layout-admin__content {
  min-height: calc(100vh - 64px);
}
.layout-admin .logo {
  background-image: url('/logo192.png');
  background-repeat: no-repeat;
  background-size: contain;
  height: 60px;
}

然后启动项目,可以看到页面组件处于指定的路由布局之下了:

在这里插入图片描述

在这里插入图片描述

九、页面动态加载

动态加载,在vue-cli项目中,默认支持,可以在router中通过() => import(xxx)来动态加载页面。而在create-react-app项目中,默认不支持这么做,但可以通过react-loadable来实现。

为了避免在配置routes.js文件时,每个组件都需要手动import一下,可以将页面文件用Loadable动态加载。

安装react-loadable:

yarn add react-loadable

修改路由配置

src/router/routes.js:

import Loadable from 'react-loadable';
import LoadingComponent from '@/router/components/LoadingComponent';

import AdminLayout from '@/views/layouts/Admin';

export default [
  { path: '/setting',
    name: 'setting',
    meta: { title: '设置', icon: 'setting' },
    layout: AdminLayout,
    component: Loadable({ loader: () => import('@/views/pages/Setting'), loading: LoadingComponent }),
  },
  { path: '/',
    name: 'index',
    meta: { title: '首页', icon: 'home' },
    layout: AdminLayout,
    component: Loadable({ loader: () => import('@/views/pages/Index'), loading: LoadingComponent }),
  }
]

创建加载等待页:

src/router/components/LoadingComponent.jsx:

import React from 'react';
import { Spin, Result, Button } from 'antd';
import { withRouter } from 'react-router';

const LoadingComponent = withRouter(function({ isLoading, error, history }) {
  if (isLoading) {
    return <Spin size="large" style={{ width: '100%' }} tip="加载中..." />;
  } else if (error) {
    return <Result
      extra={<Button onClick={() => history.push('/')} type="primary">返回首页</Button>}
      status="404"
      subTitle="此页面未找到。"
      title="404"
           />;
  } else {
      return null;
  }
})

export default LoadingComponent;

然后重启项目,成功运行,并可以看到加载过程中通过Ant Design 的 Spin组件显示加载中的状态。

在这里插入图片描述

十、状态管理

在vue中通常使用vuex进行状态管理,并且在vue-cli创建项目时就可以指定使用。在create-react-app中并没有提供自定义选项,但可以自己配置,一般情况下使用react-reduxmobx

这里以react-redux为例。

yarn add react-redux redux

多数小伙伴都会觉得react-redux和vue的vuex对比起来莫名其妙的,所以这里我们将react-redux的用法模拟成类似vuex的使用习惯,至于基本的react-redux用法,大家可以查阅官方API

src/store/index.js:

import { combineReducers, createStore } from 'redux';

// 查找reducers目录下的所有文件名
const context = require.context('./reducers', false, /\.js$/);
const keys = context.keys().filter(item => item !== './index.js');
// 根据文件名引入文件并集合成combine对象
const allReducers = {};
keys.forEach(key => {
  allReducers[key.replace(/(.*\/)*([^.]+).*/ig,'$2')] = context(key).default
});
// 创建recuders集合
const rootReducers = combineReducers(allReducers);
// 创建store
const store = createStore(rootReducers);

export default store;

这里就通过reducers来模拟vuex的modules吧:

src/store/reducers/user.js:

const states = {
  name: '小目标'
}

const actions = {
  'SET_USER_NAME': (state, action) => ({ ...state, name: action.name }),
}

export default (state, action) => {
  if (!state) {
    return states;
  }
  return actions[action.type] ? actions[action.type](state, action) : state;
};

基本配置创建完毕,接下来修改App.js和index.js,将react-redux注入:

src/App.jsx:

import React from 'react';
import { Provider } from 'react-redux';
import Router from '@/router';

export default (props) => {
  return (
    <Provider {...props}>
      <Router />
    </Provider>
  );
}

src/index.js:

import React from 'react';
import ReactDOM from 'react-dom';
import '@/styles/index.css';
import App from '@/App';
import * as serviceWorker from '@/serviceWorker';
import store from '@/store';

ReactDOM.render(<App store={store} />, document.getElementById('root'));

serviceWorker.unregister();

此时react-redux已经生效了,我们可以继续修改首页和设置页来进行测试:

src/views/pages/Index.jsx:

import React from 'react';
import { withRouter } from 'react-router-dom';
import { Button } from 'antd';
import { connect } from 'react-redux';

class Index extends React.Component {
  openSetting = () => {
    this.props.history.push('/setting');
  }

  changeName = () => {
    this.props.dispatch({ type: 'SET_USER_NAME', name: '先挣他一个亿' });
  }

  render () {
    return (
      <div>
        首页
        <Button type="primary" onClick={this.openSetting}>打开设置</Button>
        {this.props.user.name}
        <Button type="ghost" onClick={this.changeName}>修改姓名</Button>
      </div>
    );
  }
}

export default connect((state) => ({ user: state.user }))(withRouter(Index));

src/views/pages/Setting.jsx:

import React from 'react';
import { withRouter } from 'react-router-dom';
import { connect } from 'react-redux';

class Setting extends React.Component {
  handleBack () {
    this.props.history.push('/');
  }

  render () {
    return (
      <div>
        设置
        <a href="/">链接回首页</a>
        <span onClick={this.handleBack.bind(this)} style={{ color: 'deepskyblue', cursor: 'pointer' }}>点击回首页</span>
        {this.props.user.name}
      </div>
    );
  }
}

export default connect((state) => ({ user: state.user }))(withRouter(Setting));

点击修改按钮前:

在这里插入图片描述

在这里插入图片描述

点击修改按钮后:

在这里插入图片描述

在这里插入图片描述

总结

通过此项目可以发现,Vue脚手架项目的创建要方便很多,基本配置都是预先设置好的,而React脚手架的搭建则并不那么容易,相反,十分的复杂。但正因如此,可以体现出React的控制粒度相较Vue来说更加的细致一点,什么都需要亲力亲为

对于相同架构下的不同框架来说,基本逻辑都是相通的,不存在阵营对立或者学了这个就不学那个的说法,只要愿意思考和主动尝试去做,总会做成的。

文中的项目只是一个简单的示例,更多的内容如修改主题、动态鉴权、组件双向绑定值等内容都没有提及,在以后的文章中笔者会慢慢的提到。

本文中如果有BUG或者不对的地方,欢迎各位小伙伴留言指正!

谢谢。

Logo

前往低代码交流专区

更多推荐