Skip to content

Latest commit

 

History

History
774 lines (749 loc) · 20.1 KB

第10章:了不起的UI.md

File metadata and controls

774 lines (749 loc) · 20.1 KB

第10章:了不起的UI

对于用户来说,什么样的产品最吸引人?流畅性?安全?这些都是要使用才能感受到的,但是吸引用户使用你的产品的决定性因素是:颜值。只有漂亮好看的产品才能吸引用户,因此,作为产品的一部分,前端们有责任让产品的UI变的尽可能好看。

使用CSS预处理器

CSS预处理器几乎成为了每个合格前端的必修课了,什么是CSS预处理器?请看代码:

.a .b {
  border-radius: 2px;
  border: 2px solid #000;
}
.a:hover {
  border-radius: 5px;
  border: 5px solid #000;
}
.a .c {
  border-radius: 3px;
  border: 3px solid #000;
}
.a .d {
  border-radius: 4px; 
  border: 4px solid #000;
}

这种代码简直让人抓狂,因为你编写它的的时候觉得自己在浪费时间,但是CSS又只能这样写,所以有人想:能不能让CSS像编程语言一样,自动化程度高一点?于是CSS预处理器出现了。 现在主流的CSS预处理器有SassLess,我们不比较二者的区别,它们都是很好的工具,这里我选择sass作为预处理器,像前文那种代码,sass可以写成:

@mixin border($value) {
  border-radius: #{$value}px;
  border: #{$value}px solid #000;
}
.a {
  .b {
    @include border(2)
  }
  .c {
    @include border(3)
  }
  .d {
    @include border(4)
  }
  &:hover {
    @include border(5)
  }
}

一下就节约了不少工作量。

Sass支持变量,函数,循环,计算,嵌套等等功能,可以让我们更高效的编写繁琐的CSS。现在我们就在开发环境中集成Sass,在项目文件夹中依次安装: $ npm i node-sass --save-dev $ npm i css-loader --save-dev $ npm i style-loader --save-dev $ npm i sass-loader --save-dev

打开webpack.config.js,添加对于Sass的编译:

...
loaders: [
  ... //js处理
  {
    test: /\.scss$/,
    loaders: 'style!css!sass'
  }
]

这样,我们的webpack在遇到Sass的时候就会打包Sass了。

挑选配色

MD规范

对于程序员来说,设计是一个很头疼的问题,我们不可能像设计师一样做出很专业的设计,但是想要让一个页面“能看”,还是有很多办法的,比如查阅Google的设计语言Material Design。 Material Design是一种设计规范,提供了例如配色表,动画规范,排版指引,结构指导等一系列规范,遵从MD规范,我们的App也能变成高颜值。

运用你的优势

程序员比起设计师在UI上的唯一优势就是:我们了解代码,所以我们可以清楚的知道哪些华丽的效果可以实现,而哪些效果不行,因此,灵活运用我们的优势,发挥你对于动画的想象力,让UI动起来无疑是良策。

挑选配色

挑选配色可能是程序员最痛苦的环节了,有了MD规范,再也不为配色烦恼了,我们只要打开MD调色盘,选择自己喜欢的颜色,就可以得到一组漂亮的配色。

在你的项目中新建一个src/theme文件夹,存放我们的scss文件,在这里新建theme.js文件,然后新建一个src/theme/lib/文件夹,在其中新建两个文件:colors.scsslib.scss

colors.scss负责存放颜色变量,打开进行编辑:

$dark-primary: #512DA8;
$primary: #673AB7;
$light-primary: #D1C4E9;
$text-and-icons: #FFFFFF;
$accent: #FF4081;
$primary-text: #212121;
$secondary-text: #757575;
$divider: #BDBDBD;

这就是我们的应用要用到的所有颜色了。

然后我们打开lib.scss,引入colors.scss:

@import 'colors';

响应式

好的WebAPP应该能够在不同的客户端表现出理想的状态,举个例子: media

