React全家桶构建一款Web音乐App实战(九):皮肤切换

这一节是这款React Web音乐App实战的最后一节:皮肤切换功能。皮肤切换是Web音乐App中一个与核心无关的功能,加入这个功能可以为应用增添不少趣味性

实现思路

实现皮肤切换功能的大致原理就是将样式提取出来作为一份单独的样式,给需要做皮肤切换的dom元素添加上这些样式,切换样式的时候替换指定样式属性值,再动态插入到DOM中,使CSS样式生效

准备

在做这个功能前,需要新加入一些小图标,新增的小图标在下图红色方框中标出

在第一节已经制作了一份字体图标文件和样式,这次加入新的图标需要使用svg图片重新来制作新的字体图标文件和样式,和第二节一样使用icomoon这个网站来制作,详细步骤见第一节字体图标制作

字体图标制作完成后会有一份icomoon.zip包,解压后将里面fonts目录下的4个文件重命名为icomusic,然后进入到项目中将src/assets/stylus/fonts目录下面的4个文件替换成刚刚重命名的4个字体图标文件。回到src/assets/stylus下面的font.styl中,打开解压后的目录中的style.css,复制里面的.icon-开头的所有样式,替换掉font.styl中.icon-开头的样式

提取样式

在做皮肤切换功能前先将需要切换的样式提取出来。这里因为是所有功能做完之后再做皮肤切换功能,所以提取样式是非常繁琐的过程。以下是列举出所有需要提取样式的地方

文件位置样式名称
app.styl.app,.app-header,.music-tab,.active
recommend/recommend.styl.title,.album-wrapper,.album-name
album/album.styl.music-album,.album-wrapper,.song-name,.song-singer,.album-title
ranking/ranking.styl.ranking-wrapper,.ranking-title,.index,.singer
ranking/rankinginfo.styl.ranking-info,.ranking-wrapper,.song-name,.song-singer,.ranking-title
singer/singer.styl.music-singer,.singer-wrapper,.song-name,.song-singer
singer/singerlist.styl.nav,a.choose,.singer-name
search/search.styl.search-box,.search-input,.title,.hot-item,.album-wrapper .song,.singer,.song-wrapper .song
play/miniplayer.styl.mini-player,.player-img,.singer,.player-right,.filter:after

将以上样式中的colorbackground-color属性注释掉

实现皮肤切换的做法有很多种。可能你见过将不同的样式写在一份css文件里面,多少种皮肤就有多少份css文件,使用某种皮肤的时候将其引入,这种做法重复定义的属性太多,大量的css属性冗余,复用性太差。还有可能你见过写一份样式文件,样式属性的值使用指定字符占位,需要切换皮肤的时候请求这个文件,拿到样式文本后,替换掉占位的字符为实际的属性值,然后插入到HTML DOM中,这种做法每次切换皮肤的时候都要发送一次请求

这里将利用字符串模板返回一个样式文本字符串,样式属性值使用对象的属性占位,将css属性值定义成对象属性值,在切换皮肤的时候传入指定的对象。在util目录下新建skin.js,先定义用来保存样式属性值的对象skin,和一个返回样式文本的方法getSkinStyle

const skin = {};

skin.coolBlack = {
    appColor: "#DDDDDD",
    appBgColor: "#212121",
    /* 首页header */
    appHeaderColor: "#FFD700",
    appHeaderBgColor: "transparent",
    /* 首页tab */
    tabColor: "#DDDDDD",
    tabBgColor: "transparent",
    /* 最新专辑 */
    albumColor: "rgba(221, 221, 221, 0.7)",
    albumNameColor: "#FFFFFF",
    /* 排行榜 */
    rankingWrapperBgColor: "#333333",
    rankingSingerColor: "rgba(221, 221, 221, 0.7)",
    /* 搜索 */
    searchBgColor: "#212121",
    searchBoxBgColor: "#333333",
    searchBoxWrapperBgColor: "#212121",
    searchTitleColor: "#FFD700",
    searchHotColor: "#DDDDDD",
    searchHotBorderColor: "transparent",
    searchResultBorderColor: "transparent",
    /* 详情 */
    detailBgColor: "#212121",
    detailSongColor: "#FFFFFF",
    detailSingerColor: "rgba(221, 221, 221, 0.7)",
    /* mini播放器 */
    miniPlayerBgColor: "#333333",
    miniImgBorderColor: "rgba(221, 221, 221, 0.3)",
    miniProgressBarBgColor: "rgba(0, 0, 0, 0.3)",
    miniRightColor: "#FFD700",
    miniSongColor: "#FFFFFF",
    activeColor: "#FFD700"
};

