項目最開始目的是爲了熟悉React用法,以後加入的東西變多,這裏分紅三部分,三篇博客拆開來說。javascript
前端部分html
後端部分前端
運維部分vue
博客網站分爲兩個部分,前臺博客展現頁面,後臺管理頁面。先看下效果:
前臺頁面:
也能夠看我線上正在用的博客前臺,點這裏
後臺頁面:java
功能描述react
先後臺頁面項目結構相似,都分爲前端項目和後端項目兩個部分。前端項目開發環境使用webpack的devserver,單獨啓動一個服務,訪問後端服務的接口。生產環境直接打包到後端項目指定目錄,線上只啓動後端服務。
前臺頁面:前端項目代碼地址 在這裏。後端項目代碼地址在這裏。
後臺頁面:前端項目代碼地址 在這裏。後端項目代碼地址在這裏。webpack
這裏講一下項目中React踩坑和一些值得留意的問題。nginx
下圖是一開始blog-react項目一個報錯
由於項目裏我使用webpack來運行項目,以及添加相關配置,而不是剛建好項目時的react命令,因此一開始啓動的時候會報 Module build failed: SyntaxError: Unexpected token 錯誤。說明ReactDom.render這句話沒法解析。
解決方法,添加babel的react-app預設,我直接加到了package.json裏。git
"scripts": { "start": "cross-env NODE_ENV=development webpack-dev-server --mode development --inline --progress --config build/webpack.config.dev.js", "build": "cross-env NODE_ENV=production webpack --env=test --progress --config ./build/webpack.config.prod.js", "test": "react-scripts test", "eject": "react-scripts eject" }, "babel": { "presets": [ "react-app" ] },
React跟Vue不同的一點是,組件裏定義的方法,若是直接調用會報錯。
好比:github
class App extends React.Component{ state={ name:'sx } handleClick(){ console.log(this.state.name) } render() { return <Button onClick={this.handleClick}>click</Button> } }
這樣會直接報錯,必需要給方法綁定this。
解決方法:
export default class LeftImg extends React.Component { handleClick(){} render() { return <Button onClick={this.handleClick.bind(this)}></Button> } }
export default class LeftImg extends React.Component { state={ name:'sx' } handleClick = () => { console.log(this.state.name) }; render() { const { src } = this.props; return ( <div className="left-img-container"> <img src={src} onClick={this.handleClick} /> </div> ); } }
也能夠:
<div className="tip left" onClick={() => this.handleControlPage("left")}> {left} </div>
export default class JoinUs extends React.Component { constructor(props) { super(props); this.handleScroll = this.handleScroll.bind(this); } }
注意,不要這麼寫
handleClick(){this.setState({name:'xx'})}
<Button onClick={this.handleClick('edit')}></Button>
React這裏跟vue不一樣,這麼寫初始化就會執行這個方法,而不是觸發事件纔會調用。會致使報錯:
setState方法,當你想賦的新state值是經過舊state計算獲得時,要使用
this.setState((preState,preProps)=>{return {name:preState.name+'ox'}})
preState是改變前的state,preProps是改變前的props。
注意:
1.setState第二個參數是改變state的回調,由於setState是異步方法,改變後的操做須要在回調裏定義。用的不 多可是面試可能會問。
2.setState不能再render調用。
項目中使用antDesign做爲UI庫,當使用Upload組件上傳時,發生onChange只執行一次的現象,代碼以下:
handleChange = info => { console.log("info", info); if (info.file.status === "uploading") { return; } if (info.file.status === "done") { this.setState({ fileList: [...info.fileList] }); this.setState({ uploadImg: { ...info.file.response.data, uid: info.file.uid } }); } };
<Upload name="image" listType="picture-card" className="avatar-uploader" fileList={this.state.fileList} onChange={this.handleChange} > {this.state.fileList.length >= maxImgCount ? null : uploadButton} </Upload>
經過查找,發現Upload是受控組件,也就是用戶的輸入須要再動態改變組件的值。相似與vue裏不使用v-model實現的雙向綁定。這種組件值與用戶onChange的同步,就是受控組件的特色。
這裏解決方法就是handleChange裏的全部分支裏,都須要改變fileList,以下:
handleChange = info => { this.setState({ fileList: [...info.fileList] }); if (info.file.status === "uploading") { return; } if (info.file.status === "done") { this.setState({ uploadImg: { ...info.file.response.data, uid: info.file.uid } }); } };
當子組件的state初始值取決於父組件傳入的props,如這裏的fileList
class UploadImg extends React.Component { state = { visibleUpload: false, imageUrl: "", fileList: this.props.defaultFileList, previewImage: "", previewVisible: false, uploadImg: {}, delModalVisible: false, deleteTitle: "", deletefile: {} }; }
若是props.defaultFileList變化,子組件的state不會從新計算,推薦的解決方法有如下兩種
<UploadImg visible={this.state.visibleUpload} onConcel={() => { this.setState({ visibleUpload: false }); }} onOk={this.confirmUpload} key={this.props.thumb} maxImgCount={1} defaultFileList={this.props.thumb} />
子組件使用componentWillReceiveProps鉤子函數
父組件props變化,會觸發子組件鉤子函數componentWillReceiveProps,它參數是更新後的props,能夠根據新的props改變子組件本身的state
componentWillReceiveProps(nextProps) { const { data } = this.state const newdata = nextProps.data.toString() if (data.toString() !== newdata) { this.setState({ data: nextProps.data, }) } }
高階組件是React很是實用的一個功能,裝飾器方法寫起來也很方便,可是直接寫沒法解析。
@withRouter class Header extends React.Component { }
解決方式:babel轉碼
配置babel plugin
babel解析插件的定義,此次我寫在了webpackage.config.base.js裏。下面是關鍵部分代碼
{ test: /\.(js|mjs|jsx)$/, include: resolve("src"), loader: require.resolve("babel-loader"), options: { plugins: [ [ "@babel/plugin-proposal-decorators", { legacy: true } ] ], cacheDirectory: true, cacheCompression: true, compact: true } }
v4不提供browerHistory,想要獲得history
- 使用context對象獲取, this.context.router.history - 使用withRouter - 子級建立history
// 4.0以前 <Route component={App}> <Route path="groups" components={Groups} /> <Route path="users" components={Users}> <Route path="users/:userId" component={Profile} /> </Route> </Route>
//v4.0 <Route component={App}> <Route path="groups" components={Groups} /> <Route path="users" components={Users}> </Route> </Route>
const Users = ({ match }) => ( <div> <h2>Topics</h2> <Route path={`${match.url}/:userId`} component={Profile}/> </div> )
開發過程當中,有時會遇到這種報錯
Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in the componentWillUnmount method」
大概意思就是組件已經銷燬了,不能再設置它的tate了,防止內存泄漏。出現的場景是,用AsyncComponent實現路由懶加載時(後面會講),當路由跳轉後,若是跳轉前存在異步方法,回調函數包含設置跳轉前組件的state。這時若是路由已經跳轉,舊的組件已經銷燬,再設置state就會報上面的錯。
解決方法:
利用componentWillUnmount鉤子和setState改造,使得組件銷燬後,阻止執行setState。
function inject_unount (target){ // 改裝componentWillUnmount,銷燬的時候記錄一下 let next = target.prototype.componentWillUnmount target.prototype.componentWillUnmount = function () { if (next) next.call(this, ...arguments); this.unmount = true } // 對setState的改裝,setState查看目前是否已經銷燬 let setState = target.prototype.setState target.prototype.setState = function () { if ( this.unmount ) return ; setState.call(this, ...arguments) } }
代碼比較好理解,在組件銷燬以前,設置this.unmount爲true。修改組件原型的setState方法,判斷若是this.unmount爲true,就不執行操做。
應用inject_unmount時,可使用普通的傳參,也能用修飾器
//普通用法 export default inject_unmount(BaseComponent) //修飾器 export default @inject_unount class BaseComponent extends Component { ..... }
vue-router中存在路由守衛,當實現登陸過濾及權限判斷時會特別方便。react-router裏沒有這個功能,因而手寫一個組件實現簡單的守衛功能。
後臺項目中,有登陸功能,這裏使用路由守衛判斷用戶是否登陸,沒登陸直接跳回登陸頁。
import React from "react"; import { Route, Redirect } from "react-router-dom"; import { getToken } from "@/utils/auth"; import { withRouter } from "react-router-dom"; import history from "@/utils/history"; class RouterGurad extends React.Component { render() { const { location, config } = this.props; const { pathname } = location; const token = getToken(); const targetRouterConfig = config.find(v => v.path === pathname); console.log("token", token); if (token) { if (pathname === "/login") { return <Redirect to="/" />; } else { return <div />; } } else { if (pathname !== "/login") { history.push("/login"); return <div />; } else { return <div />; } } } } export default withRouter(RouterGurad);
用戶登陸後,服務端返回的token會返回前端,並存儲在cookie裏,這裏判斷了token是否存在,不存在不讓用戶訪問login之外的頁面。
App.jsx中加入這個組件
<LocaleProvider locale={zhCN}> <Router> <div> <RouterGurad config={[]} /> <Switch> <Route path="/login" component={Login} key="/login" /> <Route path="/" component={SelftLayout} key="/" /> <Route path="*" component={notFound} key="*" /> </Switch> </div> </Router> </LocaleProvider>
在vue裏,想要實現路由懶加載組件,只要在router的component這麼定義就能夠
{ path: "welcome", name: "welcome", hidden: true, component: () => import("@/views/dashboard/index") }
這樣會在路由匹配到path時,再執行component指定的function,返回組件,實現了懶加載。
React裏,若是你直接在route組件的component屬性裏這麼寫,是不行的,由於Route裏不能傳入一個函數做爲component。這時須要手寫一個異步組件,實現渲染時纔會加載所需的組件。
//asyncComponent.js import React from "react"; import { inject_unmount } from "@/utils/unmount"; const AsyncComponent = loadComponent => { class AsyncComponent extends React.Component { state = { Component: null }; componentWillMount() { if (this.hasLoadedComponent()) { return; } loadComponent() .then(module => module.default || module) .then(Component => { if (this.state) { this.setState({ Component }); } }) .catch(err => { console.error( `Cannot load component in <AsyncComponent />` ); throw err; }); } hasLoadedComponent() { return this.state.Component !== null; } render() { const { Component } = this.state; return Component ? <Component {...this.props} /> : null; } } return inject_unmount(AsyncComponent); }; export default AsyncComponent;
這個是在網上找的,大概意思是,AsyncComponent傳入一個組件做爲參數,當不加載AsyncComponent時,它不會渲染(渲染null),加載後,渲染爲傳入組件。
在Route組件裏,component的寫法就變成這樣。
const Login = AsyncComponent(() => import("@/components/login/index")); class App extends React.Component { render() { return ( <Route path="/login" component={Login} key="/login" /> ) }
這裏之後臺項目 blog-backend-react 爲例,講一下整個項目路由的結構。
路由採用的是hash模式,後臺項目首先是在App.jsx定義三個路由,對應着登陸頁面,登陸後的管理頁面,以及404頁面。
render() { return ( <LocaleProvider locale={zhCN}> <Router> <div> <RouterGurad config={[]} /> <Switch> <Route path="/login" component={Login} key="/login" /> <Route path="/" component={SelftLayout} key="/" /> <Route path="*" component={notFound} key="*" /> </Switch> </div> </Router> </LocaleProvider> ); }
當登陸後,進入到SelfLayout組件後,又會有根據管理頁面的路由,好比文章管理,標籤管理等。這些路由配置我在前端寫在了routerConfig.js裏,固然正常來講是該後端獲取的。
React-Router其實主要就是兩部分,一部分是做爲跳轉的Link組件,一部分是爲匹配路由的組件佔位顯示的Route組件。項目中左側SideBar導航樹是要根據routeConfig生成的,並且要包含Link,知足點擊時路由跳轉。Route組件編寫須要將routeConfig裏有層級的配置扁平化以後,在循環出來,由於Route在react-router4裏不支持嵌套了。這裏代碼比較多,想看能夠直接在github裏看下吧。
大體講一下項目中Redux的結構和使用方法。
與Redux有關的主要分爲三部分
先看下reducer的寫法:
const initState = { token: "", name: "" }; const articleReducer = (state = initState, action) => { const type = action.type; switch (type) { case "SET_TOKEN": return Object.assign({}, state, { token: action.token }); case "SET_NAME": return Object.assign({}, state, { token: action.name }); default: return state; } };
這裏分爲兩部分,一個是state,做爲一個全局變量的對象存儲數據,一個是reducer方法,根據傳入的action來改變state的值。這裏跟vuex中的state和mutation有點像。項目中由於要分開不一樣的reducer,因此把各個業務的reducer分紅一個js最後再用combineReducers合併。
import { combineReducers } from "redux"; import home from "./system/home"; import article from "./system/article"; import tag from "./system/tag"; export const rootReducer = asyncReducers => { return combineReducers({ home, article, tag }); }; export default rootReducer;
action的主要做用是放一些異步改變state的方法(固然也能夠放同步方法,這裏主要講下異步方法),主要用做請求獲取數據。
const settoken = data => { return { type: "SET_TOKEN", token: data }; }; const setName = data => { return { type: "SET_NAME", name: data }; }; export function login(params) { return dispatch => { return service({ url: "/api/login", method: "post", data: params }) .then(res => { const data = res; if (data.token) { dispatch(settoken(data.token)); dispatch(setName(params.username)); } }) .catch(err => { console.error("err", err); }); }; }
上面代碼表示,當經過請求接口取得數據後,經過dispatch參數,調用reducer,根據type和其餘屬性,改變state。咱們先記住,最後調用action的是dispatch對象,例如dispatch(login(params)),具體緣由等下再解釋。
咱們先接着看建立store的方法:
//store/index.js import rootReducer from "../reducers"; import { createStore, applyMiddleware, compose } from "redux"; import thunkMiddleware from "redux-thunk"; import { createLogger } from "redux-logger"; const loggerMiddleware = createLogger(); const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; export default function configureStore(preloadedState) { return createStore( rootReducer(), preloadedState, composeEnhancers(applyMiddleware(thunkMiddleware, loggerMiddleware)) ); }
這裏先主要講一下redux-thunk的thunkMiddleware吧。默認狀況下redux只能dispatch一個plain object(簡單對象),例如:
dispatch({ type: 'SOME_ACTION_TYPE', data: 'xxxx' });
這樣就會有個問題,若是我想dispatch一個異步的函數——就像上面講的login方法——是不能夠的,因此異步獲取的數據想要改變state就沒法實現了。
thunkMiddleware 正好能夠解決這個問題,看下createThunkMiddleware源碼:
function createThunkMiddleware(extraArgument) { return ({ dispatch, getState }) => next => action => { if (typeof action === 'function') { return action(dispatch, getState, extraArgument); } return next(action); }; }
容易看出,當dispatch傳入參數是函數時,這個函數會接收到dispatch,getState做爲參數,異步方法獲得結果後,再使用傳入的dispatch參數改變state就能夠了。
btw ,redux裏的applyMiddleware做用是添加中間件,compose用於從左到右組合多個函數,有興趣的能夠查一下相關資料,compose函數解析能夠看這裏,估計面試愛問這個。
//Login.jsx import { login } from "@/action/system/login"; import { connect } from "react-redux"; const mapStateProps = state => ({ name:state.login.name }); const mapDispatchProps = dispatch => ({ login: params => { console.log("this", this); dispatch( login({ ...params }) ); } }); const enhance = connect( mapStateProps, mapDispatchProps ); export default enhance(LoginForm);
在這裏,將須要的調action的放在mapDispatchProps裏,須要讀取的state放在mapStateProps裏,再用調用connect。
connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])
connect函數返回一個connect類,裏面包含要渲染的wrappedComponent,而後將stateProps,dispatchProps還有ownProps合併起來,一塊兒唱誒wrappedComponent。詳細connect分析能夠看這裏,還有這裏。
內部組件,如LoginForm能夠經過this.props,獲得mapStateProps,mapDispatchProps的對象和方法。
記錄下整個博客項目以後須要完善的東西:
https://segmentfault.com/a/11...
https://www.jianshu.com/p/bc5...
https://segmentfault.com/a/11...
https://www.jianshu.com/p/677...
https://segmentfault.com/a/11...
https://segmentfault.com/a/11...
https://www.yangqq.com/downlo...
本文例舉了項目開發中前端遇到的一些問題和值得注意的點,主要側重講了一下初學React會遇到的基礎問題。接下來會從服務端與部署方面講下項目中遇到的具體問題,有空就寫。