0%

CSS Replaced Elements

在Web开发中,可替换元素是指如下一类元素:

  1. 内容来自外部资源。
  2. 内容定义在document之外。

这么说可能有点抽象,我们举一个例子,下面的html中通过img标签引入了一个图片:

1
<img src="https://example.com/image.jpg" alt="Example Image">

在这个例子中,img元素就是一个可替换元素,因为它的内容(图片)来自于外部资源(src属性指定的URL)。

Html中的可替换元素有如下这些:

  1. <img>:用于显示图像。
  2. <video>:用于显示视频。
  3. <iframe>:用于嵌入其他HTML文档。
  4. <embed>:用于嵌入外部内容,如PDF文件或Flash动画。
  5. <fencedframe>:用于嵌入其他HTML文档。

以下这些tag在某些情况下也可能是可替换元素:

  1. <audio>:用于显示音频。
  2. <canvas>:用于绘制图形。
  3. <object>:用于嵌入外部内容,如PDF文件或Flash动画。
  4. <input>:用于显示表单控件。(image input types only)

可替换元素的特点

  1. 有自己的固有尺寸和固有纵横比,比如对于一个image来说,这个image本身就有大小和纵横比。当然,多数情况下,我们会用CSS控制它们显示的大小。
  2. 不能使用:before:after伪元素。

Syntax

User optional chaining operator ? instead of &&

1
2
3
4
5
6
7
8
9
10
const user = {
name: 'John',
address: {
city: 'New York',
state: 'NY'
}
};

const city = user && user.address && user.address.city; // old way
const city = user?.address?.city; // new way

这种方式确实简单很多,但是处理Unit Test可能会麻烦一些。

Use nullish coalescing operator ?? instead of ||

1
2
const name = user.name || 'Guest'; // old way
const name = user.name ?? 'Guest'; // new way

XSS

XSS = Cross Site Scripting, 即跨站脚本攻击为了和CSS(层叠样式表)区分开来,所以叫XSS。这种攻击方式是黑客向目标站点注入恶意代码,当用户浏览网页对应的恶意代码就会执行以窃取用户的信息,最常见的就是cookie盗取。常见的XSS攻击有三种

存储型

存储型攻击如下图所示:
alt text

  1. 黑客发现目标网站的漏洞,并提交恶意代码到目标网站,这些恶意代码存储到目标网站的数据库中。
  2. 用户访问目标网站,目标网站从数据库中读取恶意代码并返回给用户。
  3. 用户浏览器执行恶意代码,黑客获取用户的信息。
  • 首先黑客必须先发现目标网站存在XSS漏洞,然后黑客提交恶意代码(通过留言,创建表单等方式-这是方式都是目标网站提供的功能,所有用户都能使用)到网站服务器,服务器把提交的内容存储到后代数据库。假设黑客提交了某个博客文章的留言,或者创建了某个音频网站的一个专辑。
  • 正常用户访问目标网站,看到了黑客在博客上的留言,或者黑客创建的音频专辑,这时候浏览器会执行这些恶意代码,黑客就能获取用户的信息。

反射型

Dom型

如何防范XSS攻击

输入验证

在软件开发届有一句名言:永远不要相信前端传递过来的数据,后端一定要对前端传入的数据进行验证,即使前端已经验证了,后端也要验证。只要这样,才能保证安全。

对输出进行转义

现在流行的前端框架都会对用户的输入进行转义,比如React或者Angular框架,如果用户输入<script>alert('hello')</script>,这个内容会被转义成&lt;script&gt;alert('hello')&lt;/script&gt;,这样就可以正常渲染,而不会被浏览器执行了。

对Cookies使用HttpOnly属性

标记为HttpOnly的cookie,浏览器只能通过HTTP协议访问,而不能通过JavaScript访问,这样就可以防范XSS攻击。

CPS

CPS = Content Security Policy, 即内容安全策略,这是一种安全策略,可以防范XSS攻击。CPS是一个HTTP头部,可以告诉浏览器只能加载指定的资源,比如只能加载指定的域名下的资源,或者只能加载指定的类型的资源。这样就可以防范XSS攻击。
CPS只是众多防范XSS攻击方式的一种,还有其他方法可以方法XSS攻击,比如output encoding, sanitization等。

关于CSP的详细内容,请看这里

CPS也不只用于防范XSS攻击,还可以防范其他攻击,比如:

  1. Prevent Clickjacking(点击劫持)
  2. manipulator-in-the-middle(中间人攻击)

XSRF

XSRF = Cross Site Request Forgery, 即开展请求伪造。与XSS攻击方式不同,这种方式需要黑客自己有一个恶意站点,然后诱导用户点击恶意链接跳转到黑客的恶意站点,黑客再根据用户的cookie和其他信息伪造一个请求发送给用户访问的正常站点。

  1. 目标站点存在XSRF漏洞。
  2. 黑客自己有一个恶意站点。
  3. 用户登录过目标站点,并且在浏览器上保持有该站点的登录状态
  4. 黑客诱导用户点击恶意链接,跳转到黑客的恶意站点。

