学习搭建一个网易云音乐的项目

328 阅读4分钟

原文链接

采用的技术框架

  • react 视图层框架
  • bable js转译
  • webpack 打包
  • axios 网络通讯
  • dvaJs model层框架
  • 网易云音乐api

项目步骤

babel 配置

项目中使用到了最新的特性,所以针对性的配置了 babel

package.json

"devDependencies": {
    "@babel/core": "^7.1.2",
    "@babel/plugin-proposal-class-properties": "^7.1.0", // 支持静态属性
    "@babel/plugin-proposal-decorators": "^7.1.2", //支持装饰器
    "@babel/plugin-transform-runtime": "^7.1.0",  //供编译模块复用工具函数
    "@babel/preset-env": "^7.1.0", //转译所有
    "@babel/preset-react": "^7.0.0", //转译react代码
    "babel-loader": "^8.0.4",
    "babel-plugin-import": "^1.9.1", //import 按需加载
},
"dependencies": {
    "@babel/runtime": "^7.1.2",
}
.babelrc

{
    "presets": ["@babel/preset-env", "@babel/preset-react"],
    "plugins": [
        ["@babel/plugin-proposal-decorators", {
            "legacy": true
        }],
        "@babel/plugin-proposal-class-properties",
        [
            "@babel/plugin-transform-runtime",
            {
                "corejs": false,
                "helpers": true,
                "regenerator": true,
                "useESModules": false
            }
        ]
    ]
}

webpack 配置

webpack.config.js

const HtmlWebPackPlugin = require("html-webpack-plugin"); //复制html并且添加js链接
const path = require('path'); //管理路径
const webpack = require('webpack');



module.exports = {
    entry: path.join(__dirname, "src", "index.jsx"),
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'bundle.js'
    },
    devtool: 'source-map',
    resolve: {
        extensions: ['.js', '.jsx']
    },
    module: {
        rules: [{
                test: /\.js$/,
                exclude: /node_modules/,
                use: {
                    loader: "babel-loader"
                },
                exclude: [
                    path.join(__dirname, '../node_modules') // 由于node_modules都是编译过的文件,这里我们不让babel去处理其下面的js文件
                ]
            }, {
                test: /.jsx$/, //使用loader的目标文件。这里是.jsx
                loader: 'babel-loader'
            },
            {
                test: /\.html$/,
                use: [{
                    loader: "html-loader"
                }]
            },
            {
                test: /\.css$/,
                use: ['style-loader', {
                    loader: 'css-loader',
                    // options: {
                    //     modules: true,
                    //     localIdentName: '[path][name]__[local]--[hash:base64:5]'
                    // }
                }]
            },
            {
                test: /\.less$/,
                use: [{
                    loader: "style-loader" // creates style nodes from JS strings
                }, {
                    loader: 'css-loader',
                    // options: {
                    //     modules: true,
                    //     localIdentName: '[path][name]__[local]--[hash:base64:5]'
                    // } // translates CSS into CommonJS
                }, {
                    loader: "less-loader" // compiles Less to CSS
                }],
                include: [
                    path.resolve(__dirname, "node_modules", "antd"),
                ]
            },
            {
                test: /\.(png|jpg|gif|woff|woff2|eot|ttf|svg)$/i,
                use: [{
                    loader: 'url-loader',
                    options: {
                        limit: 100000
                    }
                }]
            }
        ]
    },
    plugins: [
        new HtmlWebPackPlugin({
            template: path.join(__dirname, "src", "index.html"),
            filename: path.join(__dirname, "dist", "index.html")
        }),
        new webpack.HotModuleReplacementPlugin()
    ],
    devServer: {
        port: 4954,
        contentBase: "./dist",
        historyApiFallback: true,
        hot: true
    }
}

index.jsx 入口文件

index.jsx

import dva from "dva";
import routers from './config/Routers'
import history from './config/History'

// model
import BaseModel from './model/BaseModel';
import LoginModel from './model/LoginModel'
import SongListModel from './model/SongListModel';
import PlayListDetailModel from './model/PlayListDetailModel';

const app = dva({
    history: history, //自定义history,可以去除url上的_k和#之类的符号
    onError(error) {
        console.error(error.stack); //同意的error处理
    },
});

// 引入model层
app.model(LoginModel)
app.model(SongListModel)
app.model(PlayListDetailModel)
app.model(BaseModel)

// 引入router
app.router(routers)

// 定义些全局变量
window.App_ = {
    history: history,
    dva: app
}

// 启动,在class="root"
app.start("#root")

// webpack service配置
module.hot.accept();

History.js

History和Routers请参考

History.js

import createHistory from 'history/createBrowserHistory';
export default createHistory();

Routers.jsx

由于router-v4<Route/>组件化,所以router-v3那种配置便不可用了.在v4<Router/>可以当做子组件使用,具体参考BaseLayout.jsx

Routers.jsx

import React from 'react'
import { Route, Router, Switch } from 'react-router-dom'

import BaseLayout from '../layout/BaseLayout'


import history from './History';
const routers = () => {
    return (
        <Router history={history}>
            <Route path="/" component={BaseLayout}>
            </Route>
        </Router>
    )
}

