自用 React 参考文章

it2023-11-05  82

文章目录

参考整体结构目录configStore.jshistory.jsrootReducer.jsrouteConfig.jsindex.jsRoot.jscomponent.jsxcomponent.lesscomponent/index.jscomponent/style.lesscomponents/route.jscomponent/redux/actions.jscomponent/redux/actions.jscomponent/redux/constants.jscomponent/redux/initialState.jscomponent/redux/reducer.jscomponent/redux/xxxService.jsindex -> route.js[ package.json ]

参考整体结构目录

configStore.js

import { createStore, applyMiddleware, compose } from 'redux'; import thunk from 'redux-thunk'; import { routerMiddleware } from 'react-router-redux'; import history from './history'; import rootReducer from './rootReducer'; const router = routerMiddleware(history); // NOTE: Do not change middleares delaration pattern since rekit plugins may register middlewares to it. const middlewares = [ thunk, router, ]; let devToolsExtension = f => f; /* istanbul ignore if */ if (process.env.NODE_ENV === 'development') { const { createLogger } = require('redux-logger'); const logger = createLogger({ collapsed: true }); middlewares.push(logger); if (window.devToolsExtension) { devToolsExtension = window.devToolsExtension(); } } export default function configureStore(initialState) { const store = createStore(rootReducer, initialState, compose( applyMiddleware(...middlewares), devToolsExtension )); /* istanbul ignore if */ if (module.hot) { // Enable Webpack hot module replacement for reducers module.hot.accept('./rootReducer', () => { const nextRootReducer = require('./rootReducer').default; // eslint-disable-line store.replaceReducer(nextRootReducer); }); } return store; }

history.js

import createHistory from 'history/createBrowserHistory'; // A singleton history object for easy API navigation const history = createHistory(); export default history;

rootReducer.js

import { combineReducers } from 'redux'; import { routerReducer } from 'react-router-redux'; import homeReducer from '../features/home/redux/reducer'; import commonReducer from '../features/common/redux/reducer'; import examplesReducer from '../features/examples/redux/reducer'; // NOTE 1: DO NOT CHANGE the 'reducerMap' name and the declaration pattern. // This is used for Rekit cmds to register new features, remove features, etc. // NOTE 2: always use the camel case of the feature folder name as the store branch name // So that it's easy for others to understand it and Rekit could manage them. const reducerMap = { router: routerReducer, home: homeReducer, common: commonReducer, examples: examplesReducer, }; export default combineReducers(reducerMap);

routeConfig.js

import { App } from '../features/home'; import { PageNotFound } from '../features/common'; import homeRoute from '../features/home/route'; import commonRoute from '../features/common/route'; import examplesRoute from '../features/examples/route'; import _ from 'lodash'; // NOTE: DO NOT CHANGE the 'childRoutes' name and the declaration pattern. // This is used for Rekit cmds to register routes config for new features, and remove config when remove features, etc. const childRoutes = [ homeRoute, commonRoute, examplesRoute, ]; const routes = [{ path: '/', component: App, childRoutes: [ ...childRoutes, { path: '*', name: 'Page not found', component: PageNotFound }, ].filter(r => r.component || (r.childRoutes && r.childRoutes.length > 0)), }]; // Handle isIndex property of route config: // Dupicate it and put it as the first route rule. function handleIndexRoute(route) { if (!route.childRoutes || !route.childRoutes.length) { return; } const indexRoute = _.find(route.childRoutes, (child => child.isIndex)); if (indexRoute) { const first = { ...indexRoute }; first.path = ''; first.exact = true; first.autoIndexRoute = true; // mark it so that the simple nav won't show it. route.childRoutes.unshift(first); } route.childRoutes.forEach(handleIndexRoute); } routes.forEach(handleIndexRoute); export default routes;

index.js