let getSkinStyle = (skin) => {
    if (!skin) {
        return "";
    }
    return `
    .skin-app {
      color: ${skin.appColor};
      background-color: ${skin.appBgColor};
    }
    .skin-app-header {
      color: ${skin.appHeaderColor};
      background-color: ${skin.appHeaderBgColor};
    }
    .skin-music-tab {
      color: ${skin.tabColor};
      background-color: ${skin.tabBgColor};
    }
    .skin-recommend-title {
      color: ${skin.activeColor};
    }
    .skin-album-wrapper {
      color: ${skin.albumColor};
    }
    .skin-album-wrapper .album-name {
      color: ${skin.albumNameColor}
    }
    .skin-ranking-wrapper {
      background-color: ${skin.rankingWrapperBgColor};
    }
    .skin-ranking-wrapper .ranking-title {
      color: ${skin.albumNameColor};
    }
    .skin-ranking-wrapper .singer {
      color: ${skin.rankingSingerColor};
    }
    .skin-music-singers .choose {
      color: ${skin.activeColor} !important;
      border: 1px solid ${skin.activeColor} !important;
    }
    .skin-search {
      background-color: ${skin.searchBgColor};
    }
    .skin-search .title {
      color: ${skin.searchTitleColor};
    }
    .skin-search .hot-item {
      border: 1px solid ${skin.searchHotBorderColor};
      color: ${skin.searchHotColor};
      background-color: ${skin.searchBoxBgColor};
    }
    .skin-search-box {
      background-color: ${skin.searchBoxBgColor};
    }
    .skin-search-box input {
      color: ${skin.appColor};
    }
    .skin-search-box-wrapper {
      background-color: ${skin.searchBoxWrapperBgColor};
    }
    .skin-search-result .singer {
      color: ${skin.albumColor};
    }
    .skin-search-result .singer-wrapper .singer {
      color: ${skin.appColor};
    }
    .skin-search-result .singer-wrapper .info {
      color: ${skin.albumColor};
    }
    .skin-detail-wrapper {
      background-color: ${skin.detailBgColor};
    }
    .skin-detail-wrapper .song-name {
      color: ${skin.detailSongColor};
    }
    .skin-detail-wrapper .song-singer {
      color: ${skin.detailSingerColor};
    }
    .skin-mini-player {
      background-color: ${skin.miniPlayerBgColor};
    }
    .skin-mini-player .player-img {
      border: 2px solid ${skin.miniImgBorderColor};
    }
    .skin-mini-player .progress-bar {
      background-color: ${skin.miniProgressBarBgColor} !important;
    }
    .skin-mini-player .progress {
      background-color: ${skin.miniRightColor} !important;
    }
    .skin-mini-player .player-right {
      color: ${skin.miniRightColor};
    }
    .skin-mini-player .song {
      color: ${skin.miniSongColor};
    }
    .skin-mini-player .singer {
      color: ${skin.detailSingerColor};
    }
    .music-album, .ranking-info, .music-singer {
      background-color: ${skin.detailBgColor};
    }
    .nav-link.active {
      color: ${skin.activeColor} !important;
      border-bottom: 2px solid ${skin.activeColor};
    }
  `;
};
复制代码

skin.coolBlack中的颜色值是从上述表格中的样式中提取出来的

编写一个把样式插入到HTML DOM中的方法setSkinStyle

let setSkinStyle = (skin) => {
    let styleText = getSkinStyle(skin);
    let oldStyle = document.getElementById("skin");
    const style = document.createElement("style");
    style.id = "skin";
    style.type = "text/css";
    style.innerHTML = styleText;
    oldStyle ? document.head.replaceChild(style, oldStyle) : document.head.appendChild(style);
};
复制代码

在skin.js中调用setSkinStyle并传入skin.coolBlack

// 设置皮肤
setSkinStyle(skin.coolBlack);
复制代码

最后导出skin对象和setSkinStyle,后续使用

export {skin, setSkinStyle}
复制代码

在程序运行的时候需要把这些样式插入到HTML DOM中,所以在Root.js中导入skin.js

