快速搭建react項目骨架(按需加載、redux、axios、項目級目錄等等)
一、前言
最近整理了一下項目骨架,順便自定義了一個腳手架,友善日後使用。我會從頭開始,步驟一步步寫明白,如果還有不清楚的可以評論區留言。先大緻介紹一下這個骨架,我們采用 create-react-app 搭建基礎骨架,修改一些基礎配置; 使用webpack的import子產品實作按需加載(俗稱切片打包); 引入 react-redux; 引入axios; 規劃好項目的目錄結構。我們大緻就做這些事,大家可以根據自己項目需要,添加ui包等其他插件。部落格的代碼隻是說明大緻的流程,建議先拉代碼,對比代碼看部落格。
二、目錄
1、安裝 create-react-app 腳手架并建立APP 2、按照上線項目标準完善目錄結構 3、配置按需加載(俗稱切片打包) 4、配置react-redux及redux-sagas(sagas是我個人習慣,挺好用的,不喜歡的可以不裝) 5、配置axios統一請求(cookie、攔截、統一報錯等) 6、代碼位址 (如果覺得有用,記得給我 github 點個贊奧)
ps: 說不定部落客還會開放幾個私有倉庫
三、安裝 create-react-app 腳手架并建立APP
npm install -g create-react-app //本地全局安裝 react 腳手架
create-react-app webapp //通過腳手架指令 建立 react webapp, 注意名字不能有大寫字母,現在就可以直接跑了
npm run eject //新版本的腳手架把配置檔案等都以依賴的形式放到 node_modules 中了, eject 一下,把配置資訊釋放出來
scripts/start.js //修改一下端口号,預設是3000,改成你想要的
npm i //裝一下依賴
我們直接用react的官方腳手架搭建最基礎的骨架,通過 create-react-app 建立 react的webAPP。然後我們的項目就可以直接跑了,看一下package.json
檔案,裡面有關于項目啟動、打包、測試的指令。執行 npm run start 就可以運作我們的項目了。
然後我們修改一下這個項目,因為現在 create-react-app 腳手架會把配置檔案都以依賴的形式放到node_modelus裡面,執行 npm run eject 把配置檔案釋放出來,然後執行 npm i,裝一下依賴(好像不用裝,我們沒添加什麼,笑哭~~)。
我們看一下現在的目錄結構

