利用react + redux + react-router實現的音樂播放SPA

r-music

該項目做爲react技術棧的練手項目,使用了酷狗和網易雲的數據。javascript

其中酷狗的數據拉取,相對容易;網易雲數據的拉取,參照: https://binaryify.github.io/NeteaseCloudMusicApi/css

感謝ScorpionJay同窗,該項目前期的工做,大量都由他完成。html

前端坑多,該項目還有不少bug,歡迎一塊兒學習交流,共同爬坑。前端

目錄

參考文檔

開發這個項目,我參閱的學習文檔以下:java

在線體驗

http://cenuon.com:8666node

-

效果展現

個性推薦 排行榜列表

歌單詳情 搜索

播放音樂 播放MV

項目說明

技術棧

react + react-router + redux + webpack + ES6 + fetch + sass + flexreact

項目結構

r-music
│  .babelrc
│  .eslintrc.js
│  .gitignore
│  package.json
│  README.md
│  server.js                    //node啓動腳本
│  webpack.config.js            
│  
├─config
│      webpack.dev.js           //開發環境的webpack配置文件
│      webpack.hash.js          //開發環境的webpack配置文件
│      webpack.prod.js          //生產環境的webpack配置文件
│           
└─src
    │  api.js                   //封裝的fetch
    │  app.js                   
    │  config.js                //api接口配置文件
    │  index.hash.js
    │  index.js
    │  index.temp.hash.html
    │  index.temp.html
    │  routers.js               //路由
    │  storage.js               //window.localStorage的各類方法
    │      
    ├─components               //組件
    │          
    ├─containers               //頁面
    │      account.js
    │      album.js
    │      friend.js
    │      home.js
    │      music.js
    │      play.js
    │      
    ├─images
    │      favicon.ico
    │      
    ├─json
    │      home.json
    │       
    ├─actions                 //redux -- action
    │      album.js
    │      dialog.js
    │      home.js
    │      .
    │      .     
    ├─reducers                //redux -- reducer
    │      album.js
    │      dialog.js
    │      home.js
    │      index.js
    │      login.js
    │      message.js
    │      music.js
    │      spin.js
    │      user.js
    │      
    stores                     //redux  -- store
    │      index.js
    │      
    └─sass                    //樣式文件
            common.scss
            home.scss
            login.scss
            main.scss
            pagination.scss
            slider.scss

項目運行

git clone https://github.com/ScorpionJay/r-music.git
cd r-music
npm install

本地開發環境

npm run dev

該命令在package.json的scripts中,即"dev": "webpack-dev-server --config webpack.config.js --hot",啓動一個服務。 若是一切正常,會自動打開瀏覽器並訪問http://localhost:9999webpack

// config/webpack.dev.js部分代碼
devServer: {
    contentBase: "./src",//本地服務器所加載的頁面所在的目錄
    historyApiFallback: true,//不跳轉
    inline: true,//實時刷新
    host: '0.0.0.0',
    port:9999,
    // 設置代理
    proxy:{
        "/kugou": {
            target: "http://m.kugou.com",
            changeOrigin: true,
            pathRewrite: {"^/kugou" : ""}
        }
    }
}

由於在 config/webpack.dev.js 設置了host:'0.0.0.0',因此同局域網的其餘手機或PC也能夠經過ip+端口號訪問。nginx

proxy,設置代理,是爲了解決跨域的問題。git

生產環境

npm run build

該命令會將全部文件打包,並放在dist目錄下。

配置nginx,設置反向代理,解決跨域問題

安裝好nginx,找到nginx.conf,並添加以下代碼(與默認80端口的server同級,這裏只列出了主要的配置項)

server {
    #端口號
    listen 8666;
    #項目根目錄位置
    root E:/r-music/dist
    #訪問首頁文件
    location / {
        index index.html
	      try_files $uri  /index.html    // 解決刷新頁面404問題
   }
    #緩存靜態文件,30d表示30天,可按需調整大小
    location ~ ^/(images|javascript|js|css|flash|media|static)/ {
        expires 30d; 
    }
    #設置代理,解決跨域
    location ^~/kugou/{
      rewrite ^/kugou/(.*)$ /$1 break;
      proxy_pass http://m.kugou.com;
    }
    
    location ^~/ad/{
      rewrite ^/ad/(.*)$ /$1 break;
      proxy_pass http://ads.service.kugou.com;
    }
    
    location ^~/musicSearch/{
      rewrite ^/musicSearch/(.*)$ /$1 break;
      proxy_pass http://mobilecdn.kugou.com;
    }
    location ^~/mobilecdn/{
        rewrite ^/mobilecdn/(.*)$ /$1 break;
        proxy_pass http://mobilecdn.kugou.com;
    }
	#網易MV的數據,詳見https://binaryify.github.io/NeteaseCloudMusicApi/
    location ^~/NeteaseCloudMusicApi/{
        rewrite ^/NeteaseCloudMusicApi/(.*)$ /$1 break;
        proxy_pass http://www.cenuon.com:3000;
    }
}

重啓nginx便可

nginx -s reload

知識梳理

