我们就爱程序媛

vue移动助手实践(一)——用sass和事件巴士写一个主题换肤

2017-10-09  本文已影响0人  Katherine的小世界

(By: Kath & kimmy)

最近在做的一个几月vue的移动端小demo,其中有一块是实现各个页面的统一换肤功能的。想着写一篇文章,来写一写实现过程中遇到的一些问题。

项目在线demo

项目在线演示demo(切换到移动端调试模式哦)

项目github地址

项目github地址

一 先看一下实现效果吧

设置主题颜色
讲道理这么一个功能,我觉得这么几点可以说下,分步实现:
1. 色值的选取
2. scss 的一些小众用法(多变量CSS值的批量设置)
3. 全局事件巴士的应用

1 色值的选取和原则

推荐大家看下蚂蚁金服的设计指引,里面对常见的交互和界面设计有一套不错的指引和建议,喜欢看书的也可以看看《写给大家看的设计书》。
对于界面中的色彩元素,我们一般要保持视觉的连续性,即同一套色彩,尽量采取同一个色环上的色值

同一个圆环上的色值作为一套颜色会显得更协调

所以这里采取ant design 的建议,取某一列色值作为我们的系列主题颜色(具体色值参照它的官网吧~)

而在某些特殊场合,需要表现出颜色的差异,如抛硬币页面的两个颜色,

2 将格式色值转换成十六进制颜色值