想要在不同终端都得到理想的效果,可行的办法有两个:

  1. 为不同终端单独做不同的页面。这种做法的优点是针对性强,适合用于移动端和PC端相差较大时使用,缺点是可能会拖慢开发进度,提高维护成本。
  2. 只做一套页面,然后使用CSS的媒体查询来达到自适应布局的目的,这种做法又被称为响应式。优点是开发快,易于维护。缺点是灵活性较差。

我们这里选择使用响应式来进行布局,在src/theme/lib中新建一个media.scss文件,进行编辑:

$small-screen-up: 769px !default;
$medium-screen-up: 993px !default;
$large-screen-up: 1201px !default;
$small-screen: 768px !default;
$medium-screen: 992px !default;
$large-screen: 1200px !default;
$medium-and-up: "only screen and (min-width : #{$small-screen-up})" !default;
$large-and-up: "only screen and (min-width : #{$medium-screen-up})" !default;
$small-and-down: "only screen and (max-width : #{$small-screen})" !default;
$medium-and-down: "only screen and (max-width : #{$medium-screen})" !default;
$medium-only: "only screen and (min-width : #{$small-screen-up}) and (max-width : #{$medium-screen})" !default;

这样我们就建立了一个媒体查询表。别忘了把它引入lib.scss中:

@import 'media';

之后想要针对某个元素写媒体查询时,只需要调用即可匹配想要的尺寸了,例如:

.div {
  @media #{$small-and-down} {
    width: 3rem;
    height: 3rem;
  }
}

栅格系统

栅格系统,Grid List是一种页面组织方式,通过把一定区域分割成等宽等高的栅格来组织页面。

大多数UI框架,例如bootstrap,amazeUI等都支持栅格系统,因为它可以让我们简单高效的完成布局。我们没道理不使用栅格系统,在src/theme/lib文件夹中创建grid.scss文件,进行编辑:

$num-cols: 12 !default;
$gutter-width: 1.5rem !default;
.container {
  margin: 0 auto;
  max-width: 1280px;
  width: 90%;
  .row {
    margin: {
      left:  (-1 * $gutter-width / 2);
      right:  (-1 * $gutter-width / 2);
    }
  }
}
@media #{$medium-and-up} {
  .container {
    width: 85%;
  }
}
@media #{$large-and-up} {
  .container {
    width: 70%;
  }
}
.section {
  padding-top: 1rem;
  padding-bottom: 1rem;
  &.no-pad {
    padding: 0;
  }
  &.no-pad-bot {
    padding-bottom: 0;
  }
  &.no-pad-top {
    padding-top: 0;
  }
}
.row {
  margin-left: auto;
  margin-right: auto;
  margin-bottom: 20px;
  &:after {
    content: "";
    display: table;
    clear: both;
  }
  .col {
    float: left;
    box-sizing: border-box;
    padding: 0 $gutter-width / 2;
    min-height: 1px;
    &[class*="push-"],
    &[class*="pull-"] {
      position: relative;
    }
    $i: 1;
    @while $i <= $num-cols {
      $perc: unquote((100 / ($num-cols / $i)) + "%");
      &.s#{$i} {
        width: $perc;
        margin-left: auto;
        left: auto;
        right: auto;
      }
      $i: $i + 1;
    }
    $i: 1;
    @while $i <= $num-cols {
      $perc: unquote((100 / ($num-cols / $i)) + "%");
      &.offset-s#{$i} {
        margin-left: $perc;
      }
      &.pull-s#{$i} {
        right: $perc;
      }
      &.push-s#{$i} {
        left: $perc;
      }
      $i: $i + 1;
    }
    @media #{$medium-and-up} {
      $i: 1;
      @while $i <= $num-cols {
        $perc: unquote((100 / ($num-cols / $i)) + "%");
        &.m#{$i} {
          width: $perc;
          margin-left: auto;
          left: auto;
          right: auto;
        }
        $i: $i + 1
      }
      $i: 1;
      @while $i <= $num-cols {
        $perc: unquote((100 / ($num-cols / $i)) + "%");
        &.offset-m#{$i} {
          margin-left: $perc;
        }
        &.pull-m#{$i} {
          right: $perc;
        }
        &.push-m#{$i} {
          left: $perc;
        }
        $i: $i + 1;
      }
    }
    @media #{$large-and-up} {
      $i: 1;
      @while $i <= $num-cols {
        $perc: unquote((100 / ($num-cols / $i)) + "%");
        &.l#{$i} {
          width: $perc;
          margin-left: auto;
          left: auto;
          right: auto;
        }
        $i: $i + 1;
      }
      $i: 1;
      @while $i <= $num-cols {
        $perc: unquote((100 / ($num-cols / $i)) + "%");
        &.offset-l#{$i} {
          margin-left: $perc;
        }
        &.pull-l#{$i} {
          right: $perc;
        }
        &.push-l#{$i} {
          left: $perc;
        }
        $i: $i + 1;
      }
    }
  }
}

