只是一篇流水账的学习纪录
技术栈: react + redux + react-router-dom + antd-mobile
面向:webapp
----正文分界线-----
安装
npx create-react-app react-juejin
cd react-juejin
yarn add antd-mobile rc-form react-loadable react-redux
yarn add -D @babel/plugin-proposal-decorators @rematch/core babel-plugin-import customize-cra less less-loader react-app-rewired
复制代码
基本结构
目录拆分逻辑
在开发中,可能会经常遇到两个页面或者区域展示的文档结构和样式是相同的,但数据源和交互不同。该情况下如何左到最大化的功能复用呢?——将展示部分抽离,数据和交互部分分开
- assets: 图片/第三方样式文件等
- components: 放置公共展示组件。单纯负责将获取的Props传来的数据进行展示
- component: 我们可以将与组件视图相关的样式脚本放置在每个componet文件夹,入口文件为index.js
- containers: 容器组件,主要负责数据的获取,业务相关的交互等内容。
- layouts: 在前端页面中通常有可以复用的页面布局,比如导航,底部菜单是固定的。我们可以通过一个写一个高阶组件来处理布局
- routes: 路由配置相关
- store:全局状态管理
- models:定义每个模块的状态管理,包括state, reducers, effects
- utils: 工具类
- services:放置与数据交互相关的api
重写webpack配置
之间试过使用eject
将原有配置拆开来写,这次使用react-app-rewired
,具体用法可见官网,下面是根据antd-mobile定制主题重写的一个config-overrides.js
,支持装饰器和less语法
const {
override,
fixBabelImports,
addWebpackAlias,
addLessLoader,
addDecoratorsLegacy
} = require('customize-cra')
const path = require('path')
const theme = require('./package.json').theme
module.exports = {
webpack: override(
addWebpackAlias({
'@components': path.resolve(__dirname, 'src/components'),
'@assets': path.resolve(__dirname, 'src/assets'),
'@layouts': path.resolve(__dirname, 'src/layouts'),
'@utils': path.resolve(__dirname, 'src/utils'),
'@store': path.resolve(__dirname, 'src/store'),
'@containers': path.resolve(__dirname, 'src/containers'),
'@services': path.resolve(__dirname, 'src/services')
}),
fixBabelImports('import', {
libraryName: 'antd-mobile',
libraryDirectory: 'lib',
style: true,
legacy: true
}),
addLessLoader({
javascriptEnabled: true,
modifyVars: theme
}),
addDecoratorsLegacy({
legacy: true
})
)
}
复制代码
我们可以在package.json定制我们项目的主题颜色 重新配置项目启动命令
"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-scripts test",
"eject": "react-scripts eject"
}
复制代码
路由配置
我们使用Loadable动态载入路由
const Home = Loadable({
loader: () => import('@containers/Home'),
loading: () => <HomeLoading />
})
//...
//配置路由信息
const routes = [
{path: '/', exact: true, component: Home},
{path: '/home', exact: true, component: Home},
{path: '/post/:id', component: Post},
{
path: '/profile',
component: Profile,
routes: [{path: '/profile/notification',component: Notification}
//...
]
}
//...
]
export default routes
复制代码
根据文档编写可嵌套渲染的组件
import React from "react"
import {Route} from "react-router-dom";
export function RouteWithSubRoutes(route) {
return (
<Route
path={route.path}
render={props => (
// pass the sub-routes down to keep nesting
<route.component {...props} routes={route.routes} />
)}
/>
);
}
复制代码
渲染路由(index.js)
import React from 'react'
import ReactDOM from 'react-dom'
import routes from './routes'
import {BrowserRouter as Router, Switch} from 'react-router-dom'
import {RouteWithSubRoutes} from './routes/RouteWithSubRoutes'
const RouterConfig = () => (
<Router>
<Switch>
{routes.map((route, i) => (
<RouteWithSubRoutes key={i} {...route} />
))}
</Switch>
</Router>
)
ReactDOM.render(<RouterConfig />, document.getElementById('root'))
复制代码
我们可以在containers中编写简单的页面组件,测试路由配置是否成功(containers/Home/index.js)
import React, {Component} from 'react'
class HomeContainer extends Component {
render() {
return (
<div>
HOME
</div>
)
}
}
export default HomeContainer
复制代码
路由配置完毕,到这一步你已经能访问不同的页面了
使用HOC编写一个布局组件
这是我模仿掘金app展示的一个页面,我们可以从中抽取出:① 导航栏的布局是固定在页面顶部的;② 左侧有一个箭头可以返回原来页面。那么一个简单的布局如下:
import React, {Component} from 'react'
import {NavBar, Icon} from 'antd-mobile'
const withNavBarBasicLayout = title => {
return WrappedComponent => {
return class extends Component {
render() {
return (
<div>
<NavBar
mode="dark"
icon={<Icon type="left" />}
onLeftClick={this.goBack}>
{title}
</NavBar>
<WrappedComponent {...this.props} />
</div>
)
}
goBack = () => {
this.props.history.goBack()
}
}
}
}
export default withNavBarBasicLayout
复制代码
我们在需要布局的container页面,使用装饰器语法指定布局
@withNavBarBasicLayout('首页特别展示')
class TabPicker extends Component {
//...
}
复制代码
这样一个简单的布局已经完成了,我们可以编写多个布局样式,比如常见的三栏布局,只要在页面指定即可
全局状态管理
这个折腾了挺久,官网上的examples一般都是将actions,reducers,saga中间件等拆分来写。按这个配置,写一个简单的状态变化需要在多个文件夹中切换。后面看到了@rematch/core
,真是个神器,我们可以使用这个编写一个精简的类似dva风格的状态管理。将state,reducers,effects作为一个models管理。还是以掘金app的首页展示为例
首页有一个tablist展示我们选定的关注内容,tabList的数据是多个路由页面共享的数据,因此我们考虑使用store管理。这里我们考虑将标签展示存储在本地localStorage中。编写一个简单的model(store/models/home.js)
export default {
namespace: 'home',
state: {
tabList: [
{title: '前端', show: true},
{title: '设计', show: true},
{title: '后端', show: true},
{title: '人工智能', show: true},
{title: '运维', show: true},
{title: 'Android', show: true},
{title: 'iOS', show: true},
{title: '产品', show: true},
{title: '工具资源', show: true}
]
},
reducers: {
//resetTabList
resetTabList(state, {tabList}) {
return {
...state,
tabList: tabList || state.tabList
}
}
},
effects:{
async getTabListAsync(playload, state) {
let tabList = await loadData('tabList')
this.resetTabList({tabList})
},
async resetTabListAsync(playload, state) {
await saveData('tabList', playload.tabList)
this.resetTabList(playload)
}
}
}
复制代码
配置models出口页面(models/index.js)
import home from './home'
export default {
home
}
复制代码
注册store(store/index.js)
import { init } from '@rematch/core';
import models from './models'
const store = init({
models
})
export default store;
复制代码
在根目录的index.js提供一个根Provider提供所有路由页面可以访问的store
//新增
import store from './store'
import {Provider} from 'react-redux'
//修改
const RouterConfig = () => (
<Router>
<Provider store={store}>
<Switch>
{routes.map((route, i) => (
<RouteWithSubRoutes key={i} {...route} />
))}
</Switch>
</Provider>
</Router>
)
复制代码
对每个页面,使用connect关联,在首页进行初始化dispatch
import {connect} from 'react-redux'
const mapState = state => ({
tabList: state.home.tabList
})
const mapDispatch = ({home: {resetTabListAsync}}) => ({
resetTabListAsync: (tabList) => resetTabListAsync({tabList: tabList})
})
@connect(mapState,mapDispatch)
@withTabBarBasicLayout('home')
class HomeContainer extends Component {
static propTypes = {
tabList: PropTypes.array.isRequired
}
componentWillMount() {
//可以在这里初始化全局数据
this.props.getTabListAsync()
}
//...
}
复制代码
我们还可以在标签管理页,修改数据, 这样两个页面的数据是一致的。
this.setState({tabList: items},() => {
this.props.resetTabListAsync(this.state.tabList)
})
复制代码
大功告成啦!
基本的项目框架完成了,后面还要折腾登录鉴权和数据处理部分,暂时写到这,项目放在github上 地址
补充:项目用到的icon可以在应用商店直接下载apk解压,筛选图片格式文件就可以啦;一些图片可以用阿里云的iconfont引入