整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

纯CSS文字聚光灯效果~#web前端

纯CSS文字聚光灯效果~#web前端

个视频用CSS来写聚光灯的效果,大家可以先想一下思路。

·来看一下代码,这里就写了一个h1标签,还有一些基本的样式。

·先来看下布局,这里需要把hello world多准备一份,可以直接复制一份h1或者添加一个伪元素,保持html结构简洁一点。

·<htmicontent里面可以直接把hello world写在里面,或者用之前说的attr()属性获取函数,然后定义一个data-text属性,在h1里面就来定义这个属性,再把hello world放在里面,这样子也可以,再给它定一下位。

·现在这两个hello world通过定位重叠在一起了,然后要对伪元素的hello world裁切一小部分出来。

·这里用clip-path属性裁一个小圆出来用circle,半径大小为100像素,位置在水平的最左边。现在只是显示了水平最左边的这小部分了,把它调到中间位置看一下。

·是不是可以通过一个动画控制它水平方向的值就可以了?来写个动画,这里设置三个区间,一开始的时候是在最左边的,100%的时候也是回到最左边,50%的时候就去到最右边,这里改成100%就可以了。

·来绑定一下动画,聚光灯的效果其实就已经完成了,但是背景不怎么好看,给它加一个渐变的背景。

·这里准备了一张比较好看的渐变背景图,再给它放大一点,背景图是出来了,但是要让背景只是显示在文字里面,这里就要对背景进行裁切,让它按照文本来裁切。

·现在背景其实已经裁出来了,只是因为字体颜色把背景给挡住了,把字体的颜色改成透明就可以了,动画的时间稍微改长一点。

聚光灯的效果就完成了。

这个视频就到这里,感谢大家的收看。

知道浏览器和服务端是通过 HTTP 协议进行数据传输的,而 HTTP 协议又是纯文本协议,那么浏览器在得到服务端传输过来的 HTML 字符串,是如何解析成真实的 DOM 元素的呢,也就是我们常说的生成 DOM Tree,最近了解到状态机这样一个概念,于是就萌生一个想法,实现一个 innerHTML 功能的函数,也算是小小的实践一下。

函数原型

我们实现一个如下的函数,参数是 DOM 元素和 HTML 字符串,将 HTML 字符串转换成真实的 DOM 元素且 append 在参数一传入的 DOM 元素中。

function html(element, htmlString) {
 // 1. 词法分析
 // 2. 语法分析
 // 3. 解释执行
}
复制代码

在上面的注释我已经注明,这个步骤我们分成三个部分,分别是词法分析、语法分析和解释执行。

词法分析

词法分析是特别重要且核心的一部分,具体任务就是:把字符流变成 token 流。

词法分析通常有两种方案,一种是状态机,一种是正则表达式,它们是等效的,选择你喜欢的就好。我们这里选择状态机。

首先我们需要确定 token 种类,我们这里不考虑太复杂的情况,因为我们只对原理进行学习,不可能像浏览器那样有强大的容错能力。除了不考虑容错之外,对于自闭合节点、注释、CDATA 节点暂时均不做考虑。

接下来步入主题,假设我们有如下节点信息,我们会分出哪些 token 来呢。

<p class="a" data="js">测试元素</p>
复制代码

对于上述节点信息,我们可以拆分出如下 token

  • 开始标签:<p
  • 属性标签:class="a"
  • 文本节点:测试元素
  • 结束标签:</p>

状态机的原理,将整个 HTML 字符串进行遍历,每次读取一个字符,都要进行一次决策(下一个字符处于哪个状态),而且这个决策是和当前状态有关的,这样一来,读取的过程就会得到一个又一个完整的 token,记录到我们最终需要的 tokens 中。

万事开头难,我们首先要确定起初可能处于哪种状态,也就是确定一个 start 函数,在这之前,对词法分析类进行简单的封装,具体如下

function HTMLLexicalParser(htmlString, tokenHandler) {
 this.token=[];
 this.tokens=[];
 this.htmlString=htmlString
 this.tokenHandler=tokenHandler
}
复制代码

简单解释下上面的每个属性

  • token:token 的每个字符
  • tokens:存储一个个已经得到的 token
  • htmlString:待处理字符串
  • tokenHandler:token 处理函数,我们每得到一个 token 时,就已经可以进行流式解析

我们可以很容易的知道,字符串要么以普通文本开头,要么以<开头,因此 start 代码如下