这时候Sass的优势就体现出来了,我们可以用循环轻松构建栅格系统。别忘了把它引入lib.scss中,需要注意的是,我们的栅格系统依赖于之前的媒体查询系统,因此要在媒体查询系统之后进行引用:

@import 'grid';

想要使用栅格系统很简单,只要在需要的节点上加上指定类名即可,例如:

<div class="row">
  <div class="col s12"></div>
  <div class="col s6"></div>
  <div class="col s6"></div>
</div>

在这个小小的栅格系统中,我提供了:

  • container 容器(居中)
  • section 容器(无边距)
  • row 容器(栅格格式化)
  • s1-12 栅格
  • m1-12 栅格
  • l1-12 栅格
  • push 修饰符
  • pull 修饰符

动手写UI

清除默认样式

浏览器有一些默认样式,例如按钮,列表等,在开始编写UI前,为了防止这些默认样式对我们造成影响,我们先把会造成影响的样式清除。

src/theme/lib中新建一个文件reset.scss,写入以下代码:

body, div, ul {
  padding: 0;
  margin: 0;
}
li {
  list-style: none;
}
li, p, span, input, button, textarea {
  font-family: 'Roboto';
}
input, button, textarea {
  border: none;
  -webkit-appearance: none;
  outline: 0;
  background-color: transparent;
  -webkit-tap-highlight-color: transparent;
}

编写Todo的HTML

开始之前,我们先编写todo list的html 我们希望todo有两对状态:

  • 完成——未完成
  • 展开——非展开
  • 编辑——非编辑
    <div class="container">
      <ul class="todos row">
        <li class="todo done">
          <div class="info">
            <div class="toggle">
              <i class="material-icons">check_box</i>
              <i class="material-icons">check_box_outline_blank</i>
            </div>
            <div class="content">
              <span>这里是一个Todo</span>
              <input type="text" value="这里是一个Todo" />
            </div>
            <div class="operate">
              <i class="material-icons">mode_edit</i>
              <i class="material-icons">info</i>
              <i class="material-icons">clear</i>
              <i class="material-icons">done</i>
            </div>
          </div>
          <div class="detail">
            <div class="members">
              <i class="material-icons">people</i>
              <i class="material-icons">person_add</i>
              <ul>
                <li class="member">
                  <span>mirone</span>
                  <i class="material-icons">clear</i>
                </li>
              </ul>
            </div>
            <div>
              <i class="material-icons">access_time</i>
              <span>2016-12-25</span>
            </div>
          </div>
        </li>
      </ul>
    </div>

Scss编写

src/theme/components中新建文件todo.scss,写入代码:

