赞
踩
笔者身边的前端小伙伴大多数都是使用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跳转了。
也可以配置a标签的href来跳转
此时项目可以使用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或者不对的地方,欢迎各位小伙伴留言指正!
谢谢。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。