前端有哪些轮子(不包括 Node 后端领域)
1.标准库的扩充 - underscore.js 扩充了 Array 和 Object 相关 API - moment.js 扩充了 Date - bluebird.js / hax/my-promise 实现了 Promise(JS还没有实现Promise之前) - async.js 模拟了 async 操作符(在支持async之前去模拟async) - es5shim 用 ES 3 语法部分实现了 ES 5 特性 (支持ES5之前去模拟ES5) - handlebars.js 实现模板字符串功能(ES6模板字符串出现之前去模拟)
备注:
标准库就是JS自带的功能,由于JS自带的功能太弱了,开发者就提供了扩充
轮子大概的意思:写一些功能比较单一的东西分享给人用,轮子的最大的特点就是给别人用
2.DOM 的扩充 - jQuery.js 操作 DOM - video.js 操作 video - Fabric.js 操作 canvas
3.UI 组件 - 纯 CSS 的 UI 组件库,如 Bulma - 大而全的 UI 框架(CSS + JS),如 Bootstrap、Element UI - 垂直领域的 UI 组件 - 专门做轮播的 Swiper - 专门做输入提示的 typeahead.js - 专门做文件上传的 fine-uploader - 专门做 3D 瓦片效果的 vanilla-tilt.js - 专门做视差效果的 parallax.js - 专门做数据可视化的 D3.js - 专门做图表的 echarts.js - 专门做动画的 velocity.js - 专门做粒子效果的 particle.js - 专门做手势识别的 hammer.js
备注:
很多UI组件(库)放到一块,形成风格统一的UI界面,那么它就是UI框架
4.编程思想类的轮子(将其他语言的思想带到JS中来) - 实现 MVC 思想的 backbone.js(在JS的基础上实现的MVC思想) - 实现 MVVM 思想的 AngularJS 1 和 Vue 1 - 实现 Virtual DOM 的 React 和 Preact - 实现单向数据流(FLUX)思想的 Redux - 实现 Reactive 思想的 Rx.js - 实现 Rails 思想的 Ember.js - 实现函数式思想的 Ramda
我们的重点
重点是 UI 组件,因为
1.我们日常工作中经常用到的就是 UI 组件
2.UI 组件一般是由 HTML、CSS 和 JS 组成,把 UI 组件做好了,就能更好的做网页(就是大一点的UI组件)
3.UI 组件做起来更有趣,所有效果你都能用眼睛看到,而做编程思想类、DOM扩充类组件可能过于抽象
原则
(内部)分层原则:正交原则
(对外)封装原则:面向接口编程
正交原则
组件内部的代码要用正交的思想来分层,html只负责内容、css只负责样式、js只负责行为
其中正交的意思就如上图:假如我们现在在原点,沿X轴往前走2格,只会影响在X轴的坐标,不会影响Y轴和Z轴,同样的如果再接着沿着Z轴移动2格,只会影响在Z轴的坐标;所以正交的意思,就是当我们在操作html、css、或者JS中的一个的时候,不会干涉影响到另外2个,这就叫正交
下面讲一个不正交的栗子:
例如jQuery中$div.show()
,它是用JS去改变div的css属性display,隐藏的时候是none,但是显示的时候不一定是block还有可能是inline-block;flex;table我们根本不知道到底是什么,所以这个操作就很危险
主张不要用show,改成$div.addClass('active')
,写成这样JS就没有干涉css,JS只是将div加了一个class让它处于激活的状态,具体怎样样,交给css去处理,这样jS对html和css影响最小,这也是最正交的写法,正交原则也叫内容、样式、行为分离原则
面向接口编程
给别人用的时候要面向接口编程
面向接口编程是我们首先要考虑要提供给别人的接口长什么样子,然后再去想怎么实现的细节
A: 假设我们现在要写一个对话框组件,我们首先要想什么?
B: 我们要想怎样在页面中出现这个对话框!
C: 不,不要一开始就想这些细节,而是要假想用你组件的人会如何调用,也就是考虑输入和输出
例如:
button.onclick = function(){
var diolog = new Diolog({
// 输入为: content、title、buttons
content: 'Hi',
title: ''提示,
buttons:{
ok:{
text: '确定',
action: function(){}
},
cancle:{
text: '取消',
action: function(){}
}
}
})
// 输出为一个对象,这个对象有open和close方法
diolog.open()
diolog.close()
}
Tab组件
提供怎样的接口?
接口越简单越好,如果我要用这个组件最好类似像下面这样就可以实现一个Tab
第一步:先自己写一个非组件的Tab
也就是当作页面来写,写完之后再想怎么封装成组件
JSBin 示例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Static Template</title>
<link rel="stylesheet" href="./style.css" />
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="./main.js"></script>
</head>
<body>
<div class="tabs">
<ol class="tabs-bar">
<li>1</li>
<li>2</li>
<li>3</li>
</ol>
<ol class="tabs-content">
<li>content 1</li>
<li>content 2</li>
<li>content 3</li>
</ol>
</div>
</body>
</html>
.tabs > ol {
list-style: none;
margin: 0;
padding: 0;
}
.tabs > ol.tabs-bar {
display: flex;
border-bottom: 1px solid blue;
}
.tabs > ol.tabs-bar > li {
border: 1px solid transparent;
border-bottom: none;
padding: 2px 8px;
}
.tabs > ol.tabs-bar > li:not(:first-child){margin-left: 10px;}
.tabs > ol.tabs-bar > li:hover {
border-color: red;
cursor: pointer;
}
.tabs > ol.tabs-bar > li.active {
border-color: blue;
}
.tabs > ol.tabs-content > li{display:none;}
.tabs > ol.tabs-content > li.active {
display:block;
}
// 将第一个tabs-bar > li 和 .tabs-content > li 设置为激活状态
$(".tabs").each(function(index,element){
console.log(element)
$(element).children('.tabs-bar').children('li').eq(0).addClass('active')
$(element).children('.tabs-content').children('li').eq(0).addClass('active')
})
// tab切换
$(".tabs").on("click", ".tabs-bar>li", function(e) {
var $li = $(e.currentTarget);
$li.addClass("active").siblings().removeClass('active');
var index = $li.index();
// 这里要从内往外找,否则如果页面有多个.tabs不知道点击的是哪个.tabs
// 反例从外往里找:$('.tabs').find('.tabs-content > li').eq(index)
// 要从里往外找,要找到离$li最近的.tabs,再从它下面找到对应index的.tabs-content的li显示
var $content = $li.closest('.tabs').find('.tabs-content > li').eq(index)
$content.addClass('active').siblings().removeClass('active')
});
备注:
新手错误思维:.show .hide,用JS直接去控制css
正交思维:JS 不要控制样式,只切换状态,切换class,样式交给css去处理
到此只是做成了一个关于Tab的页面,并没有封装成接口,下面将它封装成接口
第二步: 将写死的静态页面改为封装成接口
JSBin 链接
function Tabs(selector) {
this.elements = $(selector)
this.elements.each(function(index, element) {
$(element).children('.tabs-bar').children('li').eq(0).addClass('active')
$(element).children('.tabs-content').children('li').eq(0).addClass('active')
})
this.elements.on("click", ".tabs-bar>li", function(e) {
var $li = $(e.currentTarget);
$li.addClass("active").siblings().removeClass('active');
var index = $li.index();
var $content = $li.closest('.tabs').find('.tabs-content > li').eq(index)
$content.addClass('active').siblings().removeClass('active')
});
}
// 表示作用于'.xxx'的选择器,但是html中必须有'.tabs'
var tabs = new Tabs('.xxx')
备注
如果用户需要使用我们的轮子,只需要html写成这样:
<div class="tabs xxx">
<ol class="tabs-bar">
<li>1</li>
<li>2</li>
<li>3</li>
</ol>
<ol class="tabs-content">
<li>content 1</li>
<li>content 2</li>
<li>content 3</li>
</ol>
</div>
然后引入我们的css文件和JS文件,用户只需要写一句话:var tabs=new Tabs('.xxx')
,然后页面上就有了Tabs切换效果了
第三步: 用原型链对代码进行改进
构造函数中做的事情太多了,将代码分功能放到原型上
JSBin 示例链接
function Tabs(selector) {
this.elements = $(selector)
this.init()
this.bindEvents()
}
Tabs.prototype.init = function(){
this.elements.each(function(index, element) {
$(element).children('.tabs-bar').children('li').eq(0).addClass('active')
$(element).children('.tabs-content').children('li').eq(0).addClass('active')
})
}
Tabs.prototype.bindEvents = function(){
this.elements.on("click", ".tabs-bar>li", function(e) {
var $li = $(e.currentTarget);
$li.addClass("active").siblings().removeClass('active');
var index = $li.index();
var $content = $li.closest('.tabs').find('.tabs-content > li').eq(index)
$content.addClass('active').siblings().removeClass('active')
});
}
// 表示作用于'.xxx'的选择器,但是html中必须有'.tabs'
var tabs = new Tabs('.xxx')
第四步:用ES6的class语法对代码进行改造
从原型链到class语法的改造,就仅仅是加了class的语法糖,从形式上发生了变化,其他的基本不变
JSBin 示例链接
class Tabs {
constructor(selector) {
this.elements = $(selector)
this.init()
this.bindEvents()
}
init() {
this.elements.each(function(index, element) {
$(element).children('.tabs-bar').children('li').eq(0).addClass('active')
$(element).children('.tabs-content').children('li').eq(0).addClass('active')
})
}
bindEvents() {
this.elements.on("click", ".tabs-bar>li", function(e) {
var $li = $(e.currentTarget);
$li.addClass("active").siblings().removeClass('active');
var index = $li.index();
var $content = $li.closest('.tabs').find('.tabs-content > li').eq(index)
$content.addClass('active').siblings().removeClass('active')
});
}
}
// 表示作用于'.xxx'的选择器,但是html中必须有'.tabs'
var tabs = new Tabs('.xxx')
存在的问题
要根据文档去写html和css,如果不按照要求写css可能tabs就没有样式,也就是说这里的css依赖了html的结构
解决的办法
需要在文档中说清楚html和css该按照何种形式写?存在哪些依赖的关系
Sticky组件
sticky.js 链接地址
首先要考虑的是Sticky组件该提供怎样的接口,Sticky有一种是吸顶的Sticky一种是距离顶部有一定距离的Sticky,所以需要一个参数来设置距离顶部的距离,接口的形式类似:
// selector为选择器 n为距离顶部的距离
class Sticky{
constructor(selector,n){
this.elements = $(selector)
this.offset = n
}
}
var sticky = new Sticky(selector,n)
我们得思路就是只要检测到用户鼠标发生了向下滚动,就给topbar添加fixed定位,那么为什么不在一开始就给topbar加fixed定位呢?因为刚开始如果就加fixed定位,所有在topbar后面的元素就会跑到topbar的后面去,这就需要给每个topbar后面的元素加margin-top以免被topbar挡住。而且我们做不到一些更加复杂的应用场景:例如一个button随着鼠标的滚动往上移动,滚动到距离顶部一定距离后再fixed定位
正确的思路:给topbar加上一个sticky的样式,一旦鼠标向下滚动就给topbar添加sticky的样式,一旦鼠标往上滚动就去除sticky的样式。
第一步:实现页面应该怎么实现
存在一个问题:只要用户鼠标向下滚动1px,那么topbar后面的元素,就都会回跑到topbar的后面且位于最上面,原因是topbar脱离了文档流,那么怎样才能让topbar fixed的同时占住它原来的位置呢?可以在top-bar的外层加一个包裹的元素,作用就是占住topbar原来的位置
未占位
已占位
所以写Sticky的时候,在给需要sticky的元素的外鞥包裹一个元素很重要,它的作用就是占位。以后在写页面的时候,千万不要让一个元素轻易的就从页面的正常的文档流中离开,否则页面的布局就会整个乱掉,如果元素要离开正常的文档流需要使用占位元素占住需要离开元素的位置
下面再看一下如何让按钮滚动到指定位置后sticky
实现了页面的效果
我们得首先获取按钮距离当前文档所处的位置,即按钮的上边缘距离页面的上边缘的距离(不是距离浏览器窗口的距离),当滚动到这个距离后就让按钮绝对定位
要注意获取按钮的上边缘距离页面的上边缘的距离不是实时的获取,而是一开始就获取
第二步:分析页面的代码
var buttonOffsetTop = $('button').offset()
$(window).on('scroll',function(){
var scrollY = window.scrollY
if(scrollY>0){
$('.topbar').addClass('sticky')
}else {
$('.topbar').removeClass('sticky')
}
if(scrollY + 60 > buttonOffsetTop.top){
$('button').addClass('sticky')
}else{
$('button').removeClass('sticky')
}
});
目前为止是给2个元素加上了sticky,topbar是scrollY和0去做比较,button是scrollY + 60和buttonOffsetTop.top做比较。这2个是不一样的代码,难道不同的元素要写不一样的代码吗?实际上是一样的,.topbar一开始距离顶部的距离就是0
var topbarOffsetTop = $('.topbar').offset
var buttonOffsetTop = $('button').offset()
$(window).on('scroll',function(){
var scrollY = window.scrollY
if(scrollY + 0 > topbarOffsetTop.top){
$('.topbar').addClass('sticky')
}else {
$('.topbar').removeClass('sticky')
}
if(scrollY + 60 > buttonOffsetTop.top){
$('button').addClass('sticky')
}else{
$('button').removeClass('sticky')
}
});
经过这样的变形,2个元素的sticky的代码就一样了,都是滚动的距离加上一个偏移值和当前元素距离顶部的距离作比较,如果超出了就sticky,反之就移除sticky。因此这样JS的代码就可以做到统一了,不管是这里的topbar还是button,都可以使用一套同样的代码去实现
第三步:封装成组件
初版Sticky组件
但是还是存在一个问题:为了避免sticky元素的fixed定位,会脱离普通文档流,会对页面布局产生影响,所以一般都会给sticky的元素外面加一层wrapper元素并指定它的高度和sticky的元素高度一致来占位。但是一旦写成组件的时候就最好不要让用户自己手动的加,我们应该通过JS给每个sticky的元素外面加一层wrapper并指定wrapper的高度和sticky元素的高度一致
完成版Sticky组件
组件的样式很多需要用户自己写。我们能做的是可以为用户提供sticky的样式,但是不能指定宽度100%,那样类似button在stick的时候就宽度变成100%了,但是如果不加topbar fixed定位后宽度又会收缩为内容的宽度,所以宽度需要用户自己写,这点要在文档中说清楚
另外我们可以帮用户指定top的值为this.offset(也即n),虽然这里使用JS去操作css违反了正交的原则,但是这样可以为用户提供很大的方便
完整代码:
// HTML
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
<script src="https://code.jquery.com/jquery-2.2.4.js"></script>
</head>
<body>
<div class="topbar">
topbar
</div>
<main>
主要内容
<p>段落1</p>
<p>段落2</p>
<p>段落3</p>
<p>段落4</p>
<p>段落5</p>
<p>段落6</p>
<p>段落7</p>
<p>段落8</p>
<p>段落9</p>
<p>段落10</p>
<button>黏住的按钮</button>
<p>段落11</p>
</main>
</body>
</html>
// CSS
/****组件提供的样式******/
.sticky{
position: fixed;
top:0;
left:0;
}
/**用户自己写的样式**/
*{margin:0;padding:0;}
.topbar{
background: green;
height: 60px;
text-align: center;
color: #fff;
opacity: .5;
}
main{
height: 1800px;
background: #ddd;
}
.topbar.sticky{
width:100%;
}
// JS
class Sticky {
constructor(selector, n) {
this.elements = $(selector)
this.offset = n || 0
this.addWrapper()
this.cacheOffset()
this.listenToScroll()
}
addWrapper() {
this.elements.each((index, element) => {
$(element).wrap("<div class='stickyWrapper'></div>")
$(element).parent().height($(element).height())
})
}
cacheOffset() {
this.offsets = []
this.elements.each((index, element) => {
this.offsets[index] = $(element).offset()
})
}
listenToScroll() {
$(window).on('scroll', () => {
var scrollY = window.scrollY
this.elements.each((index, element) => {
var $element = $(element)
if (scrollY + this.offset > this.offsets[index].top) {
$element.addClass('sticky').css({top: this.offset})
} else {
$element.removeClass('sticky')
}
})
})
}
}
new Sticky('.topbar', 0)
new Sticky('button', 60)
需要在文档中说明
1.组件为每一个sticky的元素的外层添加了一个div,class叫stickyWrapper
2.组件提供的样式
.sticky{
position: fixed;
top:0;
left:0;
}
3.组件为每个sticky的元素添加了样式top: n
4.sticky元素的宽度需要用户自己设定
Dialog组件
首先应该思考Dialog组件的接口应该是怎样的?也就是说别人在使用Dialog组件的时候应该如何使用这个组件?
接口应该类似这样:
button.onclick = function(){
var dialog = new Dialog({
title:'标题',
// content如果支持html就有可能被注入的风险(要做过滤,比如干掉script)
content: '<b>欢迎使用Dialog组件</b>',
buttons:[
{text:'确定',action:function(){
setTimeout(()=>{
dialog.close()
})
}},
{text:'取消',action:function(){
dialog.close()
}}
]
})
dialog.show()
}
第一步:去页面中实现Dialog组件
JS Binjs.jirengu.com。。。 未完