export default routers

BaseLayout.jsx

class BasicLayout extends React.Component {

    push(){
        // 这是跳转页面的方法,具体使用参考history库
        // App_.history是在index.jsx中定义的全局变量
        App_.history.push("/songList")
    }

    render() {
        return (
            <Layout>
                <Header>
                    Header
                </Header>
                <Content>
                    <div>
                        <Switch>
                            <Route path="/login" component={Login}></Route>
                            <Route path="/songList" component={SongList}></Route>
                            <Route path="/playListDetail" component={PlayListDetail}>
                            </Route>
                        </Switch>
                    </div>
                </Content>
                <Footer style={{ textAlign: 'center', height: "150px" }}>
                    Footer
                </Footer>
            </Layout>
        )
    }
}

model层案例

namespace是一个作用域,用于你查找对应model的数据:
使用dvaJS管理state后,会有一个this.state对象,所有的state都以namespacekey,整合到了this.state中.那么如何获取我对应modelstate呢?
这个时候只要通过this.state.[namespace]就能获取到对应的state了,比如this.state.songListNamespace就能获取到下面state了.

export default {
    namespace: "songListNamespace", 
    state: {
        playlist: [],
        loading: false,
    },
    effects: {
        * getDataList(_, {
            call,
            put,
            select
        }) {
            // call暂时不知道干嘛的,还没用过

            // select 用于获取state 例如:
            // const loading = yield select(state => state.songListNamespace.loading) 就是获取loading状态.PS:state是全局的,所以要加上namespace

            // 调用接口
            const response = yield RecommendService.songList()
            const data = response.result
            // put 相当于一个动作,目的是调用下面reducers中的addData方法
            yield put({
                type: "addData",
                payload: {
                    data: data
                }
            })
        },

        // 展示下怎么传递参数到effects
        * pushPlayListDetail({
            payload //传递的参数,里面也可以放回调函数,回调到view层
        }, {
            put
        }) {
            // push带参数
            App_.history.push({
                pathname: '/playListDetail',
                state: {
                    id: payload.id
                }
            })
        }
    },
    reducers: {
        addData(state, action) {
            return {
                ...state,
                playlist: action.payload.data
            }
        }
    }
}

上面model对应的view

import React from 'react'
import { List, Avatar, Spin } from 'antd';
import { connect } from "dva";

// 作用域
const namespace = "songListNamespace"
// 将state 转化为 this.props.songListData PS:只是使用方式上的转化
const mapStateToProps = (state) => {
    const songListData = state[namespace]
    return {
        songListData
    }
}
// 定义的action,用于调用model中的effects中的方法.PS:需要指明作用域,可以跨域调用,state同理
const mapDispatchToProps = (dispatch) => {
    return {
        getDataList: () => {
            dispatch({
                type: `${namespace}/getDataList`
            })
        },
        onPushSongList: (id) => {
            dispatch({
                type: `${namespace}/pushPlayListDetail`,
                payload: {
                    id, id
                }
            })
        }
    }
}

class SongList extends React.Component {
    state = {
        playlist: [],
        loading: false
    }

    componentDidMount() {
        // 调用model中的getDataList方法,调用流程如下
        // 当前文件 mapDispatchToProps -> getDataList -> model -> effects -> getDataList
        this.props.getDataList()
    }

    render() {
        return (
            <Spin tip="加载数据,请稍等..." spinning={this.props.songListData.loading} delay="500">
                <List
                    itemLayout="horizontal"
                    dataSource={this.props.songListData.playlist}
                    renderItem={(item) => (
                        <List.Item>
                            <List.Item.Meta
                                avatar={<Avatar src={item.picUrl} />}
                                title={<span
                                    onClick={() => {
                                        this.props.onPushSongList(item.id)
                                    }}
>{item.name}</span>}
                                description={<span>{item.copywriter}</span>}
                            >
                            </List.Item.Meta>
                        </List.Item>
                    )}
                />
            </Spin>
        )
    }
}
// 将mapStateToProps,mapDispatchToProps,SongList整合为一个整体
export default connect(mapStateToProps, mapDispatchToProps)(SongList);

const songListData = state[namespace] return { songListData }

connect的方便写法

下面这种写法要通过ES7的装饰器才可以使用,上面babel中有介绍

@connect(
    _state => ({
        songListData: _state[namespace], // 总的
        playlist:_state[namespace].playlist, //分开的
        loading:_state[namespace].loading, //分开的
    }),
    _dispatch => ({
        getDataList: () => {
            _dispatch({
                type: `${namespace}/getDataList`
            })
        },
        onPushSongList: () => {
            _dispatch({
                type: `${namespace}/pushPlayListDetail`,
                payload: {
                    id, id
                }
            })
        }
    }))
class SongList extends React.Component {
    //使用
    // this.props.songListData 
    // this.props.playlist 
    // this.props.loading
}

总结

前端的技术栈有点长,要慢慢的整理.现在我也只是简单使用,dvaJs更是入门.
接下来还有reactPureComponent,无状态组件,生命周期等等.
dva的话还要了解react-router/react-saga/react-redux,等等.学习的路还很长啊.