ul.todos {
  box-shadow: 0 2px 5px 0 rgba(0,0,0,.16), 0 2px 10px 0 rgba(0,0,0,.12);
  i {
    cursor: pointer;
  }
  li.todo {
    border-bottom: 1px solid $divider;
    input {
      display: none;
    }
    & > div {
      display: flex;
      flex-wrap: wrap;
      align-items: stretch;
      justify-content: center;
      &.info > div {
        height: 24px;
        line-height: 24px;
        padding: 16px;
      }
      > .toggle {
        order: 0;
        flex-grow: 1;
      }
      > .content {
        order: 1;
        flex-grow: 9;
        font-size: 18px;
        input {
          font-size: 18px;
        }
      }
      > .operate {
        flex-grow: 2;
        order: 2;
        text-align: right;
        i {
          padding: 0 4px;
          &:last-child {
            display: none;
          }
        }
      }
    }
    & > .detail {
      display: none;
      color: $secondary-text;
      & > div {
        padding: 16px;
        flex: 1;
      }
      span {
        vertical-align: super;
      }
      ul {
        display: inline-flex;
        flex-wrap: wrap;
        align-items: stretch;
        justify-content: center;
      }
    }
  }
  li.undone {
    .info {
      color: $primary-text;
      transition: all 0.25s;
      cursor: default;
      i {
        transition: all 0.25s;
        color: transparent;
        visibility: hidden;
      }
      .toggle {
        i:first-child {
          display: none;
        }
      }
      &:hover {
        background-color: $light-primary;
        color: $dark-primary;
        i {
          color: $dark-primary;
          visibility: visible;
        }
      }
    }
  }
  li.done {
    .info {
      color: $primary-text;
      transition: all 0.25s;
      cursor: default;
      .content {
        text-decoration: line-through;
      }
      i {
        transition: all 0.25s;
        color: transparent;
        visibility: hidden;
      }
      .toggle {
        > i {
          display: none;
          &:first-child {
            display: inline-block;
            color: $primary-text;
            visibility: visible;
          }
        }
      }
      &:hover {
        background-color: $light-primary;
        color: $dark-primary;
        i {
          visibility: visible;
          color: $dark-primary !important;
        }
      }
    }
  }
  .members {
    flex: 2 !important;
    & > i {
      color: $secondary-text;
      &:last-of-type {
        display: none;
      }
    }
    .member {
      margin: 0 4px;
      i {
        display: none;
      }
    }
  }
  li.spread {
    .info {
      background-color: $primary;
      color: $text-and-icons;
      .toggle {
        i:first-child {
          color: $text-and-icons;
          visibility: visible;
        }
      }
      &:hover {
        .toggle {
          i:first-child {
            color: $dark-primary;
          }
        }
        .operate {
          i:nth-child(2) {
            color: $dark-primary;
          }
        }
      }
    }
    .operate {
      i:nth-child(2) {
        color: $text-and-icons;
        visibility: visible !important;
      }
    }
    .detail {
      display: flex;
    }
  }
  li.edit {
    .info {
      background-color: $light-primary;
      &:hover {
        i {
          visibility: hidden;
        }
        .operate {
          i {
            &:last-child {
              visibility: visible;
              display: inline-block !important;
            }
          }
        }
      }
    }
    .content {
      span {
        display: none;
      }
      input {
        display: block;
        background-color: $text-and-icons;
        width: 100%;
      }
    }
    .toggle {
      i {
        color: $primary-text !important;
      }
    }
    .operate {
      i {
        visibility: hidden;
        color: $primary-text !important;
        &:last-child {
          visibility: visible;
          display: inline-block !important;
        }
      }
    }
    .member {
      i {
        display: inline-block;
      }
    }
  }
}

现在我们的UI应该比较好看了: todo_ui

绑定数据