四、按照上線項目标準完善目錄結構
目前為止這個項目隻有一個預設頁面,放在src錄下。我們按照上線項目大緻會用到的東西,先完善一下目錄結構
assets 靜态資源,存放 字型、圖檔、css hack 等
components 建立公共元件檔案夾,這是放公共元件的
layouts 建立布局檔案夾 确定好你的項目布局樣式
constants 全局常亮檔案夾 存放全局常亮
helpers 公共函數檔案夾 存放公共函數、一些插件的啟動配置函數
modules 我們具體的功能子產品 存放我們項目的實際頁面
services 接口檔案夾 存放所有請求
store 裝redux的
我們在src目錄下建立上述檔案夾,具體功能都已标明了。出于規範化、子產品化考慮我們暫時将檔案如此分類。
然後我們建立幾個簡單的頁面,内容自定義,可參考下述代碼:
import React, { PureComponent } from 'react';
export default class Register extends PureComponent {
state = {}
componentDidMount () {}
render() {
return (
<div className="g-default">
預設頁
</div>
)
}
}
這樣我們建立幾個頁面 登入、注冊、預設頁等等,這個随意啦。然後我們看一下現在的目錄結構:
五、配置按需加載(俗稱切片打包)
現在我們已經有了幾個最基礎的頁面了,我們開始做路由按需加載。
為什麼要按需加載? 因為 單頁應用,隻有一個html,一個主要的css、js。傳統打包方式是将整個項目的js、css都打包成一個檔案引入。使用者浏覽我們的頁面
時就需要将整個項目拉下來才行(動不動就是幾M甚至幾十M),非常不友好。按需加載,按照路由切割js、css,使用者看哪個,就加載哪個頁面代碼。首次加載體驗非常好。
按需加載方法有很多,我們介紹一種目前配置簡單、效率也高的一種。我們采用 webpack 的 import 子產品來實作按需加載。
首先,封裝一個異步加載子產品的元件,然後用這個元件去引入要加載的子產品。代碼如下:
//這是異步加載元件的代碼
import {PureComponent} from 'react';
export default class Bundle extends PureComponent {
constructor(props) {
super(props);
this.state = {
mod: null
};
}
componentWillMount() {
this.load(this.props)
}
componentWillReceiveProps(nextProps) {
if (nextProps.load !== this.props.load) {
this.load(nextProps)
}
}
load(props) {
this.setState({
mod: null
});
//注意這裡,使用Promise對象; mod.default導出預設
props.load().then((mod) => {
this.setState({
mod: mod.default ? mod.default : mod
});
});
}
render() {
return this.state.mod ? this.props.children(this.state.mod) : null;
}
}
..............
//這是他的使用,建立Router.js檔案,配置路由
import React from 'react';
import {BrowserRouter, Route, Switch} from 'react-router-dom';
import Bundle from './Bundle';
const Login = (props) => (<Bundle load={() => import('./modules/login')}>{(Login) => <Login {...props}/>}</Bundle>);
const Register = (props) => (<Bundle load={() => import('./modules/register')}>{(Register) => <Register {...props}/>}</Bundle>);
const Default = (props) => (<Bundle load={() => import('./modules/default')}>{(Default) => <Default {...props}/>}</Bundle>);
const Blog = (props) => (<Bundle load={() => import('./modules/blog')}>{(Blog) => <Blog {...props}/>}</Bundle>);
const User = (props) => (<Bundle load={() => import('./modules/user')}>{(User) => <User {...props}/>}</Bundle>);
const BasicRoute = () => (
<BrowserRouter>
<Switch>
<Route exact path="/login" component={Login}/>
<Route exact path="/register" component={Register}/>
<Route exact path="/default" component={Default}/>
<Route exact path="/blog" component={Blog}/>
<Route exact path="/user" component={User}/>
<Route exact path="/" component={Login}/>
<Route exact
component={Login}/>
</Switch>
</BrowserRouter>
);
export default BasicRoute;
.............
//這是對index檔案的修改
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import BasicRoute from './Router';
import * as serviceWorker from './serviceWorker';
import store from './store'
ReactDOM.render( <BasicRoute />, document.getElementById('root'));
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: http://bit.ly/CRA-PWA
serviceWorker.unregister();
注意,我把之前的App.js檔案删掉了,我們用不上。然後建立了一個Router.js,這個檔案就是我們所有子產品的路由配置。然後修改src/index.js 檔案,引入我們的路由檔案。現在我們的路由以及路由的按需加載就都OK了。我們可以多建幾個檔案加上内部路由跳轉試一下。執行 npm run build 看我們build檔案夾, build/static/js 可以看到我們已經實作切片打包了。
六、 配置react-redux及redux-sagas(sagas是我個人習慣,挺好用的不喜歡的可以不裝)
為什麼要裝redux? 因為react單頁應用,我們會涉及大量的資料,像使用者資訊等資料會在很多地方用到,這會導緻元件間的資料傳輸很麻煩,是以我們使用redux,将變量統一管理,中心思想很簡單。和我們定義一個命名空間,裡面放很多變量,然後寫一些方法指定性讀取、修改這些變量一樣,大緻可以這麼了解。
然後,我們安裝
npm install --save redux 安裝redux
npm install --save react-redux 安裝react的綁定庫
npm install --save redux-saga 安裝sagas, Redux-saga是Redux的一個中間件,主要集中處理react架構中的異步處理工作
我習慣用sagas,不喜歡的可以不裝哈。但是後序代碼我都會用它來寫。
裝好了依賴,接下來是如何使用。大緻步驟是這樣的,建立reducer和sagas,然後用redux的Provider元件包裹項目,注入redux,然後就可以在組建中使用了,我們貼一下代碼
// src/index.js 引入redux,并注入資料
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux'
import './index.css';
import BasicRoute from './Router';
import * as serviceWorker from './serviceWorker';
import store from './store'
ReactDOM.render( <Provider store={store}> <BasicRoute /> </Provider>, document.getElementById('root'));
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: http://bit.ly/CRA-PWA
serviceWorker.unregister();
// src/store/index.js 配置redux和sagas,并引入我們的reducer和sagas
import { createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'
import rootReducer, { createReducer } from './reducers'
import rootSaga from './sagas'
const sagaMiddleware = createSagaMiddleware()
const middlewares = [sagaMiddleware]
const configureStore = (initialState = {}) => {
const store = createStore(
rootReducer,
initialState,
applyMiddleware(...middlewares),
)
sagaMiddleware.run(rootSaga)
store.runSaga = sagaMiddleware.run
store.asyncReducers = store.asyncReducers || {}
store.asyncSagas = store.asyncSagas || []
return store
}
export const injectAsyncReducer = ({ name, asyncReducer, store }) => {
if ( store.asyncReducers[name] ) return
store.asyncReducers[name] = asyncReducer
store.replaceReducer(createReducer(store.asyncReducers))
}
export const injectAsyncSagas = ({ name, sagas, store }) => {
if ( !store.asyncSagas.includes(name) ) {
sagas.forEach(store.runSaga)
store.asyncSagas.push(name)
}
}
export default configureStore({})
// 配置reducer入口檔案
import { combineReducers } from 'redux'
import user from './user'
const rootReducer = {
user,
}
export const createReducer = asyncReducers => combineReducers({
...rootReducer,
...asyncReducers,
})
export default combineReducers(rootReducer)
// user子產品的 reducer
const initState = {
loading: false,
dataSource: {
list: [],
pagination: {
pageSize: 10,
current: 1,
},
},
}
const reducer = (state = initState, action) => {
switch (action.type) {
case 'saveAssetsLoading':
return {
...state,
loading: action.payload,
}
default:
return {
...state,
}
}
}
export default reducer
// sagas 的入口檔案
import { all } from 'redux-saga/effects'
import userSagas from './user'
const run = sagas => sagas.map(saga => saga())
export default function* rootSaga() {
yield all([
...run([
...userSagas,
]),
])
}
// user子產品的sagas
import { put, call, takeEvery } from 'redux-saga/effects'
import { login1, login2} from '../../services/user'
const sagas = {
* login1({ payload, callback }) {
const result = yield call(login1, payload)
if (callback) callback(result)
},
* login2({ payload, callback }) {
const result = yield call(login2, payload)
if (callback) callback(result)
},
}
export default Object.keys(sagas).map(item => {
return function * s() {
yield takeEvery(item, function *(args) {
try {
yield sagas[item](args)
} catch (e) {
console.log(e)
}
})
}
})
// 在user元件中使用
import React, { PureComponent } from 'react';
import { Link } from 'react-router-dom'
import { connect } from 'react-redux';
export default
@connect(state => ({
user: state.user
}))
class User extends PureComponent {
state = {
}
componentDidMount () {
// this.fetch()
this.fetch2()
}
fetch = () => {
const params = {
test: 'web'
}
this.props.dispatch({
type: 'login1',
payload: params,
callback: result => {
console.log(result)
}
})
}
render() {
return (
<div className="g-login">
測試2
<br />
<Link to={`/default`}>
登入
</Link>
</div>
)
}
}
使用redux大緻就是這樣,先引入redux,然後做具體的檔案配置,最後直接元件中使用即可。注意: sagas這裡,我沒有做作用域處理,sagas方法名不能重複。
七、配置axios統一請求(cookie、攔截、統一報錯等)
為什麼封裝axios? 首先,我們使用axios作為請求方式,各方面性能吧都不錯。其次,在單頁應用中,涉及到的請求會非常多,對于請求攔截、響應攔截、錯誤統一處理等正常操作,我們把axios進行二次封裝會節省大量的代碼,好處不用我多說了。下邊是封裝axios的流程,以及使用sagas調用的方式,直接貼代碼了
// src/constants/index.js 設定請求的ip,這個根據個人情況來
export const ORIGIN = {
production: window.location.origin,
development: `http://${window.location.hostname}:3009`,
test: window.location.origin,
// dev: 'http://localhost:3009',
}[process.env.NODE_ENV || 'development'];
// 對axios的封裝
import axios from 'axios'
import { ORIGIN } from '../constants'
// 添加一個請求攔截器
axios.interceptors.request.use(config => {
return config // 暫時沒啥好寫的,我們隻是個骨架
}, error => {
return Promise.reject(error);
})
// 添加一個響應攔截器
axios.interceptors.response.use(response => {
return response.data // 其他的不要了,隻拿data就好
}, error => {
console.log(error.response)
if (error.response.status === 401) {
window.location.pathname = '/login'
}
// ......在做别的統一處理
return Promise.reject(error);
});
export default function request(url, options = {}) {
return axios({
url: /^http/.test(url) ? url : `${ORIGIN}${url}`,
method: 'get',
// 攜帶cookie資訊
withCredentials: true,
...options,
data: options.body,
})
}
// 添加具體的請求函數,供前端使用
import request from '../helpers/request'
import { stringify } from 'querystring';
// 測試1
export function login1(params) {
return request(`/xxx/xxx1?${stringify(params)}`)
}
// 測試2
export function login2(data) {
return request(`/xxx/xxx2`, {
method: 'post',
body: data,
})
}
這個代碼裡面都有注釋,這裡簡單說明一下,這個位址常亮,大家根據自己實際情況來改。關于請求的封裝,這裡主要寫了加cookie,未登入401報錯直接跳到登入頁,至于其他錯誤處理,大家根據自己項目錯誤碼來就好。項目中涉及到一些node.js的小功能函數,大家一百度就知道了,比如說 stringify。封裝好的請求要麼直接用,要麼在sagas裡面用。大緻就是這樣。
八、代碼位址 (如果覺得有用,記得給我 github 點個贊奧。)
https://github.com/Aaron-China/react-cli
這是代碼位址,覺得不錯,您别吝啬, 位址右上方start點一下,謝謝。
小結
至此,一個精簡的react骨架就出來了,沒有做太多的配置,以免影響靈活度。這幾項幾乎都是項目中必須的東西。是以,就寫到這。後期看看反應吧,把ui架構加上去,再做上菜單、權限的配置,再敲幾個常用的頁面。如果做的話,我會在git上開一個分支,不會影響這個基本骨架。如果部落格中哪裡寫的有問題,歡迎評論區留言。
ps: 有點懶,好久沒寫部落格了,将持續放點幹貨,希望能幫到你。
原文位址
https://www.cnblogs.com/pengfei-nie/p/10443310.html