如何防范XSRF攻击

充分利用Cookies的SameSite属性

验证请求的来源站点

使用CSRF Token


前端性能优化有很多方面,细节很多,最近面试也经常考到这个问题,现整理如下,以便日后查阅。

Http层面的优化:

  1. 减少HTTP请求次数, Http请求是很耗时的操作,減少HTTP请求次数是提高网站性能的一个重要手段。
  2. 使用缓存,缓存可以减少服务器的压力,提高网站的访问速度。
  3. 使用CDN,CDN是内容分发网络,可以加速网站的访问速度。
  4. 使用Gzip压缩,Gzip是一种压缩算法,可以减小文件的体积,提高网站的访问速度。
  5. 使用HTTP2,HTTP2是HTTP协议的新版本,可以提高网站的访问速度。

JS/css/html层面的优化:

  1. 使用lazy loading,对于首页不需要立即展示的组件,可以采用延迟加载。
  2. 代码压缩,代码压缩是一种优化技术,可以减小文件的体积,提高网站的访问速度。现在的打包工具比如webpack都自动开启了代码压缩。
  3. Reduce Bundle size,减小文件的体积,提高网站的访问速度。
    1. 使用Tree Shaking,Tree Shaking是一种优化技术,可以减小文件的体积,提高网站的访问速度。
    2. 使用Code Splitting,Code Splitting是一种优化技术,可以减小文件的体积,提高网站的访问速度。
  4. 复杂的客户端计算可以用web worker来处理,web worker的好处是可以在后台线程中运行脚本,不会影响主线程的运行。不影响浏览器的渲染。
  5. CSS优化
    1. CSS很小的话,可以采用内联的方式插入到html中,这样就不必额外下载一个css文件。
    2. CSS很大的话,则要独立出一个文件来,这样可以利用缓存机制,注意index.html一般是不缓存的。

图片优化:

  1. 使用webp格式,webp是一种新的图片格式,可以减小文件的体积,提高网站的访问速度。
  2. 使用图片懒加载,图片懒加载是一种优化技术,可以减小文件的体积,提高网站的访问速度。
  3. 使用图片压缩,图片压缩是一种优化技术,可以减小文件的体积,提高网站的访问速度。
  4. 雪碧图,雪碧图是一种优化技术,可以减小文件的体积,提高网站的访问速度。

概述

Zone是Angular中非常重要的一个概念,简单来说,Zone就是一个execution context(执行上下文),这个执行上下文可以存在于多个JS虚拟机的执行轮次中。这意味着它可以在异步操作中保持状态,而不受 JavaScript 单线程执行模型的影响。它的作用主要有以下几点:

  1. 捕获异步操作并添加额外的功能以实现自动变更检测。
  2. 应用程序性能分析(profiling)
  3. 跟踪跨多个 VM 轮次的长堆栈跟踪(long stack traces),对于调试复杂的异步代码非常有用。

今天来看看在JavaScript中如何控制对象的可访问性。对象的可访问性控制在常规业务代码中都用的并不多,一般都是底层框架在用,不过了解一下总是没有坏处的。

不可扩展对象

使用Object.preventExtensions()方法可以将一个对象设置为不可扩展,不可扩展的对象不能添加新属性,但可以修改或者删除已有属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
"use strict"; // eslint-disable-line

const person1 = {
name: 'zdd',
};
console.log(Object.isExtensible(person1)); // true;

Object.preventExtensions(person1);
console.log(Object.isExtensible(person1)); // false

person1.name = 'Philip'; // OK
console.log(person1.name); // Philip

delete person1.name; // OK;
console.log(person1.name); // undefined.

person1.age = 18; // Error: Cannot add property age, object is not extensible

密封对象

使用Object.seal()方法可以将一个对象密封,密封后的对象不能添加、删除属性,但可以修改属性的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
"use strict"; // eslint-disable-line

const person1 = {
name: 'zdd',
};
console.log(Object.isSealed(person1)); // false

Object.seal(person1);
console.log(Object.isSealed(person1)); // true

person1.name = 'Philip'; // OK
console.log(person1.name); // Philip

person1.age = 18; // Error, Seal object is non-extensible.
delete person1.name; // Error in strict mode.

冻结对象

使用Object.freeze()方法可以冻结一个对象,冻结后的对象不能添加、修改、删除属性,也不能修改属性的可枚举性、可配置性、可写性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
'use strict';

const person1 = {
name: 'zdd',
};
console.log(Object.isFrozen(person1)); // false

Object.freeze(person1);
console.log(Object.isFrozen(person1)); // true

person1.name = 'ddz'; // Error. object is frozen.
person1.age = 18; // Cannot add property age, object is not extensible
delete person1.name; // TypeError: Cannot delete property 'name' of #<Object>

console.log(person1);

总结

特性 不可扩展对象 (Object.preventExtensions) 密封对象 (Object.seal) 冻结对象 (Object.freeze)
是否可新增属性
是否可修改属性值
是否可删除属性
是否可更改原型链
是否可更改可枚举性
是否可更改可配置性
是否可更改可写性

