前言
前端框架多嗎?
多!
前端UI組件庫多嗎?
更多!
我們都知道,前端生態(tài)圈里提供了各色各樣的組件庫供我們選擇使用,大多數(shù)都能滿足開發(fā)者的需求,相信大家也都用過很多。但是實(shí)際上,據(jù)我了解到的,稍微大一些有自己產(chǎn)品的公司都會有一套自定義的UI組件庫,滿足自身復(fù)雜的需求與絢麗的效果。
博主目前所在的公司也有一套自己的產(chǎn)品,PC端所用的前端框架是Angular4.
Angular4其實(shí)也有它專門定制的前端組件庫PrimeNg.就像Vue.js有Element一樣.
那么按理在開發(fā)中我們已經(jīng)有了前端組件庫可以使用,為什么還要花那么多的精力和時間來重新設(shè)計(jì)UI組件呢?話不多說,先上幾張官方提供的UI組件圖:


確實(shí)不是我吐槽,官方提供的一些組件樣式真的有點(diǎn)奇怪??...雖然Angular官網(wǎng)組件樣式這一文檔中已經(jīng)說明了可以用
::ng-deep來進(jìn)行對組件樣式的修改,但修改起來還是比較麻煩。有那時間,自己都已經(jīng)擼了一個了...(項(xiàng)目中用的
primeng是v4.2.2版本的,目前已經(jīng)迭代到了v6.1.5,所以現(xiàn)在官網(wǎng)上看到的inputSwitch組件會比這個好看點(diǎn))emmm....為了追求用戶體驗(yàn)(呸,熟悉
angular4的使用)所以博主決定利用閑暇之余自定義一些UI組件,以滿足我們產(chǎn)品"一些無禮的要求"。
一、確定組件存放的位置
一個項(xiàng)目中會有各種文件、文件夾,如何存放管理好這些文件真的很重要。不僅為自己提供了方便,也為后來的開發(fā)者提供方便。
所以我們在設(shè)計(jì)公用組件的時候也應(yīng)該把它們都?xì)w結(jié)在一起。
我習(xí)慣在項(xiàng)目中新建一個common文件夾,里面存放一些共用的compoent,service等等。