import "../util/skin"
复制代码

接下来把提取出来的样式添加到各个组件中的标签上,下表列出来样式添加的位置

组件位置元素需要添加的样式
App.jsdiv.app
div.app-header
div.music-tab
.skin-app
.skin-app-header
.skin-music-tab
recommend/Recommend.jsdiv.album-wrapper
h1.title
.skin-album-wrapper
album/Album.jsdiv.album-wrapper.skin-detail-wrapper
ranking/Ranking.jsdiv.ranking-wrapper.skin-ranking-wrapper
ranking/RankingInfo.jsdiv.ranking-wrapper.skin-detail-wrapper
singer/SingerList.jsdiv.music-singers.skin-music-singers
singer/Singer.jsdiv.singer-wrapper.skin-detail-wrapper
search/Search.jsdiv.music-search
div.search-box-wrapper
div.search-box
div.search-result
.skin-search
.skin-search-box-wrapper
.skin-search-box
.skin-search-result
play/MiniPlayer.jsdiv.mini-player

应用上以上样式后,如果没有问题整体外观和之前会相差无几

自定义皮肤

除了以上的默认皮肤外,再定义几种样式。先定义一个芒果颜色做皮肤色,另外再使用酷狗、网易、QQ音乐三大音乐播放器的主色做皮肤色,接下来扩展skin对象,在skin.js中加入以下代码

  1. 芒果黄
skin.mangoYellow = {
    appColor: "#333333",
    appBgColor: "#F8F8FF",
    appHeaderColor: "#FFFFF0",
    appHeaderBgColor: "#FFA500",
    tabColor: "rgba(0, 0, 0, .7)",
    tabBgColor: "#FFFFFF",
    albumColor: "rgba(0, 0, 0, 0.6)",
    albumNameColor: "#333333",
    rankingWrapperBgColor: "#FFFFFF",
    rankingSingerColor: "rgba(0, 0, 0, 0.5)",
    searchBgColor: "#FFFFFF",
    searchBoxBgColor: "#FFFFFF",
    searchBoxWrapperBgColor: "#F8F8FF",
    searchTitleColor: "rgba(0, 0, 0, .7)",
    searchHotColor: "#000000",
    searchHotBorderColor: "rgba(0, 0, 0, .7)",
    searchResultBorderColor: "#E5E5E5",
    detailBgColor: "#F8F8FF",
    detailSongColor: "#000000",
    detailSingerColor: "rgba(0, 0, 0, 0.6)",
    miniPlayerBgColor: "#FFFFFF",
    miniImgBorderColor: "#EEEEEE",
    miniProgressBarBgColor: "rgba(0, 0, 0, 0.1)",
    miniRightColor: "#FFD700",
    miniSongColor: "#333333",
    activeColor: "#FFA500"
};
复制代码
  1. 酷狗蓝
skin.kuGouBlue = Object.assign({}, skin.mangoYellow, {
  appHeaderBgColor: "#2CA2F9",
  activeColor: "#2CA2F9",
  searchTitleColor: "#2CA2F9",
  miniRightColor: "#2CA2F9"
});
复制代码
  1. 网易红
skin.netBaseRed = Object.assign({}, skin.mangoYellow, {
  appHeaderBgColor: "#D43C33",
  activeColor: "#D43C33",
  searchTitleColor: "#D43C33",
  miniRightColor: "#D43C33"
});
复制代码
  1. QQ绿
skin.qqGreen = Object.assign({}, skin.mangoYellow, {
  appHeaderBgColor: "#31C27C",
  activeColor: "#31C27C",
  searchTitleColor: "#31C27C",
  miniRightColor: "#31C27C"
});
复制代码

以上对象中,后三个继承自mangoYellow对象,然后将不同属性值覆盖,可以很好的减少了相同样式的冗余

皮肤切换实现

为了实现皮肤切换,需要编写一个皮肤中心组件,皮肤中心从App组件中的菜单列表进入

在App.js中添加constructor,并初始化控制显示菜单的state属性menuShow

constructor(props) {
    super(props);

    this.state = {
        menuShow: false
    };
  }
复制代码

header.app-header元素中添加图标i.icon-et-more,并添加点击事件处理,点击后将menuShow设置为true

<header className="app-header skin-app-header">
  <i className="icon-et-more app-more" onClick={() => {this.setState({menuShow: true});}}></i>
  <img src={logo} className="app-logo" alt="logo" />
  <h1 className="app-title">Mango Music</h1>
