Repository: fi3ework/react-cloud-music
Branch: master
Commit: baf392c6e5d4
Files: 136
Total size: 138.5 KB
Directory structure:
gitextract_hgejng1n/
├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── .postcssrc.js
├── .prettierrc.js
├── .vscode/
│ └── settings.json
├── LICENSE
├── README.md
├── config/
│ ├── env.js
│ ├── jest/
│ │ ├── cssTransform.js
│ │ ├── fileTransform.js
│ │ └── typescriptTransform.js
│ ├── paths.js
│ ├── polyfills.js
│ ├── webpack.config.dev.js
│ ├── webpack.config.prod.js
│ └── webpackDevServer.config.js
├── images.d.ts
├── package.json
├── public/
│ ├── index.html
│ └── manifest.json
├── scripts/
│ ├── build.js
│ ├── start.js
│ └── test.js
├── src/
│ ├── App.css.d.ts
│ ├── App.tsx
│ ├── components/
│ │ ├── Carousel/
│ │ │ ├── index.tsx
│ │ │ ├── style.scss
│ │ │ ├── style.scss.d.ts
│ │ │ └── view.tsx
│ │ ├── Cover/
│ │ │ ├── index.tsx
│ │ │ ├── style.scss
│ │ │ ├── style.scss.d.ts
│ │ │ └── view.tsx
│ │ ├── Matrix/
│ │ │ ├── index.tsx
│ │ │ ├── style.scss
│ │ │ ├── style.scss.d.ts
│ │ │ └── view.tsx
│ │ ├── SectionTitle/
│ │ │ ├── index.tsx
│ │ │ ├── style.scss
│ │ │ ├── style.scss.d.ts
│ │ │ └── view.tsx
│ │ ├── Track/
│ │ │ ├── index.tsx
│ │ │ ├── style.scss
│ │ │ ├── style.scss.d.ts
│ │ │ └── view.tsx
│ │ └── TrackList/
│ │ ├── index.tsx
│ │ ├── style.scss
│ │ ├── style.scss.d.ts
│ │ └── view.tsx
│ ├── constant/
│ │ ├── api.tsx
│ │ └── style.scss
│ ├── index.css
│ ├── index.css.d.ts
│ ├── index.tsx
│ ├── layouts/
│ │ ├── BottomBar/
│ │ │ ├── index.tsx
│ │ │ ├── style.scss
│ │ │ ├── style.scss.d.ts
│ │ │ └── view.tsx
│ │ ├── ExploreHeaderBar/
│ │ │ ├── index.tsx
│ │ │ ├── style.scss
│ │ │ ├── style.scss.d.ts
│ │ │ └── view.tsx
│ │ ├── GithubFork/
│ │ │ └── index.tsx
│ │ └── HeaderBar/
│ │ ├── index.tsx
│ │ ├── style.scss
│ │ ├── style.scss.d.ts
│ │ └── view.tsx
│ ├── pages/
│ │ ├── Account/
│ │ │ ├── index.tsx
│ │ │ ├── style.scss
│ │ │ └── style.scss.d.ts
│ │ ├── Explore/
│ │ │ ├── Banner/
│ │ │ │ ├── index.tsx
│ │ │ │ ├── style.scss
│ │ │ │ ├── style.scss.d.ts
│ │ │ │ └── view.tsx
│ │ │ ├── Custom.tsx
│ │ │ ├── List.tsx
│ │ │ ├── ListCover/
│ │ │ │ ├── index.tsx
│ │ │ │ ├── style.scss
│ │ │ │ ├── style.scss.d.ts
│ │ │ │ └── view.tsx
│ │ │ ├── RecommendList/
│ │ │ │ ├── index.tsx
│ │ │ │ ├── style.scss
│ │ │ │ ├── style.scss.d.ts
│ │ │ │ └── view.tsx
│ │ │ ├── Slider.tsx
│ │ │ ├── index.tsx
│ │ │ ├── style.scss
│ │ │ └── style.scss.d.ts
│ │ ├── Friends/
│ │ │ ├── index.tsx
│ │ │ ├── style.scss
│ │ │ └── style.scss.d.ts
│ │ ├── Mine/
│ │ │ ├── index.tsx
│ │ │ ├── style.scss
│ │ │ └── style.scss.d.ts
│ │ ├── Playing/
│ │ │ ├── ControlBar/
│ │ │ │ ├── index.tsx
│ │ │ │ ├── style.scss
│ │ │ │ ├── style.scss.d.ts
│ │ │ │ └── view.tsx
│ │ │ ├── HeaderBar/
│ │ │ │ ├── index.tsx
│ │ │ │ ├── style.scss
│ │ │ │ ├── style.scss.d.ts
│ │ │ │ └── view.tsx
│ │ │ ├── RotatingCover/
│ │ │ │ ├── index.tsx
│ │ │ │ ├── style.scss
│ │ │ │ ├── style.scss.d.ts
│ │ │ │ └── view.tsx
│ │ │ ├── index.tsx
│ │ │ ├── style.scss
│ │ │ ├── style.scss.d.ts
│ │ │ └── view.tsx
│ │ ├── Playlist/
│ │ │ ├── Header/
│ │ │ │ ├── index.tsx
│ │ │ │ ├── style.scss
│ │ │ │ ├── style.scss.d.ts
│ │ │ │ └── view.tsx
│ │ │ ├── index.tsx
│ │ │ ├── style.scss
│ │ │ ├── style.scss.d.ts
│ │ │ └── view.tsx
│ │ └── Video/
│ │ ├── index.tsx
│ │ ├── style.scss
│ │ └── style.scss.d.ts
│ ├── registerServiceWorker.ts
│ ├── router/
│ │ ├── index.tsx
│ │ ├── routerTrans.scss
│ │ ├── routerTrans.scss.d.ts
│ │ └── slideContext.tsx
│ ├── store.tsx
│ └── utils/
│ ├── calcFunctions.tsx
│ ├── ee.tsx
│ └── models/
│ ├── componentFetchModel.tsx
│ └── index.tsx
├── tsconfig.json
├── tsconfig.prod.json
├── tsconfig.test.json
└── tslint.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .eslintignore
================================================
/config/*
/src/registerServiceWorker.js
/node_modules/*
================================================
FILE: .eslintrc.js
================================================
module.exports = {
extends: ['eslint-config-alloy/typescript-react', 'prettier', 'prettier/react'],
plugins: ['prettier', 'typescript'],
globals: {
// 这里填入你的项目需要的全局变量
// 这里值为 false 表示这个全局变量不允许被重新赋值,比如:
//
// React: false,
// ReactDOM: false
},
parser: 'typescript-eslint-parser',
rules: {
// 这里填入你的项目需要的个性化配置,比如:
//
// // @fixable 一个缩进必须用两个空格替代
semi: ['error', 'never'],
// 'no-console': 'off',
'no-unused-vars': [
'warn',
{
vars: 'all',
args: 'none',
caughtErrors: 'none'
}
],
'max-nested-callbacks': 'off',
'react/no-children-prop': 'off',
'typescript/member-ordering': 'off',
'typescript/member-delimiter-style': 'off',
'react/jsx-indent-props': 'off',
'react/no-did-update-set-state': 'off',
indent: [
'off',
2,
{
SwitchCase: 1,
flatTernaryExpressions: true
}
]
}
}
================================================
FILE: .gitignore
================================================
# See https://help.github.com/ignore-files/ for more about ignoring files.
# dependencies
/node_modules
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
================================================
FILE: .postcssrc.js
================================================
module.exports = {
"plugins": {
"postcss-import": {},
"postcss-url": {},
"postcss-aspect-ratio-mini": {},
"postcss-write-svg": {
utf8: false
},
"postcss-cssnext": {},
"postcss-px-to-viewport": {
viewportWidth: 750,
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'], // (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.
},
"postcss-viewport-units": {},
"cssnano": {
preset: "advanced",
autoprefixer: false,
"postcss-zindex": false
}
}
}
================================================
FILE: .prettierrc.js
================================================
module.exports = {
printWidth: 120,
tabWidth: 2,
// useTabs: false,
semi: false,
singleQuote: true
// trailingComma: 'none'
// bracketSpacing: true,
// jsxBracketSameLine: false,
// arrowParens: 'avoid',
// rangeStart: 0,
// rangeEnd: Infinity,
// proseWrap: "preserve"
}
================================================
FILE: .vscode/settings.json
================================================
{
"javascript.implicitProjectConfig.experimentalDecorators": true,
"files.exclude": {
"**/.git": true
},
"cSpell.words": ["NETEASE"]
}
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2018 Wee
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
🎶 基于 React 实现的仿 iOS 客户端网易云音乐。
在线地址:**[戳我](http://118.24.21.99:5001/)**(PC 浏览器需切换到移动端模式)
移动端体验:

## 预览

## 技术栈
- React 16.3
- TypeScript
- Mobx + Redux
- react-redux
- react-router-v4
- Scss
## 实现细节
目前只实现了上面四个页面,但是总体的结构已经形成了,其他页面的添加只是时间上的问题 ~~(其实是懒)~~,暂时没有实现,下面是目前已实现的功能的细节:
### 局部状态管理
像首页的 banner 或者推荐歌单等,都是不会被共享的局部状态,使用 Mobx 来进行请求的发起和状态的管理。
### 全局状态
播放器的状态是一个全局状态,包括当前的播放列表,切歌,播放 / 暂停等,所以很自然的使用 redux 来进行管理,可以清楚的掌握所有改变全局状态的行为。
### TypeScript
尽管上手需要掌握一些语法,但是静态类型与自动提示都能提供很大的帮助,在这个并不大的项目中我也体验到了很大的帮助。但是要注意的是 TS 其实并不严格限制对象的类型,只要够懒,遍地 any,就会把 TS 写成 JS,所以为了充分发挥 TS 的威力,一定要有良好的 TS 代码风格。
### 手势滑动
为了模仿 iOS 端可以通过滑屏切换页面的功能,通过监听 `touchStart`,`touchMove`,`touchEnd` 来进行手势的判断并通过 `transform` 触发模拟滚动实现,在 `touchMove` 中检测监听滑动的方向及距离,在 `touchEnd` 中触发路由的切换及页面吸附到整屏的位置。
### 歌单的状态保留
有这么一个操作需要注意:用户在某歌单往下滑了几下,然后点了某歌播放然后进入了播放器,会发生路由的改变,如果此时从播放器返回,会丢包包括滚动位置在内的歌单页的所有状态丢失(因为 re-mount 了)。
我造了一个轮子来解决这个问题:[react-live-route](https://github.com/fi3ework/react-live-route),是对 react-router-v4 中 Route 组件的增强,简单的说就是将歌单页隐藏掉而不是 unmount 掉,具体的解决思路可以参考轮子里的文档。
### 跨组件传递状态
在 iOS 版的网易云中,可以滑动来切换页面,同时会触发顶部 tab 下的滑块移动。在项目中,滑动页面与滑块分属于两个兄弟组件的子组件且嵌套层次较深,如果直接通过 prop 来传递略显丑陋,有如下解决方案:
1. 通过 redux,但是 redux 最好只负责领域数据,这种 UI 的状态就不要往 store 中放了。
2. 通过 event-emitter,其实和 redux 差不多,因为 redux 也是基于 event-emitter 实现的, 但是不经过 react-redux 虽然可以实现,但是破坏了 react 整个自顶向下界面更新的原则。
3. 通过新的 context API 实现,如下图:

## API
项目中用到的网易云音乐的 API 来自 [NeteaseCloudMusicApi](https://binaryify.github.io/NeteaseCloudMusicApi/#/?id=neteasecloudmusicapi)。
## TODO
目前还有一些部分没完成,包括但不限于:
- [ ] code splitting
- [ ] 组件中有些功能还是有耦合,需要再抽象
- [ ] SSR
## 开发
克隆代码到本地之后,需要在 4000 端口运行 [NeteaseCloudMusicApi](https://binaryify.github.io/NeteaseCloudMusicApi/#/?id=neteasecloudmusicapi)。
================================================
FILE: config/env.js
================================================
'use strict';
const fs = require('fs');
const path = require('path');
const paths = require('./paths');
// Make sure that including paths.js after env.js will read .env variables.
delete require.cache[require.resolve('./paths')];
const NODE_ENV = process.env.NODE_ENV;
if (!NODE_ENV) {
throw new Error(
'The NODE_ENV environment variable is required but was not specified.'
);
}
// https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use
var dotenvFiles = [
`${paths.dotenv}.${NODE_ENV}.local`,
`${paths.dotenv}.${NODE_ENV}`,
// Don't include `.env.local` for `test` environment
// since normally you expect tests to produce the same
// results for everyone
NODE_ENV !== 'test' && `${paths.dotenv}.local`,
paths.dotenv,
].filter(Boolean);
// Load environment variables from .env* files. Suppress warnings using silent
// if this file is missing. dotenv will never modify any environment variables
// that have already been set. Variable expansion is supported in .env files.
// https://github.com/motdotla/dotenv
// https://github.com/motdotla/dotenv-expand
dotenvFiles.forEach(dotenvFile => {
if (fs.existsSync(dotenvFile)) {
require('dotenv-expand')(
require('dotenv').config({
path: dotenvFile,
})
);
}
});
// We support resolving modules according to `NODE_PATH`.
// This lets you use absolute paths in imports inside large monorepos:
// https://github.com/facebookincubator/create-react-app/issues/253.
// It works similar to `NODE_PATH` in Node itself:
// https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders
// Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored.
// Otherwise, we risk importing Node.js core modules into an app instead of Webpack shims.
// https://github.com/facebookincubator/create-react-app/issues/1023#issuecomment-265344421
// We also resolve them to make sure all tools using them work consistently.
const appDirectory = fs.realpathSync(process.cwd());
process.env.NODE_PATH = (process.env.NODE_PATH || '')
.split(path.delimiter)
.filter(folder => folder && !path.isAbsolute(folder))
.map(folder => path.resolve(appDirectory, folder))
.join(path.delimiter);
// Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be
// injected into the application via DefinePlugin in Webpack configuration.
const REACT_APP = /^REACT_APP_/i;
function getClientEnvironment(publicUrl) {
const raw = Object.keys(process.env)
.filter(key => REACT_APP.test(key))
.reduce(
(env, key) => {
env[key] = process.env[key];
return env;
},
{
// Useful for determining whether we’re running in production mode.
// Most importantly, it switches React into the correct mode.
NODE_ENV: process.env.NODE_ENV || 'development',
// Useful for resolving the correct path to static assets in `public`.
// For example, .
// This should only be used as an escape hatch. Normally you would put
// images into the `src` and `import` them in code to get their paths.
PUBLIC_URL: publicUrl,
}
);
// Stringify all values so we can feed into Webpack DefinePlugin
const stringified = {
'process.env': Object.keys(raw).reduce(
(env, key) => {
env[key] = JSON.stringify(raw[key]);
return env;
},
{}
),
};
return { raw, stringified };
}
module.exports = getClientEnvironment;
================================================
FILE: config/jest/cssTransform.js
================================================
'use strict';
// This is a custom Jest transformer turning style imports into empty objects.
// http://facebook.github.io/jest/docs/en/webpack.html
module.exports = {
process() {
return 'module.exports = {};';
},
getCacheKey() {
// The output is always the same.
return 'cssTransform';
},
};
================================================
FILE: config/jest/fileTransform.js
================================================
'use strict';
const path = require('path');
// This is a custom Jest transformer turning file imports into filenames.
// http://facebook.github.io/jest/docs/en/webpack.html
module.exports = {
process(src, filename) {
return `module.exports = ${JSON.stringify(path.basename(filename))};`;
},
};
================================================
FILE: config/jest/typescriptTransform.js
================================================
// Copyright 2004-present Facebook. All Rights Reserved.
'use strict';
const tsJestPreprocessor = require('ts-jest/preprocessor');
module.exports = tsJestPreprocessor;
================================================
FILE: config/paths.js
================================================
'use strict';
const path = require('path');
const fs = require('fs');
const url = require('url');
// Make sure any symlinks in the project folder are resolved:
// https://github.com/facebookincubator/create-react-app/issues/637
const appDirectory = fs.realpathSync(process.cwd());
const resolveApp = relativePath => path.resolve(appDirectory, relativePath);
const envPublicUrl = process.env.PUBLIC_URL;
function ensureSlash(path, needsSlash) {
const hasSlash = path.endsWith('/');
if (hasSlash && !needsSlash) {
return path.substr(path, path.length - 1);
} else if (!hasSlash && needsSlash) {
return `${path}/`;
} else {
return path;
}
}
const getPublicUrl = appPackageJson =>
envPublicUrl || require(appPackageJson).homepage;
// We use `PUBLIC_URL` environment variable or "homepage" field to infer
// "public path" at which the app is served.
// Webpack needs to know it to put the right