InfoPool

私人信息记录

0%

@Bean注解的使用和详解

简单来说,Bean就是一个对象,只不过这个对象是由Spring容器来初始化,装配,管理的,

@Bean 基础概念

  • @Bean:Spring的@Bean注解用于告诉方法,产生一个Bean对象,然后这个Bean对象交给Spring管理。产生这个Bean对象的方法Spring只会调用一次,随后这个Spring将会将这个Bean对象放在自己的IOC容器中;
  • SpringIOC 容器管理一个或者多个bean,这些bean都需要在@Configuration注解下进行创建,在一个方法上使用@Bean注解就表明这个方法需要交给Spring进行管理;
  • @Bean是一个方法级别上的注解,主要用在@Configuration注解的类里,也可以用在@Component注解的类里。添加的bean的id为方法名;
  • 使用Bean时,即是把已经在xml文件中配置好的Bean拿来用,完成属性、方法的组装;比如@Autowired , @Resource,可以通过byTYPE(@Autowired)、byNAME(@Resource)的方式获取Bean;
  • 注册Bean时,@Component , @Repository , @ Controller , @Service , @Configration这些注解都是把你要实例化的对象转化成一个Bean,放在IoC容器中,等你要用的时候,它会和上面的@Autowired , @Resource配合到一起,把对象、属性、方法完美组装;
  • @Configuration与@Bean结合使用:@Configuration可理解为用spring的时候xml里面的标签,@Bean可理解为用spring的时候xml里面的标签;

@Bean注解的源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Bean {
@AliasFor("name")
String[] value() default {};

@AliasFor("value")
String[] name() default {};

/** @deprecated */
@Deprecated
Autowire autowire() default Autowire.NO;

boolean autowireCandidate() default true;

String initMethod() default "";

String destroyMethod() default "(inferred)";
}

@Bean的属性:

  • value:bean别名和name是相互依赖关联的,value,name如果都使用的话值必须要一致;
  • name:bean名称,如果不写会默认为注解的方法名称;
  • autowire:自定装配默认是不开启的,建议尽量不要开启,因为自动装配不能装配基本数据类型、字符串、数组等,这是自动装配设计的局限性,并且自动装配不如依赖注入精确;
  • initMethod:bean的初始化之前的执行方法,该参数一般不怎么用,因为完全可以在代码中实现;
  • destroyMethod:默认使用javaConfig配置的bean,如果存在close或者shutdown方法,则在bean销毁时会自动执行该方法,如果你不想执行该方法,则添加@Bean(destroyMethod=””)来防止出发销毁方法;
  • 如果发现销毁方法没有执行,原因是bean销魂之前程序已经结束了,可以手动close下如下:
    1
    2
    3
    4
    5
    AnnotationConfigApplicationContext applicationContext2 = new AnnotationConfigApplicationContext(MainConfig.class);
    User bean2 = applicationContext2.getBean(User.class);
    System.out.println(bean2);
    //手动执行close方法
    applicationContext2.close();

    Bean的生命周期

    Spring 容器可以管理 singleton 作用域 Bean 的生命周期,在此作用域下,Spring 能够精确地知道该 Bean 何时被创建,何时初始化完成,以及何时被销毁。

而对于 prototype 作用域的 Bean,Spring 只负责创建,当容器创建了 Bean 的实例后,Bean 的实例就交给客户端代码管理,Spring 容器将不再跟踪其生命周期。每次客户端请求 prototype 作用域的 Bean 时,Spring 容器都会创建一个新的实例,并且不会管那些被配置成 prototype 作用域的 Bean 的生命周期。

了解 Spring 生命周期的意义就在于,可以利用 Bean 在其存活期间的指定时刻完成一些相关操作。这种时刻可能有很多,但一般情况下,会在 Bean 被初始化后和被销毁前执行一些相关操作。

在 Spring 中,Bean 的生命周期是一个很复杂的执行过程,我们可以利用 Spring 提供的方法定制 Bean 的创建过程。

当一个 Bean 被加载到 Spring 容器时,它就具有了生命,而 Spring 容器在保证一个 Bean 能够使用之前,会进行很多工作。Spring 容器中 Bean 的生命周期流程如图 1 所示。
图 0

Bean 生命周期的整个执行过程描述如下。

1)根据配置情况调用 Bean 构造方法或工厂方法实例化 Bean。

2)利用依赖注入完成 Bean 中所有属性值的配置注入。

3)如果 Bean 实现了 BeanNameAware 接口,则 Spring 调用 Bean 的 setBeanName() 方法传入当前 Bean 的 id 值。

4)如果 Bean 实现了 BeanFactoryAware 接口,则 Spring 调用 setBeanFactory() 方法传入当前工厂实例的引用。

5)如果 Bean 实现了 ApplicationContextAware 接口,则 Spring 调用 setApplicationContext() 方法传入当前 ApplicationContext 实例的引用。

6)如果 BeanPostProcessor 和 Bean 关联,则 Spring 将调用该接口的预初始化方法 postProcessBeforeInitialzation() 对 Bean 进行加工操作,此处非常重要,Spring 的 AOP 就是利用它实现的。

7)如果 Bean 实现了 InitializingBean 接口,则 Spring 将调用 afterPropertiesSet() 方法。

8)如果在配置文件中通过 init-method 属性指定了初始化方法,则调用该初始化方法。

9)如果 BeanPostProcessor 和 Bean 关联,则 Spring 将调用该接口的初始化方法 postProcessAfterInitialization()。此时,Bean 已经可以被应用系统使用了。

10)如果在 中指定了该 Bean 的作用范围为 scope=“singleton”,则将该 Bean 放入 Spring IoC 的缓存池中,将触发 Spring 对该 Bean 的生命周期管理;如果在 中指定了该 Bean 的作用范围为 scope=“prototype”,则将该 Bean 交给调用者,调用者管理该 Bean 的生命周期,Spring 不再管理该 Bean。

11)如果 Bean 实现了 DisposableBean 接口,则 Spring 会调用 destory() 方法将 Spring 中的 Bean 销毁;如果在配置文件中通过 destory-method 属性指定了 Bean 的销毁方法,则 Spring 将调用该方法对 Bean 进行销毁。

Spring 为 Bean 提供了细致全面的生命周期过程,通过实现特定的接口或 的属性设置,都可以对 Bean 的生命周期过程产生影响。虽然可以随意配置 的属性,但是建议不要过多地使用 Bean 实现接口,因为这样会导致代码和 Spring 的聚合过于紧密。

Springboot依赖注入Bean的三种方式

首先明确Java变量的初始化顺序:静态变量或静态语句块–>实例变量或初始化语句块–>构造方法–>@Autowired->@PostConstruct(注释的方法)

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

@Resource
private MyDependency resourceDependency;

@Autowired
private MyDependency autowiredDependency;

privatefinal MyDependency finalDependency;

publicMyService(MyDependency finalDependency){
this.finalDependency = finalDependency;
}

// ...
}

public class MyDependency{
// ...
}

在上面的示例中,MyService类使用了三种不同的方式来注入 MyDependency依赖。

@Resource

@Resource注解是Java标准库提供的一种依赖注入方式,因此它可以与Java SE和Java EE应用程序一起使用,不仅限于Spring框架。根据规范,@Resource注解默认按照字段或方法参数的名称进行依赖的匹配,也可以通过name属性指定特定的依赖名称。

下面是几种常见的使用方式:

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
//字段注入:
public class MyService{

@Resource
private MyDependency dependency;
// ...
}