</header>
复制代码

样式如下

App.styl

.app-header
    height: 55px
    line-height: 55px
    /*color: #FFD700*/
    text-align: center
    position: relative
    .app-more
      position: absolute
      top: 15px
      left: 15px
      font-size: 20px
复制代码

在components目录下新建setting目录,然后新建Menu.jsmenu.styl

Menu.js

import React from "react"
import {CSSTransition} from "react-transition-group"

import "./menu.styl"

class Menu extends React.Component {
    constructor(props) {
        super(props);
    }
    close = () => {
        this.props.closeMenu();
    }
    render() {
        return (
            <div>
                <CSSTransition in={this.props.show} timeout={300} classNames="fade"
                       onEnter={() => {
                           this.refs.bottom.style.display = "block";
                       }}
                       onExited={() => {
                           this.refs.bottom.style.display = "none";
                       }}>
                    <div className="bottom-container" onClick={this.close}  ref="bottom">
                        <div className="bottom-wrapper">
                            <div className="item">
                                皮肤中心
                            </div>
                            <div className="item-close" onClick={this.close}>
                                关闭
                            </div>
                        </div>
                    </div>
                </CSSTransition>
            </div>
        );
    }
}

export default Menu
复制代码

menu.styl请在源码中查看

在App.js中导入Menu组件

import MusicMenu from "./setting/Menu"
复制代码

放置在如下位置,并传递showcloseMenu两个props,其中show用来控制Menu组件的显示和隐藏动画,closeMenu传递给Menu,当点击取消或背景遮罩时关闭自身

<Router>
  <div className="app skin-app">
    ...
    <MusicPlayer/>
    <MusicMenu show={this.state.menuShow}
               closeMenu={() => {this.setState({menuShow: false});}} />
  </div>
</Router>
复制代码

我们把当前皮肤的key值交给Redux,在Skin组件中列出所有的皮肤,将Redux中保存的key对应的皮肤打上对钩的标记,点击单个皮肤可以设置当前皮肤。给皮肤添加Redux属性skin,actionType,action和reducer

actionTypes.js

export const SET_SKIN = "SET_SKIN";
复制代码

actions.js

export function setSkin(skin) {
	return {type:ActionTypes.SET_SKIN, skin};
}
复制代码

reducers.js

const initialState = {
    skin: "coolBlack",
    ...
};

//设置皮肤
function skin(skin = initialState.skin, action) {
	switch (action.type) {
		case ActionTypes.SET_SKIN:
			return action.skin;
		default:
			return skin;
	}
}

...

const reducer = combineReducers({
    skin,
    ...
});
复制代码

在setting目录下新建Skin.js和skin.styl

import React from "react"
import {CSSTransition} from "react-transition-group"

import "./skin.styl"

class Skin extends React.Component {
    constructor(props) {
        super(props);
        this.skins = [
            {key: "mangoYellow", name: "芒果黄", color: "#FFD700"},
            {key: "coolBlack", name: "炫酷黑", color: "#212121"},
            {key: "kuGouBlue", name: "酷狗蓝", color: "#2CA2F9"},
            {key: "netBaseRed", name: "网易红", color: "#D43C33"},
            {key: "qqGreen", name: "QQ绿", color: "#31C27C"}
        ]
    }
    render() {
        return (
            <CSSTransition in={this.props.show} timeout={300} classNames="pop"
                           onEnter={() => {
                               this.refs.skin.style.display = "block";
                           }}
                           onExited={() => {
                               this.refs.skin.style.display = "none";
                           }}>
                <div className="music-skin" ref="skin">
                    <div className="header">
                        皮肤中心
                        <span className="cancel" onClick={() => {this.props.close();}}>取消</span>
                    </div>
                    <div className="skin-title">推荐皮肤</div>
                    <div className="skin-container">
                        {
                            this.skins.map(skin => (
                                <div className="skin-wrapper" key={skin.key}>
                                    <div className="skin-color" style={{backgroundColor: skin.color, boxShadow: `0 0 3px ${skin.color}`}}>
                                        <i className="icon-right" style={{display: skin.key === this.props.currentSkin ? "" : "none"}}></i>
                                    </div>
                                    <div>{skin.name}</div>
                                </div>
                            ))
                        }
                    </div>
                </div>
            </CSSTransition>
        );
    }
}

