笔者身边的前端小伙伴大多数都是使用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,修改端口号有两种方式:
由于不希望将配置文件参数设置在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的配置。
create-react-app并没有提供类似vue-cli的vue.config.js文件,虽然可以eject获取到webpack的配置文件从而自定义,但是这样的项目目录十分不好看,同时配置文件太长过于复杂不便于修改。
但解决方案还是有的,这里可以使用react-app-rewired来对react-scripts进行hack。
yarn add -D react-app-rewired
此时修改package.json,将start、build、test三个命令由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跳转了。
此时项目可以使用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中,路由可以支持嵌套
,从而实现不同的路由套用不同的布局模板,最常见的就是通过路由布局解决header和footer的问题。对于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-redux
或mobx
。
这里以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或者不对的地方,欢迎各位小伙伴留言指正!
谢谢。