//方法注入:
public class MyService{

private MyDependency dependency;

@Resource
public void setDependency(MyDependency dependency){
this.dependency = dependency;
}
// ...
}

//构造函数注入:
public class MyService{

private MyDependency dependency;

@Resource
public MyService(MyDependency dependency){ t
his.dependency = dependency;
}
// ...
}

@Autowired

@Autowired注解是Spring Framework提供的一种依赖注入方式。@Autowired支持按照类型、名称和限定符等方式进行依赖的解析和注入Injection。

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
//字段注入:
public class MyService {

@Autowired
private MyDependency dependency;
// ...
}

//方法注入:
public class MyService {

private MyDependency dependency;

@Autowired
public void setDependency(MyDependency dependency) {
this.dependency = dependency;
}
// ...
}

//构造函数注入:
public class MyService {

private MyDependency dependency;

@Autowired
public MyService(MyDependency dependency) {
this.dependency = dependency;
}
// ...
}

  • 优点

    • 代码少,简洁明了。
    • 新增依赖十分方便,不需要修改原有代码
  • 缺点

    • 容易出现空指针异常。Field 注入允许构建对象实例时依赖的对象为空,导致空指针异常不能在启动时就爆出来,只能在用到它时才发现。空指针异常不是必现的,与bean的实例化顺序有关。有时,把依赖的bean改个名字就会报空指针异常。
    • 会出现循环依赖的隐患。

@Autowired与@Resource的区别

  1. 来源和依赖关系:@Autowired是Spring特有的注解,用于实现Spring的依赖注入机制。它通过类型匹配来解析依赖关系。而 @Resource是Java标准库中的注解,它可以与Java SE和Java EE应用程序一起使用,不仅限于Spring框架。@Resource通过名称匹配来解析依赖关系。
  2. 配置方式:@Autowired注解通常与 @Component、@Service等Spring注解一起使用,通过组件扫描和自动装配来实现依赖注入。而 @Resource注解不需要特定的配置,它可以直接用于字段、方法和构造函数上。
  3. 名称匹配规则:@Autowired注解默认按照类型进行依赖的匹配。如果存在多个匹配的Bean,可以使用 @Qualifier注解指定特定的Bean名称或限定符。而 @Resource注解默认按照名称进行依赖的匹配,也可以通过 name属性指定特定的依赖名称。
  4. Null处理:当无法找到匹配的依赖时,@Autowired注解的字段或方法参数可以为 null,而 @Resource注解要求必须找到匹配的依赖,否则会抛出异常。

需要注意的是,尽管 @Autowired和 @Resource有一些区别,但在大多数情况下,它们可以互换使用。

priavet final

在Spring Boot中,对于 private final字段的自动注入,Spring使用构造函数注入Constructor Injection)来实现。当一个Bean有一个或多个 private final字段需要注入时,Spring会尝试在容器中查找与这些字段类型匹配的Bean,并使用构造函数注入来实例化Bean对象。

需要注意的是,由于private final字段是不可变的,一旦注入后就无法修改。这种不可变性有助于确保字段的安全性和线程安全性。

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

@Autowired
private Person person;

private String car;

public test(){

this.car = person.car;
}
}

上面这段代码能不能正常执行呢?
答案是不阔能,按照上面的初始化顺序,执行构造方法时,person里面是没有内容的,所以执行上面的那段代码会出现 java.lang.NullPointerException。

解决方案是使用 priavet final注解,这样就可以明确变量注入时的加载顺序。

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

private final Person person;

private String car

public test(Person person)
{
this.person = person;
this.car = this.person.car;
}
}

这样有如下两个优点:

  1. 这样当test类创建的时候,强制依赖person对象,确保创建每个test对象的对象都是有效状态
  2. 构造器中可以添加对象初始化的校验逻辑

注意:不能提供无参构造方法,否则Springboot默认会加载无参的构造方法,Bean实例对象会为null

@RequiredArgsConstructor

@RequiredArgsConstructor会将类的每一个final字段或者non-null字段生成一个构造方法。自动生成包含final字段的构造函数的代码,以便将final字段绑定到具体实现。

ApplicationRef

两个主要个作用:

(1)可以通过appRef.tick()来全局性调用变化检测;

ApplicationRef.tick() - 检查所有组件树

(2)可以将视图用attachView()包含到变化检测中 用detachView()将视图移除变化检测 ;

不用ViewContainerRef,动态插入一个组件到一个特定的DOM节点:

ComponentRef

表示组件的实例。

ComponentRef提供对组件实例的访问以及与此组件实例相关的其他对象,并允许您通过destroy方法销毁组件实例。

抽象类定义如下:

1
2
3
4
5
6
7
8
9
10
abstract class ComponentRef<C> {
abstract location: ElementRef // 组件实例的宿主元素所在的位置
abstract injector: Injector // 组件实例存在的注射器
abstract instance: C // 组件实例
abstract hostView: ViewRef // 组件实例的宿主视图ViewRef
abstract changeDetectorRef: ChangeDetectorRef // 组件的ChangeDetectorRef
abstract componentType: Type<any> // 组件类型
abstract destroy(): void // 销毁组件实例及其附加数据
abstract onDestroy(callback: Function): void // 允许注册将在组件销毁时调用的回调。
}

injector: Injector

我们在使用一个命令式引导一个Component之前需要在NgModule中的declarations部分声明这个Component,在使用这个Component时,通过依赖注入为Component提供的Service实际上是在NgModule声明,并且由NgModule提供的——NgModule就是Injector。

所以这个Injector一般就是你在使用这个Component时所在的NgModule。

总结一句话,就是在动态加载Component时,你需要在哪个宿主(Component Host)下工作,那就获取哪个宿主的Injector,让宿主的Injector为你提供服务(provide service)。

location: ElementRef

这个命名很容易让人困惑。实际上它是需要被动态加载的组件(Component)的视图(View)部分。它的其中一个属性叫做rootNodes,就是一个Native Element。

hostView: ViewRef

上面已经存在一个ElementRef是为了提供视图的,而这个ViewRef看起来更像是视图的引用。没错,这很容易混淆。实际上ViewRef是用来提供数据检测和视图更新的。我们来看一下ViewRef的定义。

这三个部分就是一个Component最重要的组成部分:injector提供服务,location管理原生视图,hostView负责数据-视图更新。

TemplateRef

在介绍 TemplateRef 前,我们先来了解一下 HTML 模板元素template。模板元素是一种机制,允许包含加载页面时不渲染,但又可以随后通过 JavaScript 进行实例化的客户端内容。我们可以将模板视作为存储在页面上稍后使用的一小段内容。

用于表示内嵌的template模板元素,通过TemplateRef实例,我们可以方便的创建内嵌视图(Embedded Views),且可以轻松地访问到通过 ElementRef 封装后的 nativeElement。需要注意的是组件视图中的 template 模板元素,经过渲染后会被替换成 comment 元素。

1
2
3
4
abstract class TemplateRef<C> {
abstract elementRef: ElementRef
abstract createEmbeddedView(context: C): EmbeddedViewRef<C>
}

templateRef创建Embedded Views视图的方法如下:

1
2
3

@ViewChild('tp1') tp1: TemplateRef<any>;
const view = this.tp1.createEmbeddedView(null);

ViewContainerRef

动态视图的核心是视图容器,它决定了视图的插入位置,用 ViewContainerRef 类表示。要创建它很简单,假设组件视图中有:<ng-container #dynamicHost></ng-container> 通过 @ViewChild 装饰器读取即可:

1
@ViewChild('dynamicHost', { read: ViewContainerRef }) container!: ViewContainerRef;

这段代码的注意点有:

  • @ViewChild 用来从组件视图中获取元素的引用,可以是 DOM 对象、子组件或者指令的实例、或是某个依赖注入的 Provider。
  • 第二个参数中的 read 指示具体获取哪个类型,例如从 <button mat-button> 可以获取到这个 button 元素对象,也可以是这个标签上添加的 Material 按钮指令。
  • 要获取一个容器,并非只能使用 <ng-container><ng-template> 、组件或者别的 DOM 标签都可以。
  • 虽然是叫容器,不过插入的内容可不是在这个 <ng-container> 标签内,而是在它的下方(类似 <router-outlet>)。所以使用 ng-container 是为了不渲染多余的 DOM。

ViewContainerRef 实例可以创建、插入、移除、移动或是销毁它其中的视图(ViewRef)。视图代表着 Angular中可显示元素的最小分组单位,它由组件或者模板定义。多个视图构造成了 Angular 应用的视图树。

简明起见,后文都将 ViewContainerRef 实例称之为 “视图容器”

以代码方式插入

视图容器有两个方法(createComponent,createEmbeddedView),用来动态创建组件视图和模板视图。

动态插入组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Component({
template: `
<div class="alert-container mat-elevation-z2" [class]="classConfig()">
<span class="message">{{message}}</span>
<button mat-button (click)="emitCloseEvent()">关闭</button>
</div>`
})
export class AlertComponent {
@Input() message = '空消息提示';
@Input() type = 'success';
@Output() closeAlert = new EventEmitter();
classConfig() {
return {
success: this.type === 'success',
warning: this.type === 'warning'
};
}

const

emitCloseEvent(): void {
this.closeAlert.emit();
}
}

这个组件有两个输入属性,一个输出事件。以便演示动态创建的组件,如何和外界交互。

通知组件写好后,就可以创建并动态插入,将这个对象传入视图容器的 createComponent 方法。通过 createComponent 方法,就可以将这个组件插入到视图中了,并返回这个组件实例。

有了组件实例,和这个通知组件交互也就不成问题了:

  • 输入属性传值:componentRef.instance.message = “外部传入的警告信息”。
  • 绑定输出事件:
1
2
3
4
componentRef.instance.closeAlert.subscribe(() => {
const index = this.container.indexOf(componentRef.hostView);
this.container.remove(index);
});

图 0

动态插入模板

和 createComponent 类似的,通过 createEmbeddedView 就可以插入模板。

首先先创建一个模板示例,这个模板根据上下文对象声明了一个 “param” 属性:

1
2
3
4
5
6
<ng-template #templateView let-param="message">
<section class="template-wrapper">
<span>来自 ng-template 的动态内容</span>
<span>{{param}}</span>
</section>
</ng-template>

要插入一个带上下文数据的模板,具体分为三步:

  1. 获取模板的引用对象:
    @ViewChild('templateView', { read: TemplateRef }) template!: TemplateRef<any>;

  2. 声明上下文对象:

templateContext = { message: '来自模板上下文的值' };

  1. 通过视图容器的 createEmbeddedView 方法插入模板:

    const embeddedViewRef = this.container.createEmbeddedView(this.template, this.templateContext);

方法创建一个 EmbeddedViewRef 对象,并将它放入。通过这个对象,视图容器可以查找它的索引,所以也可以和之前组件视图的引用一样,在容器内移动顺序、移除渲染、或是被销毁。

图 1

以指令方式插入

除了使用视图容器的两个方法来创建和插入视图,Angular 还提供了两个指令来简化工作。

ngComponentOutlet

首先,再创建另一个示例组件,和前面的哪个通知组件不同,它没有输入输出属性,但是多了一个需要注入的依赖项,以便演示带依赖注入的组件插入:

1
2
3
4
5
6
7
8
9
@Component({
template: `
<section class="template-wrapper">
<span>来自另一个动态组件:{{param.message}}</span>
</section>`
})
export class AnotherComponent {
constructor(public param: ExampleService) { }
}

使用指令的方式创建组件就简单多了,只需要两步:

  1. 引入这个组件类,并赋值给一个属性:

    1
    2
    3
    4
    import { AnotherComponent } from '../shared/another-component';
    export class ViewContainerExampleComponent implements OnInit, OnDestroy {
    public anotherComponent = AnotherComponent;
    }
  2. 在视图中声明即可:

1
<ng-container *ngComponentOutlet="anotherComponent"></ng-container>

传入依赖注入器

正常情况下,这样就把组件插入指定位置了,不过如果动态组件所声明的依赖项,需要由这个组件本身提供呢?

这里就要再给这个组件传入注入对象:

1
<ng-container *ngComponentOutlet="anotherComponent;injector:costumeInjector">

这个 costumeInjector 可以通过 Injector 类的静态方法创建:

1
2
3
4
5
6
7
8
9

constructor(
injector: Injector
) {
this.costumeInjector = Injector.create({
providers: [{ provide: ExampleService, deps: [] }],
parent: injector
});
}

这样一来,每个动态创建的组件,都会拥有一个独立的 ExampleService 实例。

✨ 视图容器的 createComponent 方法同样可以指定依赖注入器,效果是一样的,前面只是为了简明而省略。

当然,常见的情况依旧是给 ExampleService 的装饰器声明为全局服务:@Injectable({ providedIn:’root’})

传入内容映射

除了可以指定注入器,还可以传入内容映射。

先给组件做一点小修改,新增一个 标记,使得这个组件可以接收外部内容映射:

1
2
3
4
5