export default Skin
复制代码

skin.styl代码请在源码中查看

上诉代码在constructor中定义5中皮肤对象,用key属性标识某一个皮肤,这个key值对应到Redux中的skin,name和color对应皮肤名称和皮肤主色。在containers目录下新建Skin.js将Skin包装成容器组件

import {connect} from "react-redux"
import {setSkin} from "../redux/actions"
import Skin from "../components/setting/Skin"

const mapStateToProps = (state) => ({
    currentSkin: state.skin
});

const mapDispatchToProps = (dispatch) => ({
    setSkin: (skin) => {
        dispatch(setSkin(skin));
    }
});

export default connect(mapStateToProps, mapDispatchToProps)(Skin)
复制代码

在Menu组件中导入Skin容器组件,然后添加一个state属性skinShow控制Skin显示或隐藏,同时编写一个改变skinShow的方法showSetting

导入Skin

import Skin from "../../containers/Skin"
复制代码

constructor初始化skinShow

constructor(props) {
    super(props);
    this.state = {
        skinShow: false
    };
}
复制代码

showSetting方法中先关闭当前页面,然后将skinShow设置为true或false

showSetting = (status) => {
    this.close();
    // menu关闭后打开设置
    setTimeout(() => {
        this.setState({
            skinShow: status
        });
    }, 300);
}
复制代码

SKin组件放置在如下位置,传入show控制显示和隐藏,close方法用来关闭皮肤中心页面

<div>
    <CSSTransition in={this.props.show} timeout={300} classNames="fade"
       ...
    </CSSTransition>
    <Skin show={this.state.skinShow} close={() => {this.showSetting(false);}} />
</div>
复制代码

给皮肤中心添加点击事件,点击后调用showSetting,显示皮肤中心页面

<div className="bottom-wrapper">
    <div className="item" onClick={() => {this.showSetting(true);}}>
        皮肤中心
    </div>
    ...
</div>
复制代码

回到Skin组件中,给皮肤添加点击事件,点击后将当前皮肤的key传入,调用util下skin.js中导出的setSkinStyle方法设置皮肤,然后将皮肤设置到Redux的状态属性中保存,再调用props中的close方法关闭页面

skin.js

import {skin, setSkinStyle} from "../../util/skin"
复制代码
setCurrentSkin = (key) => {
    // 设置皮肤
    setSkinStyle(skin[key]);
    this.props.setSkin(key);
    // 关闭当前页面
    this.props.close();
}
复制代码
<div className="skin-wrapper" onClick={() => {this.setCurrentSkin(skin.key);}} key={skin.key}>
    ...
</div>
复制代码

皮肤持久化保存

做完皮肤切换功能后,每次刷新或重新进入页面上次设置的皮肤会切换为默认的黑色,我们想让上一次设置的皮肤在刷新或重新进入时都是一样的,这时需要将皮肤的key值保存到本地

在util目录下的storage.js中添加两个方法

let localStorage = {
    setSkin(key) {
        window.localStorage.setItem("skin", key);
    },
    getSkin() {
        let skin = window.localStorage.getItem("skin");
        return !skin ? "coolBlack" : skin;
    },
    ...
}
复制代码

在reducers.js将skin写死的默认值从localStorage中获取,设置skin的reducer方法中将皮肤的key保存到localStorage中

const initialState = {
    skin: localStorage.getSkin(),  //皮肤
    ...
};

//设置皮肤
function skin(skin = initialState.skin, action) {
    switch (action.type) {
        case ActionTypes.SET_SKIN:
            localStorage.setSkin(action.skin);
            return action.skin;
        default:
            return skin;
    }
}
复制代码

然后在util下的skin.js中将调用setSkinStyle(skin.coolBlack)中的skin.coolBlack换成从localStorage中获取

import localStorage from "./storage"

...

setSkinStyle(skin[localStorage.getSkin()]);
复制代码

效果

总结

本节主要内容是切换皮肤的功能,实现的原理总的来说就是提取样式,切换的时候替换样式,再插入到HTML DOM中,实现的方式有多种,这里主要选取一种比较合适的方式来做。

本系列所有章节到此结束

本章节代码在chapter9分支

完整项目地址:github.com/code-mcx/ma…

体验地址:code-mcx.github.io/mango-music

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值