你所不知道的 CSS 阴影技巧与细节 滚动视差?CSS 不在话下 神奇的选择器 :focus-within 当角色转换为面试官之后 NPOI 教程 - 3.2 打印相关设置 前端XSS相关整理 委托入门案例

2022-11-29,,,,

你所不知道的 CSS 阴影技巧与细节

 

关于 CSS 阴影,之前已经有写过一篇,box-shadow 与 filter:drop-shadow 详解及奇技淫巧,介绍了一些关于 box-shadow 的用法。

最近一个新的项目,CSS-Inspiration,挖掘了其他很多有关 CSS 阴影的点子,是之前的文章没有覆盖到的新内容,而且有一些很有意思,遂打算再起一篇。

本文的题目是 CSS 阴影技巧与细节。CSS 阴影,却不一定是 box-shadow 与 filter:drop-shadow,为啥?因为使用其他属性也可以模拟阴影,而且是各种各样的阴影。下面且听我娓娓道来~

单侧投影

先说单侧投影,关于 box-shadow,大部分时候,我们使用它都是用来生成一个两侧的投影,或者一个四侧的投影。如下:

OK,那如果要生成一个单侧的投影呢?

我们来看看 box-shadow 的用法定义:

1
2
3

{
    box-shadow: none | [inset? && [ <offset-x> <offset-y> <blur-radius>? <spread-radius>? <color>? ] ]#
}

以 box-shadow: 1px 2px 3px 4px #333 为例,4 个数值的含义分别是,x 方向偏移值、y 方向偏移值 、模糊半径、扩张半径。

这里有一个小技巧,扩张半径可以为负值。

继续,如果阴影的模糊半径,与负的扩张半径一致,那么我们将看不到任何阴影,因为生成的阴影将被包含在原来的元素之下,除非给它设定一个方向的偏移量。所以这个时候,我们给定一个方向的偏移值,即可实现单侧投影:

CodePen Demo -- css单侧投影

投影背景 / 背景动画

接着上面的说。

很明显,0 = -0,所以当 box-shadow 的模糊半径和扩张半径都为 0 的时候,我们也可以得到一个和元素大小一样的阴影,只不过被元素本身遮挡住了,我们尝试将其偏移出来。

CSS代码如下:

1
2
3
4
5
6
7

div {
    width80px;
    height80px;
    border1px solid #333;
    box-sizing: border-box;
    box-shadow: 80px 80px 0 0 #000;
}

得到如下结果:

有什么用呢?好像没什么意义啊。

额,确实好像没什么用。不过我们注意到,box-shadow 是可以设置多层的,也就是多层阴影,而且可以进行过渡变换动画(补间动画)。但是 background-image: linear-gradient(),也就是渐变背景是不能进行补间动画的。

这又扯到哪里去了。好我们回来,利用上面的特性,我们可以利用 box-shadow 实现原本只能利用渐变才能实现的背景图:

用 box-shadow,实现它的 CSS 代码如下(可以更简化):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

.shadow {
    positionrelative;
    width250px;
    height250px;
}
 
.shadow::before {
    content"";
    positionabsolute;
    width50px;
    height50px;
    top-50px;
    left-50px;
    box-shadow:
        50px 50px #000150px 50px #000250px 50px #000,
        50px 100px #000150px 100px #000250px 100px #000,
        50px 150px #000150px 150px #000250px 150px #000,
        50px 200px #000150px 200px #000250px 200px #000,
        50px 250px #000150px 250px #000250px 250px #000;
}

用渐变来实现的话,只需要这样:

1
2
3
4
5
6

