对于用户来说,什么样的产品最吸引人?流畅性?安全?这些都是要使用才能感受到的,但是吸引用户使用你的产品的决定性因素是:颜值。只有漂亮好看的产品才能吸引用户,因此,作为产品的一部分,前端们有责任让产品的UI变的尽可能好看。
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预处理器有Sass和Less,我们不比较二者的区别,它们都是很好的工具,这里我选择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了。
对于程序员来说,设计是一个很头疼的问题,我们不可能像设计师一样做出很专业的设计,但是想要让一个页面“能看”,还是有很多办法的,比如查阅Google的设计语言Material Design。 Material Design是一种设计规范,提供了例如配色表,动画规范,排版指引,结构指导等一系列规范,遵从MD规范,我们的App也能变成高颜值。
程序员比起设计师在UI上的唯一优势就是:我们了解代码,所以我们可以清楚的知道哪些华丽的效果可以实现,而哪些效果不行,因此,灵活运用我们的优势,发挥你对于动画的想象力,让UI动起来无疑是良策。
挑选配色可能是程序员最痛苦的环节了,有了MD规范,再也不为配色烦恼了,我们只要打开MD调色盘,选择自己喜欢的颜色,就可以得到一组漂亮的配色。
在你的项目中新建一个src/theme
文件夹,存放我们的scss文件,在这里新建theme.js
文件,然后新建一个src/theme/lib/
文件夹,在其中新建两个文件:colors.scss
和lib.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应该能够在不同的客户端表现出理想的状态,举个例子:
想要在不同终端都得到理想的效果,可行的办法有两个:
- 为不同终端单独做不同的页面。这种做法的优点是针对性强,适合用于移动端和PC端相差较大时使用,缺点是可能会拖慢开发进度,提高维护成本。
- 只做一套页面,然后使用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前,为了防止这些默认样式对我们造成影响,我们先把会造成影响的样式清除。
在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 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>
在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和之前的数据结合起来使用,重新编辑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了。