利用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合并。

相关文章
相关标签/搜索