这里我们通过设置主题颜色的透明度来实现区分不同颜色, 然后我们是通过存储一个诸如#123456的16进制颜色全局变量作为我们主题,这里就需要我们把这样一个格式的色值转化成 rgba 表示的颜色值啦,代码如下,备用

  hexToRgba (hex, opacity = 0.3) {
    let color = []
    let rgb = []
    hex = hex.replace(/#/, '')
    for (let i = 0; i < 3; i++) {
      color[i] = '0x' + hex.substr(i * 2, 2)
      rgb.push(parseInt(Number(color[i])))
    }
    return `rgba(${rgb.join(',')},${opacity})`
  }

3 scss 的一些小众用法

我们最终拿到这么一串我们想要的主题颜色

$colors: #f04134, #00a854, #108ee9, #f5317f, #f56a00, #7265e6, #ffbf00, #00a2ae, #2e3238;

一个很直接的思路,我们需要在各个view页面里面,去定义我们需要设置主题的元素的颜色,比如文字和icon的color, 以及头部的background 等。 于是我们在app 里面定义一个color变量,派发到各个view组件里面去,通过这个全局的变量来控制所有路由页面的颜色,以实现不同的主题效果。
派发的实现在下一个部分说,这里我们先来完成我们的第一步,我们可以容易提取出我们的需求:
4 设置并保存一个全局颜色
界面的小事:
我在首页直接实现这个功能,项目中我引入了mint-ui 框架(饿了么团队的移动端框架,稍微遗憾使用感觉没有element.ui 的舒服), 设置的交互就用弹层 mt-popup 的形式好了,然后直接点击色块便设置对应颜色值

 <!-- 設置顏色 -->
    <mt-popup v-model="changColor" position="bottom" class="color-panel">
      <div class="color-items">
          <span class="color-item" v-for="(item, $index) in colors"  :key="$index" @click="chooseColor(item)">
            <span class="color-cycle" :class="'bg-color' + ($index + 1)"></span>
            </span>
      </div>
    </mt-popup>

接着就是色块div的呈现,从上面代码发现,我会很容易出现类似这样的css样式表

.bg-color1 {background: #f04134}
.bg-color2 {background: #f04134}
.bg-color3 {background: #f04134}
.bg-color4 {background: #f04134}
···

写代码时候如果我们一般发现,一件类似的东西重复出现了,就总隐隐觉得可以开始表演了,然后可预见的是,这样的情况意味着在项目增长后,还可能出现许多单一设置字体颜色或border颜色的样式表,诸如color1, borderColor1···,这样每种形式的表现我们都需要根据我们主题颜色的数组去逐条书写,修改成本也会变高 。于是我的书写风格是这样的,

// mixin.scss:
$colors: #f04134, #00a854, #108ee9, #f5317f, #f56a00, #7265e6, #ffbf00, #00a2ae, #2e3238;

// setColor.vue:
@import '~@/assets/mixin.scss';
···
@for $i from 1 to 10 {
        .bg-color#{$i} {
          background-color: nth($colors, $i)
        }
      }

scss 除了常用的类名嵌套书写外,还有许多···低调奢华的语法, 对于这类需要重复书写的样式类型,我的约定是添加一个scss变量在mixin 文件中, 在需要书写重复循环样式时候作为变量引入,并在书写样式时候,利用sass的循环,引用其中对应的值,这样无论设置颜色的样式怎么拓展和变化,变成颜色背景边框都好,我都只需要维护一份mixin的的文件里的色值就行了, 同样的实践也可以应用于项目里面字体大小和间距值的统一之类,总之我们多尝试体验下吧

5 逻辑的小事

这个项目里面localstorage 基本被当成数据库使用了,所以点击色块设置主题时候,我们假装发出请求,在localstorage存储我们改变的颜色就好了( ./static/api.json 是一个返回helloword 的json, 为了写实在这里这么用,$bus 事件巴士下面说, 作用就是设置全局的主题颜色变量,localStorage 模拟我们把设置存储到后台,每次重新打开页面就去获取这些设置值), 目前为止,我们的设置页面就大致完成了

// 假装调用接口设置颜色
    chooseColor (color) {
      this.$axios.get('./static/api.json')
        .then((data) => {
          this.$bus.$emit('set-theme', color)
          this.changColor = false
          localStorage.setItem('themeColor', color)
        })
        .catch((data) => {
          console.log(data)
        })
    }

6 事件巴士的运用

在上一步最后我们有个关键的东西没完成, this.$bus.$emit('set-theme', color),将选取的颜色设置到全局,我的代码结构是这样的

子组件

<setColor>home 页面的一个子组件,而在一开始我们已经说了,我们想在我们在app.vue (home.vue和其他view的父组件) 里面定义一个color变量,派发到各个view组件里面去。 于是这其实就是个,从setColor 触发app.vue的设置颜色事件, 子组件向父组件通信的问题。

我们可以很直接地用绑定事件配合emit()的做法,在app.vue 定义一个setglobalColor方法, 并绑定到router-view(包含了home.vue),接着在home组件继续定义一个setglobalColor方法, 实现的功能就是 emit('setglobalColor') 去触发app.vue的方法, 并把home.vue的这个setglobalColor继续绑定到<setColor>组件, 组件里面点选颜色时候,直接emit这个方法就行了。
为什么我想用事件巴士 .vue 的事件巴士和 vuex, 在一些有追求的程序员手里总是小心翼翼的,我也一样,因为作为涉及全局的东西,一般觉得能不用就不用,代码能精简就精简,我们经常用一个词,不提倡。
可是有朝一日我经常在想,代码的可读性可维护性,和性能以及“风险”相对比,到底哪个更重要。对于事件巴士和vuex 这类全局性质的方案的主要担忧大部分在于, 他们是全局的,可能因为一个事件名变量名一致就造成冲突,在小型项目还会造成冗余和额外开销。 但事实上,事件和变量的命名我们都可以通过约定去规范,而在表现上,使用了事件巴士和vuex的项目,在性能上和直接props传递数据,emit 回调事件的项目相比,其实并没有太大区别,反而是无止境的props 和 emit,给人一种麻烦难以维护的感觉。 像上述的setglobalColor, 仅仅是跨越了两层组件, 过程就显得繁琐了。所以我建议在出现两级以上组件层次,数据流稍微多的项目中都可以这么去做,定义一个全局的事件巴士

export default (Vue) => {
  let eventHub = new Vue()
  Vue.prototype.$bus = {
    $on (...arg) {
      eventHub.$on(...arg)
    },
    $off (...arg) {
      eventHub.$off(...arg)
    },
    $emit (...arg) {
      eventHub.$emit(...arg)
    }
  }
}

将事件巴士绑定到当前vue对象,使用时候只需要:

this.$bus.$on('set-theme', (color) => {··· })
this.$bus.$emit('set-theme', '#000000')

在这个demo中,我在app.vue 绑定了

this.$bus.$on('set-theme', (color) => {
      this.loadingColor = color
      this.userinfo.color = color
    })

而在setColor.vue则在点击颜色块时候触发 this.$bus.$emit('set-theme', color), 则能实现我们设置全局颜色的效果。这样的好处在于,对于跨了多个层次,或者兄弟组件的通信,我们不再需要太繁琐的props,比如我在header.vue 也绑定了this.$bus.$on('set-theme', (color) => { }),在 this.$bus.$emit发生时候,header 的背景颜色就能直接改变,而不需要等待app.vue 将 全局的color值props传递到header.vue里面(仅做示例,这里header.vue只是app.vue的下一层级,通过props数据流会更清晰)
而对于其他路由页面组件,和app.vue 都是直接上下级关系,我们依然采用props保持一个清晰的数据流向下传递,demo里我是将color存在userinfo(以后还有其他数据), userinfo传到每个子路由, 最后,每个页面在创建时候,通过拿到这个全局的颜色,再用dom去更改对应的样式就好啦,例如

mounted () {
    this.$nextTick(() => {
      // 绑定设置主题的事件,一旦触发修改主题,则将当前字体颜色改为对应颜色
      this.$el.querySelector('.myTitle').style.color = this.userinfo.color
      this.$el.querySelector('.weui-btn_primary').style.backgroundColor = this.userinfo.color
      this.$el.querySelector('.add_icon').style.color = this.userinfo.color
    })
  }

详细的实现请参照项目代码,这里我只挑一些比较清奇的点出来讨论,项目和代码的一些规范和习惯还是挺重要的,希望有好的实践能互相借鉴进步~

项目在线demo

项目在线演示demo(切换到移动端调试模式哦)

项目github地址

项目github地址

上一篇下一篇

猜你喜欢

热点阅读