.gradient {
    width250px;
    height250px;
    background-image: linear-gradient(90deg, #000 0%#000 50%#fff 50%#fff 100%);
    background-size:  100px 100px;
}

为什么选择更为复杂的 box-shadow 呢?因为它可以进行补间动画,像这样,这是使用渐变做不到的:

CodePen Demo -- box-shadow实现背景动画

当然,这只是个示例 Demo,运用点想象力还有很多有意思的效果,再贴一个:

CodePen Demo -- CSS Checker Illusion( By David Khourshid )

嗯,很有意思,就是实际用途可能不大。

立体投影

好,我们继续。下一个主题是立体投影。

这个说法很奇怪,阴影的出现,本就是为了让原本的元素看起来更加的立体,那这里所谓的立体投影,是个怎么立体法?

这里所谓的立体投影,并不一定是使用了 box-shadowtext-shadow 或者 drop-shadow,而是我们使用其他元素或者属性模拟元素的阴影。而这样做的目的,是为了能够突破 box-shadow 这类元素的一些定位局限。让阴影的位置、大小、模糊度可以更加的灵活。

OK,让我们来看看,这样一个元素,我们希望通过自定义阴影的位置,让它更加立体:

上图 div 只是带了一个非常浅的 bos-shadow ,看上去和立体没什么关系,接下来,我们通过 div 的伪元素,给它生成一个和原图边角形状类似的图形,再通过 transform 位移一下,可能是这样:

OK,最后对这个用伪元素生成的元素进行一些虚化效果(filter或者box-shadow都可以),就可以实现一个边角看起来像被撕开的立体效果:

代码非常简单,伪 CSS 代码示意如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

div {
    positionrelative;
    width600px;
    height100px;
    background: hsl(48100%50%);
    border-radius: 20px;
}
 
div::before {
    content"";
    positionabsolute;
    top50%;
    left5%;
    right5%;
    bottom0;
    border-radius: 10px;
    background: hsl(48100%20%);
    transform: translate(0-15%) rotate(-4deg);
    transform-origin: center center;
    box-shadow: 0 0 20px 15px hsl(48100%20%);
}

所以总结一下:

立体投影的关键点在于利于伪元素生成一个大小与父元素相近的元素,然后对其进行 rotate 以及定位到合适位置,再赋于阴影操作
颜色的运用也很重要,阴影的颜色通常比本身颜色要更深,这里使用 hsl 表示颜色更容易操作,l 控制颜色的明暗度

还有其他很多场景:

CodePen Demo -- 立体投影

文字立体投影 / 文字长阴影

上面的立体效果在文字上就完全不适用了,所以对待文字的立体阴影效果,还需要另辟蹊径。

正常而言,我们使用 text-shadow 来生成文字阴影,像这样:

1
2
3
4
5

<div> Txt Shadow</div>
-----
div {
    text-shadow6px 6px 3px hsla(14100%30%1);
}

嗯,挺好的,就是不够立体。那么要做到立体文字阴影,最常见的方法就是使用多层文字阴影叠加。

Tips:和 box-shadow 一样,text-shadow 是可以叠加多层的!但是对于单个元素而言, drop-shadow的话就只能是一层。

好,上面的文字,我们试着叠加个 50 层文字阴影试一下。额,50 层手写,其实很快的~

好吧,手写真的太慢了,还容易出错,所以这里我们需要借助一下 SASS/LESS 帮忙,写一个生成 50 层阴影的 function 就好,我们每向右和向下偏移 1px,生成一层 text-shadow:

1
2
3
4
5
6
7
8
9
10
11
12
13

@function makeLongShadow($color) {
    $val: 0px 0px $color;
 
    @for $i from 1 through 50 {
        $val: #{$val}, #{$i}px #{$i}px #{$color};
    }
 
    @return $val;
}
 
div {
    text-shadow: makeLongShadow(hsl(14100%30%));
}

上面的 SCSS 代码。经过编译后,就会生成如下 CSS:

1
2
3

div {
      text-shadow0px 0px #9924001px 1px #9924002px 2px #9924003px 3px #9924004px 4px #9924005px 5px #9924006px 6px #9924007px 7px #9924008px 8px #9924009px 9px #99240010px 10px #99240011px 11px #99240012px 12px #99240013px 13px #99240014px 14px #99240015px 15px #99240016px 16px #99240017px 17px #99240018px 18px #99240019px 19px #99240020px 20px #99240021px 21px #99240022px 22px #99240023px 23px #99240024px 24px #99240025px 25px #99240026px 26px #99240027px 27px #99240028px 28px #99240029px 29px #99240030px 30px #99240031px 31px #99240032px 32px #99240033px 33px #99240034px 34px #99240035px 35px #99240036px 36px #99240037px 37px #99240038px 38px #99240039px 39px #99240040px 40px #99240041px 41px #99240042px 42px #99240043px 43px #99240044px 44px #99240045px 45px #99240046px 46px #99240047px 47px #99240048px 48px #99240049px 49px #99240050px 50px #992400;
}

看看效果:

额,很不错,很立体。但是,就是丑,而且说不上来的奇怪。

问题出在哪里呢,阴影其实是存在明暗度和透明度的变化的,所以,对于渐进的每一层文字阴影,明暗度和透明度应该都是不断变化的。这个需求,SASS 可以很好的实现,下面是两个 SASS 颜色函数:

fade-out 改变颜色的透明度,让颜色更加透明
desaturate 改变颜色的饱和度值,让颜色更少的饱和

关于 SASS 颜色函数,可以看看这里:Sass基础—颜色函数

我们使用上面两个 SASS 颜色函数修改一下我们的 CSS 代码,主要是修改上面的 makeLongShadow function 函数:

1
2
3
4
5
6
7
8
9
10

@function makelongrightshadow($color) {
    $val: 0px 0px $color;
 
    @for $i from 1 through 50 {
        $color: fade-out(desaturate($color, 1%), .02);
        $val: #{$val}, #{$i}px #{$i}px #{$color};
    }
 
    @return $val;
}

好,看看最终效果:

嗯,大功告成,这次顺眼了很多~

CodePen Demo -- 立体文字阴影

当然,使用 CSS 生成立体文字阴影的方法还有很多,下面再贴出一例,使用了透明色叠加底色的多重线性渐变实现的文字立体阴影,感兴趣的同学可以去看看具体实现:

线性渐变配合阴影实现条纹立体阴影条纹字

长投影

上面提到了通过多层阴影叠加实现文字的立体阴影。运用在 div 这些容器上也是可以的。当然这里还有一种挺有意思的方法。假设我们,有一个矩形元素,希望给他添加一个长投影,像下面这样:

要生成这种长投影,刚刚说的叠加多层阴影可以,再就是借助元素的两个伪元素,其实上面的图是这样的:

关键点在于,我们通过对两个伪元素的 transform: skew() 变换以及从实色到透明色的背景色变化,实现了长投影的效果:

CodePen Demo -- 线性渐变模拟长阴影

彩色投影

通常而言,我们生成阴影的方式大多是 box-shadow 、filter: drop-shadow() 、text-shadow 。但是,使用它们生成的阴影通常只能是单色或者同色系的。

你这么说,难道还可以生成渐变色的阴影不成?

额,当然不行。

这个真不行,但是通过巧妙的利用 filter: blur 模糊滤镜,我们可以假装生成渐变色或者说是颜色丰富的阴影效果。

假设我们有下述这样一张头像图片:

下面就利用滤镜,给它添加一层与原图颜色相仿的阴影效果,核心 CSS 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

.avator {
    positionrelative;
    backgroundurl($img) no-repeat center center;
    background-size100% 100%;
     
    &::after {
        content"";
        positionabsolute;
        top10%;
        width100%;
        height100%;
        background: inherit;
        background-size100% 100%;
        filter: blur(10px) brightness(80%) opacity(.8);
        z-index-1;
    }
}

看看效果:

其简单的原理就是,利用伪元素,生成一个与原图一样大小的新图叠加在原图之下,然后利用滤镜模糊 filter: blur() 配合其他的亮度/对比度,透明度等滤镜,制作出一个虚幻的影子,伪装成原图的阴影效果。

嗯,最重要的就是这一句 filter: blur(10px) brightness(80%) opacity(.8); 。

CodePen Demo -- filter create shadow

使用 box-shadow 实现的灯光效果

好,上文主要是一些实现各种阴影的方法,接下来是效果篇。先来看看使用 box-shadow实现的一些灯光效果。

box-shadow 实现霓虹氖灯文字效果

这个效果也叫 Neon,Codepen 上有很多类似的效果,本质上都是大范围的 box-shadow 过渡效果与白色文字的叠加:

CodePen Demo -- box-shadow实现霓虹氖灯文字效果

使用box-shadow实现阴影灯光show

和上面的效果类似,本质上都是多重阴影的过渡效果,或许再来点 3D 效果?

合理搭配,效果更佳:

CodePen Demo -- 使用box-shadow实现阴影灯光show

使用 drop-shadow | box-shadow 实现单标签抖音 LOGO

嗯哼,既然标题叫你所不知道的 CSS 阴影技巧与细节,那么本文也应该有一点奇技淫巧。

先来看这个,单个标签实现仿抖音 LOGO,当然由于限定在一个元素,所以细节方面还是有很多瑕疵。

想着仿的缘由是某天刷抖音的时候看见这个 LOGO 的一时兴起,CSS 写多了,看见什么东西都会条件反射的想这个能不能用 CSS 实现。

我们先来看看抖音的 LOGO:

其实很简单,主体其实是由3个颜色不同类似 J 的形状组成。而单独拎出一个,又可以把它分成四分之三圆、|以及㇏组成。

正好,一个元素加上它的两个伪元素,刚好可以凑成这三个形状,我们试着实现以下,简单 CSS 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

<div></div>
---
div {
    positionrelative;
    width37px;
    height218px;
    background#fff;
 
    &::before {
        content"";
        positionabsolute;
        width100px;
        height100px;
        border37px solid #fff;
        border-top37px solid transparent;
        border-radius: 50%;
        top123px;
        left-137px;
        transform: rotate(45deg);
    }
     
        &::after {
        content"";
        positionabsolute;
        width140px;
        height140px;
        border30px solid #fff;
        border-right30px solid transparent;
        border-top30px solid transparent;
        border-left30px solid transparent;
        top-100px;
        right-172px;
        border-radius: 100%;
        transform: rotate(45deg);
    }
}

上面的代码就可以生成整个形状的主体:

接下来就是轮到 filter: drop-shadow() 登场,它可以在元素呈现之前,为元素的渲染提供一些效果,最常见的也就用它渲染整体阴影。我们通常会用它来实现对话框的小三角与整个对话框的阴影效果,像下面这样,左边是使用 drop-shadow 的效果,右边是使用普通 box-shadow的效果。

本文假定读者已经了解了 drop-shadow 的基本用法,上图效果来自这里:CodePen Demo -- Drop-shadow vs box-shadow (2) By Kseso

OK,回到我们正文,下面我们使用 filter: drop-shadow() 生成它的第一层左边的蓝色阴影,添加在主体 div:

1
2
3
4
5
6
7
8
9
10
11
12

div {
    positionrelative;
    width37px;
    height218px;
    background#fff;
    filter:drop-shadow(-10px -10px 0 #24f6f0);
 
   &::before,
   &::after {
    ...
    }
}

得到如下效果:

好,接下来我们只需要再添加一层红色 filter: drop-shadow() 在右侧就大功告成!

等等!哪里不对,上面我也有提到过, 和 box-shadow 一样,text-shadow 是可以叠加多层的!但是对于单个元素而言, drop-shadow 的话就只能是一层。

也就是说,无法在 div 上再使用 filter: drop-shadow() 生成另一侧的红色投影,不过还好,我们还有两个伪元素的filter: drop-shadow() 以及 box-shadow 还没有用上,经过一番尝试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

div {
    positionrelative;
    width37px;
    height218px;
    background#fff;
    filter:drop-shadow(-10px -10px 0 #24f6f0) contrast(150%) brightness(110%);
    box-shadow: 11.6px 10px 0 0 #fe2d52;
     
    &::before {
        ....
        filter: drop-shadow(16px 0px 0 #fe2d52);
    }
     
    &::after {
        ....
        filter:drop-shadow(14px 0 0 #fe2d52);
    }
}

我们分别再利用 div 的 box-shadow 以及两个伪元素的 filter: drop-shadow() ,在单个标签的限制下,最终结果如下:

CodePen Demo -- 单标签实现抖音LOGO

总结一下:

主要借助了两个伪元素实现了整体结构,借助了 drop-shadow 生成一层整体阴影
drop-shadow 只能是单层阴影,所以另一层阴影需要多尝试
contrast(150%) brightness(110%) 则可以增强图像的对比度和亮度,更贴近抖音LOGO的效果

当然,关于 CSS 阴影还有很多有意思的技巧和细节,本文限于篇幅不再一一罗列。

我在 Git 上开了个仓库,CSS-Inspiration,以分类的形式,展示不同 CSS 属性或者不同的课题使用 CSS 来解决的各种方法。更多有意思的 CSS 技巧可以在这里找到,而且是每日更新。

最后

感谢耐心读完。更多精彩 CSS 技术文章汇总在我的 Github -- iCSS ,持续更新,欢迎点个 star 订阅收藏。

好了,本文到此结束,希望对你有帮助 🙂

如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。

 

滚动视差?CSS 不在话下

 

何为滚动视差

视差滚动(Parallax Scrolling)是指让多层背景以不同的速度移动,形成立体的运动效果,带来非常出色的视觉体验。 作为网页设计的热点趋势,越来越多的网站应用了这项技术。

通常而言,滚动视差在前端需要辅助 Javascript 才能实现。当然,其实 CSS 在实现滚动视差效果方面,也有着不俗的能力。下面就让我们来见识一二:

认识 background-attachment

background-attachment 算是一个比较生僻的属性,基本上平时写业务样式都用不到这个属性。但是它本身很有意思。

background-attachment:如果指定了 background-image ,那么 background-attachment 决定背景是在视口中固定的还是随着包含它的区块滚动的。

单单从定义上有点难以理解,随下面几个 Demo 了解下 background-attachment 到底是什么意思:

background-attachment: scroll

scroll 此关键字表示背景相对于元素本身固定, 而不是随着它的内容滚动。

background-attachment: local

local 此关键字表示背景相对于元素的内容固定。如果一个元素拥有滚动机制,背景将会随着元素的内容滚动, 并且背景的绘制区域和定位区域是相对于可滚动的区域而不是包含他们的边框。

background-attachment: fixed

fixed 此关键字表示背景相对于视口固定。即使一个元素拥有滚动机制,背景也不会随着元素的内容滚动。

注意一下 scroll 与 fixed,一个是相对元素本身固定,一个是相对视口固定,有点类似 position 定位的 absolute 和 fixed

可以感受下 3 种不同取值的不同效果:

CodePen Demo -- bg-attachment Demo

使用 background-attachment: fixed 实现滚动视差

首先,我们使用 background-attachment: fixed 来实现滚动视差。fixed 此关键字表示背景相对于视口固定。即使一个元素拥有滚动机制,背景也不会随着元素的内容滚动。

这里的关键在于,即使一个元素拥有滚动机制,背景也不会随着元素的内容滚动。也就是说,背景图从一开始就已经被固定死在初始所在的位置。

我们使用,图文混合排布的方式,实现滚动视差,HTML 结构如下,.g-word 表示内容结构,.g-img 表示背景图片结构:

1
2
3
4
5
6
7

<section class="g-word">Header</section>
<section class="g-img">IMG1</section>
<section class="g-word">Content1</section>
<section class="g-img">IMG2</section>
<section class="g-word">Content2</section>
<section class="g-img">IMG3</section>
<section class="g-word">Footer</section>

关键 CSS:

1
2
3
4
5
6
7
8
9
10

section {
    height100vh;
}
 
.g-img {
    background-imageurl(...);
    background-attachmentfixed;
    background-size: cover;
    background-positioncenter center;
}

效果如下:

CodePen Demo -- https://codepen.io/Chokcoco/pen/JBaQoY

嗯?有点神奇,为什么会是这样呢?可能很多人会和我一样,第一次接触这个属性对这样的效果感到懵逼。

我们把上面 background-attachment: fixed 注释掉,或者改为 background-attachment: local,再看看效果:

CodePen Demo -- bg-attachment:local

这次,图片正常跟随滚动条滚动了,按常理,这种效果才符合我们大脑的思维。

而滚动视差效果,正是不按常理出牌的一个效果,重点来了:

当页面滚动到图片应该出现的位置,被设置了 background-attachment: fixed 的图片并不会继续跟随页面的滚动而跟随上下移动,而是相对于视口固定死了。

好,我们再来试一下,如果把所有 .g-word 内容区块都去掉,只剩下全部设置了 background-attachment: fixed 的背景图区块,会是怎么样呢?

HTML 代码如下:

1
2
3

<section class="g-img">IMG1</section>
<section class="g-img">IMG2</section>
<section class="g-img">IMG3</section>

1
2
3
4
5
6
7
8
9
10

section {
    height100vh;
}
 
.g-img {
    background-imageurl(...);
    background-attachmentfixed;
    background-size: cover;
    background-positioncenter center;
}

效果如下:

CodePen Demo

结合这张 GIF,相信能对 background-attachment: fixed 有个更深刻的认识,移动的只有视口,而背景图是一直固定死的。

综上,就是 CSS 使用 background-attachment: fixed 实现滚动视差的一种方式,也是相对而言比较容易的一种。当然,background-attachment: fixed 本身的效果并不仅只是能有用来实现滚动视差效果,合理运用,还可以实现其他很多有趣的效果,这里简单再列一个:

background-attachment: fixed 实现图片点击水纹效果

利用图片相对视口固定,可以有很多有趣的效果,譬如下面这个,来源于这篇文章CSS Water Wave (水波效果):

CodePen Demo -- bg-attachment:fixed Wave

利用图片相对视口固定的特性实现点击的水纹效果。

上面这个效果有点瑕疵,图片在放大容器变大的过程中发生了明显的抖动。当然,效果还是可以的,background-attachment 还有很多有意思的效果可以挖掘。

使用 transform: translate3d 实现滚动视差

言归正传,下面介绍另外一种使用 CSS 实现的滚动视差效果,利用的是 CSS 3D。

原理就是:

    我们给容器设置上 transform-style: preserve-3d 和 perspective: xpx,那么处于这个容器的子元素就将位于3D空间中,

    再给子元素设置不同的 transform: translateZ(),这个时候,不同元素在 3D Z轴方向距离屏幕(我们的眼睛)的距离也就不一样

    滚动滚动条,由于子元素设置了不同的 transform: translateZ(),那么他们滚动的上下距离 translateY 相对屏幕(我们的眼睛),也是不一样的,这就达到了滚动视差的效果。

关于 transform-style: preserve-3d 以及 perspective 本文不做过多篇幅展开,默认读者都有所了解,还不是特别清楚的,可以先了解下 CSS 3D。

核心代码表示就是:

1
2
3
4
5

<div class="g-container">
    <div class="section-one">translateZ(-1)</div>
    <div class="section-two">translateZ(-2)</div>
    <div class="section-three">translateZ(-3)</div>
</div>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

html {
    height100%;
    overflowhidden;
}
 
body {
    perspective: 1px;
    transform-style: preserve-3d;
    height100%;
    overflow-y: scroll;
    overflow-x: hidden;
}
 
.g-container {
    height150%;
 
    .section-one {
        transform: translateZ(-1px);
    }
    .section-two {
        transform: translateZ(-2px);
    }
    .section-three {
        transform: translateZ(-3px);
    }
}

总结就是父元素设置 transform-style: preserve-3d 和 perspective: 1px,子元素设置不同的 transform: translateZ,滚动滚动条,效果如下:

CodePen Demo -- CSS 3D parallax

很明显,当滚动滚动条时,不同子元素的位移程度从视觉上看是不一样的,也就达到了所谓的滚动视差效果。

滚动视差文字阴影/虚影效果

那么,运用 translate3d 的视差效果,又能有一些什么好玩的效果呢?下面这个滚动视差文字阴影/虚影效果很有意思:

CodePen Demo -- CSS translate3d Parallax

当然,通过调整参数(perspective: ?px 以及 transform: translateZ(-?px);),还能有其他很有意思的效果出现:

CodePen Demo -- CSS translate3d Parallax 2

是不是很有电影开片的厂商 LOGO 的特效的感觉  。

师父领进门,修行在个人,怎么制作更好更有意思的效果还是需要花时间钻研和琢磨,这里我仅仅是抛砖引玉,希望能见到更多 Nice 的效果。

最后

感谢耐心读完。更多精彩 CSS 技术文章汇总在我的 Github -- iCSS ,持续更新,欢迎点个 star 订阅收藏。

好了,本文到此结束,希望对你有帮助 🙂

如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。

神奇的选择器 :focus-within

 

CSS 的伪类选择器和伪元素选择器,让 CSS 有了更为强大的功能。

伪类大家听的多了,伪元素可能听到的不是那么频繁,其实 CSS 对这两个是有区分的。

有个错误有必要每次讲到伪类都提一下,有时你会发现伪类元素使用了两个冒号 (::) 而不是一个冒号 (:),这是 CSS3 规范中的一部分要求,目的是为了区分伪类和伪元素,大多数浏览器都支持下面这两种表示方式。

通常而言,

1
2
3
4
5
6
7

#id:after{
 ...
}
 
#id::after{
...
}

符合标准而言,单冒号(:)用于 CSS3 伪类,双冒号(::)用于 CSS3 伪元素。

当然,也有例外,对于 CSS2 中已经有的伪元素,例如 :before,单冒号和双冒号的写法 ::before 作用是一样的。

所以,如果你的网站只需要兼容 webkit、firefox、opera 等浏览器或者是移动端页面,建议对于伪元素采用双冒号的写法,如果不得不兼容低版本 IE 浏览器,还是用 CSS2 的单冒号写法比较安全。

伪类选择器 :focus-within

言归正传,今天要说的就是:focus-within 伪类选择器。

它表示一个元素获得焦点,或,该元素的后代元素获得焦点。划重点,它或它的后代获得焦点。

这也就意味着,它或它的后代获得焦点,都可以触发 :focus-within

:focus-within 的冒泡性

这个属性有点类似 Javascript 的事件冒泡,从可获焦元素开始一直冒泡到根元素 html,都可以接收触发 :focus-within 事件,类似下面这个简单的例子这样:

1
2
3
4
5

<div class="g-father">
    <div class="g-children">
        <input type="button" value="Button">
    </div>
</div>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

html,
body,
.g-father,
.g-children {
    padding30px;
    border:1px solid #999;
}
 
input {
    ...
    &:focus {
        background#00bcd4;
    }
}
 
html:focus-within {
    background#e91e63;
}
body:focus-within {
    background#ff5722;
}
.g-father:focus-within {
    background#ffeb3b;
}
.g-children:focus-within {
    background#4caf50;
}

就是这样:

CodePen Demo -- :focus-within 冒泡触发

这个选择器的存在,让 CSS 有了进一步的让元素持久停留在一种新状态的的能力。

下面几个例子,看看 :focus-within 可以提供什么能力,做些什么事情。

感应用户聚焦区域

它或它的后代获得焦点,这一点使得让感知获焦区域变得更大,所以,最常规的用法就是使用 :focus-within 感应用户操作聚焦区域,高亮提醒。

下面的效果没有任何 JS 代码:

这里是什么意思呢?:focus-within 做了什么呢?

我们无须去给获焦的元素设置 :focus 伪类,而是可以给需要的父元素设置,这样当元素获焦时,我可以一并控制它的父元素的样式

核心思想用 CSS 代码表达出来大概是这样:

1
2
3
4
5
6
7
8

<div class="g-container">
    <div class="g-username">
        <input type="text" placeholder="user name" class="g_input" >
    </div>
    <div class="g-username">
        <input type="text" placeholder="code" class="g_input" >
    </div>
</div>

1
2
3
4
5
6
7

.g-container:focus-within {
    ...
 
    input {
        ....
    }
}

DEMO -- CSS focus-within INPUT

运用上面思想,我们可以把效果做的更炫一点点,在某些场景制作一些增强用户体验的效果:

DEMO -- PURE CSS FOCUS By :focus-within

TAB导航切换

在之前的一篇文章里,介绍了两种纯 CSS 实现的 TAB 导航栏切换方法:

纯CSS的导航栏Tab切换方案

现在又多了一种方式,利用了 :focus-within 可以在父节点获取元素获得焦点的特性,实现的TAB导航切换:

DEMO -- focus-within switch tab

主要的思路就是通过获焦态来控制其他选择器,以及最重要的是利用了父级的 :not(:focus-within) 来设置默认样式:

1
2
3
4
5
6
7
8
9
10
11

.nav-box:not(:focus-within) {
    // 默认样式
}
 
.nav-A:focus-within ~ .content-box .content-A {
    displayblock;
}
 
.nav-B:focus-within ~ .content-box .content-B {
    displayblock;
}

配合 :placeholder-shown 伪类实现表单效果

:focus-within 一个人能力有限,通常也会配合其他伪类实现一些不错的效果。这里要再简单介绍的是另外一个有意思的伪类 :placeholder-shown

:placeholder-shown:The :placeholder-shown CSS pseudo-class represents any or <textarea> element that is currently displaying placeholder text.

另外,划重点,这个伪类是仍处于实验室的方案。也就是未纳入标准,当然我们的目的是探寻有意思的 CSS 。

意思大概就是,当 input 类型标签使用了 placeholder 属性有了默认占位的文字,会触发此伪类样式。配合:not()伪类,可以再改变当默认文字消失后的样式,再配合本文的主角,我们可以实现表单的一系列效果。

CSS 代码大概呈现成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

.g-container {
    width500px;
    height60px;
 
    input {
        height100%;
        width100%;
 
        &:not(:placeholder-shown) {
            ...
        }
 
        &:placeholder-shown {
            ...
        }
    }
 
    &:focus-within {
        ...
    }
}

实际效果如下:

可以看到,上面的效果没有用到任何 JS,可以实现:

    整个 input(包括父元素所在区域)获焦与非获焦样式控制
    placeholder 属性设置的文字出现与消失后样式控制

CodePen Demo -- :placeholder-shown && :focus-within

实现离屏导航

这个是其他很多文章都有提到过的一个功能,利用 focus-within 便捷的实现离屏导航,可以说将这个属性的功能发挥的淋漓尽致,这里我直接贴一个 codepen 上 Dannie Vinther 对这个效果的实现方案:

CodePen Demo -- Off-screen nav with :focus-within [PURE CSS]

实现掘金登录动效切换

juejin.im是我很喜欢的一个博客网站,它的登录有一个小彩蛋,最上面的熊猫在你输入帐号密码的时候会有不同的状态,效果如下:

利用本文所讲的 focus-within ,可以不借助任何 Javascript,实现这个动效:

感兴趣的可以戳这里看看完整的Demo代码:

CodePen Demo -- 掘金登录效果纯CSS实现

兼容性

好了,例子举例的也差不多了,下面到了杀人诛心的兼容性时刻,按照惯例,这种属性大概率是一片红色,看看 CANIUSE,截图日期(2018/08/02),其实也还不算特别惨淡。

一. 面试官需要做到三件事

1. 自己复习好或去学一下相关技术点

没错,面试官也是需要准备的。要防止理解不来候选人的技术,也为了能够hold住现场。

2. 想一些要问的问题,想好怎么评判候选人

问题的选择还要有连贯性,更进一步地,还要预先想到候选人可能会怎么答,自己要怎么接。作为面试官,最好提问要思路清晰不要断。

3. 想好怎么在面试过程中记录重点的讨论,用以佐证面试结果

二、怎么考察校招候选人

在我们这边,初面一般会限制在15分钟左右,要在这么短时间内考察一个人是有点难的,所以要抓好关键点。

对于应届校招生来说,我个人会从这四个角度考察

1. 计算机基础

对于在校生来说,会比较看重基础,数据结构、算法、网络、操作系统 这些还是会涉及的,但不会太难。

社招有另外的玩法,另外,在校的暑期实习生会降低一些要求

基本都会从以下抽几个来问问,前端基础和项目经验失分的话,这里就是得分点了

数据结构:栈和队列的区别,JS里面的栈和队列,二叉树的几种遍历方式(高级)

算法:二分查找,冒泡排序,插入排序,快排(高级),深度/广度优先搜索(高级)

网络:OSI七层模型,HTTP/TCP在哪一层,HTTP和HTTPS区别,HTTP三次握手和四次挥手,常见状态码和首部字段,GET和POST区别,HTTPS连接过程(高级),了解HTTP2么(高级)

操作系统:进程和线程的区别

2. 前端基础

虽然对在校生看重基础,也不代表不看重前端的专业基础。毕竟你是要做前端的,就应该有所准备。

问题首先会选前面三个基础的,然后会根据候选人用过的技术、框架来调整,一般都会问为什么要用XX技术,XX技术相比起来有什么优点。

HTML:怎么理解语义化标准化,HTML5新增的特性(别只会说新标签,我希望你能说出新的API)

CSS: 垂直居中的实现,position属性值的区别,浮动的问题和解决,怎么用CSS画圆画三角形,Flex布局用过没,rem是什么以及和em的区别(高级),BFC(高级),内联盒模型(高级),CSS动画的简单使用

JS: 闭包是什么以及特点,怎么继承举个栗子,作用域是什么举个栗子,setTimeout/setInterval区别,了解过哪些事件(很多人把双击事件说成是doubleclick),原生获取DOM元素(希望能说出新的API)、获取元素宽高方式,clientHeight/offsetHeight/scrollHeight区别(高级),在元素后面放元素(高级)

ES6:用过哪些新特性,let和var区别,Promise状态及为什么用它,箭头函数this指向

jQuery: 链式调用怎么实现,有看过源码么(说一下知道有哪些实现),它有什么缺点

bootstrap: 为什么用它(希望能说出响应式),简单说几个用法,自己可以实现栅格化么(高级)

NodeJS:它有什么特点,为什么不用其他后台语言

Webpack:有自己配置过么,loader和plugins的区别,和gulp/grunt的区别

综合:前端安全的认识,前端优化的方法,强制缓存/协商缓存相关,cookie和session,websocket和http区别(不用它则怎么实现实时),url从输入到页面渲染的过程(dns解析过关,DOM构建过程加分,浏览器进线程加加分)

框架:为什么用Angular/React/Vue/Redux/Vuex(说出特点,它们解决了什么问题),生命周期,setState不保证同步,组件间通信

其他: ...

3. 项目经验

项目经验主要考察候选人的实践,以及解决问题能力。一般来说一个项目太少了,列2-4个比较好,太多也看不了那么多

做过的最好的项目是什么,这个项目里面用到什么技术(期间会从关键词展开问技术点)

在项目里面遇到过什么棘手的问题没,怎么解决的(希望不要只说百度搜索)

如果让你优化这个项目,你觉得可以怎么改进

这个部分也包括过往在公司里面的实习经历

实习期间做的是什么,有什么收获

实习期间的开发流程

如果前面技术基础和前端基础答得不错了,这个部分就不会问太多了,因为时间不够用,且一般也会放在下一轮面试中细问。

如果前面都答得不好,如果项目经历这个部分有优势,还是有希望的。

4. 综合能力

态度,有没有迟到

会稍稍关注专业课的成绩(不一定)

学前端多久了,怎么学的,看过哪些书,有没有技术总结

个人优点/缺点

关注技术热情,职业规划有没有

沟通顺不顺畅(要注意反应不要太迟钝,自己注意什么时候该收口了),思维条理清晰与否

上面列举了这么多想要问的问题,问题是列举不完的,时间限制也不允许问那么多,也没必要,所以会从各方面抽取几个问题来问。

首先,我会点几个计算机基础,然后着重问前端基础,如果前端基础挺好,就再过一下项目经验,不出差错基本就通过了。

如果前端基础和项目经验这块挺一般的,就会再回去问一些计算机基础,结合专业成绩考察,如果计算机基础还不错,从第四点综合能力判断值不值得培养,如果面试人数很多的话会放到备考虑,一般会放到通过(看我人多好~)

如果计算机基础,前端基础都挺一般,那肯定是不通过了。

最纠结的是评判备考虑,某些点好某些点又不好的难以抉择,所以希望候选人不要有“突出”的短板,为自己也为面试官。

三、面试官是怎么记录面试过程的

面试过程只有十几分钟,面试官在和候选人沟通的同时需要记录相关的关键词,用以佐证面试结果。

我们这里面试记录有候选人阐述和面试官评价两个部分,下面就列一下我某条“通过”的记录,

候选人阐述

在校期间成绩还可以,奖学金,大赛获奖

大三开始学前端,看视频,看书,在自己博客总结记录

JS比CSS好一些

实习前自己做了三个项目

去stackoverflow,github,官方文档解决问题

规划:先深入基础,后面会做一些NodeJS相关的,走入全栈

面试官评价

osi七层模型,http/s区别,http四次挥手

进程和线程区别 不了解

前端优化方法 ok   前端安全 一般

语义化 ok

url -> 页面渲染过程 中等(dns查询,网页渲染流程)

垂直居中,使用position, flex布局

js闭包,继承 ok    原生js操作dom 一般

看过jq源码 只知道无new式对象的实现

session和cookie区别

react比jq优点,虚拟dom,setState,组件间通信(props回调,发布订阅)中等

二分查找 熟悉

反应较快,沟通顺畅,话稍多

技术基础:中等

前端基础:中等

综合:中等

这个“通过”的关键词有点多,有时也不会列举辣么多。

通过的时候会着重列举好的地方,不通过的时候一般多为不好的

虽说一天之内面了那么多人,也只能说是面试菜菜,还需要多多改进练习,参与主宰一个人的工作机会,这种感觉很特别呀~

希望各位前端儿,能够尽快巩固好自己的基础,包括计算机基础和前端基础。

其一,它是敲门砖;其二,工作之后会懒得去学,也没那么多时间去学基础了。

NPOI 教程 - 3.2 打印相关设置

 

打印设置主要包括方向设置、缩放、纸张设置、页边距等。NPOI 1.2支持大部分打印属性,能够让你轻松满足客户的打印需要。

首先是方向设置,Excel支持两种页面方向,即纵向和横向。

在NPOI中如何设置呢?你可以通过HSSFSheet.PrintSetup.Landscape来设置,Landscape是布尔类型的,在英语中是横向的意思。如果Landscape等于true,则表示页面方向为横向;否则为纵向。

接着是缩放设置,

这里的缩放比例对应于HSSFSheet.PrintSetup.Scale,而页宽和页高分别对应于 HSSFSheet.PrintSetup.FitWidth和HSSFSheet.PrintSetup.FitHeight。要注意的是,这里的 PrintSetup.Scale应该被设置为0-100之间的值,而不是小数。

接下来就是纸张设置了,对应于HSSFSheet.PrintSetup.PaperSize,但这里的PaperSize并不是随便设置的,而是由一些固定的值决定的,具体的值与对应的纸张如下表所示:

纸张
1 US Letter 8 1/2 x 11 in
2 US Letter Small 8 1/2 x 11 in
3 US Tabloid 11 x 17 in
4 US Ledger 17 x 11 in
5 US Legal 8 1/2 x 14 in
6 US Statement 5 1/2 x 8 1/2 in
7 US Executive 7 1/4 x 10 1/2 in
8 A3 297 x 420 mm
9 A4 210 x 297 mm
10 A4 Small 210 x 297 mm
11 A5 148 x 210 mm
12 B4 (JIS) 250 x 354
13 B5 (JIS) 182 x 257 mm
14 Folio 8 1/2 x 13 in
15 Quarto 215 x 275 mm
16 10 x 14 in
17 11 x 17 in
18 US Note 8 1/2 x 11 in
19 US Envelope #9 3 7/8 x 8 7/8
20 US Envelope #10 4 1/8 x 9 1/2
21 US Envelope #11 4 1/2 x 10 3/8
22 US Envelope #12 4 \276 x 11
23 US Envelope #14 5 x 11 1/2
24 C size sheet
25 D size sheet
26 E size sheet
27 Envelope DL 110 x 220mm
28 Envelope C5 162 x 229 mm
29 Envelope C3 324 x 458 mm
30 Envelope C4 229 x 324 mm
31 Envelope C6 114 x 162 mm
32 Envelope C65 114 x 229 mm
33 Envelope B4 250 x 353 mm
34 Envelope B5 176 x 250 mm
35 Envelope B6 176 x 125 mm
36 Envelope 110 x 230 mm
37 US Envelope Monarch 3.875 x 7.5 in
38 6 3/4 US Envelope 3 5/8 x 6 1/2 in
39 US Std Fanfold 14 7/8 x 11 in
40 German Std Fanfold 8 1/2 x 12 in
41 German Legal Fanfold 8 1/2 x 13 in
42 B4 (ISO) 250 x 353 mm
43 Japanese Postcard 100 x 148 mm
44 9 x 11 in
45 10 x 11 in
46 15 x 11 in
47 Envelope Invite 220 x 220 mm
48 RESERVED--DO NOT USE
49 RESERVED--DO NOT USE
50 US Letter Extra 9 \275 x 12 in
51 US Legal Extra 9 \275 x 15 in
52 US Tabloid Extra 11.69 x 18 in
53 A4 Extra 9.27 x 12.69 in
54 Letter Transverse 8 \275 x 11 in
55 A4 Transverse 210 x 297 mm
56 Letter Extra Transverse 9\275 x 12 in
57 SuperA/SuperA/A4 227 x 356 mm
58 SuperB/SuperB/A3 305 x 487 mm
59 US Letter Plus 8.5 x 12.69 in
60 A4 Plus 210 x 330 mm
61 A5 Transverse 148 x 210 mm
62 B5 (JIS) Transverse 182 x 257 mm
63 A3 Extra 322 x 445 mm
64 A5 Extra 174 x 235 mm
65 B5 (ISO) Extra 201 x 276 mm
66 A2 420 x 594 mm
67 A3 Transverse 297 x 420 mm
68 A3 Extra Transverse 322 x 445 mm
69 Japanese Double Postcard 200 x 148 mm
70 A6 105 x 148 mm
71 Japanese Envelope Kaku #2
72 Japanese Envelope Kaku #3
73 Japanese Envelope Chou #3
74 Japanese Envelope Chou #4
75 Letter Rotated 11 x 8 1/2 11 in
76 A3 Rotated 420 x 297 mm
77 A4 Rotated 297 x 210 mm
78 A5 Rotated 210 x 148 mm
79 B4 (JIS) Rotated 364 x 257 mm
80 B5 (JIS) Rotated 257 x 182 mm
81 Japanese Postcard Rotated 148 x 100 mm
82 Double Japanese Postcard Rotated 148 x 200 mm
83 A6 Rotated 148 x 105 mm
84 Japanese Envelope Kaku #2 Rotated
85 Japanese Envelope Kaku #3 Rotated
86 Japanese Envelope Chou #3 Rotated
87 Japanese Envelope Chou #4 Rotated
88 B6 (JIS) 128 x 182 mm
89 B6 (JIS) Rotated 182 x 128 mm
90 12 x 11 in
91 Japanese Envelope You #4
92 Japanese Envelope You #4 Rotated
93 PRC 16K 146 x 215 mm
94 PRC 32K 97 x 151 mm
95 PRC 32K(Big) 97 x 151 mm
96 PRC Envelope #1 102 x 165 mm
97 PRC Envelope #2 102 x 176 mm
98 PRC Envelope #3 125 x 176 mm
99 PRC Envelope #4 110 x 208 mm
100 PRC Envelope #5 110 x 220 mm
101 PRC Envelope #6 120 x 230 mm
102 PRC Envelope #7 160 x 230 mm
103 PRC Envelope #8 120 x 309 mm
104 PRC Envelope #9 229 x 324 mm
105 PRC Envelope #10 324 x 458 mm
106 PRC 16K Rotated
107 PRC 32K Rotated
108 PRC 32K(Big) Rotated
109 PRC Envelope #1 Rotated 165 x 102 mm
110 PRC Envelope #2 Rotated 176 x 102 mm
111 PRC Envelope #3 Rotated 176 x 125 mm
112 PRC Envelope #4 Rotated 208 x 110 mm
113 PRC Envelope #5 Rotated 220 x 110 mm
114 PRC Envelope #6 Rotated 230 x 120 mm
115 PRC Envelope #7 Rotated 230 x 160 mm
116 PRC Envelope #8 Rotated 309 x 120 mm
117 PRC Envelope #9 Rotated 324 x 229 mm
118 PRC Envelope #10 Rotated 458 x 324 mm

(此表摘自《Excel Binary File Format (.xls) Structure Specification.pdf》)

HSSFSheet下面定义了一些xxxx_PAPERSIZE的常量,但都是非常常用的纸张大小,如果满足不了你的需要,可以根据上表自己给PaperSize属性赋值。所以,如果你要设置纸张大小可以用这样的代码:

HSSFSheet.PrintSetup.PaperSize=HSSFSheet.A4_PAPERSIZE;

HSSFSheet.PrintSetup.PaperSize=9; (A4 210*297mm)

再下来就是打印的起始页码,它对应于HSSFSheet.PrintSetup.PageStart和 HSSFSheet.PrintSetup.UsePage,如果UsePage=false,那么就相当于“自动”,这时PageStart不起作用; 如果UsePage=true,PageStart才会起作用。所以在设置PageStart之前,必须先把UsePage设置为true。

“打印”栏中的“网格线”设置对应于HSSFSheet.IsPrintGridlines,请注意,这里不是 HSSFSheet.PrintSetup下面,所以别搞混了。这里之所以不隶属于PrintSetup是由底层存储该信息的record决定的,底层是 把IsGridsPrinted放在GridsetRecord里面的,而不是PrintSetupRecord里面的,尽管界面上是放在一起的。另外还 有一个HSSFSheet.IsGridsPrinted属性,这个属性对应于底层的gridset Record,但这个record是保留的,从微软的文档显示没有任何意义,所以这个属性请不要去设置。

“单色打印”则对应于HSSFSheet.PrintSetup.NoColors,这是布尔类型的,值为true时,表示单色打印。

“草稿品质”对应于HSSFSheet.PrintSetup.IsDraft,也是布尔类型的,值为true时,表示用草稿品质打印。

这里的打印顺序是由HSSFSheet.PrintSetup.LeftToRight决定的,它是布尔类型的,当为true时,则表示“先行后列”;如果是false,则表示“先列后行”。

在NPOI 1.2中,“行号列标”、“批注”和“错误单元格打印为”、“页边距”暂不支持,将在以后的版本中支持。

前端XSS相关整理

 

前端安全方面,主要需要关注 XSS(跨站脚本攻击 Cross-site scripting) 和 CSRF(跨站请求伪造 Cross-site request forgery)

当然了,也不是说要忽略其他安全问题:后端范畴、DNS劫持、HTTP劫持、加密解密、钓鱼等

CSRF主要是借用已登录用户之手发起“正常”的请求,防范措施主要就是对需要设置为Post的请求,判断Referer以及token的一致性,本文不展开

相对来说,XSS的内容就非常庞大了,下面就来整理一下一些XSS的知识点。比较匆忙,可能有点乱哈~

一、XSS

恶意攻击者向页面中注入可执行的JS代码来实现XSS的攻击。

如常见的

Payload:<script>alert(1)</script>
<div>[输出]</div>

<div><script>alert(1)</script></div>

这个 Payload 可以从编辑区域而来

<input type="text" value="[输入]" />

当然,输入和输出的位置还可以出现在其他地方,根据输入输位置的不同,可以形成不同类型的XSS,相应的防范措施也不同。

1.1 XSS的分类

一般来说,可以将XSS分为三类:反射型XSS、存储型XSS、DOM-base 型XSS

1.1.1 反射型XSS

大多通过URL进行传播,发请求时,XSS代码出现在URL中,提交给服务端。服务端未进行处理或处理不当,返回的内容中也带上了这段XSS代码,最后浏览器执行XSS代码

比如在 php的smarty模板中直接获取url的参数值

Payload: <script>alert(1)</script>
http://local.abc.com/main/?r=abc/index&param=<script>alert(1)</script>

<div><{$smarty.get.param}></div>

X-XSS-Protection

新版Chrome和Safari中,已自动屏蔽了这种XSS,形如

这个屏蔽是由 XSS Auditor操作的,它由HTTP返回头部进行控制,有四个可选值

X-XSS-Protection : 0    关闭浏览器的XSS防护机制
X-XSS-Protection : 1    删除检测到的恶意代码(如果不指定,IE将默认使用这个)
X-XSS-Protection : 1; mode=block   如果检测到恶意代码,将不渲染页面 (如果不指定,Chrome将默认使用这个)
X-XSS-Protection : 1; report=<reporting-uri> 删除检测到的恶意代码,并通过report-uri发出一个警告。

前三个在IE和Chrome中有效,最后一个只在Chrome中有效

可以手动在设置请求头看看变化

header('X-XSS-Protection: 1; mode=block');

建议配置为后两个的结合,禁止页面渲染并进行上报

header('X-XSS-Protection: 1; mode=block; report=www.xss.report');

不建议仅仅配置为1,因为它删除恶意代码的功能有时比较鸡肋,可能会弄巧成拙。

另外,这个配置只能充当辅助作用,不能完全依赖,其也可能会产生一些问题

不过在Firefox中并未屏蔽

在IE中的XSS Filter也默认也开启了屏蔽,也可手动关闭试试,或者通过HTTP头部进行控制

1.1.2 存储型XSS

提交的XSS代码会存储在服务器端,服务端未进行处理或处理不当,每个人访问相应页面的时候,将会执行XSS代码

如本文开始的第一个例子

1.1.3 DOM-base 型XSS

这个类型和反射型的有点类似,区别是它不需要服务端参与

比如在JS中直接获取URL中的值

Payload: alert('xss')
http://local.abc.com/main/?r=abc/index#alert('xss')

<script>
    var hash = eval(location.hash.slice(1));
</script>

另外,有些攻击方式的类型是单一的,有些是混合的。防范攻击,不应仅根据类型来防范,而应根据输入输出的不同来应对。

在反射型和DOM-base型中,一般会通过设置一些有诱导性质的链接,用户点击链接后则触发链接中的XSS

Content Security Policy(CSP)内容安全策略

为了防范XSS,CSP出现了。

CSP 的实质就是白名单制度,开发者明确告诉客户端,哪些外部资源可以加载和执行,提供了这种白名单之后,实现和执行则由浏览器完成

通过一系列的自定义配置,可以在很大程度上防止恶意脚本的攻击,建议进行配置。

不过策略比较新,在各浏览器也有一些兼容性的问题。另外,似乎还是可以通过一些手段绕过的,这里就不展开了

Cookie 配置

大多使用cookie来实现对用户的认证。如果攻击者拿到了这个认证cookie,就可以登录了用户的账号了

XSS的主要目的是为了得到cookie,当然也不仅是为了获取cookie

cookie安全注意点

Httponly:防止cookie被xss偷

https:防止cookie在网络中被偷

Secure:阻止cookie在非https下传输,很多全站https时会漏掉

Path :区分cookie的标识,安全上作用不大,和浏览器同源冲突

通过设置 cookie的几个属性,可以在一定程度上保障网站的安全

不过并没有十全十美的东西,虽然攻击门槛提高了,但HttpOnly在某些特定情况下还是能绕过的,道高一尺魔高一点一尺呀

1.2 执行JS代码

XSS的目的一般是盗取cookie,一般需要通过JS 的 document.cookie来获取这个值。

所以要先思考的是:在什么地方可以执行JS相关的代码

然后要思考的是:攻击者能不能在这些地方构造出能够执行的脚本

1.2.1  <script>标签中

<script>alert(1);</script>

1.2.2 HTML中的某些事件

<img src="1" onerror="alert(1)" >

<input type="text" onfocus="alert(1)">

<span onmouseover="alert(1)"></span>

1.2.3  javascript: 伪协议

<a href="javascript:alert(1)">test</a>
<iframe src="javascript:alert(1)"></iframe>

location.href = 'javascript:alert(1)'

对于事件的执行触发,是有机会防御的,围观 这篇文章

1.2.4  base64编码的  data: 伪协议

Payload: <script>alert('XSS')</script> ,它的base64编码为PHNjcmlwdD5hbGVydCgnWFNTJyk8L3NjcmlwdD4K
<a href="data:text/html;base64,PHNjcmlwdD5hbGVydCgnWFNTJyk8L3NjcmlwdD4K">test</a>

1.2.5  css中的expression表达式

仅在IE8以下才支持expression,可以忽略这个了

<span style="color:1;zoom:expression(alert(1));"></span>

1.2.6 css中的src

很多文章都说到这个payload,然鹅并没有生效,不知真假

根据一些讨论,在css中是很难实现xss的

.abc {
    background: url(...)
} 

1.2.7 使用 eval、new Function、setTimeout 执行字符串时

setTimeout('alert(1)');

eval('alert(2)');

var f = new Function('alert(3)');
f();

1.3 编码与解码

防范XSS,比较通用的做法是:提交保存前对特殊字符进行过滤转义,进行HTML实体的编码

var escape = {
    '&': '&amp;',
    '<': '&lt;',
    '>': '&gt;',
    '"': '&quot;',
    "'": ''',
    '`': '`'
};

事实上,仅仅这样做还是不够的

那为什么要进行HTML实体的编码呢?

这涉及到浏览器的解析过程。

浏览器在解析HTML文档期间,根据文档中的内容,会经过 HTML解析、JS解析和URL解析几个过程

首先浏览器接收到一个HTML文档时,会触发HTML解析器对HTML文档进行词法解析,这完成HTML解码工作并创建DOM树。

如果HTML文档中存在JS的上下文环境,JavaScript解析器会介入对内联脚本进行解析,完成JS的解码工作。

如果浏览器遇到需要URL的上下文环境,URL解析器也会介入完成URL的解码工作。

URL解析器的解码顺序会根据URL所在位置不同,可能在JavaScript解析器之前或之后解析

1.3.1 HTML实体编码

浏览器会对一些字符进行特殊识别处理,比如将 < > 识别为标签的开始结束。

要想在HTML页面中呈现出特殊字符,就需要用到对应的字符实体。比如在HTML解析过程中,如果要求输出值为 < > ,那么输入值应该为其对应的实体 &lt; &gt;

字符实体以&开头 + 预先定义的实体名称,以分号结束,如“<”的实体名称为&lt;

或以&开头 + #符号 以及字符的十进制数字,如”<”的实体编号为<

或以&开头 + #x符号 以及字符的十六进制数字,如”<”的实体编号为<

字符都是有实体编号的但有些字符没有实体名称。

普通编码与实体编码的在线转换

1.3.2 Javascript编码

Unicode 是字符集,而 utf-8,utf-16,utf-32 是编码规则

最常用的如“\uXXXX”这种写法为Unicode转义序列,表示一个字符,其中xxxx表示一个16进制数字

如”<” Unicode编码为“\u003c”,不区分大小写

普通编码与Unicode转义序列的在线转换

Unicode字符集大全

1.3.3 URL编码

%加字符的ASCII编码对于的2位16进制数字,如”/”对应的URL编码为%2f

转换可以使用 JS 自带的 encodeURIComponent 和 decodeURLComponent 方法来对特殊字符进行转义,也可以对照ASCII表为每个字符进行转换

1.3.4 编码解码分析

<span class="a<b">abc</span>
等价于
<span class="a&lt;b">abc</span>

上述代码中

编码顺序:HTML编码

解码顺序:HTML解码

<a href="//www.baidu.com?a=1&b=2">abc</a>
等价于
<a href="//www.baidu.com?a=1%26b=2">abc</a>
等价于
<a href="//www.baidu.com?a=1%26b=2">abc</a>

上述代码中

编码顺序:URL编码 -> HTML编码

解码顺序:HTML解码 -> URL解码

<a href="#" onclick="alert(1)">abc</a>
等价于
<a href="#" onclick="\u0061\u006c\u0065\u0072\u0074(1)">abc</a>
等价于
<a href="#" onclick="\u0061\u006c\u0065\u0072\u0074(1)">abc</a>

上述代码中

编码顺序:Javascript编码 -> HTML编码

解码顺序:HTML解码 -> Javascript解码

需要注意的是,在JS的解码中,相关的标识符才能被正确解析(如这里的 alert 标识符),

像圆括号、双引号、单引号等等这些控制字符,在进行JavaScript解析的时候仅会被解码为对应的字符串文本(比如这里并未对 (1) 进行编码,如果对括号及括号里面内容做JS编码,将无法执行alert函数 )

<a href="javascript:alert(1<2)">abc</a>
等价于
<a href="javascript:\u0061\u006c\u0065\u0072\u0074(1<2)">abc</a>
等价于(使用JS的方法进行的URL编码)
<a href="javascript:alert(1%3C2)">abc</a>
等价于(使用转换成对应ASCII编码对应2位16进制数字的URL编码)
<a href="javascript:%5c%75%30%30%36%31%5c%75%30%30%36%63%5c%75%30%30%36%35%5c%75%30%30%37%32%5c%75%30%30%37%34%28%31%3C%32%29">abc</a>
等价于
<a href="javascript:alert(1%3C2)">abc</a>

上述代码中

编码顺序:Javascript编码 -> URL编码 -> HTML编码

解码顺序:HTML解码 -> URL解码 -> Javascript解码

这里还需要注意的是,在URL的编码中,不能对协议类型(这里的 javascript: )进行编码,否则URL解析器会认为它无类型,导致无法正确识别

应用这个解析顺序,看以下这个例子

输入源 abc为URL中的值,如果后端仅进行了HTML的编码,还是有问题的

Payload-0: http://local.abc.com/main/?r=abc/index&abc=');alert('11
<span onclick="test('<{$abc}>')">test</span>

<span onclick="test('');alert('11')">test</span>

解码顺序先是进行HTML解码,此时会将 &#x27解析成 ' 号,接着进行Javascript的解码,识别到 ' 即可闭合test函数,调用成功

所以,这种情况下,后端需要先进行Javascript编码再进行HTML的编码

当然,还有其他顺序的混合。也需要考虑编码工作能不能正确地进行过滤

<a href="javascript:window.open('[输入源]')">

解码顺序:

HTML解码 -> URL解码 -> Javascript解码 -> URL解码

引申出去,还有一些字符集的知识点,脑壳疼,就不在这整理了

1.4 常见XSS攻击方式

XSS的攻击脚本多种多样,在使用了模板(前端模板和后端模板)之后,需要格外注意数据的输入输出

下面列举几个常见的

1.4.1 PHP使用Yii框架中的Smarty模板

有时候会使用 $smarty.get.abc 获取URL中的参数,未经转义

Payload-1: http://local.abc.com/main/?r=abc/index&abc=<script>alert(1)</script>
<span><{$smarty.get.abc}></span>

<span><script>alert(1)</script></span>

Payload-2: http://local.abc.com/main/?r=abc/index&abc="><script>alert(1)</script>
<a href="/main/?param=<{$smarty.get.abc}>">abc</a>

<a href="/main/?param="><script>alert(1)</script>">abc</a>

Payload-3: http://local.abc.com/main/?r=abc/index&abc=" onmouseover=alert(1)
<a href="/main/?param=<{$smarty.get.abc}>">abc</a>

<a href="/main/?param=" onmouseover="alert(1)" ">abc</a>

Payload-4: http://local.abc.com/main/?r=abc/index&urlTo=javascript:alert(1)
<a href="<{$smarty.get.urlTo}>">urlTo</a>

<a href="javascript:alert(1)">urlTo</a>

Payload-5: http://local.abc.com/main/?r=abc/index&urlTo=data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pgo=
<a href="<{$smarty.get.urlTo}>">urlTo</a>

<!-- 对 <script>alert(1)</script> 进行 base64编码 为 PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pgo= -->
<a href="data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pgo=">urlTo</a>

Payload-6: http://local.abc.com/main/?r=abc/index&abc=</script><script>alert(1)//
<script>
    var abc = '<{$smarty.get.abc}>';
</script>

<script>
    // 第一个 script标签被闭合,虽然会报错,但不会影响第二个script标签,注意需要闭合后面的引号或注释,防止报错
    var abc = '</script><script>alert(1)//';
</script>

Payload-7: http://local.abc.com/main/?r=abc/index&abc=alert(1)
<script>
    if (<{$smarty.get.abc}> == 'abc') {
        console.log(1);
    }
</script>

<script>
    // 此处因为没有用引号,所以可以直接执行 alert(1)
    if (alert(1) == 'abc') {
        console.log(1);
    }
</script>

Payload-8: http://local.abc.com/main/?r=abc/index&abc='){}if(alert(1)){//
<script>
    if ('<{$smarty.get.abc}>' == 'abc') {
        console.log(1);
    }
</script>

<script>
    // 用了引号之后,闭合难度增加,不过还是可以闭合起来的
    if (''){}if(alert(1)){//' == 'abc') {
        console.log(1);
    }
</script>

Payload-9: http://local.abc.com/main/?r=abc/index&abc=');alert('1
Payload-10: http://local.abc.com/main/?r=abc/index&abc=%26%2339%3B);alert(%26%2339%3B1    对参数进行了HTML的实体编码
<span onclick="test('<{$smarty.get.abc}>')">test</span>

<span onclick="test('');alert('1')">test</span>

Payload-11: http://local.abc.com/main/?r=abc/index&abc=" onfocus="alert(1)" autofocus="autofocus"
<input type="text" id="input" value="<{$smarty.get.abc}>">

<input id="input" value="" onfocus="alert(1)" autofocus="autofocus" "="" type="text">

在线 base64编码解码

解决方式为:

不使用 $smarty.get 相关获取参数,改用后端过滤数据后再返回参数;

Yii框架中相应位置配置:'escape_html' => true

在页面标签内嵌的脚本中直接使用后端返回的数据并不安全,后端可能过滤不完善(见Payload-7和Payload-0)避免直接使用

可以改用将数据存储在属性中,再通过脚本获取属性的方式

1.4.2 JS操作DOM的时候是否会有XSS隐患?

使用 jQuery的append相关方法时(比如 html方法)可能会

// 执行
$($0).html('<script>alert(1);</script>');

// 执行
$($0).html('\u003cscript\u003ealert(1);\u003c/script\u003e');

// 执行
$($0).append('<script>alert(1);</script>');

// 不执行
$0.innerHTML = '<script>alert(1);</script>';

原因是在jQuery中使用了eval方法执行相应的脚本,需要注意的是,Unicode编码的字符在运算中会被解析出来

所以,要注意的是

使用jQuery设置DOM内容时,记得先对内容进行转义

对于设置输入框的值,是安全的

<input type="text" id="input">
<textarea value="12" id="textarea"></textarea>

<script>
    // 不执行
    document.getElementById('input').value = '"><script>alert(1);<\/script>';
    document.getElementById('textarea').value = '"><script>alert(1);<\/script>';

    // 不执行
    $('#input').val('" onmouseover="alert(1)"');
    $('#textarea').val('" onmouseover="alert(1)"');
</script>

对于设置属性的值,是安全的

<input type="text" id="input">
<textarea value="12" id="textarea"></textarea>

<script>
    // 不执行
    document.getElementById('input').setAttribute('abc', '"><script>alert(1);<\/script>');
    document.getElementById('textarea').setAttribute('abc', '"><script>alert(1);<\/script>');

    // 不执行
    $('#input').attr('abc', '" onmouseover="alert(1)"');
    $('#textarea').attr('abc', '" onmouseover="alert(1)"');
</script>

1.4.3 前端Handlebars模板中的安全问题

后端有Smarty模板,前端也可以有Handlebars模板,使用模板有利于开发维护代码。不过和后端一样,使用模板也要考虑到XSS的问题

Handlebars模板中可选择是否开启转义

<!-- 转义,如果name的值已经被后端转义为实体符&gt; 那么Handlebars将会转换成 &amp;gt; 在浏览器中将会显示 &gt; -->
<!-- 所以此时需要先将 &gt; 转回 > 再传入Handlebars模板,才能看到正确的 > 符号 -->
<span>{{name}}</span>

<!-- 不转义 -->
<span>{{{name}}}</span>

所以要注意的第一点是:

如果使用了转义占位符,就需要先进行还原;如果不使用转义,就不要还原,否则将造成XSS

另外,Handlebars模板可以自定义helper,helper有两种使用方式,直接返回数据或返回子层

<!-- 模板 [A] -->
<script type="text/template" id="test-tpl">
    <span abc="{{#abc attrData}}{{/abc}}">111{{#abc data}}{{/abc}}</span>
    <span>
        <input type="text" value="{{#abc attrData}}{{/abc}}">
    </span>
</script>

<!-- 模板 [B] -->
<!-- <script type="text/template" id="test-tpl">
    <span abc="{{#abc attrData}}{{attrData}}{{/abc}}">111{{#abc data}}{{data}}{{/abc}}</span>
    <span>
        <input type="text" value="{{#abc attrData}}{{attrData}}{{/abc}}">
    </span>
</script> -->

<!-- 容器 -->
<span id="test"></span>

<script src="........./handlebars/handlebars-v4.0.5.js"></script>

<script type="text/javascript">
    // 自定义helper
    Handlebars.registerHelper('abc', function (text, options) {
        // 对输入数据进行过滤 [1]
        // text = Handlebars.Utils.escapeExpression(text)

        // helper直接返回数据 [2]
        return text;

        // helper返回子层 [3]
        // return options.fn(this);
    });

    // Handlebars获取数据
    function getHtml(html, data) {
        let source = Handlebars.compile(html);
        let content = source(data);
        return content;
    }

    var data = '<script>alert(1);<\/script>';
    var attrData = '" onmouseover="alert(2)"';

    // 渲染
    $('#test').html(getHtml($('#test-tpl').html(), {
        data: data,
        attrData: attrData
    }));
</script>

进入页面后,将会执行 alert(1) ,然后鼠标滑过span或input元素,将会执行 alert(2)

这是因为Handlebars在处理helper时,如果是返回数据,将不进行转义过滤

解决方案为:

如果使用了自定义的helper直接返回数据,先转义一遍,即取消注释[1] 处 代码

或者不直接返回数据,即注释模板[A],[1] 和[2]处,取消注释模板[B],[3]处 代码

另外,前端模板会频繁和JS进行交互,在前端直接使用JS获取URL参数并放到模板中时,要格外注意防止产生DOM-base型XSS,如下面这段代码

Payload: http://local.abc.com/main/?r=abc/index&param=%22%20onmouseover=%22alert(2)%22

function getUrlParam(name) {
    let value = window.location.search.match(new RegExp('[?&]' + name + '=([^&]*)(&?)', 'i'));
    return value ? decodeURIComponent(value[1]) : '';
}
var attrData = getUrlParam('param');

1.4.4  React JSX模板中的 dangerouslySetInnerHTML

<span dangerouslySetInnerHTML={{__html: '<script>alert(1);</script>'}}></div>

这段代码会执行么

事实上,并不会。与模板不同,它使用的是 innerHTML来更新DOM元素的内容,所以不会执行恶意代码

不过,这个内容不会显示在页面中,如果这时正常的一段内容,就应该转义之后再放入 __html的值中

1.4.5 在React的服务端渲染中,也要注意安全问题

服务端渲染需要一个初始的state,并与客户端做对应

可能会长这样子

<!-- 客户端 -->
<div id="content">
    <|- appHtml |>
</div>
<script id="preload-state">
    var PRELOAD_STATE = <|- preloadState |>
</script>

// 服务端
res.render('xxx.html', {
    appHtml: appHtml,
    preloadState: JSON.stringify(preloadState).replace(/</g, '\\u003c')
});

类似模板,服务端将数据传给客户端时,在模板组装数据的时候要防止构造出闭合 <script>标签的情景

这里可以将 < 替换成对应的Unicode字符串,在JS中获取该字符串时,可以直接识别为 <

1.4.6 百度编辑器的编辑源码,可能会有安全问题

在编辑器内直接输入这串内容,不会执行。点击查看源码,可以看到已经经过转义

我们可以直接在这里修改源码

再切换回去,一个XSS漏洞就产生了,如果稍加不注意就会被利用。

所以,在前端范畴必须将此入口去除,后端也应加强一些特殊字符的转义

1.4.7 谨防 javascript: 伪协议

链接中带有 javascript: 伪协议可执行对应的脚本,常见于 a 的 href 标签和 iframe的 src 中

<a href="javascript:alert(1)">test</a>
<!-- 冒号: 的HTML实体符 -->
<a href="javascript:alert(1)">test</a>
<iframe src="javascript:alert(1)"></iframe>

输入源多为一个完整的URL路径,输出地方多为模板与JS的操作

<a href="<{$urlTo}>">test</a>
<a href="{{{urlTo}}}">test</a>

location.href = getUrlParam('urlTo');

普通的HTML实体符并不能过滤这个伪协议

需要知道的是,javascript: 能够正常工作的前提为:开始URL解析时没有经过编码

解决方案:

1. 前端后端都要先对 '"><& 这些特殊字符进行过滤转义,特别是在与模板共用时,它们很有可能会闭合以产生攻击,或者利用浏览器解码的顺序来绕过不严格的过滤

2.严格要求输入的URL以 https:// 或 http:// 协议开头

3.严格限制白名单协议虽然可取,但有时会造成限制过头的问题。还可以单独限制伪协议,直接对 javascript: 进行过滤

过滤时需要兼容多层级的嵌套: javajavajavascript:script:script:alert(1)

同时显示的时候,将多余的冒号 : 转义成URL编码,注意避免把正常的协议头也转义了,要兼容正常的URL

转义冒号要使用 encodeURIComponent , encodeURI转义不了,另外escape也不建议使用,关于三者的区别

function replaceJavascriptScheme(str) {
    if (!str) {
        return '';
    }
    return str.replace(/:/g, encodeURIComponent(':'));
}

Handlebars.registerHelper('generateURL', function (url) {
    url = Handlebars.Utils.escapeExpression(url);

    if (!url) {
        return '';
    }

    var schemes = ['//', 'http://', 'https://'];
    var schemeMatch = false;

    schemes.forEach(function(scheme) {
        if (url.slice(0, scheme.length) === scheme) {
            url = scheme + replaceJavascriptScheme(url.slice(scheme.length));
            schemeMatch = true;
            return false;
        }
    });

    return schemeMatch ? url : '//' + replaceJavascriptScheme(url);;
});

1.4.8  注意符号的闭合  '"><  和其他特殊符号

闭合标签,闭合属性是很常见的一种攻击方式,要重点关注哪里可能被恶意代码闭合。

本文使用了模板Smarty,在使用模板的时候,一般都将模板变量放在了引号中,需要带符号来闭合来实现攻击

<span abc="<{$abc}>"></span>
" onclick=alert(1)

在设置了特殊符号转义的情况下,这种攻击方式将失效

然鹅当输出的数据不在引号当中时,防范难度将加大。因为分离属性可以使用很多符号,黑名单过滤可能列举不全

abc/index?abc=1 onclick=alert(1)

<span id="test1" abc=<{$abc}>>test</span>

所以,尽量用引号包裹起变量

另外,也要避免在 <script>标签中直接使用模板中的变量,可以改用将模板变量缓存在HTML属性中,JS再进行取值

防止该 <script>标签被恶意代码闭合,然后执行恶意代码,例子可见上文的 Payload-6

还要注意JS的语法,在某些时候,特殊符号 反斜杠\ 没有过滤的话,也有安全问题

<script>
    var aaaa = '?a=<{$a}>' + '&b=<{$b}>';
</script>

?r=abc/index&a=\&b==alert(1);function b(){}//

<script>
    // 构造处可执行的代码,如果空格也被转义了,还可以用注释占位 function/**/b(){}
    var aaaa = '?a=\' + '&b==alert(1);function b(){}//';
</script>

假设只对 ' " > < & 进行了转义,可以试试从URL拿数据,这里需要利用到JS代码中关键的 & 符号与 \ 转义符

\ 将第一个分号转义为字符串

& 与运算将前后分离

b的参数加上 = 号构造处bool运算

为了防止b未定义,在后面用函数提升特性来定义

最后注释符防止报错

为了攻击也是蛮拼的....所以最好还是要对JS操作的字符用反斜杠进行转义一下,比如 \  -> \\

1.4.9 图片 exif 信息含有恶意代码

另一种XSS攻击的方式是在图片的exif信息中注入脚本,在读取图片信息时要注意过滤

在早期的很多插件中都没有进行处理,如之前爆出的 Chrome Exif Viewer 插件问题,可能还有相关插件没有这些意识,平时也要注意

另外,站点自身在读取文件信息时也要注意,攻击者在上传文件前,可能会对文件的信息进行修改,过滤不当很可能就造成严重的存储型漏洞

委托入门案例

 

我本人对于委托最多的使用就是子线程调用主线程的控件的使用。可能使用winform或者wpf的人接触的多一点。

这里最主要还是给大家看看委托的案例吧

delegate void showMsg(string Msg);
showMsg s;

第一种委托的方法
s+=func;
s("aaa");

第二种委托方法(这种方式用的多)
s=new showMsg(func);
s("aaa")'

public void func(string s)
{
console.WriteLine("aaa"+s);
}

当然也有 这种方式的委托,在应用程序的主线程上执行指定的委托

this.Invoke(new Action(()=>{Console.WriterLine("aaa")});

  异步委托
this.Dispatcher.BeginInvoke((Action)delegate ()
                {Console.WriterLine("aaa")});
 

你所不知道的 CSS 阴影技巧与细节 滚动视差?CSS 不在话下 神奇的选择器 :focus-within 当角色转换为面试官之后 NPOI 教程 - 3.2 打印相关设置 前端XSS相关整理 委托入门案例的相关教程结束。

《你所不知道的 CSS 阴影技巧与细节 滚动视差?CSS 不在话下 神奇的选择器 :focus-within 当角色转换为面试官之后 NPOI 教程 - 3.2 打印相关设置 前端XSS相关整理 委托入门案例.doc》

下载本文的Word格式文档,以方便收藏与打印。