HTMLLexicalParser.prototype.start=function(c) {
 if(c==='<') {
 this.token.push(c)
 return this.tagState
 } else {
 return this.textState(c)
 }
}
复制代码

start处理的比较简单,如果是<字符,表示开始标签或结束标签,因此我们需要下一个字符信息才能确定到底是哪一类 token,所以返回tagState函数去进行再判断,否则我们就认为是文本节点,返回文本状态函数。

接下来分别展开tagState和textState函数。tagState根据下一个字符,判断进入开始标签状态还是结束标签状态,如果是/表示是结束标签,否则是开始标签,textState用来处理每一个文本节点字符,遇到<表示得到一个完整的文本节点 token,代码如下

HTMLLexicalParser.prototype.tagState=function(c) {
 this.token.push(c)
 if(c==='/') {
 return this.endTagState
 } else {
 return this.startTagState
 }
}
HTMLLexicalParser.prototype.textState=function(c) {
 if(c==='<') {
 this.emitToken('text', this.token.join(''))
 this.token=[]
 return this.start(c)
 } else {
 this.token.push(c)
 return this.textState
 }
}
复制代码

这里初次见面的函数是emitToken、startTagState和endTagState。

emitToken用来将产生的完整 token 存储在 tokens 中,参数是 token 类型和值。

startTagState用来处理开始标签,这里有三种情形

  • 如果接下来的字符是字母,则认定依旧处于开始标签态
  • 遇到空格,则认定开始标签态结束,接下来是处理属性了
  • 遇到>,同样认定为开始标签态结束,但接下来是处理新的节点信息

endTagState用来处理结束标签,结束标签不存在属性,因此只有两种情形

  • 如果接下来的字符是字母,则认定依旧处于结束标签态
  • 遇到>,同样认定为结束标签态结束,但接下来是处理新的节点信息

逻辑上面说的比较清楚了,代码也比较简单,看看就好啦

HTMLLexicalParser.prototype.emitToken=function(type, value) {
 var res={
 type,
 value
 }
 this.tokens.push(res)
 // 流式处理
 this.tokenHandler && this.tokenHandler(res)
}
HTMLLexicalParser.prototype.startTagState=function(c) {
 if(c.match(/[a-zA-Z]/)) {
 this.token.push(c.toLowerCase())
 return this.startTagState
 }
 if(c===' ') {
 this.emitToken('startTag', this.token.join(''))
 this.token=[]
 return this.attrState
 }
 if(c==='>') {
 this.emitToken('startTag', this.token.join(''))
 this.token=[]
 return this.start
 }
}
HTMLLexicalParser.prototype.endTagState=function(c) {
 if(c.match(/[a-zA-Z]/)) {
 this.token.push(c.toLowerCase())
 return this.endTagState
 }
 if(c==='>') {
 this.token.push(c)
 this.emitToken('endTag', this.token.join(''))
 this.token=[]
 return this.start
 }
}
复制代码

最后只有属性标签需要处理了,也就是上面看到的attrState函数,也处理三种情形

  • 如果是字母、单引号、双引号、等号,则认定为依旧处于属性标签态
  • 如果遇到空格,则表示属性标签态结束,接下来进入新的属性标签态
  • 如果遇到>,则认定为属性标签态结束,接下来开始新的节点信息

代码如下

