本文是介紹如何搭建企業級react項目,所用的技術都是最新最主流的,後面我會再寫一篇 《基於React企業級SSR項目搭建全記錄》,敬請期待!css
Package Name | Version |
---|---|
antd | ^3.16.6 |
axios | ^0.18.0 |
connected-react-router | ^6.4.0 |
classnames | ^2.2.6 |
immutable | ^4.0.0-rc.12 |
@loadable/component | ^5.10.0 |
react | ^16.8.6 |
react-redux | ^7.0.3 |
react-router-config | ^5.0.0 |
react-router-dom | ^5.0.0 |
react-scripts | 3.0.1 |
redux | ^4.0.1 |
redux-actions | ^2.6.5 |
redux-logger | ^3.0.6 |
redux-persist | ^5.10.0 |
redux-persist-expire | ^1.0.2 |
redux-persist-transform-immutable | ^5.0.0 |
redux-saga | ^1.0.2 |
history | ^4.7.2 |
create-react-app react-project
目錄結構以下所示html
|-- .gitignore |-- README.md |-- package.json |-- yarn.lock |-- 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
而後咱們把webpack暴露出來,執行以下命令:前端
yarn eject
目錄結構以下所示:node
|-- .gitignore |-- README.md |-- package.json |-- yarn.lock |-- config | |-- env.js | |-- modules.js | |-- paths.js | |-- pnpTs.js | |-- webpack.config.js | |-- webpackDevServer.config.js | |-- jest | |-- cssTransform.js | |-- fileTransform.js |-- public | |-- favicon.ico | |-- index.html | |-- logo192.png | |-- logo512.png | |-- manifest.json | |-- robots.txt |-- scripts | |-- build.js | |-- start.js | |-- test.js |-- src |-- App.css |-- App.js |-- App.test.js |-- index.css |-- index.js |-- logo.svg |-- serviceWorker.js
"dependencies": { "@babel/plugin-proposal-decorators": "^7.4.0", "@babel/plugin-syntax-dynamic-import": "^7.2.0", "@loadable/component": "^5.10.0", "antd": "^3.16.6", "axios": "^0.18.0", "babel-plugin-import": "^1.11.0", "babel-plugin-transform-decorators-legacy": "^1.3.5", "classnames": "^2.2.6", "connected-react-router": "^6.4.0", "history": "^4.7.2", "immutable": "^4.0.0-rc.12", "node-sass": "^4.11.0", "prettier": "^1.16.4", "react-redux": "^7.0.3", "react-router-config": "^5.0.0", "react-router-dom": "^5.0.0", "redux": "^4.0.1", "redux-actions": "^2.6.5", "redux-logger": "^3.0.6", "redux-persist": "^5.10.0", "redux-persist-expire": "^1.0.2", "redux-persist-transform-compress": "^4.2.0", "redux-persist-transform-encrypt": "^2.0.1", "redux-persist-transform-immutable": "^5.0.0", "redux-saga": "^1.0.2" },
添加好了依賴包,咱們運行起來看看有沒有問題yarn start
react
一、添加.editorconfig文件統一格式化標準webpack
root = true [*] indent_style = space indent_size = 2 end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true [*.yml] indent_style = space indent_size = 2
二、添加.eslintrc文件代碼檢驗標準ios
{ "extends": ["react-app", "plugin:prettier/recommended"] }
三、去掉package.json裏面的babel設置,再添加.babelrc文件對babel的支持git
{ "presets": [ "react-app" ], "plugins": [ [ "import", { "libraryName": "antd", "libraryDirectory": "es", "style": "css" }, "antd" ] ] }
目錄結構以下所示:github
|-- .babelrc |-- .editorconfig |-- .gitignore |-- README.md |-- package.json |-- yarn.lock |-- config | |-- env.js | |-- modules.js | |-- paths.js | |-- pnpTs.js | |-- webpack.config.js | |-- webpackDevServer.config.js | |-- jest | |-- cssTransform.js | |-- fileTransform.js |-- public | |-- favicon.ico | |-- index.html | |-- logo192.png | |-- logo512.png | |-- manifest.json | |-- robots.txt |-- scripts | |-- build.js | |-- start.js | |-- test.js |-- src |-- App.css |-- App.js |-- App.test.js |-- index.css |-- index.js |-- logo.svg |-- serviceWorker.js
代碼運行起來看看有沒有問題。web
目錄結構以下:
|-- .babelrc |-- .editorconfig |-- .gitignore |-- README.md |-- package.json |-- yarn.lock |-- config | |-- env.js | |-- modules.js | |-- paths.js | |-- pnpTs.js | |-- webpack.config.js | |-- webpackDevServer.config.js | |-- jest | |-- cssTransform.js | |-- fileTransform.js |-- public | |-- favicon.ico | |-- index.html | |-- logo192.png | |-- logo512.png | |-- manifest.json | |-- robots.txt |-- scripts | |-- build.js | |-- start.js | |-- test.js |-- src
一、添加index.js
import React from "react"; import ReactDOM from "react-dom"; import App from "./App"; import { ConfigProvider, message } from "antd"; import zhCN from "antd/es/locale/zh_CN"; import moment from "moment"; import "moment/locale/zh-cn"; import * as serviceWorker from "./serviceWorker"; import "./assets/css/index.css"; import "./assets/css/base.scss"; import "./assets/css/override-antd.scss"; moment.locale("zh-cn"); message.config({ duration: 2, maxCount: 1 }); //去掉全部頁面的console.log if (process.env.NODE_ENV === "production") { console.log = function() {}; } ReactDOM.render( //增長antd對中文的支持 <ConfigProvider locale={zhCN}> <App /> </ConfigProvider>, document.getElementById("root") );
二、添加App.js文件
App.js裏面包含對redux的配置,代碼以下:
import React, { Component } from "react"; import { Spin } from "antd"; import { Provider } from "react-redux"; import { renderRoutes } from "react-router-config"; import { ConnectedRouter } from "connected-react-router/immutable"; import { PersistGate } from "redux-persist/es/integration/react"; import configureStore, { history } from "./redux/store"; import AppRoute from "./layout/AppRoute"; const { persistor, store } = configureStore(); store.subscribe(() => { // console.log("subscript", store.getState()); }); class App extends Component { constructor(props) { super(props); } render() { const customContext = React.createContext(null); return ( <Provider store={store}> <PersistGate loading={<Spin />} persistor={persistor}> <ConnectedRouter history={history}> <AppRoute /> </ConnectedRouter> </PersistGate> </Provider> ); } } export default App;
三、添加assets文件夾
目錄結構以下:
|-- assets |-- audio |-- css |-- fonts |-- image |-- video
四、添加components組件文件夾
同時建立common文件夾,目錄結構以下:
|-- components |-- common
五、添加config文件夾
並添加base.config.js文件,裏面包含一些框架基礎信息以及後臺的url和port數據,代碼以下:
export default { company: "Awbeci", title: "後臺管理系統平臺", subTitle: "後臺管理系統平臺", copyright: "Copyright © 2019 Awbeci All Rights Reserved.", logo: require("../assets/image/hiy_logo.png"), host: "http://10.0.91.189", port: "19101", persist: "root" };
目錄結構以下:
|-- config |-- base.conf.js
六、添加HOC高階組件文件夾(可選)
同時建立control.js文件,做用是根據屏幕分辨率自動計算寬高,代碼以下:
import React from "react"; import { is, Map, fromJS } from "immutable"; const control = WrappedComponent => class extends React.Component { constructor(props) { super(props); this.state = { // 可視區高度和寬度 document: { body: { width: 0, height: 0 }, //側邊欄高度和寬度 sidebar: { width: 0, height: 0 }, //內容區域高度和寬度 content: { width: 0, height: 0 }, header: Map({ height: 64, width: 0, menu: Map({ height: 0, width: 0 }) }) } }; } componentWillMount() { let cw = document.body.clientWidth; let ch = document.body.clientHeight; this.computedLayout(cw, ch); } componentDidMount() { window.addEventListener("resize", this.computedLayout); } componentWillUnmount() { window.removeEventListener("resize", this.computedLayout); } computedLayout = () => { let width = document.body.clientWidth; let height = document.body.clientHeight; this.setState((state, props) => ({ //todo: })); }; shouldComponentUpdate(nextProps, nextState) { const thisProps = this.props || {}; const thisState = this.state || {}; nextState = nextState || {}; nextProps = nextProps || {}; if (Object.keys(thisProps).length !== Object.keys(nextProps).length || Object.keys(thisState).length !== Object.keys(nextState).length) { return true; } for (const key in nextProps) { if (!is(thisProps[key], nextProps[key])) { return true; } } for (const key in nextState) { if (!is(thisState[key], nextState[key])) { return true; } } return false; } render() { return <WrappedComponent {...this.state} {...this.props} />; } }; export default control;
目錄結構以下:
|-- HOC |-- control.js
七、添加app文件夾
app文件夾包含框架佈局和頁面佈局組件以及路由等配置文件,目錄結構以下:
|-- app |-- AppRoute.js |-- Loading.js |-- RouterView.js |-- layout | |-- index.js | |-- index.scss |-- master |-- index.js |-- index.scss
八、添加pages文件夾
pages文件夾包含登陸和首頁頁面文件,目錄結構以下:
|-- pages |-- Index.js |-- NoFound.js |-- NoPermission.js |-- login |-- Login.js |-- login.scss
九、添加redux文件夾
redux文件夾包含redux-actions和redux-saga以及middleware中間件配置,目錄結構以下:
|-- redux |-- reducers.js |-- sagas.js |-- store.js |-- auth | |-- authAction.js | |-- authReducer.js | |-- authSaga.js |-- layout | |-- layoutPageAction.js | |-- layoutPageReducer.js |-- middleware |-- authTokenMiddleware.js
十、添加router文件夾
同時添加index.js文件,目錄結構以下:
|-- router |-- index.js
十一、添加service文件夾
service文件夾封裝了對後臺api接口的請求 ,目錄結構以下:
|-- service |-- apis | |-- 1.0 | |-- index.js | |-- urls.js |-- request |-- ApiRequest.js
上面把代碼和目錄結構都已經給出,下面咱們詳細講解下如何配置redux、redux-saga、react-acitons、immutable等等
一、配置action
配置action咱們選用的是 redux-actions
插件,以下所示
import { createActions } from "redux-actions"; export const authTypes = { AUTH_REQUEST: "AUTH_REQUEST", AUTH_SUCCESS: "AUTH_SUCCESS", AUTH_FAILURE: "AUTH_FAILURE", SIGN_OUT: "SIGN_OUT", CHANGE_PASSWORD: "CHANGE_PASSWORD" }; export default createActions({ [authTypes.AUTH_REQUEST]: ({ username, password }) => ({ username, password }), [authTypes.AUTH_SUCCESS]: data => ({ data }), [authTypes.AUTH_FAILURE]: () => ({}), [authTypes.SIGN_OUT]: () => ({}), [authTypes.CHANGE_PASSWORD]: (oldPassword, newPassword) => ({ oldPassword, newPassword }) });
二、配置reducer
跟actions相似,使用的也是redux-actions,代碼以下:
import { handleActions } from "redux-actions"; import { authTypes } from "./authAction"; import { Map, fromJS, merge } from "immutable"; const initState = fromJS({ user: null, token: "" }); const authReducer = handleActions( { [authTypes.AUTH_SUCCESS]: (state, action) => { return state.merge({ user: action.data.user, token: action.data.token }); }, [authTypes.SIGN_OUT]: (state, action) => { return state.merge({ user: null, token: "" }); } }, initState ); export default authReducer;
添加完了,不要忘了註冊一下reducer
import { combineReducers } from "redux"; import { connectRouter, LOCATION_CHANGE } from "connected-react-router/immutable"; import layoutReducer from "./layout/layoutReducer"; import authReducer from "./auth/authReducer"; export default history => combineReducers({ router: connectRouter(history), layoutReducer, authReducer });
三、配置redux-saga
代碼以下所示:
import { call, put, takeLatest, select } from "redux-saga/effects"; import { push } from "connected-react-router"; import authAction, { authTypes } from "./authAction"; import { layoutPageTypes } from "../layout/layoutAction"; import { message } from "antd"; import Apis from "../../service/apis/1.0"; import config from "../../config/base.conf"; function strokeItem(name, value) { localStorage.setItem(name, value); } function clearItem(name) { localStorage.removeItem(name); } function* test() { yield put({ type: authTypes.AUTH_SUCCESS, data: { user: { name: "Awbeci" }, token: "awbeci token" } }); yield put({ type: layoutPageTypes.GET_MENUS, menus: [ { icon: "file", id: 1, isShow: "1", title: "頁面一", url: "/" }, { icon: "file", id: 2, isShow: "1", title: "頁面二", url: "/departmentManage" }, { icon: "file", id: 3, isShow: "1", title: "頁面三", url: "/userManage" } ] }); yield put({ type: layoutPageTypes.SAVE_MENU_INDEX, payload: { keyPath: ["1"] } }); yield put(push("/")); } function* signout(action) { yield call(clearItem, "token"); yield call(clearItem, `persist:${config.persist}`); //清除token // 設置選中第一個菜單 yield put({ type: layoutPageTypes.SAVE_MENU_INDEX, payload: { keyPath: ["126"] } }); yield put({ type: layoutPageTypes.SAVE_MENU_COLLAPSED, payload: { collapsed: false } }); yield put({ type: layoutPageTypes.GET_MENUS, menus: [] }); //跳轉到登陸頁面 yield put(push("/login")); } function* signin(action) { try { yield call(test); } catch (error) { message.info("用戶名或密碼錯誤"); yield call(clearItem, "token"); } finally { } } export default function* watchAuthRoot() { yield takeLatest(authTypes.AUTH_REQUEST, signin); yield takeLatest(authTypes.SIGN_OUT, signout); }
添加saga文件不要忘了註冊一下,以下:
import { all, fork } from "redux-saga/effects"; import authSaga from "./auth/authSaga"; /*添加對action的監聽 */ export default function* rootSaga() { yield all([fork(authSaga)]); }
四、配置store
配置store的時候其實已經把redux-logger、redux-persist、immutable.js一塊兒配置了,代碼以下所示:
import { createStore, compose, applyMiddleware } from "redux"; import { routerMiddleware } from "connected-react-router/immutable"; import { createMigrate, persistStore, persistReducer } from "redux-persist"; import createEncryptor from "redux-persist-transform-encrypt"; import immutableTransform from "redux-persist-transform-immutable"; import storage from "redux-persist/es/storage"; import createSagaMiddleware from "redux-saga"; import logger from "redux-logger"; import { createBrowserHistory } from "history"; import createRootReducer from "./reducers"; import rootSaga from "./sagas"; import config from "../config/base.conf"; import { authTokenMiddleware } from "./middleware/authTokenMiddleware"; export const history = createBrowserHistory(); // create the router history middleware const historyRouterMiddleware = routerMiddleware(history); // create the saga middleware const sagaMiddleware = createSagaMiddleware(); // 組合middleware const middleWares = [sagaMiddleware, historyRouterMiddleware, logger]; // 加密localstorage const encryptor = createEncryptor({ secretKey: "hiynn", onError: function(error) {} }); const persistConfig = { transforms: [ immutableTransform() ], key: config.persist, storage, version: 2 }; const finalReducer = persistReducer(persistConfig, createRootReducer(history)); export default function configureStore(preloadedState) { const store = createStore(finalReducer, preloadedState, compose(applyMiddleware(...middleWares))); let persistor = persistStore(store); sagaMiddleware.run(rootSaga); return { persistor, store }; }
五、使用store
在App.js文件中添加對store的引用,代碼以下:
import React, { Component } from "react"; import { Spin } from "antd"; import { Provider } from "react-redux"; import { renderRoutes } from "react-router-config"; import { ConnectedRouter } from "connected-react-router/immutable"; import { PersistGate } from "redux-persist/es/integration/react"; import configureStore, { history } from "./redux/store"; import AppRoute from "./app/AppRoute"; const { persistor, store } = configureStore(); store.subscribe(() => { // console.log("subscript", store.getState()); }); class App extends Component { constructor(props) { super(props); } render() { const customContext = React.createContext(null); return ( <Provider store={store}> <PersistGate loading={<Spin />} persistor={persistor}> <ConnectedRouter history={history}> <AppRoute /> </ConnectedRouter> </PersistGate> </Provider> ); } } export default App;
AppRoute.js
import React, { Component } from "react"; import { connect } from "react-redux"; import { Switch, Redirect } from "react-router"; import { BrowserRouter as Router, HashRouter, Route } from "react-router-dom"; import Login from "../pages/login/Login"; import LayoutContainer from "./layout"; import NoFound from "../pages/NoFound"; @connect(store => ({ store })) class AppRoute extends Component { // 用戶認證 Authentication() { return this.props.store.authReducer.get("token") ? <Redirect to="/" /> : <Login />; } render() { return ( <> {/* 解決github gh-pages發佈必須以Hash瀏覽不然history模式就會報錯問題, 若是想使用history模式去掉下面的HashRouter便可 */} {/* <HashRouter> */} <Switch> <Route path="/login" render={() => this.Authentication()} /> <Route path="/" exact component={LayoutContainer} /> <Route component={NoFound} /> </Switch> {/* </HashRouter> */} </> ); } } export default AppRoute;
六、配置middleware
中間件做用是當刷新頁面的時候從新把token設置到ApiRequest這樣token就不會丟失了。
import { REHYDRATE } from "redux-persist/lib/constants"; import ApiRequest from "../../service/request/ApiRequest"; import { authTypes } from "../auth/authAction"; import { fromJS } from "immutable"; /**保存token中間件 */ export const authTokenMiddleware = store => next => action => { /**當刷新頁面 persist會觸發 action = REHYDRATE*/ if (action.type === REHYDRATE) { if (typeof action.payload !== "undefined") { let authReducer = action.payload.authReducer; if (authReducer) { const token = authReducer.get("token"); ApiRequest.setToken(token ? token : null); } } } /**當登陸成功會觸發 action = AUTH_SUCCESS*/ if (action.type === authTypes.AUTH_SUCCESS) { ApiRequest.setToken(action.data.token); } return next(action); };
七、配置靜態路由
import React from "react"; import loadable from "@loadable/component"; import RouterView from "../app/RouterView"; import NoFound from "../pages/NoFound"; import NoPermission from "../pages/NoPermission"; import Loading from "../app/Loading"; const Index = loadable(() => import("../pages/Index"), { fallback: <Loading /> }); // 注意區分前端路由和前端菜單是兩個不一樣的東西 // 注:菜單和路由都是基於該路由數據生成 // 菜單能夠不所有展現在頁面上(隱藏),但路由必須所有要定義 // 後期能夠加入權限控制 const routes = [ { key: "1", name: "首頁", path: "/", exact: true, component: Index } ]; export default routes;
靜態路由須要react-router-config配合使用,代碼以下:
import { renderRoutes } from "react-router-config"; import routes from "../../router"; // 這裏的routes就是上面的路由文件 renderRoutes(routes)
八、封裝axios
包含get、post、delete、put、upload等等
import axios from "axios"; import { message } from "antd"; import config from "../../config/base.conf"; /** * Http服務類 * get * post * upload * put * patch * delete */ class ApiRequest { constructor() { //建立axios實例 this.instance = axios.create({ baseURL: `${config.host}:${config.port}` }); } /** * 經過authTokenMiddleware中間件監聽action=REHYDRATE|AUTH_SUCCESS來設置token */ setToken = token => { this.instance.defaults.headers.common["Authorization"] = token; }; authentication = str => { let errJson = JSON.parse(str); if (errJson.response && errJson.response.status === 401) { message.error("用戶認證出錯,正在跳轉登陸頁面!"); setTimeout(() => { localStorage.removeItem(`persist:${config.persist}`); window.location.href = "/login"; }, 1500); } }; upload(url, formData) { return new Promise((resolve, reject) => { this.instance .post(url, formData, { headers: { "Content-Type": "multipart/form-data" } }) .then(({ data }) => { resolve(data); }) .catch(error => { let errStr = JSON.stringify(error); this.authentication(errStr); reject(errStr); }); }); } get(url, params = {}) { return new Promise((resolve, reject) => { this.instance .get(url, { params: { ...params } }) .then(({ data }) => { resolve(data); }) .catch(error => { let errStr = JSON.stringify(error); this.authentication(errStr); reject(errStr); }); }); } delete(url, params = {}) { return new Promise((resolve, reject) => { this.instance .delete(url, { params: { ...params } }) .then(({ data }) => { resolve(data); }) .catch(error => { let errStr = JSON.stringify(error); this.authentication(errStr); reject(errStr); }); }); } post(url, params = {}) { return new Promise((resolve, reject) => { this.instance .post(url, { ...params }) .then(({ data }) => { resolve(data); }) .catch(error => { let errStr = JSON.stringify(error); if (url.includes("login")) { reject(errStr); } else { this.authentication(errStr); } }); }); } put(url, params = {}) { return new Promise((resolve, reject) => { this.instance .put(url, { ...params }) .then(({ data }) => { resolve(data); }) .catch(error => { let errStr = JSON.stringify(error); this.authentication(errStr); reject(errStr); }); }); } patch(url, params = {}) { return new Promise((resolve, reject) => { this.instance .patch(url, { ...params }) .then(({ data }) => { resolve(data); }) .catch(error => { let errStr = JSON.stringify(error); this.authentication(errStr); reject(errStr); }); }); } } export default new ApiRequest();
一、其實redux、redux-saga、react-router都有介紹如何配置,只是整合時插件先後順序有問題
二、ConnectedRouter是鏈接redux reducer和react-router的插件,而且要支持immutable.js
三、React項目集成Immutable.js
四、antd-layoutui
五、本文代碼