流程圖解

經過我本身的理解方式,簡單地整理了react、redux、react-redux三者之間的關係圖,以下:

經過代碼,梳理redux、react-redux

注:下面代碼只列出搜索功能的關鍵部分,源碼地址:https://github.com/ScorpionJay/r-music

1. Provider

react-redux提供的Provider組件,可讓容器組件取得state

src/index.js
import configureStore from './stores'
const store = configureStore()

<Provider store={store}>
	<Router history={browserHistory} routes={routers} />
</Provider>

上面代碼中,Provider使得Router的全部子組件能夠取得state

import configureStore from './stores'爲redux的store,以下:

src/store/index.js
import reducers from '../reducers/index';

export default function(initialState) {
	let createStoreWithMiddleware

	// 判斷環境是否logger
	if (process.env.NODE_ENV === 'production') {
		createStoreWithMiddleware = applyMiddleware(thunk)(createStore);
	}else{
		//開發環境在console能夠看到整個狀態樹的實時日誌
		const logger = createLogger();
		createStoreWithMiddleware = applyMiddleware(thunk,logger)(createStore);
	}
	let store = createStoreWithMiddleware(reducers, initialState);
	return store;
};

2. react:Component

src/containers/search.js
import React, { Component, PropTypes } from 'react'
import { connect } from 'react-redux'

import { searchHotAPI,searchResultAPI,clearSearchResultAPI} from '../actions/search'

class Search extends Component {

  constructor(props) {
    super(props);
  }

  componentDidMount(){
    const { dispatch } = this.props
    dispatch(searchHotAPI())
  }

  searchEvt(keyword,page=1){
    const { dispatch } = this.props;
    keyword = keyword || this.refs.keyword.value
    if(keyword!=''){
      dispatch(searchResultAPI(keyword, page));
    }else{
      dispatch(clearSearchResultAPI());
    }
    this.refs.keyword.value = keyword;
  }


  render() {
    const { dispatch,controll,search } = this.props;
    return (
      <div className='root' style={{fontSize:'1.2rem'}}>

		//...

      </div>
    )
  }
}

function map(state) {
  return {
    search: state.search,
    controll: state.music.controll
  }
}

export default connect(map)(Search)

react-redux的connect方法,用於從 UI 組件生成容器組件。

上面代碼中,connect(map)(Search)使得組件Search能夠經過props取得map返回的數據。

dispatch(searchHotAPI())dispatch(clearSearchResultAPI()),獲取數據並分發action。

3. redux

src/actions/search.js
import Config from '../config'
import { spin,spinHidden } from './spin'
import api from '../api'

import Storage from '../storage'

//定義常量
export const SEARCH_HOT = 'SEARCH_HOT'
export const SEARCH_RESULT = 'SEARCH_RESULT'

//actionCreator,這裏是一個函數,返回action對象
const searchHot = (obj) => {return {type:SEARCH_HOT, obj}}
const searchResult = (obj) => {return {type:SEARCH_RESULT, obj}}

//搜索熱門關鍵字
export function searchHotAPI(){
	return async dispatch => {
		try{
			let hots = await api( Config.searchHotAPI );
			dispatch(searchHot(hots.data.info));
		} catch(error) {
			console.log(error);
		}
	}
}

//經過關鍵字搜索
export function searchResultAPI(keyword,page){
	return async dispatch => {
		try {
			let result = await api( Config.searchResultAPI, 'get', {keyword,page} );
			//搜索歷史存到localStorage
			setSearchHistory(keyword);
			dispatch(searchResult(result.data.info));
		} catch(error) {
			console.log(error);
		}
	}
}

上面代碼中,searchHotsearchResult都是Action creator,即分別返回一個action。

action是一個帶有type關鍵字的對象,如{type:SEARCH_HOT, obj}{type:SEARCH_RESULT, obj}

searchHotAPIsearchResultAPI分別返回一個獲取數據並分發action的異步函數,通常在容器組件裏會調用。

src/reducer/search.js
import { combineReducers } from 'redux'
import { SEARCH_HOT,SEARCH_RESULT } from '../actions/search'

function hots(state = [], action){
  switch(action.type) {
    case SEARCH_HOT:
      return action.obj;
    default:
      return state;
  }
}

function result(state = [], action){
  switch(action.type) {
    case SEARCH_RESULT:
      return action.obj;
    default:
      return state;
  }
}


const Reducers = combineReducers({
  hots,result,
})

export default Reducers

上面代碼中,hots函數收到名爲SEARCH_HOT的 Action 之後,就返回一個新的 State,做爲熱門搜索的結果。

src/store/index.js中,開發環境下,引入了中間件redux-loggercreateLogger,在瀏覽器console能夠觀察到每次reducer的結果,以下:

src/reducer/index.js
import { combineReducers } from 'redux'
//...
import  search from './search'

const reducers = combineReducers({
  //...
  search,
})

export default reducers

Reducer 是一個函數,它接受 Action 和當前 State 做爲參數,返回一個新的 State,而後View發生變化。 combineReducers將多個拆分的reducer合併。

相關文章
相關標籤/搜索