<section class="template-wrapper">
<span>来自另一个动态组件:{{param.message}}</span>
<ng-content></ng-content>
</section>`

要插入映射的 DOM 内容,只需要额外给指令的表达式再传一个参数:
<ng-container *ngComponentOutlet="anotherComponent;content:costumeContent">

这个 costumeContent 是一个数组,因为组件内可以有多个 ng-content。数组内每项也是一个数组,因为每个 ng-content 位置,可以插入多个 DOM 内容块。

1
2
3
4
5
const spanContent = document.createElement('span');
const divContent = document.createElement('div');
spanContent.innerHTML = 'hello, world';
divContent.innerHTML = '<span>locotor</span>';
this.costumeContent = [[spanContent, divContent]];

ngTemplateOutlet

模板的指令只有两个输入属性:模板的引用对象、模板的上下文对象。

所以要插入一个带上下文数据的模板,具体步骤如下:

  1. 给模板添加引用名:
1
2
3
<ng-template #templateView let-param="message">
<!-- 省略内容 -->
</ng-template>
  1. 声明上下文对象:
    templateContext = { message: '来自模板上下文的值' };
  2. 传入 ngTemplateOutlet 指令中:
1
<ng-container *ngTemplateOutlet="templateView; context: templateContext"></ng-container>

ElementRef

Angular 的口号是 - “一套框架,多种平台,即同时适用手机与桌面。在应用层直接操作 DOM,就会造成应用层与渲染层之间强耦合,导致我们的应用无法运行在不同环境,通过 ElementRef 我们就可以封装不同平台下视图层中的 native 元素 (在浏览器环境中,native 元素通常是指 DOM 元素),最后借助于 Angular 提供的强大的依赖注入特性,我们就可以轻松地访问到 native 元素。

ElementRef定义如下:

1
2
3
4
class ElementRef<T> {
constructor(nativeElement: T)
nativeElement: T
}

ViewRef

抽象类定义:

1
2
3
4
5
6
7
8
9
10
11
12
abstract class ViewRef extends ChangeDetectorRef {
abstract destroyed: boolean
abstract destroy(): void
abstract onDestroy(callback: Function): any

// 继承自 core/ChangeDetectorRef
abstract markForCheck(): void
abstract detach(): void
abstract detectChanges(): void
abstract checkNoChanges(): void
abstract reattach(): void
}

ViewRef 用于表示 Angular View(视图),视图是可视化的 UI 界面。EmbeddedViewRef 继承于 ViewRef,用于表示

HTML 拖放(Drag and Drop)接口使应用程序能够在浏览器中使用拖放功能。例如,用户可使用鼠标选择可拖拽(draggable)元素,将元素拖拽到可放置(droppable)元素,并释放鼠标按钮以放置这些元素。拖拽操作期间,会有一个可拖拽元素的半透明快照跟随着鼠标指针。

拖拽事件

HTML 的 drag & drop 使用了 DOM event model 以及从 mouse events 继承而来的 drag events。一个典型的拖拽操作是这样的:用户选中一个可拖拽的(draggable)元素,并将其拖拽(鼠标不放开)到一个可放置的(droppable)元素,然后释放鼠标。

在操作期间,会触发一些事件类型,有一些事件类型可能会被多次触发(比如drag 和 dragover 事件类型)。

下面的表格提供了一个简短的事件类型描述,以及一个相关文档的链接。

事件 事件处理程序 触发时刻
drag ondrag 当拖拽元素或选中的文本时触发
dragend ondragend 当拖拽操作结束时触发 (比如松开鼠标按键或敲“Esc”键)
dragenter ondragenter 当拖拽元素或选中的文本到一个可释放目标时触发
dragleave ondragleave 当拖拽元素或选中的文本离开一个可释放目标时触发
dragover ondragover 当元素或选中的文本被拖到一个可释放目标上时触发(每 100 毫秒触发一次)
dragstart ondragstart 当用户开始拖拽一个元素或选中的文本时触发
drop ondrop 当元素或选中的文本在可释放目标上被释放时触发

注意:当从操作系统向浏览器中拖拽文件时,不会触发 dragstart 和dragend 事件。

基础

确定什么是可拖拽的

让一个元素被拖拽需要添加 draggable 属性,再加上事件处理函数 ondragstart;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script>
function dragstart_handler(ev) {
// Add the target element's id to the data transfer object
ev.dataTransfer.setData("text/plain", ev.target.id);
ev.dataTransfer.setData(
"text/uri-list",
ev.target.ownerDocument.location.href,
}

window.addEventListener("DOMContentLoaded", () => {
// Get the element by id
const element = document.getElementById("p1");
// Add the ondragstart event listener
element.addEventListener("dragstart", dragstart_handler);
});
</script>

<p id="p1" draggable="true">This element is draggable.</p>

定义拖拽数据

每个 dragevent 都有一个dataTransfer 属性,其中保存着事件的数据。这个属性也有管理拖拽数据的方法。setData() 方法为拖拽数据添加一个项,如下面的示例代码所示:

1
2
3
4
5
6
// 添加拖拽数据
ev.dataTransfer.setData("text/plain", ev.target.innerText);
ev.dataTransfer.setData("text/html", ev.target.outerHTML);
ev.dataTransfer.setData(
"text/uri-list",
ev.target.ownerDocument.location.href,

如果你试图以相同的格式添加两次数据,那么新的数据将替换旧的数据。你可以使用 clearData() 方法清除这些数据,该方法接收一个参数,即要删除的数据类型。

1
event.dataTransfer.clearData("text/uri-list");

clearData()方法的 type 参数是可选的。如果没有声明type,则所有类型的数据都会被删除。如果拖拽不包含拖拽数据项,或者所有的数据项都被清除,那么就不会出现拖拽行为。

定义拖拽图像

拖拽过程中,浏览器会在鼠标旁显示一张默认图片。当然,应用程序也可以通过 setDragImage() 方法自定义一张图片

1
2
3
var img = new Image();
img.src = "example.gif";
ev.dataTransfer.setDragImage(img, 10, 10);

定义拖拽效果

dropEffect 属性用来控制拖放操作中用户给予的反馈。它会影响到拖拽过程中浏览器显示的鼠标样式。比如,当用户悬停在目标元素上的时候,浏览器鼠标也许要反映拖放操作的类型。

有以下效果可以定义:

  1. copy 表明被拖拽的数据将从它原本的位置拷贝到目标的位置。
  2. move 表明被拖拽的数据将被移动。
  3. link 表明在拖拽源位置和目标位置之间将会创建一些关系表格或是连接。
  4. none 不允许操作

属性默认允许以上所有的操作(all),所以你不需要调整这个属性,除非你想要排除某个特定操作。在拖拽过程中,拖拽效果也许会被修改以用于表明在具体位置上具体效果是否被允许,如果允许,在该位置则被允许放置。

可以使用 none 表示在这个位置不允许任何放置

ev.dataTransfer.dropEffect = "copy";

指定放置目标

当拖拽一个项目到 HTML 元素中时,浏览器默认不会有任何响应。想要让一个元素变成可释放区域,该元素必须设置 ondragover和ondrop事件处理程序属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<script>
function dragover_handler(ev) {
ev.preventDefault();
ev.dataTransfer.dropEffect = "move";
}
function drop_handler(ev) {
ev.preventDefault();
// Get the id of the target and add the moved element to the target's DOM
var data = ev.dataTransfer.getData("text/plain");
ev.target.appendChild(document.getElementById(data));
}
</script>

<p
id="target"
ondrop="drop_handler(event)"
ondragover="dragover_handler(event)">
Drop Zone
</p>

如果你想要允许放置,你必须取消 dragenter 和 dragover 事件来阻止默认的处理。你可以在属性定义的事件监听程序返回 false,或者调用事件的 preventDefault() 方法来实现这一点。

在 dragenter 和 dragover 事件中调用 preventDefault() 方法将表明在该位置允许放置。但是,你通常希望只在某些情况下调用 preventDefault() 方法(如只当拖拽的是链接时)。

要做到这一点,调用一个函数以检查条件,并且只在满足条件时取消事件。如果条件未满足,则不取消事件,此时用户释放鼠标按钮不会执行放置。

最常见的是根据数据传输中拖拽数据的类型来接受或拒绝放置——例如,允许放置图像或链接,或者都允许。你可以检查 dataTransfer 属性的 types 属性来查看哪些类型允许放置。types 属性返回一个字符串类型的数组,这些字符串类型是在拖拽开始时添加的,顺序是从最重要到最不重要。

1
2
3
4
5
6
7
8
9
10
11
12
13
function contains(list, value) {
for (var i = 0; i < list.length; ++i) {
if (list[i] === value) return true;
}
return false;
}

function doDragOver(event) {
var isLink = contains(event.dataTransfer.types, "text/uri-list");
if (isLink) {
event.preventDefault();
}
}

如果你希望更具体地限制操作类型,你可能还需要设置 effectAllowed 或 dropEffect 属性,或者两者都设置。当然,如果你不取消这个事件,改变这两个属性不会有任何效果。

放置反馈

你还可以根据需要更新用户界面,如添加一个插入标记或使用高亮显示。对于简单的高亮显示,你可以在放置目标上使用 -moz-drag-over CSS 伪类。

1
2
3
.droparea:-moz-drag-over {
border: 1px solid black;
}

在这个例子中,当带有 droparea 类的元素是一个有效的放置目标时,即在该元素的 dragenter 事件中调用 preventDefault() 方法时,元素会出现一个 1 像素的黑色轮廓。

要使这个伪类生效,你必须在 dragenter 事件中调用 preventDefault() 方法,因为这个伪类状态不会检查 dragover 事件(译者注:即在 dragover 事件中调用 preventDefault() 方法也不会使伪类生效,尽管这个伪类叫做“-moz-drag-over”)。

你可以在 dragenter 事件中执行其他操作。例如在放置位置插入一个元素,这样的元素可以表示一个插入标记,或表示被拖拽的元素移动到了新位置。为此你可以在 dragenter 事件中创建一个新元素,然后将其插入到文档中。

dragover 事件在鼠标指向的元素上触发。自然,你可能需要将插入标记移动到事件发生的位置附近。你可以使用事件的 clientX 和 clientY 属性,还有其他鼠标事件的属性来确定鼠标的位置。

最后,dragleave 事件会在拖拽离开元素时在该元素上触发。这是移除插入标记或高亮的好时机。你不需要取消这个事件(译者注:即不需要使用 preventDefault())。使用 -moz-drag-over 伪类设置的高亮或其他视觉效果会被自动移除。即使拖拽被取消了,dragleave 事件也会照常触发,所以你可以确保在这个事件中对任何插入标记的清除操作都一定可以完成。

执行放置

如果在有效的放置目标元素上放开鼠标,放置会成功实现,drop 事件在目标元素上被触发。否则,拖拽会被取消,不会触发 drop 事件。

在所有拖拽操作相关的事件中,事件的 dataTransfer 属性会一直保存着拖拽数据。可使用 getData() 方法来取回数据。

1
2
3
4
5
function onDrop(event) {
const data = event.dataTransfer.getData("text/plain");
event.target.textContent = data;
event.preventDefault();
}

完成拖拽

拖拽操作结束时,在源元素(开始拖拽时的目标元素)上触发 dragend 事件。不管拖拽是完成还是被取消这个事件都会被触发。dragend 事件处理程序可以检查dropEffect 属性的值来确认拖拽成功与否。

如果在 dragend 事件中,dropEffect 属性值为 none,则拖拽会被取消。

在解释BFC之前,先说一下文档流。我们常说的文档流其实分为定位流、浮动流、普通流三种。而普通流其实就是指BFC中的FC。FC(Formatting Context),直译过来是格式化上下文,它是页面中的一块渲染区域,有一套渲染规则,决定了其子元素如何布局,以及和其他元素之间的关系和作用。常见的FC有BFC、IFC,还有GFC和FFC。

BFC(Block Formatting Context)块级格式化上下文,是用于布局块级盒子的一块渲染区域。MDN上的解释:BFC是Web页面 CSS 视觉渲染的一部分,用于决定块盒子的布局及浮动相互影响范围的一个区域。

注意:一个BFC的范围包含创建该上下文元素的所有子元素,但不包括创建了新BFC的子元素的内部元素。这从另一方角度说明,一个元素不能同时存在于两个BFC中。因为如果一个元素能够同时处于两个BFC中,那么就意味着这个元素能与两个BFC中的元素发生作用,就违反了BFC的隔离作用。

形成BFC的条件

1、根元素,即HTML元素
1、浮动元素,float 除 none 以外的值;
2、定位元素,position(absolute,fixed);
3、display 为以下其中之一的值 inline-block,table-cell,table-caption;
4、overflow 除了 visible 以外的值(hidden,auto,scroll);

BFC布局规则

1.内部的Box会在垂直方向,一个接一个地放置。
2.Box垂直方向的距离由margin决定。属于同一个BFC的两个相邻Box的margin会发生重叠
3.每个元素的margin box的左边, 与包含块border box的左边相接触(对于从左往右的格式化,否则相反)。即使存在浮动也是如此。
4.BFC的区域不会与float box重叠。
5.BFC就是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面的元素。反之也如此。
6.计算BFC的高度时,浮动元素也参与计算

BFC可以解决哪些问题?

解决浮动元素令父元素高度坍塌的问题

方法:给父元素开启BFC

原理:计算BFC的高度时,浮动子元素也参与计算

图 0

非浮动元素被浮动元素覆盖

方法:给非浮动元素开启BFC
原理:BFC的区域不会与float box重叠

图 1

两栏自适应布局

方法:给固定栏设置固定宽度,给不固定栏开启BFC。
原理:BFC的区域不会与float box重叠

图 2

外边距垂直方向重合的问题

方法:给上box或者下box任意一个包裹新的box并开启BFC

原理:属于同一个BFC的两个相邻的Box的margin会发生重叠。

图 3

IFC

内联格式化上下文,是用于 布局内联元素 盒子的一块 渲染区域。

IFC的形成条件

块级元素中仅包含内联级别元素

IFC的布局规则

  1. 盒是从包含块的顶部开始一个挨一个水平放置的

  2. 水平padding、border、margin都有效,垂直方向上不被计算。

  3. 在垂直方向上,子元素会以不同形式来对齐vertical-align

  4. 能把在一行上的框都完全包含进去的一个矩形区域,被称为该行的行框(line box)。行框的宽度是由包含块(containing box)和与其中的浮动来决定。

  5. IFC中的“line box”一般左右边贴紧其包含块,但float元素会优先排列。

  6. IFC中的“line box”高度由 CSS 行高计算规则来确定,同个IFC下的多个line box高度可能会不同。

  7. 当 inline-level boxes的总宽度少于包含它们的line box时,其水平渲染规则由text-align 属性值来决定。

  8. 当一个“inline box”超过父元素的宽度时,它会被分割成多个boxes,这些 boxes 分布在多个“line box”中。如果子元素未设置强制换行的情况下,“inline box”将不可被分割,将会溢出父元素。

实例

元素垂直居中

布局规则第3条:在垂直方向上,子元素会以不同形式来对齐vertical-align

图 4
想要实现文字与图片垂直居中,只要给图片加上一个vertical-align: middle 属性就可以实现:

图 5

标准流(Normal Flow)

默认情况下,元素都是按照normal flow(标准流、常规流、正常流、文档流)进行排布的。

  • 排布顺序:在浏览器中从左到右,从上到下顺序摆放;
  • 默认情况下,元素互相之间不存在层叠现象;
    图 0

什么情况下元素会脱标?

脱离标准流(简称“脱标”),那么什么情况下元素会脱离标准流呢?常见有以下两种:

  • 元素设置position,并且position的值为fixed或absolute;
  • 元素添加浮动float,并且float的值不为none;

脱标元素的特点

  1. 可以随意设置宽高,宽高默认由内容决定。
    • 块级元素:在标准流下,默认占满父元素的宽度;脱标后,宽高由内容撑开。
    • 行内元素:在标准流下,默认由内容撑开,且不能设置宽高;脱标后,宽高还是由内容撑开,但是可以设置宽高。
  2. 不再受标准流的约束。
  3. 不再给父元素汇报宽高数据,也就是不能将父元素撑开。

脱标和display有什么关系?

根据以上脱标元素的特点,可能会有人认为脱标其实就是将元素转换成了inline-block,因为其展示效果和设置display: inline-block;效果一致,但是其中的原因却不是这样。

当不同元素进行了脱标,对应会展示成display的何种属性值?在MDN和W3C官方网站上都有进行解释,并且提供了参考表格:

  • MDN中,以搜索float属性为例;
    图 1

  • 在W3C官方文档中,有关于display、position和float之间的关系进行了说明;

图 2

大部分元素在脱标后都会转换成block类型。

这里可以抛出一个疑问,block类型不是占据父元素的宽度么,为什么脱标元素最终是由内容撑开的?
解答:元素脱标后,已经不受标准流约束,其位置也是不局限在父元素之内,很难说父元素是谁,且块级(block)元素默认宽高都为auto,难以参考父元素宽度,所以最好的展示形式就是默认由内容撑开。

利用position可以对元素进行定位,常用取值有4个:

static

  1. static为position属性的默认值,在不设置position属性时就是static。
  2. 元素按照标准流进行排布(忽略 top, bottom, left, right 或者 z-index 声明)。
  3. 元素和元素之间不会重叠,这个位置就是元素的默认位置。
  4. 相邻元素都设置外边距,最终外边距=两者外边距中最大的。
  5. 左右外边距设置auto,这个块水平居中。

relative

  1. 元素按照标准流进行排布。而且占据的文档空间不会随 top / right / left / bottom 等属性的偏移而发生变动。
  2. 相对定位相对的是它原本在文档流中的位置而进行的偏移。
  3. 可以通过left、right、top、bottom来进行位置调整。
  4. 相对定位应用场景:在不影响其它元素位置的前提下,可以对当前元素位置进行微调。

absolute

  1. 元素脱离标准流(脱标)
  2. 定位参照对象是最近一级拥有定位的祖先元素(相对于static定位以外的第一个父元素进行定位),可以通过left、right、top、bottom来进行位置调整。
  3. 如果一直往上层元素找不到有定位的元素,那么最终的参照对象为浏览器窗口。
  4. 宽高由内容撑开。
  5. 包含块就是由它的最近的 position 的值不是 static 的祖先元素的内边距区的边缘组成。

fixed

  1. 元素脱离标准流(脱标)
  2. 生成绝对定位的元素,相对于浏览器窗口进行定位。元素的位置通过 “left”, “top”, “right” 以及 “bottom” 属性进行规定。
  3. 当浏览器窗口滚动时,元素的位置是固定不动的。

inherit

规定应该从父元素继承 position 属性的值。

子绝父相

在绝大多数情况下,子元素的绝对定位都是相对于父元素进行定位的,虽然给父元素的position设置relative、absolute、fixed都可以,但是如果不希望父元素脱标,常用解决方案是:

  • 父元素设置position: relative;(让父元素成为定位元素,又不脱离标准流);
  • 子元素设置position: absolute;;

示例

1
2
3
4
5
6
<body>
<div class="div1">第一个div</div>
<div class="div2">第二个div</div>
<div class="div3">第三个div</div>
<div class="div4">第四个div</div>
</body>

添加背景色,效果如下:

图 3

给第二个div设置absolute

1
2
3
4
5
6
7
.div2{
height:100px;
background-color: blueviolet;
position:absolute;
top:50px;
left:50px;
}

效果如图:

图 4

第二个div设置了absolute,则该div的宽度就由文本决定,且下面的div会上移占据之前第二个div的位置,top和left是相对于离它最近且不是static定位的父元素来定位的,在此div2因为没有父元素,所以第二个div相对于根元素即html元素来定位。

将第二个div设置为relative

1
2
3
4
5
6
7
.div2{
height:100px;
background-color: blueviolet;
position:relative;
left:50px;
top:50px;
}

效果如图:

图 5

设置relative的div不会影响其他div的位置,且top和left是相对于它原本自身的位置来定位。

给第二个div添加一个父div

1
2
3
4
5
6
7
8
9
<body>
<div class="div1">第一个div</div>
<div class="container1">
第二个div的父div
<div class="div2">第二个div</div>
</div>
<div class="div3">第三个div</div>
<div class="div4">第四个div</div>
</body>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
.container1{
position:absolute;
height:200px;
background-color: greenyellow;
}

.div2{
height:100px;
background-color: blueviolet;
position:absolute;
top:50px;
left:50px;
}

效果如图:
图 6

div2的父div设置为absolute,下面的div3,div4会上移,div2也设置为absolute,div2就会相对于父div来定位。

若将div2即第二个div的absolute改为relative:

1
2
3
4
5
6
7
.div2{
height:100px;
background-color: blueviolet;
position:relative;
top:50px;
left:50px;
}

则效果图如下:

图 7

上面两个图的第二个div与父div的上边距是不同的,第一个是相对父div来定位,第二个是相对原来本身的位置来定位。可能此时你会注意到两个图的第二个div的宽度不同,在没有给div设置宽度的情况下,第一个是设为absolute,所以宽度为文本宽度,第二个是relative,所以宽度与父元素宽度相同。

若保持上面的两种情况,都将第二个div的宽度设为500px,得到效果如下:

图 8

图 9

由上图可以知道,absolute定位的子元素宽度不会影响父元素的宽,而relative定位的子元素会撑大父元素。

小结

  • Absolution:元素会脱离文档流,定位是相对于离它最近的且不是static定位的父元素而言,若该元素没有设置宽度,则宽度由元素里面的内容决定,且宽度不会影响父元素,定位为absolution后,原来的位置相当于是空的,下面的的元素会来占据。

  • Relative:元素仍处于文档流中,定位是相对于原本自身的位置,若没有设置宽度,则宽度为父元素的宽度,该元素的大小会影响父元素的大小。

  • 绝对(absolute)定位对象在可视区域之外会导致滚动条出现。而放置相对(relative)定位对象在可视区域之外,滚动条不会出现。

  • 使用absoulte或fixed定位的话,必须指定 left、right、 top、 bottom 属性中的至少一个,否则left/right/top/bottom属性会使用它们的默认值 auto ,这将导致对象遵从正常的HTML布局规则,在前一个对象之后立即被呈递,简单讲就是都变成relative,会占用文档空间,这点非常重要,很多人使用absolute定位后发现没有脱离文档流就是这个原因,这里要特别注意~~~

  • 少了left/right/top/bottom属性不行,那如果我们多设了呢?例如,我们同时设置了top和bottom的属性值,那元素又该往哪偏移好呢?记住下面的规则:

    • 如果top和bottom一同存在的话,那么只有top生效。
    • 如果left和right一同存在的话,那么只有left生效。
  • 祖先类的margin会让子类的absoulte跟着偏移,而padding却不会让子类的absoulte发生偏移。总结一下,就是absoulte是根据祖先类的border进行的定位。

  • 无论父级盒子是正常显示还是以border-box显示,在没有规定left和top的属性值时,都呈现在父级盒子的内容区(不包含padding)左上角若规定了left:0;top:0;则统一相对父级盒子的左上角显示(包含padding)

  • 当只给元素定义了position:absolute时,如果top, bottom, left, right都没有指定时,则left,top值与原文档流位置一致(当然了它是不占位的)。所以只设置了top属性而未设置left属性就造成了它在水平方向上仍然保持原有位置,而这个位置绝对不是left:0。实际上不论是何种定位,规则都如上所述。

box-shadow 属性用于在元素的框架上添加阴影效果。 你可以在同一个元素上设置多个阴影效果,并用 逗号 将他们分隔开。 该属性可设置的值包括阴影的X轴偏移量、Y轴偏移量、模糊半径、扩散半径和颜色。

box-shadow: [inset] <offset-x> <offset-y> [<blur-radius>] [<spread-radius>] color [,]

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

/* x偏移量 | y偏移量 | 阴影颜色 */
box-shadow: 60px -16px teal;

/* x偏移量 | y偏移量 | 阴影模糊半径 | 阴影颜色 */
box-shadow: 10px 5px 5px black;

/* x偏移量 | y偏移量 | 阴影模糊半径 | 阴影扩散半径 | 阴影颜色 */
box-shadow: 2px 2px 2px 1px rgba(0, 0, 0, 0.2);

/* 插页(阴影向内) | x偏移量 | y偏移量 | 阴影颜色 */
box-shadow: inset 5em 1em gold;

/* 任意数量的阴影,以逗号分隔 */
box-shadow: 3px 3px red, -1em 0 0.4em olive;

/* 全局关键字 */
box-shadow: inherit;
box-shadow: initial;
box-shadow: inset;

  • 如果只给出两个值, 那么这两个值将会被当作 来解释。 设置水平偏移量,正值阴影则位于元素右边,负值阴影则位于元素左边。 设置垂直偏移量,正值阴影则位于元素下方,负值阴影则位于元素上方
  • 如果给出了第三个值, 那么第三个值将会被当作解释。值越大,模糊面积越大,阴影越大越淡。不能为负值。
  • 如果给出了第四个值, 那么第四个值将会被当作解释。取正值时,阴影扩大;取负值时,阴影收缩。默认为0,此时阴影与元素同样大。
  • 可选,inset关键字。默认阴影在边框外,即阴影向外扩散。使用 inset 关键字 会使得阴影落在盒子内部。 此时阴影会在边框之内 、背景之上、内容之下。可选,值。
1
2
3
4
5
6
7
8
9
10
11
12

.shadow {
width: 40px;
height: 40px;
margin: 100px auto;
border: 2px solid orange;
box-shadow:
-50px 0px 10px 0px blue,
0px -60px 0px 10px red,
50px 0px 0px 0px green,
0px 50px 0px -10px yellow;
}
1
<div class="shadow"></div>

图 0

观察图中四色方块可知:第三个数值(模糊半径)控制模糊程度,不改变阴影大小;第四个数值(扩散半径)会改变阴影大小,正值->阴影扩展,负值->阴影收缩。

常见问题:

制作单边阴影时,明明设置了x,y轴方向的偏移,为什么别的边还是有阴影出现?

解决方案:增加此边所在方向的偏移量(数值大小与正负),并适当缩小阴影大小(第四个数值为负值),减少模糊半径的视觉影响;

各种阴影的书写方式

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62

.shadow div {
float: left;
margin: 50px 120px;
width: 100px;
height: 100px;
border: 2px solid yellowgreen;
text-align: center;
line-height: 100px;
}

.top {
box-shadow: 0px -5px 10px -5px red;
}

.right {
box-shadow: 5px 0 10px -5px red;
}

.bottom {
box-shadow: 0 5px 10px -5px red;
}

.left {
box-shadow: -5px 0 10px -5px red;
}

.left-top {
box-shadow: -5px -5px 10px -4px red;
}

.right-top {
box-shadow: 5px -5px 10px -4px red;
}

.right-bottom {
box-shadow: 5px 5px 10px -4px red;
}

.left-bottom {
box-shadow: -5px 5px 10px -4px red;
}

.no-top {
/* .left-bottom, .right-bottom 组合 */
box-shadow: -5px 5px 10px -4px red, 5px 5px 10px -4px red;
}

.no-right {
/* .left-top, .left-bottom 组合 */
box-shadow: -5px -5px 10px -4px red, -5px 5px 10px -4px red;
}

.no-bottom {
/* .left-top, .right-top 组合 */
box-shadow: -5px -5px 10px -4px red, 5px -5px 10px -4px red;
}

.no-left {
/* .right-bottom, .right-top 组合 */
box-shadow: 5px 5px 10px -4px red, 5px -5px 10px -4px red;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

<div class="shadow">
<div class="top">顶部阴影</div>
<div class="right">右部阴影</div>
<div class="bottom">底部阴影</div>
<div class="left">左边阴影</div>
<div class="left-top">左上阴影</div>
<div class="right-top">右上阴影</div>
<div class="right-bottom">右下阴影</div>
<div class="left-bottom">左下阴影</div>
<div class="no-top">无上阴影</div>
<div class="no-right">无右阴影</div>
<div class="no-bottom">无下阴影</div>
<div class="no-left">无左阴影</div>
</div>

图 1

我们在处理图片时,经常使用的一个功能就是滤镜,它能使一张图像呈现各种不同的视觉效果。在 CSS 中,也有一个filter属性,让我们能用 CSS 代码为元素指定各种滤镜效果,比如模糊、灰度、明暗度、颜色偏移等。

filter的基础使用非常简单,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

/* <filter-function> 值 */
filter: blur(5px);
filter: brightness(0.4);
filter: contrast(200%);
filter: drop-shadow(16px 16px 20px blue);
filter: grayscale(50%);
filter: hue-rotate(90deg);
filter: invert(75%);
filter: opacity(25%);
filter: saturate(30%);
filter: sepia(60%);

/* URL */
filter: url("filters.svg#filter-id");

/* 多个滤镜 */
filter: contrast(175%) brightness(3%);
filter: drop-shadow(3px 3px red) sepia(100%) drop-shadow(-3px -3px blue);

/* 不使用滤镜 */
filter: none;

/* 全局值 */
filter: inherit;
filter: initial;
filter: revert;
filter: revert-layer;
filter: unset;

函数

  • blur():将高斯模糊应用于输入图像。
  • brightness(): 将线性乘法器应用于输入图像,以调整其亮度。值为 0% 将创建全黑图像;值为 100% 会使输入保持不变,其他值是该效果的线性乘数。如果值大于 100% 将使图像更加明亮。
  • contrast():调整输入图像的对比度。值是 0% 将使图像变灰;值是 100%,则无影响;若值超过 100% 将增强对比度。
  • drop-shadow():使用 参数沿图像的轮廓生成阴影效果,阴影语法类似于 (在 CSS 背景和边框模块中定义),但不允许使用 inset 关键字以及 spread 参数。与所有 filter 属性值一样,任何在 drop-shadow() 后的滤镜同样会应用在阴影上。
  • grayscale():将图像转换为灰度图。值为 100% 则完全转为灰度图像,若为初始值 0% 则图像无变化。值在 0% 到 100% 之间,则是该效果的线性乘数。
  • hue-rotate():应用色相旋转。 值设定图像会被调整的色环角度值。值为 0deg,则图像无变化。
  • invert():反转输入图像。值为 100% 则图像完全反转,值为 0% 则图像无变化。值在 0% 和 100% 之间,则是该效果的线性乘数。
  • opacity():应用透明度。值为 0% 则使图像完全透明,值为 100% 则图像无变化。
  • saturate():改变图像饱和度。值为 0% 则是完全不饱和,值为 100% 则图像无变化。超过 100% 则增加饱和度。
  • sepia():将图像转换为深褐色。值为 100% 则完全是深褐色的,值为 0% 图像无变化.

组合函数

你可以组合任意数量的函数来控制渲染。滤镜将按声明顺序依次应用。以下示例增强了图像的对比度和亮度。

filter: contrast(175%) brightness(103%);

滤镜在日常开发中是很常见的,比如使用drop-shadow给不规则形状添加阴影;使用blur来实现背景模糊,以及毛玻璃效果等。

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

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Document</title>
<style>
.box {
background-color: #fff;
position: relative;
width: 125px;
height: 204px;
margin: 50px;
filter: drop-shadow(0px 2px 8px rgba(198, 198, 198, 0.5));
/* box-shadow: 0px 2px 8px 0px rgba(198, 198, 198, 0.5); */
}

.box::after {
content: "";
position: absolute;
top: -40px;
left: calc(50% - 20px);
width: 0;
height: 0;
border: 20px solid transparent;
border-bottom-color: #fff;
}

</style>
</head>
<body>
<div class="box"></div>
</body>
</html>

图 0

MouseEvent 接口指用户与指针设备(如鼠标)交互时发生的事件。使用此接口的常见事件包括:click、dblclick、mouseup、mousedown。

打印出来的 MouseEvent 如下:

图 0

经典的图:
图 1

属性

  • MouseEvent.pageX:实验性,鼠标指针相对于整个文档的 X 坐标;

  • MouseEvent.pageY:实验性,鼠标指针相对于整个文档的 Y 坐标;

    MouseEvent.page* 鼠标事件发生时,鼠标指针相对于整个文档左上角的水平距离和垂直距离。它受页面滚动的影响,页面滚动,它的值也会随之改变。

  • MouseEvent.clientX:鼠标指针在点击元素(DOM)中的 X 坐标。

  • MouseEvent.clientY:鼠标指针在点击元素(DOM)中的 Y 坐标。

MouseEvent.client*: 鼠标指针相对于浏览器窗口左上角的水平距离和垂直距离。它不受页面滚动的影响,即使页面滚动,它的值也不会改变

  • MouseEvent.x 实验性,MouseEvent.clientX的别名。

  • MouseEvent.y 实验性,MouseEvent.clientY的别名。

  • MouseEvent.screenX:鼠标指针相对于全局(屏幕)的 X 坐标;

  • MouseEvent.screenY:鼠标指针相对于全局(屏幕)的 Y 坐标;

  • MouseEvent.offsetX:实验性,鼠标指针相对于目标节点内边位置的 X 坐标

  • MouseEvent.offsetY:实验性,鼠标指针相对于目标节点内边位置的 Y 坐标

  • MouseEvent.movementX:鼠标指针相对于最后mousemove事件位置的 X 坐标。

  • MouseEvent.movementY:鼠标指针相对于最后mousemove事件位置的 Y 坐标。

  • MouseEvent.altKey:当鼠标事件触发的时,如果 alt 键被按下,返回 true;

  • MouseEvent.button:当鼠标事件触发的时,如果鼠标按钮被按下(如果有的话),将会返回一个数值。0 表示左键,1 表示中键,2 表示右键

  • MouseEvent.buttons:当鼠标事件触发的时,如果多个鼠标按钮被按下(如果有的话),将会返回一个或者多个代表鼠标按钮的数字。

  • MouseEvent.ctrlKey:当鼠标事件触发时,如果 control 键被按下,则返回 true;

  • MouseEvent.metaKey:当鼠标事件触发时,如果 meta 键被按下,则返回 true;

  • MouseEvent.region:返回被点击事件影响的点击区域的 id,如果没有区域被影响则返回 null。

  • MouseEvent.relatedTarget:鼠标事件的次要目标(如果有的话)

  • MouseEvent.shiftKey:当鼠标事件触发时,如果 shift 键被按下,则返回 true;

  • MouseEvent.which 非标准,当鼠标事件触发时,表示被按下的按钮。

  • MouseEvent.mozInputSource 非标准,生成事件的类型(若干 MOZ_SOURCE_*常量如下列出)。可通过该属性获知鼠标事件是否由真实鼠标设备触发,亦或通过触摸事件触发(这可能影响处理坐标事件时的精确程度)。

  • MouseEvent.webkitForce 非标准,点击时施加的压力量。

说明

  1. 当页面没有滚动时,MouseEvent.clientX 和 MouseEvent.pageX 的值相等。但当页面滚动时,MouseEvent.clientX 的值不变,而 MouseEvent.pageX 的值会随着页面滚动而增加或减少。
  2. 如果需要获得相对于事件目标元素的坐标,应该使用 MouseEvent.offsetX。如果需要获得相对于当前窗口的坐标,应该使用 MouseEvent.clientX。
  3. preventDefault():阻止事件的默认行为。例如,在链接上单击鼠标时,会跳转到链接指向的地址。如果在单击事件处理程序中调用了 preventDefault() 方法,则不会跳转到链接地址。
  4. stopPropagation():阻止事件进一步传播到其他元素。例如,在一个元素上发生的单击事件可能会传播到该元素的父元素或其他子元素。如果在单击事件处理程序中调用了 stopPropagation() 方法,则事件将不会进一步传播。

pointer-events 属性是一个指针属性,是用于控制在什么条件下特定的图形元素可以成为指针事件的目标,当这个属性设置为none时,可以禁用 HTML 元素的 hover/focus/active 等动态效果,元素则不接收hover、click事件,由他后面的元素进行接收。

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

/* Keyword values */
pointer-events: auto;
pointer-events: none;
pointer-events: visiblePainted; /* SVG only */
pointer-events: visibleFill; /* SVG only */
pointer-events: visibleStroke; /* SVG only */
pointer-events: visible; /* SVG only */
pointer-events: painted; /* SVG only */
pointer-events: fill; /* SVG only */
pointer-events: stroke; /* SVG only */
pointer-events: all; /* SVG only */

/* Global values */
pointer-events: inherit; 从其父元素继承此属性
pointer-events: initial; 将此属性设置为其默认值

针对HTML元素

  • none:该元素永远不会成为鼠标事件的 target。但是,当其后代元素的 pointer-events 属性指定其他值时,鼠标事件可以指向后代元素,在这种情况下,鼠标事件将在捕获或冒泡阶段触发父元素的事件侦听器 (鼠标的动作将不能被该元素及其子元素所捕获,但是能够被其父元素所捕获)。
  • auto:默认值,表示指针事件已启用;此时元素会响应指针事件,阻止这些事件在其下面的元素上触发。
  • inherit:将使用 pointer-events 元素的父级的值。

除去SVG的独有属性,其他是对浏览器来说生效的属性值。

未设置属性pointer-events

未设置属性的情况下,在光标移动到box1可以正常的触发hover,并且移动到box1和box2重叠的部分也是触发box1的hover

图 0

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
<style>
.box1 {
width: 100px;
height: 100px;
background: #04BBD4;
margin: 20px;
z-index: 3;
}
.box2 {
width: 100px;
height: 100px;
background: #090A0E;
margin: -80px 40px 20px;
z-index: 2;
/* pointer-events: none; */
}

.box1:hover {
background:#078404;
}

.box2:hover {
background:#E98889;
}
</style>
<div class="box1"></div>
<div class="box2"></div>

设置box1属性pointer-events为none

设置属性的情况下,在光标移动到box1无法正常的触发hover,此时hover已经失效,移动到box1和box2重叠的部分则是触发box2的hover

图 1

事件传播

父元素如果设置了pointer-event:none 并不意味着父元素上的事件侦听器永远不会被触发,当子元素上设置pointer-event值不是none,那么都可以通过事件传播机制来触发父元素上的事件。

该属性也可用来提高滚动时的帧频。的确,当滚动时,鼠标悬停在某些元素上,则触发其上的 hover 效果,然而这些影响通常不被用户注意,并多半导致滚动出现问题。对body元素应用pointer-events:none,禁用了包括hover在内的鼠标事件,从而提高滚动性能。