只是一篇流水賬的學習紀錄
技術棧: 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引入