今天我们来学习一下如何在JavaScript中实现原型继承,话不多说,直接上代码。

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
// 父类的构造函数
function Animal(name) {
this.name = name;
}

// 父类的方法
Animal.prototype.printName = function () {
console.log(this.name);
};

// 子类构造函数
function Dog(name, category) {
// 好用父类构造函数,初始化父类属性。
Animal.call(this, name);

// 初始化子类的属性
this.category = category;
}

// 设置 Dog 的原型为 Animal 的实例,建立原型链
Dog.prototype = Object.create(Animal.prototype);

// 修复子类的 constructor 指向(上面一行代码已经将constructor指向了Animal)
Dog.prototype.constructor = Dog;

// 子类独有的方法。
Dog.prototype.printCategory = function () {
console.log(`Dog name: ${this.name} barking`);
};

const dog = new Dog('dog', 'Pet');
dog.printName(); // 调用父类的方法
dog.printCategory(); // 调用自己的方法

今天在HackerRank上做Angular题目,偶然遇到一个循环固定次数的问题,这里记录一下。

假设需要在template里面循环count次,每次生成一个div,而count是后台api返回的一个变量,该如何实现呢?

方法一,使用数组

我能想到的最简单的方法,就是定义一个含有count个元素的数组,然后使用ngFor遍历这个数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export class AppComponent implements OnInit {
nums: number[] = [];

ngOnInit() {
this.fetchData().then(count => {
// 创建含有count个元素的数组,数组元素的值无所谓,我们只用到元素个数。
this.nums = Array(count).fill(0);
})
}

// 模拟后台api返回的count
fetchData() {
// Random int between 0 - 10
const randomInt = Math.floor((Math.random() * 10))
return Promise.resolve(randomInt);
}
}

然后使用ngFor遍历这个数组。这样会循环生成count个div元素。

1
2
3
<ng-container *ngFor="let n of nums; let i = index">
<div>{{i}}</div>
</ng-container>

方法二,在模板中构造数组

1
2
3
<div *ngFor="let i of [].constructor(count).keys()">
第 {{i + 1}} 次循环 <!-- 显示从1开始的序号 -->
</div>

注意:该方法会导致一个错误:需要研究一下,为什么?

1
core.mjs:6662 ERROR RuntimeError: NG0100: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: '[object Array Iterator]'. Current value: '[object Array Iterator]'. Expression location: _AppComponent component. Find more at

方法三,使用自定义指令

使用时,需要在模块或者独立组件的imports中引入RepeatDirective,否则会报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 自定义指令代码
import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';

@Directive({
standalone: true,
selector: '[appRepeat]',
})
export class RepeatDirective {
constructor(
private templateRef: TemplateRef<any>,
private viewContainer: ViewContainerRef,
) {}

@Input('appRepeat') set count(value: number) {
this.viewContainer.clear(); // 清空容器
for (let i = 0; i < value; i++) {
this.viewContainer.createEmbeddedView(this.templateRef, {
$implicit: i + 1, // 传递索引值
});
}
}
}

调用自定义指令

1
2
3
<div *appRepeat="count; let i">
这是第 {{ i }} 次循环
</div>

方法四:自定义管道

使用时,需要在模块或者独立组件的imports中引入RangePipe,否则会报错。

1
2
3
4
5
6
7
8
9
10
11
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
standalone: true,
name: 'range'
})
export class RangePipe implements PipeTransform {
transform(value: number): number[] {
return Array.from({ length: value }, (_, i) => i);
}
}

使用自定义管道

1
2
3
<div *ngFor="let i of count | range">
第 {{i + 1}} 次循环 <!-- 显示从1开始的序号 -->
</div>

介绍

Pull和Push是两种数据流的传递方式,他们决定了数据如何从生产者传递到消费者。

Pull

Pull是拉模型,也就是消费者主动向生产者请求数据,消费者是主动的一方。在JavaScript中,所有的函数都是Pull模型的,函数相当于数据的生产者,函数调用者相当于数据的消费者。调用这个函数,就是在请求数据。

下面的代码中num.reduce函数用来求数组的和,const sum = nums.reduce(...)调用函数请求数据。

1
2
3
const nums = [1, 2, 3, 4, 5];
const sum = nums.reduce((acc, cur) => acc + cur, 0);
console.log(sum); // 15

Push

Push是推模型,由数据的生产者主动向外推送数据,生产者是主动的一方。在JavaScript中,Promise就是Push模型的,与函数不同,Promise决定什么时候发送数据。

下面代码中的p.then(console)会在Promise resolve之后执行,Promise决定了何时发送数据。

1
2
3
4
5
6
const p = new Promise((resolve) => {
setTimeout(() => {
resolve(42);
}, 1000);
});
p.then(console.log); // 42

RxJS中的Observable也是推模型的,Observable决定了何时发送数据。

References

  1. https://rxjs.dev/guide/observable