源码地址(实时更新)github.com/yayxs/Netea…
wiki 地址 github.com/yayxs/Netea…
前言
项目背景
基于两点:(一)是笔者在企业中是使用react
开发,不过有的处理问题的方案还是比较老旧。举个例子:关于redux
还是使用原始的switch
一通写,比如使用thunk
要处理 网络请求的几种状态 包括 加载之前 加载中 记载异常等等,这些都要处理,会有很多相似的看起来很尴尬的代码,但是又不好改企业项目的方案。所以打算开发一个完整的企业级项目 整合当下更优解决问题的方案,(二)只注重于前端
的实现,外加有个非常完美的接口
(这里指的的是开源的网易云
的Api,相必大家都知道)。所以选择网抑云
音乐。
关于项目
此项目是一个 JS 版本
的(目前是从JS版本开始) 个人练手项目,整合当下React
项目中的最极实践,每周更新进度 ,旨在更好的走react
企业级项目开发流程。
分享react
开发中的各个小点。望一起交流。代码会第一时间同步到文首的 仓库地址里(暂时没有放在码云)。
每周争取写一篇进度分享
。感兴趣的小伙纸 点个 star
是我更新的动力。
第一周成果展示
主要完成的部分
- 弹出
webpack
配置,添加src
别名 - 配置
.vscode
添加调试json
配置 - 整理
Redux
相关实践流程,跑通Redux
流程 Api
接口的proxy
代理调试axios
的封装,以及api
的配置react-router-dom
路由的初步化配置
Preview
基本介绍
首先要说的一点是项目是使用create react app 来创建的 。然后通过 npm run eject
弹出 webpack
的配置 ,删除单个构建依赖项
目录结构
|-- LICENSE
|-- README.md // 描述文件
|-- build
|-- config
| |-- webpack.config.js // webpack 配置文件
|-- docs // 配套文档博文
| `-- images
|-- examples
| `-- proxy-middleware // nestjs 项目 用于测试接口代理
|-- jsconfig.json
|-- package.json
|-- public
| |-- favicon.ico
| `-- index.html
|-- scripts
|-- src
| |-- App.js // App 主应用
| |-- api // api 接口
| |-- assets // 资源
| |-- common // 公用配置
| |-- components // 组件
| |-- index.js
| |-- layouts // 布局
| |-- pages // 页面 views
| |-- router // react-router-dom 路由配置
| |-- services // axios 网络请求封装
| |-- store // redux 配置
| |-- styles // 样式文件
| |-- utils // 工具方法
|-- yarn.lock
依赖环境
npm 脚本
截止最新时间 scripts
的脚本
"scripts": {
"analyze": "source-map-explorer 'build/static/js/*.js'",
"start": "node scripts/start.js",
"build": "node scripts/build.js",
"test": "node scripts/test.js",
"clear": "rimraf node_modules && yarn add",
"check": "npm install -g && npm-check-updates",
"ncu": "ncu -u && npm i"
},
package包
点击下方的链接直接跳转官网,方便查看
-
"react-router-dom": "^5.2.0" (react 路由 V5大版本)
-
其他有待补充
"antd": "^4.6.1",
"axios": "^0.20.0",
"classnames": "^2.2.6",
"http-proxy-middleware": "^1.0.5",
"husky": "^4.2.5",
"immutable": "^4.0.0-rc.12",
"normalize.css": "^8.0.1",
"react-redux": "^7.2.1",
"react-router-config": "^5.1.1",
"redux": "^4.0.5",
"redux-immutable": "^4.0.0",
"styled-components": "^5.1.1",
"webpack": "4.44.1",
简单的说一下
antd
用的是 V4大版本 截止目前最新,因为和 V3 版本写法上有不同的地方immutable
redux-immutable
redux 数据流不可变的一种方案(并非最好)redux
react-redux
全部都是最新包react
版本是官方最新
Node
等环境
- node v14.8.0
运行项目
clone项目
https://github.com/yayxs/NeteaseCloudMusic.git
安装依赖
npm run check && ncu // 检查依赖包版本 更新至最新并安装
项目运行
npm run start
以上的命令
同时可以替换为 yarn
等,如果你喜欢的话
关于项目的样式
采用antd
组件库 + styled-components
结合 sass
以及 normalize.css
重置样式
import styled from "styled-components";
export const WrapperContainer = styled.div`
height: 285px;
width: 100vw;
background: url(${(props) => props.bgImage}) center center/6000px;
.banner {
height: 285px;
display: flex;
position: relative;
}
`;
用上述的方式写样式,结合 sass
共同完成页面的样式部分
axios网络请求封装
/*
* @Author: yayxs
* @Date: 2020-08-26 21:37:00
* @LastEditTime: 2020-08-26 23:54:10
* @LastEditors: yayxs
* @Description:
* @FilePath: \NeteaseCloudMusic\src\services\request.js
* @
*/
// 引入axios
import axios from "axios";
// import * as commonConfig from "../common/config";
const instance = axios.create({
baseURL: process.env.REACT_APP_BASE_URL,
timeout: Number(process.env.REACT_APP_TIME_OUT),
});
// Add a request interceptor
// 全局请求拦截,发送请求之前执行
instance.interceptors.request.use(
function (config) {
// Do something before request is sent
// 设置请求的 token 等等
// config.headers["authorization"] = "Bearer " + getToken();
return config;
},
function (error) {
// Do something with request error
return Promise.reject(error);
}
);
// Add a response interceptor
// 请求返回之后执行
instance.interceptors.response.use(
function (response) {
// Any status code that lie within the range of 2xx cause this function to trigger
// Do something with response data
return response.data;
},
function (error) {
// Any status codes that falls outside the range of 2xx cause this function to trigger
// Do something with response error
return Promise.reject(error);
}
);
/**
* get请求
* @param {*} url 请求地址
* @param {*} params
*/
export function get(url, params) {
return instance.get(url, {
params,
});
}
/**
* post请求
* @param {*} url 请求地址
* @param {*} data
*/
export function post(url, data) {
return instance.post(url, data);
}
/**
* put请求
* @param {*} url 请求地址
* @param {*} data
*/
export function put(url, data) {
return instance.put(url, data);
}
/**
* delete请求
* @param {*} url
*/
export function del(url) {
return instance.delete(url);
}
使用的时候直接可以在@/api
文件夹下创建对应的 js
文件,然后配置对应的接口
/*
* @Author: yayxs
* @Date: 2020-08-26 22:35:37
* @LastEditTime: 2020-08-26 23:48:17
* @LastEditors: yayxs
* @Description: 推荐 API
* @FilePath: \NeteaseCloudMusic\src\api\recommend.js
* @
*/
// import request from "@/services/request";
import { get } from "@/services/request";
const fetchBannerListApi = () => get("/banner");
export { fetchBannerListApi };
关于接口
组件书写方式
以头部的header组件为例子
@/components/header
-
index.jsx
import React, { memo } from "react"; import classnames from "classnames"; import { WarpperContainer, StyledLeft, StyledRight } from "./styled"; import { NavLink } from "react-router-dom"; import { headerNavConfig } from "../../common/config"; const HeaderComp = memo(() => { return ( <WarpperContainer> <div className="wrap_1100_center container"> <StyledLeft> <a hidefocus="true" href="/#" className="logo sprite_topbar"> 网易云音乐 </a> <ul> {headerNavConfig.map((item) => ( <li key={item.title} className={classnames("setected_nav")}> <NavLink to={item.path}> {item.title} <i className="sprite_topbar icon"></i> </NavLink> </li> ))} </ul> </StyledLeft> <StyledRight></StyledRight> </div> </WarpperContainer> ); }); export default HeaderComp;
-
styled.js
/* * @Author: yayxs * @Date: 2020-08-24 23:28:16 * @LastEditTime: 2020-08-25 23:28:36 * @LastEditors: yayxs * @Description: * @FilePath: \NeteaseCloudMusic\src\components\header\styled.js * @ */ import styled from "styled-components"; export const WarpperContainer = styled.div` width: 100vw; height: 70px; background-color: #242424; .container { display: flex; } `; export const StyledLeft = styled.div` display: flex; .logo { display: block; width: 176px; height: 69px; background-position: 0 0; text-indent: -9999px; } ul { width: 508px; height: 70px; display: flex; align-items: center; justify-content: space-between; li { font-size: 14px; color: #ccc; } } `; export const StyledRight = styled.div``;
redux 流程
此项目的数据分为两种 一种是组件内部的状态。这时候,我们采用 react hooks
中的 useState hook
来解决
const [currIndex, setCurrIndex] = useState(0);
axios
网络请求的数据我们一律存取在 redux
整体的流程
import { Provider } from "react-redux";
import store from "./store";
ReactDOM.render(
<>
<Provider store={store}>
<App />
</Provider>
</>,
document.getElementById("root")
);
- store/index.js
/*
* @Author: yayxs
* @Date: 2020-08-22 11:48:40
* @LastEditTime: 2020-08-26 23:01:17
* @LastEditors: yayxs
* @Description:
* @FilePath: \NeteaseCloudMusic\src\store\index.js
* @
*/
import { createStore, applyMiddleware, compose } from "redux";
import thunk from "redux-thunk";
import reducer from "./reducer";
const middlewares = [thunk];
if (process.env.NODE_ENV === `development`) {
const { logger } = require(`redux-logger`);
middlewares.push(logger);
}
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
reducer,
/* preloadedState, */ composeEnhancers(applyMiddleware(...middlewares))
);
export default store;
局部组件状态
关于局部的组件流程,我们分为几部分处理
actionTypes.js
/*
* @Author: yayxs
* @Date: 2020-08-26 22:27:29
* @LastEditTime: 2020-08-26 22:32:33
* @LastEditors: yayxs
* @Description:
* @FilePath: \NeteaseCloudMusic\src\pages\foundMusic\childrenPages\recommend\store\actionTypes.js
* @
*/
// 获取轮播图数据
export const FETCH_BANNER_LIST_SUCCESS = "FETCH_BANNER_LIST_SUCCESS";
export const FETCH_BANNERLIST_BEGIN = "FETCH_BANNERLIST_BEGIN";
export const FETCH_BANNERLIST_ERROR = "FETCH_BANNERLIST_ERROR";
actionCreators
/*
* @Author: yayxs
* @Date: 2020-08-26 22:27:33
* @LastEditTime: 2020-08-27 21:07:49
* @LastEditors: yayxs
* @Description:
* @FilePath: \NeteaseCloudMusic\src\pages\foundMusic\childrenPages\recommend\store\actionCreators.js
* @
*/
import { fetchBannerListApi } from "@/api/recommend.js";
import { FETCH_BANNER_LIST_SUCCESS } from "./actionTypes";
export const changeBannerLsitAction = (data) => ({
type: FETCH_BANNER_LIST_SUCCESS,
payload: { data },
});
export const getBannerListAsyncAction = () => (dispatch) => {
fetchBannerListApi()
.then((res) => {
if (res.code === 200) {
dispatch(changeBannerLsitAction(res.banners));
}
})
.catch((err) => {});
};
reducer
/*
* @Author: yayxs
* @Date: 2020-08-26 22:27:34
* @LastEditTime: 2020-08-27 21:09:44
* @LastEditors: yayxs
* @Description:
* @FilePath: \NeteaseCloudMusic\src\pages\foundMusic\childrenPages\recommend\store\reducer.js
* @
*/
import { Map } from "immutable";
import * as actionTypes from "./actionTypes";
const initState = Map({
bannersList: [],
});
export default (state = initState, action) => {
switch (action.type) {
case actionTypes.FETCH_BANNER_LIST_SUCCESS:
return state.set("bannersList", action.payload.data);
default:
return state;
}
};
关于以上的redux
的流程,一方面 你可以关注本项目查看完整的代码 , 其次**我有整理一篇 redux 流程的纵向分析实践(2020年8月更新)**
当然你也可以直接在 juejin 查看
(如果遇到网络问题) juejin.cn/post/684490…
路由懒加载
const YYHeaderComp = lazy(() => import("./components/header/index"));
const YYFooterComp = lazy(() => import("./components/footer/index"));
webpack别名配置
此项目通过简单的修改webpack
的配置来添加别名(alias)
const resolveDir =(dir)=>{
let res = path.resolve(__dirname, dir)
console.log(res)
return res
}
alias: {
// webpack 配置别名
'@': resolveDir('../src') ,// 这样配置后 @ 可以指向 src 目录
'components': resolveDir("../src/components"),
// Support React Native Web
// https://www.smashingmagazine.com/2016/08/a-glimpse-into-the-future-with-react-native-for-web/
'react-native': 'react-native-web',
// Allows for better profiling with ReactDevTools
...(isEnvProductionProfile && {
'react-dom$': 'react-dom/profiling',
'scheduler/tracing': 'scheduler/tracing-profiling',
}),
...(modules.webpackAliases || {}),
},
CRA的配置
proxy代理的配置
在开发中代理 API
请求 ,通过http-proxy-middleware
然后在 src 目录下新建 setupProxy.js
文件。注意是src
目录 并不是 项目的根目录
const { createProxyMiddleware } = require('http-proxy-middleware');
module.exports = function(app) {
app.use(
'/api',
createProxyMiddleware({
target: 'http://localhost:5000',
changeOrigin: true,
})
);
};
测试代理
如果你想测试代理,可以启动 examples/proxy-middleware
(github.com/yayxs/Netea…) 这是一个 nest 项目,安装依赖,然后 npm run start
其他
编辑器的基本配置
写React
的项目可以使用vscode
插件 ,快捷的生成代码片段
在项目目录的最顶端有个.vscode
并没有添加 到 忽略文件,如果你使用vscode
编辑器的话,会看到
-
launch.json
{ "version": "0.2.0", "configurations": [ { "name": "Chrome", "type": "chrome", "request": "launch", "url": "http://localhost:3001", "webRoot": "${workspaceFolder}/src", "sourceMapPathOverrides": { "webpack:///src/*": "${webRoot}/*" } } ] }
-
settings.json
{ "emmet.includeLanguages": { "javascript": "javascriptreact" } }
具体的含义及作用请自行 搜索
资源
网易云官方精灵图
- s2.music.126.net/style/web2/… 头部 logo 等
- s2.music.126.net/style/web2/… 下载客户端
关联阅读
Q&A
有什么问题还请 多多交流 github.com/yayxs/Netea…
也可以添加交流群
- 前端互鱼1群 713593204
这是 网易云web版本项目的第一篇分享
你们的小赞是我更新的动力,能看到这里的人啊,都是“闲人” ,老粉晓得咱们的玩法,评论区 赞数最多的朋友,下一篇直接置顶 【读读评论】。给个 star
吧 亲