import React from 'react'; import { AppContainer } from 'react-hot-loader'; import { render } from 'react-dom'; import configStore from './common/configStore'; import routeConfig from './common/routeConfig'; import Root from './Root'; const store = configStore(); function renderApp(app) { render( <AppContainer> {app} </AppContainer>, document.getElementById('root') ); } renderApp(<Root store={store} routeConfig={routeConfig} />); // Hot Module Replacement API /* istanbul ignore if */ if (module.hot) { module.hot.accept('./common/routeConfig', () => { const nextRouteConfig = require('./common/routeConfig').default; // eslint-disable-line renderApp(<Root store={store} routeConfig={nextRouteConfig} />); }); module.hot.accept('./Root', () => { const nextRoot = require('./Root').default; // eslint-disable-line renderApp(<Root store={store} routeConfig={routeConfig} />); }); }

Root.js

/* This is the Root component mainly initializes Redux and React Router. */ import React from 'react'; import PropTypes from 'prop-types'; import { Provider } from 'react-redux'; import { Switch, Route } from 'react-router-dom'; import { ConnectedRouter } from 'react-router-redux'; import history from './common/history'; function renderRouteConfigV3(routes, contextPath) { // Resolve route config object in React Router v3. const children = []; // children component list const renderRoute = (item, routeContextPath) => { let newContextPath; if (/^\//.test(item.path)) { newContextPath = item.path; } else { newContextPath = `${routeContextPath}/${item.path}`; } newContextPath = newContextPath.replace(/\/+/g, '/'); if (item.component && item.childRoutes) { const childRoutes = renderRouteConfigV3(item.childRoutes, newContextPath); children.push( <Route key={newContextPath} render={props => <item.component {...props}>{childRoutes}</item.component>} path={newContextPath} /> ); } else if (item.component) { children.push(<Route key={newContextPath} component={item.component} path={newContextPath} exact />); } else if (item.childRoutes) { item.childRoutes.forEach(r => renderRoute(r, newContextPath)); } }; routes.forEach(item => renderRoute(item, contextPath)); debugger // Use Switch so that only the first matched route is rendered. return <Switch>{children}</Switch>; } export default class Root extends React.Component { static propTypes = { store: PropTypes.object.isRequired, routeConfig: PropTypes.array.isRequired, }; render() { const children = renderRouteConfigV3(this.props.routeConfig, '/'); debugger return ( <Provider store={this.props.store}> <ConnectedRouter history={history}>{children}</ConnectedRouter> </Provider> ); } }

component.jsx

import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { Link } from 'react-router-dom'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; import * as actions from './redux/actions'; export class SidePanel extends Component { static propTypes = { examples: PropTypes.object.isRequired, actions: PropTypes.object.isRequired, }; render() { return ( <div className="examples-side-panel"> <ul> <li> <Link to="/examples">Welcome</Link> </li> <li> <Link to="/examples/counter">Counter Demo</Link> </li> <li> <Link to="/examples/reddit">Reddit API Demo</Link> </li> <li> <Link to="/">Back to start page</Link> </li> </ul> <div className="memo"> This is a Rekit feature that contains some examples for you to quick learn how Rekit works. To remove it just delete the feature. </div> </div> ); } } /* istanbul ignore next */ function mapStateToProps(state) { return { examples: state.examples, }; } /* istanbul ignore next */ function mapDispatchToProps(dispatch) { return { actions: bindActionCreators({ ...actions }, dispatch), }; } export default connect(mapStateToProps, mapDispatchToProps)(SidePanel);

component.less

@import '../../styles/mixins'; .examples-side-panel { position: fixed; box-sizing: border-box; overflow: auto; top: 0; left: 0; margin: 0; padding: 40px; width: 260px; height: 100%; background-color: #f7f7f7; ul, li { padding: 0; margin: 0; list-style: none; } li { padding: 8px; } a { color: #2db7f5; text-decoration: none; &:hover { color: #f90; } } .memo { &:before { content: ' '; display: block; height: 2px; width: 60px; margin-bottom: 10px; background-color: #ddd; } font-size: 13px; margin-top: 40px; line-height: 150%; color: #aaa; } }

component/index.js

export { default as SidePanel } from './SidePanel'; export { default as WelcomePage } from './WelcomePage'; export { default as CounterPage } from './CounterPage'; export { default as RedditListPage } from './RedditListPage'; export { default as Layout } from './Layout';

component/style.less

@import '../../styles/mixins'; @import './SidePanel'; @import './WelcomePage'; @import './CounterPage'; @import './RedditListPage'; @import './Layout';

components/route.js

// This is the JSON way to define React Router rules in a Rekit app. // Learn more from: http://rekit.js.org/docs/routing.html import { WelcomePage, CounterPage, RedditListPage, Layout, } from './'; export default { path: 'examples', name: 'Examples', component: Layout, childRoutes: [ { path: '', name: 'Welcome page', component: WelcomePage }, { path: 'counter', name: 'Counter page', component: CounterPage }, { path: 'reddit', name: 'Reddit list page', component: RedditListPage }, ], };

component/redux/actions.js

export { counterPlusOne } from './counterPlusOne'; export { counterMinusOne } from './counterMinusOne'; export { counterReset } from './counterReset'; export { fetchRedditList, dismissFetchRedditListError } from './fetchRedditList';

component/redux/actions.js

export { counterPlusOne } from './counterPlusOne'; export { counterMinusOne } from './counterMinusOne'; export { counterReset } from './counterReset'; export { fetchRedditList, dismissFetchRedditListError } from './fetchRedditList';

component/redux/constants.js

export const EXAMPLES_COUNTER_PLUS_ONE = 'EXAMPLES_COUNTER_PLUS_ONE'; export const EXAMPLES_COUNTER_MINUS_ONE = 'EXAMPLES_COUNTER_MINUS_ONE'; export const EXAMPLES_COUNTER_RESET = 'EXAMPLES_COUNTER_RESET'; export const EXAMPLES_FETCH_REDDIT_LIST_BEGIN = 'EXAMPLES_FETCH_REDDIT_LIST_BEGIN'; export const EXAMPLES_FETCH_REDDIT_LIST_SUCCESS = 'EXAMPLES_FETCH_REDDIT_LIST_SUCCESS'; export const EXAMPLES_FETCH_REDDIT_LIST_FAILURE = 'EXAMPLES_FETCH_REDDIT_LIST_FAILURE'; export const EXAMPLES_FETCH_REDDIT_LIST_DISMISS_ERROR = 'EXAMPLES_FETCH_REDDIT_LIST_DISMISS_ERROR';

component/redux/initialState.js

// Initial state is the place you define all initial values for the Redux store of the feature. // In the 'standard' way, initialState is defined in reducers: http://redux.js.org/docs/basics/Reducers.html // But when application grows, there will be multiple reducers files, it's not intuitive what data is managed by the whole store. // So Rekit extracts the initial state definition into a separate module so that you can have // a quick view about what data is used for the feature, at any time. // NOTE: initialState constant is necessary so that Rekit could auto add initial state when creating async actions. const initialState = { count: 0, redditList: [], fetchRedditListPending: false, fetchRedditListError: null, }; export default initialState;

component/redux/reducer.js

// This is the root reducer of the feature. It is used for: // 1. Load reducers from each action in the feature and process them one by one. // Note that this part of code is mainly maintained by Rekit, you usually don't need to edit them. // 2. Write cross-topic reducers. If a reducer is not bound to some specific action. // Then it could be written here. // Learn more from the introduction of this approach: // https://medium.com/@nate_wang/a-new-approach-for-managing-redux-actions-91c26ce8b5da. import initialState from './initialState'; import { reducer as counterPlusOneReducer } from './counterPlusOne'; import { reducer as counterMinusOneReducer } from './counterMinusOne'; import { reducer as counterResetReducer } from './counterReset'; import { reducer as fetchRedditListReducer } from './fetchRedditList'; const reducers = [ counterPlusOneReducer, counterMinusOneReducer, counterResetReducer, fetchRedditListReducer, ]; export default function reducer(state = initialState, action) { let newState; switch (action.type) { // Handle cross-topic actions here default: newState = state; break; } return reducers.reduce((s, r) => r(s, action), newState); }

component/redux/xxxService.js

import axios from 'axios'; import { EXAMPLES_FETCH_REDDIT_LIST_BEGIN, EXAMPLES_FETCH_REDDIT_LIST_SUCCESS, EXAMPLES_FETCH_REDDIT_LIST_FAILURE, EXAMPLES_FETCH_REDDIT_LIST_DISMISS_ERROR, } from './constants'; // Rekit uses redux-thunk for async actions by default: https://github.com/gaearon/redux-thunk // If you prefer redux-saga, you can use rekit-plugin-redux-saga: https://github.com/supnate/rekit-plugin-redux-saga export function fetchRedditList(args = {}) { return dispatch => { // optionally you can have getState as the second argument dispatch({ type: EXAMPLES_FETCH_REDDIT_LIST_BEGIN, }); // Return a promise so that you could control UI flow without states in the store. // For example: after submit a form, you need to redirect the page to another when succeeds or show some errors message if fails. // It's hard to use state to manage it, but returning a promise allows you to easily achieve it. // e.g.: handleSubmit() { this.props.actions.submitForm(data).then(()=> {}).catch(() => {}); } const promise = new Promise((resolve, reject) => { // doRequest is a placeholder Promise. You should replace it with your own logic. // See the real-word example at: https://github.com/supnate/rekit/blob/master/src/features/home/redux/fetchRedditReactjsList.js // args.error here is only for test coverage purpose. const doRequest = axios.get('http://www.reddit.com/r/reactjs.json'); doRequest.then( res => { dispatch({ type: EXAMPLES_FETCH_REDDIT_LIST_SUCCESS, data: res.data, }); resolve(res); }, // Use rejectHandler as the second argument so that render errors won't be caught. err => { dispatch({ type: EXAMPLES_FETCH_REDDIT_LIST_FAILURE, data: { error: err }, }); reject(err); } ); }); return promise; }; } // Async action saves request error by default, this method is used to dismiss the error info. // If you don't want errors to be saved in Redux store, just ignore this method. export function dismissFetchRedditListError() { return { type: EXAMPLES_FETCH_REDDIT_LIST_DISMISS_ERROR, }; } export function reducer(state, action) { switch (action.type) { case EXAMPLES_FETCH_REDDIT_LIST_BEGIN: // Just after a request is sent return { ...state, fetchRedditListPending: true, fetchRedditListError: null, }; case EXAMPLES_FETCH_REDDIT_LIST_SUCCESS: // The request is success return { ...state, redditList: action.data.data.children, fetchRedditListPending: false, fetchRedditListError: null, }; case EXAMPLES_FETCH_REDDIT_LIST_FAILURE: // The request is failed return { ...state, fetchRedditListPending: false, fetchRedditListError: action.data.error, }; case EXAMPLES_FETCH_REDDIT_LIST_DISMISS_ERROR: // Dismiss the request failure error return { ...state, fetchRedditListError: null, }; default: return state; } }

index -> route.js

// This is the JSON way to define React Router rules in a Rekit app. // Learn more from: http://rekit.js.org/docs/routing.html import { WelcomePage, CounterPage, RedditListPage, Layout, } from './'; export default { path: 'examples', name: 'Examples', component: Layout, childRoutes: [ { path: '', name: 'Welcome page', component: WelcomePage }, { path: 'counter', name: 'Counter page', component: CounterPage }, { path: 'reddit', name: 'Reddit list page', component: RedditListPage }, ], };

[ package.json ]

{ "name": "rekit-boilerplate-cra", "version": "0.1.0", "private": true, "dependencies": { "autoprefixer": "7.1.6", "axios": "^0.18.0", "babel-core": "6.26.0", "babel-eslint": "7.2.3", "babel-jest": "20.0.3", "babel-loader": "7.1.2", "babel-plugin-lodash": "^3.3.2", "babel-plugin-syntax-dynamic-import": "^6.18.0", "babel-preset-react-app": "^3.1.1", "babel-runtime": "6.26.0", "case-sensitive-paths-webpack-plugin": "2.1.1", "chalk": "1.1.3", "css-loader": "0.28.7", "dotenv": "4.0.0", "dotenv-expand": "4.2.0", "enzyme": "^3.3.0", "enzyme-adapter-react-16": "^1.1.1", "eslint": "4.10.0", "eslint-config-react-app": "^2.1.0", "eslint-loader": "1.9.0", "eslint-plugin-flowtype": "2.39.1", "eslint-plugin-import": "2.8.0", "eslint-plugin-jsx-a11y": "5.1.1", "eslint-plugin-react": "7.4.0", "express-history-api-fallback": "^2.2.1", "extract-text-webpack-plugin": "3.0.2", "file-loader": "1.1.5", "fs-extra": "3.0.1", "html-webpack-plugin": "2.29.0", "jest": "20.0.4", "less": "^3.0.1", "less-loader": "^4.1.0", "lodash": "^4.17.5", "nock": "^9.2.3", "node-sass-chokidar": "^1.2.1", "object-assign": "4.1.1", "postcss-flexbugs-fixes": "3.2.0", "postcss-loader": "2.0.8", "promise": "8.0.1", "raf": "3.4.0", "react": "^16.2.0", "react-dev-utils": "^5.0.0", "react-dom": "^16.2.0", "react-hot-loader": "^4.0.0", "react-redux": "^5.0.7", "react-router-dom": "^4.2.2", "react-router-redux": "5.0.0-alpha.9", "redux": "^3.7.2", "redux-logger": "^3.0.6", "redux-mock-store": "^1.5.1", "redux-thunk": "^2.2.0", "rekit-core": "^2.3.0", "rekit-studio": "^2.3.0", "sass-loader": "^6.0.7", "style-loader": "0.19.0", "sw-precache-webpack-plugin": "0.11.4", "url-loader": "0.6.2", "webpack": "3.8.1", "webpack-dev-server": "2.9.4", "webpack-manifest-plugin": "1.3.2", "whatwg-fetch": "2.0.3" }, "scripts": { "start": "node scripts/start.js", "build": "node scripts/build.js", "test": "node scripts/test.js --env=jsdom" }, "rekit": { "devPort": 6075, "studioPort": 6076, "plugins": [], "css": "less" }, "jest": { "collectCoverageFrom": [ "src/**/*.{js,jsx,mjs}" ], "setupFiles": [ "<rootDir>/config/polyfills.js", "<rootDir>/tests/setup.js" ], "testMatch": [ "<rootDir>/tests/**/*.test.{js,jsx,mjs}" ], "testEnvironment": "node", "testURL": "http://localhost", "transform": { "^.+\\.(js|jsx|mjs)$": "<rootDir>/node_modules/babel-jest", "^.+\\.css$": "<rootDir>/config/jest/cssTransform.js", "^(?!.*\\.(js|jsx|mjs|css|json)$)": "<rootDir>/config/jest/fileTransform.js" }, "transformIgnorePatterns": [ "[/\\\\]node_modules[/\\\\].+\\.(js|jsx|mjs)$" ], "moduleNameMapper": { "^react-native$": "react-native-web" }, "moduleFileExtensions": [ "web.js", "mjs", "js", "json", "web.jsx", "jsx", "node" ] }, "eslintConfig": { "extends": "react-app" } }
最新回复(0)