这样还不够,我们还要把UI和之前的数据结合起来使用,重新编辑html:

    <div id="todo" class="container">
      <ul class="todos row" data-list="todos">
        <li data-list-item="todos">
          <div class="todo" data-class="todos:finish">
          <div class="info">
            <div class="toggle" data-event="toggleTodo">
              <i class="material-icons">check_box</i>
              <i class="material-icons">check_box_outline_blank</i>
            </div>
            <div class="content">
              <span data-model="todos:todo"></span>
              <input type="text" data-model="todos:todo" />
            </div>
            <div class="operate">
              <i class="material-icons" data-event="edit">mode_edit</i>
              <i class="material-icons" data-event="spread">info</i>
              <i class="material-icons" data-event="remove">clear</i>
              <i class="material-icons" data-event="update">done</i>
            </div>
          </div>
          <div class="detail">
            <div class="members">
              <i class="material-icons">people</i>
              <i class="material-icons">person_add</i>
              <ul>
                <li class="member">
                  <span>mirone</span>
                  <i class="material-icons">clear</i>
                </li>
              </ul>
            </div>
            <div>
              <i class="material-icons">access_time</i>
              <span>2016-12-25</span>
            </div>
          </div>
          </div>
        </li>
      </ul>
    </div>

这样就写好了前端数据模版,然后进入src/user.js绑定数据:

import 'whatwg-fetch'
import './theme/theme.scss'
import Parser from './lib/parser'
var data = {
}
var eventList = {
  toggleTodo: {
    type: 'click',
    fn: function() {
      const _i = Array
        .prototype
        .indexOf
        .call(
          this.parentNode.parentNode.parentNode.parentNode.children, 
          this.parentNode.parentNode.parentNode
        )
      let _todo = data.todos[_i],
        state
      if(_todo.finish === 'done') {
        state = 'undone'
      } else {
        state = 'done'
      }
      fetch(`/todo/${_todo._id}`, {
        method: 'PUT',
        body: JSON.stringify({finish: state}),
        headers: {
          'Content-Type': 'application/json'
        }
      }).then(function(res){
        return res.json()
      }).then(function(json){
        if(json.finish) {
          _todo.finish = 'done'
        } else {
          _todo.finish = 'undone'
        }
      })
    }
  },
  edit: {
    type: 'click',
    fn: function() {
      const _todo = this.parentNode.parentNode.parentNode
      if(!_todo.classList.contains('edit')) {
        _todo.classList.add('edit')
      }
    }
  },
  spread: {
    type: 'click',
    fn: function() {
      const _todo = this.parentNode.parentNode.parentNode
      _todo.classList.toggle('spread')
    }
  },
  update: {
    type: 'click',
    fn: function() {
      const _i = Array
        .prototype
        .indexOf
        .call(
          this.parentNode.parentNode.parentNode.parentNode.parentNode.children, 
          this.parentNode.parentNode.parentNode.parentNode
        )
      const _node = this.parentNode.parentNode.parentNode
      let _todo = data.todos[_i]
      const _value = this.parentNode.previousElementSibling.lastElementChild.value
      fetch(`/todo/${_todo._id}`, {
        method: 'PUT',
        body: JSON.stringify({todo: _value}),
        headers: {
          'Content-Type': 'application/json'
        }
      }).then(function(res){
        return res.json()
      }).then(function(json){
        console.log(json)
        _todo.todo = json.todo
        _node.classList.remove('edit')
      })
    }
  },
  remove: {
    type: 'click',
    fn: function() {
      const _i = Array
        .prototype
        .indexOf
        .call(
          this.parentNode.parentNode.parentNode.parentNode.parentNode.children, 
          this.parentNode.parentNode.parentNode.parentNode
        )
      console.log(_i)
      let _todo = data.todos[_i]
      fetch(`/todo/${_todo._id}`, {
        method: 'DELETE',
      }).then(function(res){
        if(res) {
          data.todos.splice(_i, 1)
        }
      })
    }
  }
}
window.addEventListener('load', function(){
  fetch('/todo').then(function(res) {
    return res.json()
  }).then(function(json) {
    data.todos = []
    if(json) {
      json.forEach(function(todo) {
        let _thisTodo = {}
        _thisTodo.todo = todo.todo
        _thisTodo._id = todo._id
        if(todo.finish) {
          _thisTodo.finish = 'done' 
        } else {
          _thisTodo.finish = 'undone'
        }
        data.todos.push(_thisTodo)
      })
    }
    new Parser('#todo', data, eventList)
    console.log(data)
  })
})

打包编译,我们的应用就有还不错的UI了。