1. 程式人生 > >使用react+redux+react-redux+react-router+axios+scss技術棧從0到1開發一個applist應用

使用react+redux+react-redux+react-router+axios+scss技術棧從0到1開發一個applist應用

先看效果圖

github地址

github倉庫

線上訪問

初始化專案

#建立專案
create-react-app applist
#如果沒有安裝create-react-app的話,先安裝
npm install -g create-react-app

目錄結構改造

|--config    
|--node_modules
|--public
|--scripts
|--src
    |-----api   //api介面
    |-----components  //元件
    |-----pages  //頁面
    |-----plugins   //外掛相關 axios
    |-----router  //路由
    |-----store   //redux
    |-----styles   //公共樣式
    |-----utils   //工具包
    |-----index.js   //入口
|--package.json

Vscode外掛安裝

所謂工欲善其事,必先利其器。這裡我們推薦一些好用的vscode外掛(參考https://www.cnblogs.com/xbzhu/p/10823300.html)

1. 程式碼提示類外掛
1.1 Reactjs code snippets
1.2 React Redux ES6 Snippets
1.3 React-Native/React/Redux snippets for es6/es7
1.4 JavaScript (ES6) code snippets(es6程式碼片段)
1.5 Typescript React code snippets(這是tsx的react元件片段)

2. 美化類外掛
2.1 One Dark Pro(atom風格主題)
2.2 vscode-icons(檔案圖示)

3. 其他實用類外掛
3.1 Beautify css/sass/scss/less(樣式程式碼格式化)
3.2 npm Intellisense(對package.json內中的依賴包的名稱提示)
3.3 Path Intellisense(檔案路徑補全)
3.4 cssrem(px轉換為rem)
3.5 CSS Modules(對使用了css modules的jsx標籤的類名補全和跳轉到定義位置)

4.vscode配置裝置同步
Settings Sync
有了它就不用每次換個開發環境又重新配置一遍vscode了


5.另外,react的jsx補全html標籤需要在vscode單獨設定一下

首選項-設定-搜尋‘includeLanguages’-編輯settings.json新增如下程式碼即可
"emmet.includeLanguages": {
        "javascript": "javascriptreact"
    }

最後,安裝完外掛之後,以下兩個快捷鍵可能會經常使用

rcc 生成有狀態的元件程式碼塊
rfc 生成無狀態的元件程式碼塊

使用axios外掛請求資料並封裝api請求

1、安裝

npm isntall axios --save

2、建立axios.js檔案

主要是用來建立axios例項,新增請求攔截,全域性處理一些業務邏輯,例如全域性loading展示,返回狀態碼處理等 。
具體的配置可檢視axios

3、建立api目錄,並新建index.js檔案

import axios from '../plugins/axios';

let api = {
  // app列表
  appListData(params){
    return axios.get('/mock/appListData.json', params);
  },
  // 推薦
  recommendData(params) {
    return axios.get('/mock/recomendData.json', params);
  },
  // 搜尋
  lookUp(params) {
    return axios.get('/mock/lookUp.json', params);
  }
}

export default api

4、元件中使用

import $api from '../api/index.js';

$api.recommendData({}).then((response) => {
      let feed = response.feed;
      this.setState({
        recommendList: feed.entry
      })
    }).catch(err => {
      console.log(err)
    })

axios攔截器新增全域性loading,多個請求合併一個loading

通過配置axios的過濾器,可以攔截使用者請求,我們在這裡新增全域性loading,返回時在隱藏loading的顯示。這裡有個問題需要解決的是,如果同一時刻我們發起多個請求,那麼會出現多個loading的問題,解決辦法就是,通過設定一個count變數來記錄當前介面請求數量,當count為0時再結束loading。

showFullScreenLoading、tryHideFullScreenLoading要乾的事兒就是將同一時刻的請求合併,宣告一個變數needLoadingRequestCount,每次呼叫showFullScreenLoading方法 needLoadingRequestCount + 1。呼叫tryHideFullScreenLoading()方法,needLoadingRequestCount - 1。needLoadingRequestCount為 0 時,結束 loading。
另外還可以通過引數形式設定不需要顯示loading的請求,在攔截處通過判斷來顯示

1、在common.js檔案中新增如下程式碼

import { Toast } from 'antd-mobile'
/**
 * 顯示loading
 */
function showLoading(){
  Toast.loading('載入中...', 0);
}

/**
 * 隱藏loading
 */
function hideLoading(){
  Toast.hide();
}


/**
 * 合併請求,同一時刻只顯示一個loading
 */
let needLoadingRequestCount = 0
export function showFullScreenLoading() {
  if (needLoadingRequestCount === 0) {
    showLoading()
  }
  needLoadingRequestCount++
}

export function hideFullScreenLoading() {
  if (needLoadingRequestCount <= 0){
    return
  }
  needLoadingRequestCount--
  if (needLoadingRequestCount === 0) {
    hideLoading()
  }
}

2、在axios中使用

import { showFullScreenLoading, hideFullScreenLoading} from '../utils/commons'

// Add a request interceptor
_axios.interceptors.request.use(function (config) {
  // Do something before request is sent
  showFullScreenLoading();
  return config;
}, function (error) {
  // Do something with request error
  return Promise.reject(error);
});

// Add a response interceptor
_axios.interceptors.response.use(function (response) {
  // Do something with response data
  setTimeout(() => {
    hideFullScreenLoading();
  }, 1000);
  return response.data;
}, function (error) {
  // Do something with response error
  return Promise.reject(error);
});

配置react-router

在React中,常用的有兩個包可以實現這個需求,那就是react-router和react-router-dom,這兩個不同之處就是後者比前者多出了 這樣的 DOM 類元件,所以我們只需要使用react-router-dom就可以了

1、安裝

npm install react-router-dom --save-dev

2、建立路由元件router/index.js

import React from 'react';
import { HashRouter, Route, Switch } from 'react-router-dom';
import Home from '../pages/Home';
import Profile from '../pages/profile/Profile';


const BasicRoute = () => (
  <HashRouter>
    <Switch>
      <Route exact path="/" component={Home} />
      <Route exact path="/profile" component={Profile} />
    </Switch>
  </HashRouter>
);

export default BasicRoute;

將兩個頁面元件Home和Detail使用Route元件包裹,外面套用Switch作路由匹配,當路由元件檢測到位址列與Route的path匹配時,就會自動載入響應的頁面

3、入口檔案index.js引入router元件

import React from 'react';
import ReactDOM from 'react-dom';
import Router from './router/router';

ReactDOM.render(
  <Router/>,
  document.getElementById('root')
);

4、路由跳轉

this.props.history.push("/search/result");

新增vw適配手機螢幕

1、預設webpack的配置是隱藏的,通過eject 顯示webpack配置(此操作不可逆)

npm run eject  

2、安裝postcss

npm install --save postcss-aspect-ratio-mini postcss-px-to-viewport postcss-write-svg postcss-cssnext postcss-viewport-units cssnano

3、webpack配置

修改webpack.config.js,新增如下程式碼:

{
        // Options for PostCSS as we reference these options twice
        // Adds vendor prefixing based on your specified browser support in
        // package.json
        loader: require.resolve('posREtcss-loader'),
        options: {
          // Necessary for external CSS imports to work
          // https://github.com/facebook/create-react-app/issues/2677
          ident: 'postcss',
          plugins: () => [
            require('postcss-flexbugs-fixes'),
            require('postcss-preset-env')({
              autoprefixer: {
                flexbox: 'no-2009',
              },
              stage: 3,
            }),
            // Adds PostCSS Normalize as the reset css with default options,
            // so that it honors browserslist config in package.json
            // which in turn let's users customize the target behavior as per their needs.
            postcssNormalize(),
            // 新增vw配置 start
            postcssAspectRatioMini({}),
            postcssPxToViewport({
              viewportWidth: 750, // (Number) The width of the viewport.
              viewportHeight: 1334, // (Number) The height of the viewport.
              unitPrecision: 3, // (Number) The decimal numbers to allow the REM units to grow to.
              viewportUnit: 'vw', // (String) Expected units.
              selectorBlackList: ['.ignore', '.hairlines', '.list-row-bottom-line', '.list-row-top-line'], // (Array) The selectors to ignore and leave as px.
              minPixelValue: 1, // (Number) Set the minimum pixel value to replace.
              mediaQuery: false // (Boolean) Allow px to be converted in media queries.
            }),
            postcssWriteSvg({
              utf8: false
            }),
            postcssPresetEnv({}),
            // postcssViewportUnits({
            //  filterRule: rule => rule.selector.indexOf('::after') === -1 && rule.selector.indexOf('::before') === -1 && rule.selector.indexOf(':after') === -1 && rule.selector.indexOf(':before') === -1
            // }),
            postcssViewportUnits({}),
            cssnano({
              "cssnano-preset-advanced": {
                zindex: false,
                autoprefixer: false
              },
            })
            // 新增vw配置 end
          ],
          sourceMap: isEnvProduction && shouldUseSourceMap,
        },
      },

這裡,配置之後執行專案會發現有個報錯
ReferenceError: postcssPresetEnv is not defined
是因為我們沒有引入postcssPresetEnv

安裝並新增以下依賴

npm install postcss-preset-env --save-dev
const postcssPresetEnv = require('postcss-preset-env');

配置好了之後,再訪問我們的頁面,可以發現已經自動轉成vw了

4、相容低版本android,加入viewport-units-buggyfill hack

下載viewport-units-buggyfill.min.js到public資料夾下面,修改index.html新增如下程式碼:

<script src='%PUBLIC_URL%/viewport-units-buggyfill.min.js'></script>
    <script>
      window.onload = function () {
        window.viewportUnitsBuggyfill.init({
          hacks: window.viewportUnitsBuggyfillHacks
        });
      }
    </script>

或者使用cdn的方式引入
<script src="//g.alicdn.com/fdilab/lib3rd/viewport-units-buggyfill/0.6.2/??viewport-units-buggyfill.hacks.min.js,viewport-units-buggyfill.min.js"></script>

安裝scss

npm install node-sass sass-loader --save

在React中的幾種樣式寫法

行內樣式、宣告樣式、引入樣式、CSS Modules模組化

1、行內樣式

<div style={{ background: '#eee', width: '200px', height: '200px'}}>
        <p style= {{color:'red', fontSize:'40px'}}>行內樣式</p>
      </div>

2、宣告樣式

const style1={    
      background:'#eee',
      width:'200px',
      height:'200px'
    }


<div style={style1}>
        <p style= {style2}>行內樣式</p>
      </div>

3、引入樣式

.person{
    width: 60%;
    margin:16px auto;
}
import './Person.css';
<div className='person'>
        <p>person:Hello world</p>
      </div> 

4、css module
CSS Modules 的做法就是通過配置將.css檔案進行編譯,編譯後在每個用到css的元件中的css類名都是獨一無二的,從而實現CSS的區域性作用域。
在create-react-app2.0之前的版本,配置CSS Modules是需要eject彈出webpack來配置的,幸運的是,create-react-app自從2.0.版本就已經開始支援CSS Modules了

(1)區域性樣式

命名規則: xxx.module.css     

                  
                 引入方式 import xxx from 'xxx.module.css'

                  用法:<div className={xxx.styleName}>

(2)全域性樣式

命名規則: xxx.css   

                   引入方式 import ‘xxx.css’

                   用法:<div className='styleName'>

全域性樣式與區域性樣式混合使用:

<div className={`styleName ${xxx['styleName']}`} >  

其中styleName表示全域性樣式 ${xxx['styleName']表示區域性樣式,注意{ }內使用模板字串 ·

5、css多類名寫法

(1) css module模組中寫法

<div className={[`${styles.sideInBox}`,`${styles.sideTitleBox}`].join(' ')}></div>    

(2) 如果是全域性樣式寫法

className={`title ${index === this.state.active ? 'active' : ''}`}

React條件渲染的幾種方式

參考https://www.cnblogs.com/xiaodi-js/p/9119826.html

1、條件表示式

<div>
      {isLoggedIn ? (
        <LogoutButton onClick={this.handleLogoutClick} />
      ) : (
        <LoginButton onClick={this.handleLoginClick} />
      )}
    </div>

2、&&操作符

<div>
      <h1>Hello!</h1>
      {unreadMessages.length > 0 &&
        <h2>
          You have {unreadMessages.length} unread messages.
        </h2>
      }
    </div>

3、列表遍歷

jxs的語法,js程式碼要放在{}裡面,html標籤使用return ()包裹

return (
            <div className='appList-container'>
                <ul className='list'>
                    {
                        this.props.list.map((item, index) => {
                            return (
                                <li className='list-item' key={index}>
                                    <div className='app-index'>{index+1}</div>
                                    <img className='app-icon' src={item['im:image'][0].label} alt="" />
                                    <div className='app-info'>
                                        <div className='app-name'>{item['im:name'].label}</div>
                                        <div className='app-categray'>{item.category.attributes.label}</div>
                                    </div>
                                </li>
                            )
                        })
                    }
                </ul>
            </div>
        );

事件處理

<button onClick={this.handleClick}>ck me
      </button>

兩種事件傳參方式

<button onClick={(e) => this.deleteRow(id, e)}>Delete Row</button>
<button onClick={this.deleteRow.bind(this, id)}>Delete Row</button>

獲取input表單值

兩種方法,受控元件和非受控元件。
推薦使用受控元件,即通過this.state獲取,因為其符合react規範;

非受控元件,給標籤指定ref屬性

<input className='search-bar' type="text" ref='keyword' onKeyUp={this.appSearch.bind(this)}>
appSearch(e){
        let keyword = this.refs.keyword.value
    }

react中使用防抖

appSearch = debounce(() => {
    }, 500);

組合元件

參考https://www.jianshu.com/p/0b005dc60bda

在react開發中,在某些場景會遇到如下元件巢狀形式的開發,例如group和cell或者RadioGroup、RadioOption

<RadioGroup name="option">
    <RadioOption label="選項一" value="1" />
    <RadioOption label="選項二" value="2" />
  </RadioGroup>,

state定義及賦值

constructor(props) {
    super(props);
    this.state = { 
      appList:[]
    };
  }
this.setState({
        appList: feed.entry
      })

父子元件傳參

1、父傳子

<AppList list={this.state.appList}></AppList>

在子元件獲取值

this.props.list

2、子傳父

觸發父元件事件

        this.props.appSearch(keyword);

父元件監聽事件

 <Search appSearch={this.appSearch.bind(this)}></Search>

引入redux和react-redux、redux-thunk

文件
https://react-redux.js.org/introduction/quick-start
http://cn.redux.js.org/docs/introduction/ThreePrinciples.html

類似vuex,redux是一個數據狀態管理工具,但是用法和vuex有些區別

react-redux幫助你完成資料訂閱,redux-thunk可以放你實現非同步action,redux-logger是redux的日誌中介軟體

redux-thunk 是一個比較流行的 redux 非同步 action 中介軟體。redux-thunk 幫助你統一了非同步和同步 action 的呼叫方式,把非同步過程放在 action 級別解決,對 component 沒有影響

import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers';
// 建立store的時候,第二個引數是中介軟體,redux-thunk提供了一個thunk中介軟體,用於處理非同步的action
export default createStore(
  rootReducer,
  applyMiddleware(thunk)
);

1、對redux的理解

(1)單一資料來源:
整個應用的 state 被儲存在一棵 object tree 中,並且這個 object tree 只存在於唯一一個 store 中
(2)State只讀:
唯一改變 state 的方法就是觸發 action,action 是一個用於描述已發生事件的普通物件
(3)執行修改:
為了描述 action 如何改變 state tree ,你需要編寫 reducers
Reducer 只是一些純函式,它接收先前的 state 和 action,並返回新的 state
隨著應用變大,你可以把它拆成多個小的 reducers,分別獨立地操作 state tree 的不同部分

2、對mapStateToProps和mapDispatchToProps的理解

使用 React Redux 庫的 connect() 方法來生成容器元件前,需要先定義 mapStateToProps 這個函式來指定如何把當前 Redux store state 對映到展示元件的 props 中。
除了讀取 state,容器元件還能分發 action。類似的方式,可以定義mapDispatchToProps() 方法接收 dispatch() 方法並返回期望注入到展示元件的 props 中的回撥方法。它可以是一個函式,也可以是一個物件。

// 將state 對映到展示元件的 props 中
const mapStateToProps = state => {
  return {
    searchList: state.searchList
  }
}

const mapDispatchToProps = dispatch => {
  return {
    saveSearchList: searchList => dispatch(saveSearchList(searchList))
  }
}

// export default SearchResult;
// 通過connect生成容器元件
export default connect(
  mapStateToProps,
  mapDispatchToProps
)(SearchResult)

3、安裝redux react-redux redux-thunk

npm install --save redux react-redux redux-thunk
npm install --save-dev redux-logger

4、使用react-hot-loader實現區域性熱更新

#安裝
npm install --save-dev react-hot-loader
#使用
import { AppContainer } from 'react-hot-loader';
import Route from './router/';
const render = Component => {
ReactDOM.render(
    <AppContainer>
        <Component />
    </AppContainer>,
    document.getElementById("root"));
}
render(Route);

引入antd-mobile移動端UI框架

antd-mobile文件
https://mobile.ant.design/index-cn

1、安裝依賴

npm install antd-mobile --save

2、安裝 babel-plugin-import

npm install babel-plugin-import --save

3、在 package.json 配置 antd-mobile 的按需載入(在babel下新增)

"plugins": [
    [
      "import",
      {
        "libraryName": "antd-mobile",
        "style": "css"
      }
    ]
  ],

4、元件中使用

import { Toast,Button } from 'antd-mobile'
<Button type="primary">primary</Button>

上拉重新整理及載入更多

這裡使用react-pullload這個庫

1、安裝

npm install --save react-pullload

2、使用

import ReactPullLoad, { STATS } from "react-pullload";
import "react-pullload/dist/ReactPullLoad.css";


constructor(props) {
    super(props);
    this.state = { 
      appList: [],
      appListAll: [],
      recommendList:[],
      hasMore: true,
      action: STATS.init,
      pageSize:10,
      page:1
    };
  }

handleAction = action => {
    //new action must do not equel to old action
    if (action === this.state.action) {
      return false;
    }
    if (action === STATS.refreshing) {
      this.handRefreshing();
    } else if (action === STATS.loading) {
      this.handLoadMore();
    } else {
      //DO NOT modify below code
      this.setState({
        action: action
      });
    }
  };

  // 重新整理
  handRefreshing = ()=>{
    this.setState({
      action: STATS.refreshing
    });
    this.getAppList();
  }

  // 載入更多
  handLoadMore = ()=>{
    if (STATS.loading === this.state.action) {
      return false;
    }
    //無更多內容則不執行後面邏輯
    if (!this.state.hasMore) {
      return;
    }
    // 顯示正在載入
    this.setState({
      action: STATS.loading
    });
    let page = this.state.page+1;
    setTimeout(() => {
      this.getPageData(page);
    }, 1500);
  }


render() {
    return (
      <div className='container'>
        <div className='search-bar'>
          <Search onFoucs={this.onFoucs.bind(this)}></Search>
        </div>
        <ReactPullLoad
          className="block"
          isBlockContainer={true}
          downEnough={100}
          action={this.state.action}
          handleAction={this.handleAction}
          hasMore={this.state.hasMore}
          distanceBottom={100}>
          <Recommend list={this.state.recommendList}></Recommend>
          <AppList list={this.state.appList}></AppList>
        </ReactPullLoad>
      </div>
    );
  }

因為是使用的mock資料,獲取的是全部資料,所以這裡採用前端分頁的方式載入更多

// 分頁載入
  getPageData(page){
    let resultList = [], list = [];
    let appListAll = this.state.appListAll;
    let pageSize = this.state.pageSize;
    let totalPage = Math.ceil(appListAll.length / pageSize);//總頁數
    let startIndex = pageSize * (page - 1);
    let endIndex = pageSize * page;
    for (let i = startIndex; i < endIndex; i++) {
      resultList.push(appListAll[i]);
    }
    if (page >= totalPage){
      this.setState({ hasMore: false});
    }
    if (page===1){
      list = resultList;
    }else{
      list = this.state.appList.concat(resultList);
    }
    this.setState({
      appList: list,
      page: page,
      pageSize: pageSize,
      action: STATS.reset
    })
  }

問題總結

1、在react中進入頁面自動獲取input輸入焦點 ,彈出鍵盤
input中設定ref屬性(非受控元件),通過 this.refs.keyword呼叫

<input className='search-input' type="text" ref='keyword' onChange={this.appSearch.bind(this)} onFocus={this.onFoucs.bind(this)} placeholder="搜尋應用" />

也可以寫成ref={(input) => { this.textInput = input; }}方式
<input type="text" ref={(input) => { this.textInput = input; }} />
使用this.textInput.focus();方式呼叫

鉤子函式中中呼叫
componentDidMount(){
  this.refs.keyword.focus();
    }

2、父元件呼叫子元件方法(搜尋元件,有兩個地方使用到,首頁和搜尋頁,其中首頁不需要自動獲取焦點,進入搜尋頁時需要自動獲取焦點)
通過在搜尋結果頁裡面獲取搜尋子元件的例項並呼叫foucs方法進行聚焦,從而不影響其他使用搜索元件的父元件狀態

(1)子元件中定義foucs方法
focus(){
        this.refs.keyword.focus();
    }
(2)設定input的ref屬性
<input className='search-input' type="text" ref='keyword' onChange={this.appSearch.bind(this)} onFocus={this.onFoucs.bind(this)} placeholder="搜尋應用" />


(3)父元件中呼叫foucs方法
componentDidMount(){
    this.manualFocusInst.focus();
  }

<Search appSearch={this.appSearch.bind(this)} ref={(ref)=>this.manualFocusInst = ref} onCancel={this.onCancel.bind(this)} onFoucs={this.onFoucs.bind(this)} showCancelBtn={true}></Search>

3、react build的時候報錯

throw new BrowserslistError('Unknown browser query `' + selection + '`')

解決辦法是找到packpage.json裡的browserslist,然後修改

"browserslist": [
    "last 2 versions",
    "android 4",
    "opera 12"
  ],

build開啟靜態服務訪問

 npm install -g serve
 serve -s build

4、元件上面不能直接新增className,如
解決方式使用一個父div進行包裹

<div className='search-bar'>
          <Search onFoucs={this.onFoucs.bind(this)}></Search>
        </div>

5、ios 系統下img不顯示問題,解決方案如下:

/*相容ios不顯示圖片問題*/
img {
    content: normal !important
}

6、1px問題,解決方案

/*偽元素1px*/
.row-cell:before {
    content: " ";
    position: absolute;
    left: 0;
    top: 0;
    right: 0;
    height: 1px;
    border-top: 1px solid #e5e5e5;
    color: #e5e5e5;
    transform-origin: 0 0;
    transform: scaleY(0.5);
    z-index: 2;
}

相關文件

https://react.docschina.org/
https://www.redux.org.cn/
https://react-redux.js.org/
http://react-guide.github.io/react-router-cn
https://mobile.ant.design

最後

程式碼我已經提交到github上去了,如果覺得還可以,歡迎star或者fork

github倉庫

線上訪問

參考閱讀

https://www.jianshu.com/p/8954e9fb0c7e
https://blog.csdn.net/z9061/article/details/84619309
https://www.jianshu.com/p/f97aa775899f
https://www.cnblogs.com/jack-liu6/p/9927336.h