HTMLLexicalParser.prototype.attrState=function(c) {
 if(c.match(/[a-zA-Z'"=]/)) {
 this.token.push(c)
 return this.attrState
 }
 if(c===' ') {
 this.emitToken('attr', this.token.join(''))
 this.token=[]
 return this.attrState
 }
 if(c==='>') {
 this.emitToken('attr', this.token.join(''))
 this.token=[]
 return this.start
 }
}
复制代码

最后我们提供一个parse解析函数,和可能用到的getOutPut函数来获取结果即可,就不啰嗦了,上代码

HTMLLexicalParser.prototype.parse=function() {
 var state=this.start;
 for(var c of this.htmlString.split('')) {
 state=state.bind(this)(c)
 }
}
HTMLLexicalParser.prototype.getOutPut=function() {
 return this.tokens
}
复制代码

接下来简单测试一下,对于<p class="a" data="js">测试并列元素的</p><p class="a" data="js">测试并列元素的</p>HTML 字符串,输出结果为

看上去结果很 nice,接下来进入语法分析步骤

语法分析

首先们需要考虑到的情况有两种,一种是有多个根元素的,一种是只有一个根元素的。

我们的节点有两种类型,文本节点和正常节点,因此声明两个数据结构。

function Element(tagName) {
 this.tagName=tagName
 this.attr={}
 this.childNodes=[]
}
function Text(value) {
 this.value=value || ''
}
复制代码

目标:将元素建立起父子关系,因为真实的 DOM 结构就是父子关系,这里我一开始实践的时候,将 childNodes 属性的处理放在了 startTag token 中,还给 Element 增加了 isEnd 属性,实属愚蠢,不但复杂化了,而且还很难实现。仔细思考 DOM 结构,token 也是有顺序的,合理利用栈数据结构,这个问题就变的简单了,将 childNodes 处理放在 endTag 中处理。具体逻辑如下

  • 如果是 startTag token,直接 push 一个新 element
  • 如果是 endTag token,则表示当前节点处理完成,此时出栈一个节点,同时将该节点归入栈顶元素节点的 childNodes 属性,这里需要做个判断,如果出栈之后栈空了,表示整个节点处理完成,考虑到可能有平行元素,将元素 push 到 stacks。
  • 如果是 attr token,直接写入栈顶元素的 attr 属性
  • 如果是 text token,由于文本节点的特殊性,不存在有子节点、属性等,就认定为处理完成。这里需要做个判断,因为文本节点可能是根级别的,判断是否存在栈顶元素,如果存在直接压入栈顶元素的 childNodes 属性,不存在 push 到 stacks。

代码如下

function HTMLSyntacticalParser() {
 this.stack=[]
 this.stacks=[]
}
HTMLSyntacticalParser.prototype.getOutPut=function() {
 return this.stacks
}
// 一开始搞复杂了,合理利用基本数据结构真是一件很酷炫的事
HTMLSyntacticalParser.prototype.receiveInput=function(token) {
 var stack=this.stack
 if(token.type==='startTag') {
 stack.push(new Element(token.value.substring(1)))
 } else if(token.type==='attr') {
 var t=token.value.split('='), key=t[0], value=t[1].replace(/'|"/g, '')
 stack[stack.length - 1].attr[key]=value
 } else if(token.type==='text') {
 if(stack.length) {
 stack[stack.length - 1].childNodes.push(new Text(token.value))
 } else {
 this.stacks.push(new Text(token.value))
 }
 } else if(token.type==='endTag') {
 var parsedTag=stack.pop()
 if(stack.length) {
 stack[stack.length - 1].childNodes.push(parsedTag)
 } else {
 this.stacks.push(parsedTag)
 }
 }
}
复制代码

简单测试如下:

没啥大问题哈

解释执行

对于上述语法分析的结果,可以理解成 vdom 结构了,接下来就是映射成真实的 DOM,这里其实比较简单,用下递归即可,直接上代码吧

function vdomToDom(array) {
 var res=[]
 for(let item of array) {
 res.push(handleDom(item))
 }
 return res
}
function handleDom(item) {
 if(item instanceof Element) {
 var element=document.createElement(item.tagName)
 for(let key in item.attr) {
 element.setAttribute(key, item.attr[key])
 }
 if(item.childNodes.length) {
 for(let i=0; i < item.childNodes.length; i++) {
 element.appendChild(handleDom(item.childNodes[i]))
 }
 }
 return element
 } else if(item instanceof Text) {
 return document.createTextNode(item.value)
 }
}
复制代码

实现函数

上面三步骤完成后,来到了最后一步,实现最开始提出的函数

function html(element, htmlString) {
 // parseHTML
 var syntacticalParser=new HTMLSyntacticalParser()
 var lexicalParser=new HTMLLexicalParser(htmlString, syntacticalParser.receiveInput.bind(syntacticalParser))
 lexicalParser.parse()
 var dom=vdomToDom(syntacticalParser.getOutPut())
 var fragment=document.createDocumentFragment()
 dom.forEach(item=> {
 fragment.appendChild(item)
 })
 element.appendChild(fragment)
}
复制代码

三个不同情况的测试用例简单测试下

html(document.getElementById('app'), '<p class="a" data="js">测试并列元素的</p><p class="a" data="js">测试并列元素的</p>')
html(document.getElementById('app'), '测试<div>你好呀,我测试一下没有深层元素的</div>')
html(document.getElementById('app'), '<div class="div"><p class="p">测试一下嵌套很深的<span class="span">p的子元素</span></p><span>p同级别</span></div>')
复制代码

声明:简单测试下都没啥问题,本次实践的目的是对 DOM 这一块通过词法分析和语法分析生成 DOM Tree 有一个基本的认识,所以细节问题肯定还是存在很多的。

总结

其实在了解了原理之后,这一块代码写下来,并没有太大的难度,但却让我很兴奋,有两个成果吧

  • 了解并初步实践了一下状态机
  • 数据结构的魅力

*1.深拷贝*/

//(1)

function deepClone(obj) {

if (!obj && typeof obj !=='object') {

return;

}

var newObj=obj.constructor===Array ? [] : {};

for (var key in obj) {

if (obj[key]) {

if (obj[key] && typeof obj[key]==='object') {

newObj[key]=obj[key].constructor===Array ? [] : {};

//递归

newObj[key]=deepClone(obj[key]);

} else {

newObj[key]=obj[key];

}

}

}

return newObj;

}

//使用方法:

var _arr=[{ a: 1, b: 2 }, { c: 3, d: 4 }];

var _newArr=deepClone(_arr);

//(2)比较粗鲁的复制

JSON.parse(JSON.stringify(_arr));

/*2.合并数组*/

//(1)concat方法

var _arr1=["a", "b", "c"];

var _arr2=["1", "2", "3"];

var newArr=_arr1.concat(_arr2);

// 注:使用concat方法合并数组时,不会改变原有数组的结构。

//(2)push.apply()方法

var _arr1=["a", "b", "c"];

var _arr2=["1", "2", "3"];

_arr1.push.apply(_arr1, _arr2);

//使用此方法合并的数组会改变合并到的数组

/*3.判断数据类型(准确)*/

Object.prototype.toString.call();

//使用方法:

var _obj={};

Object.prototype.toString.call(_obj)==[object Object];

/*4.将函数中的arguments参数转成数组*/

var _arguments=Array.prototype.slice.apply(arguments);

//使用方法:

function add(x, y) {

var _argus=Array.prototype.slice.call(arguments);

console.log(_argus);

}

/*5.节流函数*/

function delayFn(fn, delay, mustDelay) {

var timer=null;

var t_start;

return function() {

var context=this,

args=arguments,

t_cur=+new Date();

clearTimeout(timer); //清除上一次定时器

if (!t_start) {

t_start=t_cur; //设置触发事件

}

if (t_cur - t_start >=mustDelay) { //判断时间差是否大于间隔时间

fn.apply(context, args);

t_start=t_cur;

} else {

timer=setTimeout(function() { //延迟执行函数

fn.apply(context, args);

}, delay);

}

};

}

//使用方法:

var count=0;

function fn() {

count++;

console.log(count);

}

document.onmousemove=delayFn(fn, 100, 200); //100ms内连续触发的调用,后一个调用会把前一个调用的等待处理掉,但每隔200ms至少执行一次

/*6累加函数*/

var arr1=arr.reduce(function(con, info) {

if (con) {

return con;

}

return info._show;

}, false)

/*7手机端收起弹出键盘*/

var hideKeyboard=function() {

document.activeElement.blur(); //ios

$("input").blur();

};

/*8时间格式化*/

Date.prototype.Format=function(fmt) {

var o={

"M+": this.getMonth() + 1, //月份

"d+": this.getDate(), //日

"h+": this.getHours(), //小时

"m+": this.getMinutes(), //分

"s+": this.getSeconds(), //秒

"q+": Math.floor((this.getMonth() + 3) / 3), //季度

"S": this.getMilliseconds() //毫秒

};

if (/(y+)/.test(fmt)) fmt=fmt.replace(RegExp., (this.getFullYear() + "").substr(4 - RegExp..length));

for (var k in o)

if (new RegExp("(" + k + ")").test(fmt)) fmt=fmt.replace(RegExp., (RegExp..length==1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length)));

return fmt;

}

//使用方法:(时间转成yyyy-MM-dd hh:mm:ss)

var time1=new Date().Format("yyyy-MM-dd");

var time2=new Date().Format("yyyy-MM-dd hh:mm:ss");

/*9标准时间转时间戳*/

var time1=Date.parse(new Date());

/*10时间戳转标准时间*/

new Date(time1).Format("yyyy-MM-dd hh:mm:ss");

/*11.JSON数组去重*/

/*

* JSON数组去重

* @param: [array] json Array

* @param: [string] 唯一的key名,根据此键名进行去重

*/

function uniqueArray(array, key) {

var result=[array[0]];

for (var i=1; i < array.length; i++) {

var item=array[i];

var repeat=false;

for (var j=0; j < result.length; j++) {

if (item[key]==result[j][key]) {

repeat=true;

break;

}

}

if (!repeat) {

result.push(item);

}

}

return result;

}

//使用方法:

var _json=[{ id: 'aaa', name: 'bb' }, { id: 'bbb', name: 'xx' }, { id: 'aaa', name: 'ccc' }]

uniqueArray(_json, 'id');

/*12.JSON数组去重多字段*/

var arr=[

{ "name": "1", "value": "qqq", "age": "10" },

{ "name": "1", "value": "qqq", "age": "10" },

{ "name": "2", "value": "eee", "age": "20" },

{ "name": "4", "value": "rrr", "age": "50" },

{ "name": "5", "value": "ttt", "age": "100" }

];

for (var i=0; i < arr.length; i++) {

for (var j=i + 1; j < arr.length;) {

if (arr[i].name==arr[j].name && arr[i].value==arr[j].value && arr[i].age==arr[j].age) {

arr.splice(j, 1);

} else j++;

}

}

//*13.普通数组去重*/

Array.prototype.unique=function() {

var res=[];

var json={};

for (var i=0; i < this.length; i++) {

if (!json[this[i]]) {

res.push(this[i]);

json[this[i]]=1;

}

}

return res;

}

//使用方法:

var arr=[112,112,34,'xx',112,112,34,'xx','str','str1'];

unique(arr);

/*14.console.log && console.dir*/

//使用方法:

var obj={name:'xx',age:18};

console.log(obj);

console.dir(obj);

//区别

//console.dir比log方法更易读,信息也更丰富。能够输出原始属性

/*14.获取当前时间的时间戳(三种方法)*/

var d1=new Date().getTime();

var d2=new Date().valueOf();

var d3=+new Date();

/*15 替代arguments.callee()方法*/

//给内部函数命名

//接受参数n=5,不用for循环输出数组【1,2,3,4,5】

//(1)arguments.callee()递归写法

function show(n) {

var arr=[];

return (function () {

arr.unshift(n);

n--;

if (n !=0) {

arguments.callee();

}

return arr;

})()

}

show(5)//[1,2,3,4,5]

//(2)内部函数明明写法

function show(n) {

var arr=[];

return (function fn() {

arr.unshift(n);

n--;

if (n !=0) {

fn();

}

return arr;

})()

}

show(5)//[1,2,3,4,5]

/*16 预防XSS攻击*/

//(1)使用正则替换关键字符

function safeStr(str){

return str.replace(/</g,'<').replace(/>/g,'>').replace(/"/g, """).replace(/'/g, "'");

}

//(2)使用正则转码,解码

var HtmlUtil={

/*1.用正则表达式实现html转码*/

htmlEncodeByRegExp:function (str){

var s="";

if(str.length==0) return "";

s=str.replace(/&/g,"&");

s=s.replace(/</g,"<");

s=s.replace(/>/g,">");

s=s.replace(/ /g," ");

s=s.replace(/\'/g,"'");

s=s.replace(/\"/g,""");

return s;

},

/*2.用正则表达式实现html解码*/

htmlDecodeByRegExp:function (str){

var s="";

if(str.length==0) return "";

s=str.replace(/&/g,"&");

s=s.replace(/</g,"<");

s=s.replace(/>/g,">");

s=s.replace(/ /g," ");

s=s.replace(/'/g,"\'");

s=s.replace(/"/g,"\"");

return s;

}

};

/*17。多维数组转一维数组*/ /*参考https://www.cnblogs.com/haoxl/p/6818657.html*/

//(1)使用数组map()方法,对数组中的每一项运行给定函数,返回每次函数调用的结果组成的数组

var arr=[1,[2,[[3,4],5],6]];

function unid(arr){

var arr1=(arr + '').split(',');//将数组转字符串后再以逗号分隔转为数组

var arr2=arr1.map(function(x){

return Number(x);

});

return arr2;

}

console.log(unid(arr));

//(2)只适用于二维数组转一维数组

var arr=[1,[2,3],[4,5]];

arr1=[].concat.apply([],arr);

console.log(arr1);

/*18.鼠标滑过显示溢出的文本*/

function ellipsis(e) {

var target=e.target;

var containerLength=$(target).width();

var textLength=target.scrollWidth;

var text=$(target).text();

if (textLength > containerLength) {

$(target).attr("title", text);

}

}

$("#xxx").on("mouseenter", ellipsis)