React搭建我的博客(一)項目簡介與React前端踩坑

一.簡介

項目最開始目的是爲了熟悉React用法,以後加入的東西變多,這裏分紅三部分,三篇博客拆開來說。javascript

前端部分html

  • [x] React
  • [x] React-Router4.0
  • [x] Redux
  • [x] AntDesign
  • [x] webpack4

後端部分前端

  • [x] consul+consul-template+nginx+docker搭建微服務
  • [x] cdn上傳靜態資源
  • [x] thinkJs

運維部分vue

  • [x] daocloud自動化部署
  • [x] Prometheus+Grafana監控系統

博客網站分爲兩個部分,前臺博客展現頁面,後臺管理頁面。先看下效果:
前臺頁面:
在這裏插入圖片描述
也能夠看我線上正在用的博客前臺,點這裏
後臺頁面:
在這裏插入圖片描述java

這一篇只講前端部分
  • 功能描述react

    • [x] 文章列表展現
    • [x] 登陸管理
    • [x] 文章詳情頁展現
    • [x] 後臺文章管理
    • [x] 後臺標籤管理
    • [x] MarkDown發文
  • 項目結構

先後臺頁面項目結構相似,都分爲前端項目和後端項目兩個部分。前端項目開發環境使用webpack的devserver,單獨啓動一個服務,訪問後端服務的接口。生產環境直接打包到後端項目指定目錄,線上只啓動後端服務。
前臺頁面:前端項目代碼地址 在這裏。後端項目代碼地址在這裏
後臺頁面:前端項目代碼地址 在這裏。後端項目代碼地址在這裏webpack

二.React踩坑記錄

這裏講一下項目中React踩坑和一些值得留意的問題。nginx

1.啓動報錯以下:

下圖是一開始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"
        ]
    },

2.React 組件方法綁定this問題

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。
解決方法:

  1. bind綁定this
export default class LeftImg extends React.Component {
    handleClick(){}
    render() {
    return <Button onClick={this.handleClick.bind(this)}></Button>
    }
}
  1. 使用箭頭函數繼承外部this
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>
  1. 構造函數內部綁定
    在構造函數裏綁定,這種相對最好,由於只綁定一次。
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不一樣,這麼寫初始化就會執行這個方法,而不是觸發事件纔會調用。會致使報錯:
在這裏插入圖片描述

3.setState問題

setState方法,當你想賦的新state值是經過舊state計算獲得時,要使用

this.setState((preState,preProps)=>{return {name:preState.name+'ox'}})

preState是改變前的state,preProps是改變前的props。
注意:
1.setState第二個參數是改變state的回調,由於setState是異步方法,改變後的操做須要在回調裏定義。用的不 多可是面試可能會問。
2.setState不能再render調用。

4.受控組件問題

項目中使用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 }
            });
        }
    };

5.React 父級組件傳入props變化,子級組件更新state問題

當子組件的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不會從新計算,推薦的解決方法有如下兩種

  1. 利用key變化,使子組件從新實例化
<UploadImg
                    visible={this.state.visibleUpload}
                    onConcel={() => {
                        this.setState({ visibleUpload: false });
                    }}
                    onOk={this.confirmUpload}
                    key={this.props.thumb}
                    maxImgCount={1}
                    defaultFileList={this.props.thumb}
 />
  1. 子組件使用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,
      })
    }
  }

6.高階組件的裝飾器寫法不支持問題

高階組件是React很是實用的一個功能,裝飾器方法寫起來也很方便,可是直接寫沒法解析。

@withRouter
class Header extends React.Component {
}

解決方式:babel轉碼

  1. 執行 npm install @babel/plugin-proposal-decorators
  2. 配置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
                        }
                    }

7.React Router4注意點

  1. 使用Switch組件匹配第一個Route
  2. v4不提供browerHistory,想要獲得history

    - 使用context對象獲取, this.context.router.history
    - 使用withRouter
    - 子級建立history
  3. v4以前嵌套的路由能夠放在一個router中以下,可是在4.0之後這麼作會報錯,須要單獨放置在嵌套的跟component中處理路由
// 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>
)

8.React防止服務泄漏

開發過程當中,有時會遇到這種報錯

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" />
        )
}

項目中Router結構

這裏之後臺項目 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的結構和使用方法。
與Redux有關的主要分爲三部分

1.reducer

先看下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;
2.action

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函數解析能夠看這裏,估計面試愛問這個。

3.組件中應用
//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的對象和方法。

四.TodoList

記錄下整個博客項目以後須要完善的東西:

  • 增長評論插件
  • 增長sentry監控前端異常
  • 增長react ssr
  • 增長用戶與權限管理

五.參考文檔

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會遇到的基礎問題。接下來會從服務端與部署方面講下項目中遇到的具體問題,有空就寫。

相關文章
相關標籤/搜索