如上圖,可以看到
common文件夾下導(dǎo)出的是一個名為shared的模塊。shared模塊的創(chuàng)建過程:(1)打開命令行(使用vscode編輯器的小伙可以直接使用Ctrl+` 快捷鍵打開終端,然后一路跳轉(zhuǎn)到common文件夾:
cd src\app\common
(2) 使用創(chuàng)建模塊的指令:
ng g m shared
其實(shí)很好理解:ng為angular一貫的指令,g為generate創(chuàng)建的縮寫,m為module模塊的縮寫,后面接著你的模塊名。(后面創(chuàng)建組件也是這個原理)
創(chuàng)建的模塊實(shí)際上導(dǎo)出的是一個帶有@NgModule裝飾器的類而已,其中提供了我們自定義的公有組件component,公有服務(wù)service,以及管道pipe等等。
二、創(chuàng)建組件
由于我們要創(chuàng)建的是一個switch公用組件,所以在component文件夾下在創(chuàng)建一個文件夾general-control,之前都是直接堆積在component文件夾下的,近期發(fā)現(xiàn)堆得有點(diǎn)多了,所以又單獨(dú)創(chuàng)建了一個general-control文件夾來存放一些基礎(chǔ)的公用組件。
此時你需要打開命令行(使用vscode編輯器的小伙可以直接使用Ctrl+` 快捷鍵打開終端,然后一路跳轉(zhuǎn)到general-control文件夾:
cd src\app\common\component\general-control
在此目錄下執(zhí)行指令:
ng g c switch
上面指令的意思是創(chuàng)建一個名為switch的組件,原理和創(chuàng)建模塊時一樣。
可以看到現(xiàn)在的general-control文件夾下多出了一些東西:

沒錯,就是我們使用指令創(chuàng)建的
switch組件。指令會自動幫你生成一個文件夾和4個文件。(基于
TypeScript的語法,所以生成的js文件也就是ts)很好理解,對應(yīng)的
html文件編寫HTML代碼,css文件編寫CSS代碼,ts文件編寫js代碼,至于spec.ts文件我們可以不用管它。由于我在項(xiàng)目中使用的是sass,所以將
switch.component.css這個文件的后綴名修改為scss(使用了less等其它擴(kuò)展語言的小伙同理),并在ts中對css的引用進(jìn)行修改:

使用上面的指令創(chuàng)建的組件是會被自動引用到shared這個模塊中的。
shared.module.ts:
import { SwitchComponent } from './component/general-control/switch/switch.component';//模塊中import引入組件
@NgModule({
declarations: [
SwitchComponent //模塊中聲明組件
...
]
})
上面?zhèn)z步是你在使用ng g c switch指令時自動幫你完成的,但若是你想在其它的模塊中使用這個switch組件,還得將其導(dǎo)出,導(dǎo)出的方式是將這個組件添加至shared.module.ts文件的exports中:
import { SwitchComponent } from './component/general-control/switch/switch.component';//模塊中import引入組件
@NgModule({
declarations: [
SwitchComponent //模塊中聲明組件
...
],
exports: [
SwitchComponent //模塊中導(dǎo)出組件
...
]
})
完成上面的步驟你就可以安心的來開發(fā)自己的組件了。
三、編寫switch組件
一番查找,發(fā)現(xiàn)網(wǎng)上也有很多自定義switch組件的文章和源碼,可能是大家都覺得原生的樣式不好看吧...
有使用input然后來進(jìn)行修改樣式的,也有用其它標(biāo)簽來自定義的。
博主這里找了一個最簡單方案,一個span標(biāo)簽搞定:
// switch.component.html
<span class="weui-switch" [ngClass]="currentClass" [ngStyle]="style" (click)="toggle()">
</span>
基礎(chǔ)css
// switch.comonent.scss
.weui-switch {
display: inline-block;
position: relative;
width: 38px;
height: 23px;
border: 1px solid #DFDFDF;
outline: 0;
border-radius: 16px;
box-sizing: border-box;
background-color: #DFDFDF;
transition: background-color 0.1s, border 0.1s;
cursor: pointer;
&.disabled{
opacity: 0.6;
cursor: not-allowed;
}
}
.weui-switch:before {
content: " ";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 15px;
background-color: #FDFDFD;
transition: transform 0.35s cubic-bezier(0.45, 1, 0.4, 1);
}
.weui-switch:after {
content: " ";
position: absolute;
top: 0;
left: 0;
width: 56%;
height: 97%;
border-radius: 15px;
background-color: #FFFFFF;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
transition: transform 0.35s cubic-bezier(0.4, 0.4, 0.25, 1.35);
}
.weui-switch-on {
border-color: #1AAD19;
background-color: #1AAD19;
}
.weui-switch-on:before {
border-color: #1AAD19;
background-color: #1AAD19;
}
.weui-switch-on:after {
transform: translateX(77%);
}
效果大概就是這個樣子:

(錄頻并轉(zhuǎn)換GIF推薦使用GifGam)
可以看到,組件的樣式設(shè)計(jì)大多都是使用偽類
:after和:before來實(shí)現(xiàn)的,而開關(guān)的效果是通過點(diǎn)擊的時候添加/移除
class名weui-switch-on來實(shí)現(xiàn)的。(講js的時候會講到)
由于我們創(chuàng)建的switch組件是需要在多處使用,并且要向外輸出一些值,所以在ts中我們首先要引入一下@Input、@Output裝飾器和EventEmitter。
import { Component, OnInit, Input, Output, EventEmitter, OnChanges } from '@angular/core';
并且定義一些基礎(chǔ)的變量
@Input() style;//{ 'width': '40px' }//外部組件輸入的樣式對象
@Input() isChecked: boolean = false;//開關(guān)是否打開
@Input() disabled: boolean = false;//開關(guān)是否被禁用
@Output() change: EventEmitter<any> = new EventEmitter();
_isSwitch: boolean = false;
currentClass = {}
此時我們的ts變成了這樣:
import { Component, OnInit, Input, Output, EventEmitter, OnChanges } from '@angular/core';
@Component({
selector: 'app-switch',
templateUrl: './switch.component.html',
styleUrls: ['./switch.component.scss']
})
export class SwitchComponent implements OnInit, OnChanges {
constructor() { }
@Input() style;//{ 'width': '40px' }//外部組件輸入的樣式對象
@Input() isChecked: boolean = false;//外部組件輸入進(jìn)來的:開關(guān)是否打開
@Input() disabled: boolean = false;//開關(guān)是否被禁用
@Output() change: EventEmitter<any> = new EventEmitter();
_isSwitch: boolean = false;//switch組件本身的:開關(guān)是否打開
currentClass = {} //class集合
ngOnInit() {//初始化組件的生命周期
}
ngOnChanges() {//當(dāng)被綁定的輸入屬性的值發(fā)生變化時調(diào)用
}
}
3.1 setIsSwitch()方法
組件中定義了倆個“開關(guān)是否打開”的變量isChecked和_isSwitch
一個是外部組件傳遞進(jìn)來的默認(rèn)值,一個是 switch組件自身的值。
所以在組件進(jìn)行初始化和發(fā)生改變的時候我們應(yīng)該讓其統(tǒng)一:
ngOnInit() {//初始化組件的生命周期
this.setIsSwitch();
}
ngOnChanges() {//當(dāng)被綁定的輸入屬性的值發(fā)生變化時調(diào)用
this.setIsSwitch();
}
setIsSwitch() {//設(shè)置_isSwitch
this._isSwitch = this.isChecked;
}
3.2 setStyle()方法
由于是自定義的組件,我們當(dāng)然是希望大小也可以自定義,所以我想要的效果是:
在調(diào)用組件的時候,輸入一個寬度width屬性,組件能夠自動調(diào)節(jié)尺寸。
因此我在設(shè)計(jì)的時候就定義了一個style變量
它是一個對象,可以允許開發(fā)者輸入任意的樣式,格式為{ 'width': '40px' }
同時為了減少輸入樣式的復(fù)雜度,我們還可以來編寫一個方法,讓組件能夠根據(jù)寬度來調(diào)節(jié)高度:
setStyle() {//設(shè)置樣式
if (this.style) {
if (this.style['width'] && !this.style['height']) {//若是輸入了寬度沒有輸入高度則自動計(jì)算
let width = this.getWidth(this.style['width']);
this.style['height'] = (width * 0.55) + 'px';
}
}
}
getWidth(widthStr) {//判斷用戶輸入的width帶不帶px單位
let reg = /px/;
let width = reg.test(widthStr) ? widthStr.match(/(\d*)px/)[1] : widthStr //正則獲取不帶單位的值
if (!width) width = 0;
return width;
}
可以看到,上面我編寫的setStyle()方法是判斷有沒有寬度和高度,并將高度設(shè)置為0.55 * width(0.55為我找到的最合適的比例)
3.3 setClass()方法
完成了上面的步驟我們基本就完成了對組件樣式的初始化,但是,最重要的一步當(dāng)然是通過添加/移除一些類來進(jìn)行組件的交互:
setClass() {//轉(zhuǎn)換switch時切換class
this.currentClass = {
'disabled': this.disabled,
'bg_main bor_main weui-switch-on': this._isSwitch
}
}
對象currentClass存儲的是組件變動的類名,對象的鍵名為類名,值為一個布爾類型的變量(true / false)
通過布爾類型的變量來判斷添加還是移除這些類名。
第一個類disabled表示的是開關(guān)是否被禁用,也就是用戶只能查看開關(guān),并不能對其進(jìn)行操作,它受disabled變量控制。
第二個類為三個類名的合寫bg_main、bor_main、和weui-switch-on,他們受_isSwitch變量控制,
也就是開關(guān)打開的時候則添加這三個類。
前倆個類名是我在項(xiàng)目中使用的“皮膚類名”,因?yàn)榭蛻舻男枰覀儺a(chǎn)品有幾套不同的主題色,用戶可以進(jìn)行換膚功能來切換主題色,因此就有一些類名需要用來控制主題色。
如橘色主題:
.bg_main {
background-color: #ff7920!important;
}
.bor_main {
border-color: #ff7920!important;
}
當(dāng)然,你若是沒有主題色的話請忽略這倆個類。
上面的幾個方法我們都需要在組件初始化和變量發(fā)生改變的時候調(diào)用,所以可以整合到一個函數(shù)中:
ngOnInit() {
this.initComponent();
}
ngOnChanges() {
this.initComponent();
}
initComponent() {
this.setIsSwitch();
this.setStyle();
this.setClass();
}
3.4 toggle()方法
光有樣式可沒用,我們還需要將組件和用戶的行為給結(jié)合在一起,因此給組件一個click事件來進(jìn)行交互,并編寫toggle()方法:
toggle() {//切換switch
if (this.disabled) return;//若是禁用時則直接返回
this._isSwitch = !this._isSwitch;
this.isChecked = this._isSwitch;
this.change.emit(this._isSwitch); //向外部傳遞最新的值
}
整合后的ts文件為這樣:
import { Component, OnInit, Input, Output, EventEmitter, OnChanges } from '@angular/core';
@Component({
selector: 'app-switch',
templateUrl: './switch.component.html',
styleUrls: ['./switch.component.scss']
})
export class SwitchComponent implements OnInit, OnChanges {
constructor() { }
@Input() onLabel: string = '';//暫無
@Input() offLabel: string = '';
@Input() style;//{ 'width': '40px' }//外部組件輸入的樣式對象
@Input() isChecked: boolean = false;//開關(guān)是否打開
@Input() disabled: boolean = false;//開關(guān)是否被禁用
@Output() change: EventEmitter<any> = new EventEmitter();
_isSwitch: boolean = false;
currentClass = {}
ngOnInit() {
this.initComponent();
}
ngOnChanges() {
this.initComponent();
}
initComponent() {//初始化并刷新組件
this.setIsSwitch();
this.setStyle();
this.setClass();
}
setIsSwitch() {
this._isSwitch = this.isChecked;
}
setStyle() {//設(shè)置樣式
if (this.style) {
if (this.style['width'] && !this.style['height']) {//若是輸入了寬度沒有輸入高度則自動計(jì)算
let width = this.getWidth(this.style['width']);
this.style['height'] = (width * 0.55) + 'px';
}
}
}
setClass() {//轉(zhuǎn)換switch時切換class
this.currentClass = {
'disabled': this.disabled,
'bg_main bor_main weui-switch-on': this._isSwitch
}
}
getWidth(widthStr) {//判斷用戶輸入的width帶不帶px單位
let reg = /px/;
let width = reg.test(widthStr) ? widthStr.match(/(\d*)px/)[1] : widthStr //正則獲取不帶單位的值
if (!width) width = 0;
return width;
}
toggle() {//切換switch
if (this.disabled) return;//若是禁用時則直接返回
this._isSwitch = !this._isSwitch;
this.isChecked = this._isSwitch;
this.change.emit(this._isSwitch);
}
}
四、引用switch組件
完成了上面的部分,到了我們最激動的時候了,看看我們親手制作的組件有沒有用吧,哈哈。
首先,在使用其它組件的時候,我們要將其引入進(jìn)來,由于我們最開始是將switch組件引入到shared這個模塊中,并從這個模塊中導(dǎo)出的,所以想要在其它模塊中使用 switch組件就得先引入shared模塊。
4.1 引入shared模塊
本項(xiàng)目中有另一個模塊名為coursemanage,現(xiàn)在我將其作為父組件來引用一下switch組件
首先在模塊里引用:
//coursemanage.module.ts
import { NgModule } from '@angular/core';
import { SharedModule } from "./../common/shared.module";
@NgModule({
imports: [
SharedModule
]
})
export class CourseManageModule { }
引入了shared模塊就相當(dāng)于是引入那個那個模塊中的所有組件和方法。
4.2 使用switch組件
在coursemanage模塊中,有其子組件course這個組件,在course中使用switch
<!--course.component.html-->
<app-switch [isChecked]="dataStatus" (change)="changeSwitch($event)"></app-switch>
//course.component.ts
dataStatus: boolean = false;
changeSwitch($event) {
this.dataStatus = $event;
}
此時就完成了switch組件的編寫和使用。
你也可以給組件設(shè)置另一個屬性disabled:
<!--course.component.html-->
<app-switch [isChecked]="dataStatus" [disable]="true" (change)="changeSwitch($event)"></app-switch>
后語
上述設(shè)計(jì)的switch組件應(yīng)該是UI組件中比較簡單的一種UI組件了,還有更多復(fù)雜的組件有待我們的開發(fā),通過自己設(shè)計(jì)UI組件,emmm....可以讓我們更有創(chuàng)造力吧應(yīng)該說,也促使自己多去看別人的博客與源碼,最后再寫上一篇總結(jié),我認(rèn)為這應(yīng)該是一個正向的激勵??,哈哈,全篇廢話很多,不過還是要感謝小伙的閱讀??。