首页

JavaScript必须掌握的基础 ---> this

seo达人

this

this是我们在书写代码时最常用的关键词之一,即使如此,它也是JavaScript最容易被最头疼的关键词。那么this到底是什么呢?


如果你了解执行上下文,那么你就会知道,其实this是执行上下文对象的一个属性:


executionContext = {

   scopeChain:[ ... ],

   VO:{

       ...

   },

   this:  ?

}

执行上下文中有三个重要的属性,作用域链(scopeChain)、变量对象(VO)和this。


this是在进入执行上下文时确定的,也就是在函数执行时才确定,并且在运行期间不允许修改并且是永久不变的


在全局代码中的this

在全局代码中this 是不变的,this始终是全局对象本身。


var a = 10;

this.b = 20;

window.c = 30;


console.log(this.a);

console.log(b);

console.log(this.c);


console.log(this === window) // true

// 由于this就是全局对象window,所以上述 a ,b ,c 都相当于在全局对象上添加相应的属性

如果我们在代码运行期尝试修改this的值,就会抛出错误:


this = { a : 1 } ; // Uncaught SyntaxError: Invalid left-hand side in assignment

console.log(this === window) // true

函数代码中的this

在函数代码中使用this,才是令我们最容易困惑的,这里我们主要是对函数代码中的this进行分析。


我们在上面说过this的值是,进入当前执行上下文时确定的,也就是在函数执行时并且是执行前确定的。但是同一个函数,作用域中的this指向可能完全不同,但是不管怎样,函数在运行时的this的指向是不变的,而且不能被赋值。


function foo() {

   console.log(this);

}


foo();  // window

var obj={

   a: 1,

   bar: foo,

}

obj.bar(); // obj

函数中this的指向丰富的多,它可以是全局对象、当前对象、或者是任意对象,当然这取决于函数的调用方式。在JavaScript中函数的调用方式有一下几种方式:作为函数调用、作为对象属性调用、作为构造函数调用、使用apply或call调用。下面我们将按照这几种调用方式一一讨论this的含义。


作为函数调用

什么是作为函数调用:就是独立的函数调用,不加任何修饰符。


function foo(){

   console.log(this === window); // true

   this.a = 1;

   console.log(b); // 2

}

var b = 2;

foo();

console.log(a); // 1

上述代码中this绑定到了全局对象window。this.a相当于在全局对象上添加一个属性 a 。


在严格模式下,独立函数调用,this的绑定不再是window,而是undefined。


function foo() {

   "use strict";

   console.log(this===window); // false

   console.log(this===undefined); // true

}

foo();

这里要注意,如果函数调用在严格模式下,而内部代码执行在非严格模式下,this 还是会默认绑定为 window。


function foo() {

   console.log(this===window); // true

}



(function() {

   "use strict";

   foo();

})()

对于在函数内部的函数独立调用 this 又指向了谁呢?


function foo() {

   function bar() {

       this.a=1;

       console.log(this===window); // true

   }

   bar()

}

foo();

console.log(a); // 1

上述代码中,在函数内部的函数独立调用,此时this还是被绑定到了window。


总结:当函数作为独立函数被调用时,内部this被默认绑定为(指向)全局对象window,但是在严格模式下会有区别,在严格模式下this被绑定为undefined。


作为对象属性调用

var a=1;

var obj={

   a: 2,

   foo: function() {

       console.log(this===obj); // true

       console.log(this.a); // 2

   }

}

obj.foo();

上述代码中 foo属性的值为一个函数。这里称 foo 为 对象obj 的方法。foo的调用方式为 对象 . 方法 调用。此时 this 被绑定到当前调用方法的对象。在这里为 obj 对象。


再看一个例子:


var a=1;

var obj={

   a: 2,

   bar: {

       a: 3,

       foo: function() {

           console.log(this===bar); // true

           console.log(this.a); // 3

       }

   }

}

obj.bar.foo();

遵循上面说的规则 对象 . 属性 。这里的对象为 obj.bar 。此时 foo 内部this被绑定到了 obj.bar 。 因此 this.a 即为 obj.bar.a 。


再来看一个例子:


var a=1;

var obj={

   a: 2,

   foo: function() {

       console.log(this===obj); // false

       console.log(this===window); // true

       console.log(this.a); // 1

   }

}


var baz=obj.foo;

baz();

这里 foo 函数虽然作为对象obj 的方法。但是它被赋值给变量 baz 。当baz调用时,相当于 foo 函数独立调用,因此内部 this被绑定到 window。


使用apply或call调用

apply和call为函数原型上的方法。它可以更改函数内部this的指向。


var a=1;

function foo() {

   console.log(this.a);

}

var obj1={

   a: 2

}

var obj2={

   a: 3

}

var obj3={

   a: 4

}

var bar=foo.bind(obj1);

bar();// 2  this => obj1

foo(); // 1  this => window

foo.call(obj2); // 3  this => obj2

foo.call(obj3); // 4  this => obj3

当函数foo 作为独立函数调用时,this被绑定到了全局对象window,当使用bind、call或者apply方法调用时,this 被分别绑定到了不同的对象。


作为构造函数调用

var a=1;

function Person() {

   this.a=2;  // this => p;

}

var p=new Person();

console.log(p.a); // 2

上述代码中,构造函数 Person 内部的 this 被绑定为 Person的一个实例。


总结:


当我们要判断当前函数内部的this绑定,可以依照下面的原则:


函数是否在是通过 new 操作符调用?如果是,this 绑定为新创建的对象

var bar = new foo();     // this => bar;

函数是否通过call或者apply调用?如果是,this 绑定为指定的对象

foo.call(obj1);  // this => obj1;

foo.apply(obj2);  // this => obj2;

函数是否通过 对象 . 方法调用?如果是,this 绑定为当前对象

obj.foo(); // this => obj;

函数是否独立调用?如果是,this 绑定为全局对象。

foo(); // this => window

DOM事件处理函数中的this

1). 事件绑定


<button id="btn">点击我</button>


// 事件绑定


function handleClick(e) {

   console.log(this); // <button id="btn">点击我</button>

}

       document.getElementById('btn').addEventListener('click',handleClick,false);  //   <button id="btn">点击我</button>

       

document.getElementById('btn').onclick= handleClick; //  <button id="btn">点击我</button>

根据上述代码我们可以得出:当通过事件绑定来给DOM元素添加事件,事件将被绑定为当前DOM对象。


2).内联事件


<button onclick="handleClick()" id="btn1">点击我</button>

<button onclick="console.log(this)" id="btn2">点击我</button>


function handleClick(e) {

   console.log(this); // window

}


//第二个 button 打印的是   <button id="btn">点击我</button>

我认为内联事件可以这样理解:


//伪代码


<button onclick=function(){  handleClick() } id="btn1">点击我</button>

<button onclick=function() { console.log(this) } id="btn2">点击我</button>

这样我们就能理解上述代码中为什么内联事件一个指向window,一个指向当前DOM元素。(当然浏览器处理内联事件时并不是这样的)


定时器中的this

定时器中的 this 指向哪里呢?


function foo() {

   setTimeout(function() {

       console.log(this); // window

   },1000)

}

foo();  

再来看一个例子


var name="chen";

var obj={

   name: "erdong",

   foo: function() {

       console.log(this.name); // erdong

       setTimeout(function() {

           console.log(this.name); // chen

       },1000)

   }

}

obj.foo();

到这里我们可以看到,函数 foo 内部this指向为调用它的对象,即:obj 。定时器中的this指向为 window。那么有什么办法让定时器中的this跟包裹它的函数绑定为同一个对象呢?


1). 利用闭包:


var name="chen";

var obj={

   name: "erdong",

   foo: function() {

       console.log(this.name) // erdong

       var that=this;

       setTimeout(function() {

           // that => obj

           console.log(that.name); // erdong

       },1000)

   }

}

obj.foo();

利用闭包的特性,函数内部的函数可以访问含义访问当前词法作用域中的变量,此时定时器中的 that 即为包裹它的函数中的 this 绑定的对象。在下面我们会介绍利用 ES6的箭头函数实现这一功能。


当然这里也可以适用bind来实现:


var name="chen";

var obj={

   name: "erdong",

   foo: function() {

       console.log(this.name); // erdong

       setTimeout(function() {

           // this => obj

           console.log(this.name); // erdong

       }.bind(this),1000)

   }

}

obj.foo();

被忽略的this

如果你把 null 或者 undefined 作为 this 的绑定对象传入 call 、apply或者bind,这些值在调用时会被忽略,实例 this 被绑定为对应上述规则。


var a=1;

function foo() {

   console.log(this.a); // 1  this => window

}

var obj={

   a: 2

}

foo.call(null);

var a=1;

function foo() {

   console.log(this.a); // 1  this => window

}

var obj={

   a: 2

}

foo.apply(null);

var a=1;

function foo() {

   console.log(this.a); // 1  this => window

}

var obj={

   a: 2

}

var bar = foo.bind(null);

bar();

bind 也可以实现函数柯里化:


function foo(a,b) {

   console.log(a,b); // 2  3

}

var bar=foo.bind(null,2);

bar(3);

更复杂的例子:


var foo={

   bar: function() {

       console.log(this);

   }

};


foo.bar(); // foo

(foo.bar)(); // foo


(foo.bar=foo.bar)(); // window

(false||foo.bar)();  // window

(foo.bar,foo.bar)();  // window

上述代码中:


foo.bar()为对象的方法调用,因此 this 绑定为 foo 对象。


(foo.bar)() 前一个() 中的内容不计算,因此还是 foo.bar()


(foo.bar=foo.bar)() 前一个 () 中的内容计算后为 function() { console.log(this); } 所以这里为匿名函数自执行,因此 this 绑定为 全局对象 window


后面两个实例同上。


这样理解会比较好:


(foo.bar=foo.bar)  括号中的表达式执行为 先计算,再赋值,再返回值。

(false||foo.bar)()    括号中的表达式执行为 判断前者是否为 true ,若为true,不计算后者,若为false,计算后者并返回后者的值。

(foo.bar,foo.bar)   括号中的表达式之行为分别计算 “,” 操作符两边,然后返回  “,” 操作符后面的值。

箭头函数中的this

箭头函数时ES6新增的语法。


有两个作用:


更简洁的函数

本身不绑定this

代码格式为:


// 普通函数

function foo(a){

   // ......

}

//箭头函数

var foo = a => {

   // ......

}


//如果没有参数或者参数为多个


var foo = (a,b,c,d) => {

   // ......

}

我们在使用普通函数之前对于函数的this绑定,需要根据这个函数如何被调用来确定其内部this的绑定对象。而且常常因为调用链的数量或者是找不到其真正的调用者对 this 的指向模糊不清。在箭头函数出现后其内部的 this 指向不需要再依靠调用的方式来确定。


箭头函数有几个特点(与普通函数的区别)


箭头函数不绑定 this 。它只会从作用域链的上一层继承 this。

箭头函数不绑定arguments,使用reset参数来获取实参的数量。

箭头函数是匿名函数,不能作为构造函数。

箭头函数没有prototype属性。

不能使用 yield 关键字,因此箭头函数不能作为函数生成器。

这里我们只讨论箭头函数中的this绑定。


用一个例子来对比普通函数与箭头函数中的this绑定:


var obj={

   foo: function() {

       console.log(this); // obj

   },

   bar: () => {

       console.log(this); // window

   }

}

obj.foo();

obj.bar();

上述代码中,同样是通过对象 . 方法调用一个函数,但是函数内部this绑定确是不同,只因一个数普通函数一个是箭头函数。


用一句话来总结箭头函数中的this绑定:


个人上面说的它会从作用域链的上一层继承 this ,说法并不是很正确。作用域中存放的是这个函数当前执行上下文与所有父级执行上下文的变量对象的集合。因此在作用域链中并不存在 this 。应该说是作用域链上一层对应的执行上下文中继承 this 。


箭头函数中的this继承于作用域链上一层对应的执行上下文中的this


var obj={

   foo: function() {

       console.log(this); // obj

   },

   bar: () => {

       console.log(this); // window

   }

}

obj.bar();

上述代码中obj.bar执行时的作用域链为:


scopeChain = [

   obj.bar.AO,

   global.VO

]

根据上面的规则,此时bar函数中的this指向为全局执行上下文中的this,即:window。


再来看一个例子:


var obj={

   foo: function() {

       console.log(this); // obj

       var bar=() => {

           console.log(this); // obj

       }

       bar();

   }

}

obj.foo();

在普通函数中,bar 执行时内部this被绑定为全局对象,因为它是作为独立函数调用。但是在箭头函数中呢,它却绑定为 obj 。跟父级函数中的 this 绑定为同一对象。


此时它的作用域链为:


scopeChain = [

    bar.AO,

    obj.foo.AO,

    global.VO

]

这个时候我们就差不多知道了箭头函数中的this绑定。


继续看例子:


var obj={

   foo: () => {

       console.log(this); // window

       var bar=() => {

           console.log(this); // window

       }

       bar();

   }

}

obj.foo();

这个时候怎么又指向了window了呢?


我们还看当 bar 执行时的作用域链:


scopeChain = [

    bar.AO,

    obj.foo.AO,

    global.VO

]

当我们找bar函数中的this绑定时,就会去找foo函数中的this绑定。因为它是继承于它的。这时 foo 函数也是箭头函数,此时foo中的this绑定为window而不是调用它的obj对象。因此 bar函数中的this绑定也为全局对象window。


我们在回头看上面关于定时器中的this的例子:


var name="chen";

var obj={

   name: "erdong",

   foo: function() {

       console.log(this.name); // erdong

       setTimeout(function() {

           console.log(this); // chen

       },1000)

   }

}

obj.foo();

这时我们就可以很简单的让定时器中的this与foo中的this绑定为同一对象:


var name="chen";

var obj={

   name: "erdong",

   foo: function() {

       // this => obj

       console.log(this.name); // erdong

       setTimeout(() =>  {

           // this => foo中的this => obj

           console.log(this.name); // erdong

       },1000)

   }

}

obj.foo();

JavaScript的padStart()和padEnd()格式化字符串使用技巧

seo达人

用例

让我们从介绍几种不同的填充用例开始。


标签和值

假设你在同一行上有标签和值,例如 name:zhangsan 和 Phone Number:(555)-555-1234。如果把他们放在一起看起来会有点奇怪,会是这样:


Name: zhangsan

Phone Number: (555)-555-1234

你可能想要这个。


Name:           zhangsan

Phone Number:   (555)555-1234

或这个...


       Name: zhangsan

Phone Number: (555)555-1234

金额

在中国,显示价格时通常显示两位数的角、分。所以代替这个...


¥10.1

你会想要这个。


¥10.01

日期

对于日期,日期和月份都需要2位数字。所以代替这个...


2020-5-4

你会想要这个。


2020-05-04

时间

与上面的日期类似,对于计时器,你需要2位数字表示秒,3位数字表示毫秒。所以代替这个...


1:1

你会想要这个。


01:001

padstart()

让我们从 padStart() 以及标签和值示例开始。假设我们希望标签彼此正确对齐,以使值在同一位置开始。


       Name: zhangsan

Phone Number: (555)555-1234

由于 Phone Number 是两个标签中较长的一个,因此我们要在 Name 标签的开头加上空格。为了将来的需要,我们不要把它专门填充到电话号码的长度,我们把它填充到长一点,比如说20个字符。这样一来,如果你在未来使用较长的标签,这一招仍然有效。


在填充之前,这是用于显示此信息的入门代码。


const label1 = "Name";

const label2 = "Phone Number";

const name = "zhangsan"

const phoneNumber = "(555)-555-1234";


console.log(label1 + ": " + name);

console.log(label2 + ": " + phoneNumber);


//Name: zhangsan

//Phone Number: (555)-555-1234

现在,让我们填充第一个标签。要调用 padStart(),你需要传递两个参数:一个用于填充字符串的目标长度,另一个用于你希望填充的字符。在这种情况下,我们希望长度为20,而填充字符为空格。


const label1 = "Name";

const label2 = "Phone Number";

const name = "zhangsan"

const phoneNumber = "(555)-555-1234";


console.log(label1.padStart(20, " ") + ": " + name);

console.log(label2 + ": " + phoneNumber);


//               Name: zhangsan

////Phone Number: (555)-555-1234

现在填充第二行。


const label1 = "Name";

const label2 = "Phone Number";

const name = "zhangsan"

const phoneNumber = "(555)-555-1234";


console.log(label1.padStart(20, " ") + ": " + name);

console.log(label2.padStart(20, " ") + ": " + phoneNumber);


//               Name: zhangsan

////     Phone Number: (555)-555-1234

padEnd()

对于相同的标签和值示例,让我们更改填充标签的方式。让我们将标签向左对齐,以便在末尾添加填充。


初始代码


const label1 = "Name";

const label2 = "Phone Number";

const name = "zhangsan"

const phoneNumber = "(555)-555-1234";


console.log(label1 + ": " + name);

console.log(label2 + ": " + phoneNumber);


//Name: zhangsan

//Phone Number: (555)-555-1234

现在,让我们填充第一个标签,与我们之前所做的类似,但有两个小区别。现在,我们使用 padEnd() 而不是padStart(),并且需要在填充之前将冒号与标签连接起来,这样我们就能确保冒号在正确的位置。


const label1 = "Name";

const label2 = "Phone Number";

const name = "zhangsan"

const phoneNumber = "(555)-555-1234";


console.log((label1 + ': ').padEnd(20, ' ') + name);

console.log(label2 + ": " + phoneNumber);


//Name:               zhangsan

//Phone Number: (555)-555-1234

现在两行都已填充。


const label1 = "Name";

const label2 = "Phone Number";

const name = "zhangsan"

const phoneNumber = "(555)-555-1234";


console.log((label1 + ': ').padEnd(20, ' ') + name);

console.log((label2 + ': ').padEnd(20, ' ') + phoneNumber);


//Name:               zhangsan

//Phone Number:       (555)-555-1234

数字(价格、日期、计时器等)呢?

padding函数是专门针对字符串而不是数字的,所以,我们需要先将数字转换为字符串。


价格

让我们看一下显示价格的初始代码。


const rmb = 10;

const cents = 1;

console.log("¥" + rmb + "." + cents); //¥10.1

要填充分,我们需要先将其转换为字符串,然后调用 padStart() 函数,指定长度为1且填充字符为'0';


const rmb = 10;

const cents = 1;

console.log("¥" + rmb + "." + cents.toString().padStart(2,0)); //¥10.01

日期

这是显示日期的初始代码。


const month = 2;

const year = 2020;


console.log(year + "-" + month); //2020-2

现在,让我们填充月份以确保它是两位数。


const month = 2;

const year = 2020;


console.log(year + "-" + month.toString().padStart(2,"0")); // 2020-02

计时器

最后是我们的计时器,我们要格式化两个不同的数字,即秒和毫秒。尽管有相同的原则。这是初始代码。


const seconds = 1;

const ms = 1;


console.log(seconds + ":" + ms); //1:1

现在要填充,我将在单独的行上进行填充,以便于阅读。


const seconds = 1;

const formattedSeconds = seconds.toString().padStart(2,0);

const ms = 1;

const formattedMs = ms.toString().padStart(3,0);


console.log(formattedSeconds + ":" + formattedMs); // 01:001

最后

虽然编写自己的padding函数并不难,但既然已经内置在JavaScript中,为什么还要自己去做呢?有很多有趣的函数已经内置了。在你自己构建一些东西之前,可能值得先快速搜索一下。

认识 ESLint 和 Prettier

seo达人

ESLint

先说是什么:ESLint 是一个检查代码质量与风格的工具,配置一套规则,他就能检查出你代码中不符合规则的地方,部分问题支持自动修复。


使用这么一套规则有什么用呢?如果单人开发的话倒是没什么了,但是一个团队若是存在两种风格,那格式化之后处理代码冲突就真的要命了,统一的代码风格真的很重要!


(其实以前自己做一个项目的时候,公司电脑和家庭电脑的代码风格配置不一样,在家加班的时候也经常顺手格式化了,这么循环了几次不同的风格,导致 diff 极其混乱

JavaScript中的Event Loop(事件循环)机制

seo达人

事件循环

JavaScript是单线程,非阻塞的

浏览器的事件循环


执行栈和事件队列

宏任务和微任务

node环境下的事件循环


和浏览器环境有何不同

事件循环模型

宏任务和微任务

经典题目分析

1. JavaScript是单线程,非阻塞的

单线程:


JavaScript的主要用途是与用户互动,以及操作DOM。如果它是多线程的会有很多复杂的问题要处理,比如有两个线程同时操作DOM,一个线程删除了当前的DOM节点,一个线程是要操作当前的DOM阶段,最后以哪个线程的操作为准?为了避免这种,所以JS是单线程的。即使H5提出了web worker标准,它有很多限制,受主线程控制,是主线程的子线程。


非阻塞:通过 event loop 实现。


2. 浏览器的事件循环

执行栈和事件队列

为了更好地理解Event Loop,请看下图(转引自Philip Roberts的演讲 《Help, I'm stuck in an event-loop》)

Help, I'm stuck in an event-loop


执行栈: 同步代码的执行,按照顺序添加到执行栈中


function a() {

   b();

   console.log('a');

}

function b() {

   console.log('b')

}

a();

我们可以通过使用 Loupe(Loupe是一种可视化工具,可以帮助您了解JavaScript的调用堆栈/事件循环/回调队列如何相互影响)工具来了解上面代码的执行情况。


调用情况


执行函数 a()先入栈

a()中先执行函数 b() 函数b() 入栈

执行函数b(), console.log('b') 入栈

输出 b, console.log('b')出栈

函数b() 执行完成,出栈

console.log('a') 入栈,执行,输出 a, 出栈

函数a 执行完成,出栈。

事件队列: 异步代码的执行,遇到异步事件不会等待它返回结果,而是将这个事件挂起,继续执行执行栈中的其他任务。当异步事件返回结果,将它放到事件队列中,被放入事件队列不会立刻执行起回调,而是等待当前执行栈中所有任务都执行完毕,主线程空闲状态,主线程会去查找事件队列中是否有任务,如果有,则取出排在第一位的事件,并把这个事件对应的回调放到执行栈中,然后执行其中的同步代码。


我们再上面代码的基础上添加异步事件,


function a() {

   b();

   console.log('a');

}

function b() {

   console.log('b')

   setTimeout(function() {

       console.log('c');

   }, 2000)

}

a();

此时的执行过程如下

img


我们同时再加上点击事件看一下运行的过程


$.on('button', 'click', function onClick() {

   setTimeout(function timer() {

       console.log('You clicked the button!');    

   }, 2000);

});


console.log("Hi!");


setTimeout(function timeout() {

   console.log("Click the button!");

}, 5000);


console.log("Welcome to loupe.");

img


简单用下面的图进行一下总结


执行栈和事件队列


宏任务和微任务

为什么要引入微任务,只有一种类型的任务不行么?


页面渲染事件,各种IO的完成事件等随时被添加到任务队列中,一直会保持先进先出的原则执行,我们不能准确地控制这些事件被添加到任务队列中的位置。但是这个时候突然有高优先级的任务需要尽快执行,那么一种类型的任务就不合适了,所以引入了微任务队列。


不同的异步任务被分为:宏任务和微任务

宏任务:


script(整体代码)

setTimeout()

setInterval()

postMessage

I/O

UI交互事件

微任务:


new Promise().then(回调)

MutationObserver(html5 新特性)

运行机制

异步任务的返回结果会被放到一个任务队列中,根据异步事件的类型,这个事件实际上会被放到对应的宏任务和微任务队列中去。


在当前执行栈为空时,主线程会查看微任务队列是否有事件存在


存在,依次执行队列中的事件对应的回调,直到微任务队列为空,然后去宏任务队列中取出最前面的事件,把当前的回调加到当前指向栈。

如果不存在,那么再去宏任务队列中取出一个事件并把对应的回到加入当前执行栈;

当前执行栈执行完毕后时会立刻处理所有微任务队列中的事件,然后再去宏任务队列中取出一个事件。同一次事件循环中,微任务永远在宏任务之前执行。


在事件循环中,每进行一次循环操作称为 tick,每一次 tick 的任务处理模型是比较复杂的,但关键步骤如下:


执行一个宏任务(栈中没有就从事件队列中获取)

执行过程中如果遇到微任务,就将它添加到微任务的任务队列中

宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)

当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染

渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)

简单总结一下执行的顺序:

执行宏任务,然后执行该宏任务产生的微任务,若微任务在执行过程中产生了新的微任务,则继续执行微任务,微任务执行完毕后,再回到宏任务中进行下一轮循环。


宏任务和微任务


深入理解js事件循环机制(浏览器篇) 这边文章中有个特别形象的动画,大家可以看着理解一下。


console.log('start')


setTimeout(function() {

 console.log('setTimeout')

}, 0)


Promise.resolve().then(function() {

 console.log('promise1')

}).then(function() {

 console.log('promise2')

})


console.log('end')

浏览器事件循环


全局代码压入执行栈执行,输出 start

setTimeout压入 macrotask队列,promise.then 回调放入 microtask队列,最后执行 console.log('end'),输出 end

调用栈中的代码执行完成(全局代码属于宏任务),接下来开始执行微任务队列中的代码,执行promise回调,输出 promise1, promise回调函数默认返回 undefined, promise状态变成 fulfilled ,触发接下来的 then回调,继续压入 microtask队列,此时产生了新的微任务,会接着把当前的微任务队列执行完,此时执行第二个 promise.then回调,输出 promise2

此时,microtask队列 已清空,接下来会会执行 UI渲染工作(如果有的话),然后开始下一轮 event loop, 执行 setTimeout的回调,输出 setTimeout

最后的执行结果如下


start

end

promise1

promise2

setTimeout

node环境下的事件循环

和浏览器环境有何不同

表现出的状态与浏览器大致相同。不同的是 node 中有一套自己的模型。node 中事件循环的实现依赖 libuv 引擎。Node的事件循环存在几个阶段。


如果是node10及其之前版本,microtask会在事件循环的各个阶段之间执行,也就是一个阶段执行完毕,就会去执行 microtask队列中的任务。


node版本更新到11之后,Event Loop运行原理发生了变化,一旦执行一个阶段里的一个宏任务(setTimeout,setInterval和setImmediate)就立刻执行微任务队列,跟浏览器趋于一致。下面例子中的代码是按照的去进行分析的。


事件循环模型

┌───────────────────────┐

┌─>│        timers         │

│  └──────────┬────────────┘

│  ┌──────────┴────────────┐

│  │     I/O callbacks     │

│  └──────────┬────────────┘

│  ┌──────────┴────────────┐

│  │     idle, prepare     │

│  └──────────┬────────────┘      ┌───────────────┐

│  ┌──────────┴────────────┐      │   incoming:   │

│  │         poll          │<──connections───     │

│  └──────────┬────────────┘      │   data, etc.  │

│  ┌──────────┴────────────┐      └───────────────┘

│  │        check          │

│  └──────────┬────────────┘

│  ┌──────────┴────────────┐

└──┤    close callbacks    │

  └───────────────────────┘

事件循环各阶段详解

node中事件循环的顺序


外部输入数据 --> 轮询阶段(poll) --> 检查阶段(check) --> 关闭事件回调阶段(close callback) --> 定时器检查阶段(timer) --> I/O 事件回调阶段(I/O callbacks) --> 闲置阶段(idle, prepare) --> 轮询阶段...


这些阶段大致的功能如下:


定时器检测阶段(timers): 这个阶段执行定时器队列中的回调如 setTimeout() 和 setInterval()。

I/O事件回调阶段(I/O callbacks): 这个阶段执行几乎所有的回调。但是不包括close事件,定时器和setImmediate()的回调。

闲置阶段(idle, prepare): 这个阶段仅在内部使用,可以不必理会

轮询阶段(poll): 等待新的I/O事件,node在一些特殊情况下会阻塞在这里。

检查阶段(check): setImmediate()的回调会在这个阶段执行。

关闭事件回调阶段(close callbacks): 例如socket.on('close', ...)这种close事件的回调

poll:

这个阶段是轮询时间,用于等待还未返回的 I/O 事件,比如服务器的回应、用户移动鼠标等等。

这个阶段的时间会比较长。如果没有其他异步任务要处理(比如到期的定时器),会一直停留在这个阶段,等待 I/O 请求返回结果。

check:

该阶段执行setImmediate()的回调函数。


close:

该阶段执行关闭请求的回调函数,比如socket.on('close', ...)。


timer阶段:

这个是定时器阶段,处理setTimeout()和setInterval()的回调函数。进入这个阶段后,主线程会检查一下当前时间,是否满足定时器的条件。如果满足就执行回调函数,否则就离开这个阶段。


I/O callback阶段:

除了以下的回调函数,其他都在这个阶段执行:


setTimeout()和setInterval()的回调函数

setImmediate()的回调函数

用于关闭请求的回调函数,比如socket.on('close', ...)

宏任务和微任务

宏任务:


setImmediate

setTimeout

setInterval

script(整体代码)

I/O 操作等。

微任务:


process.nextTick

new Promise().then(回调)

Promise.nextTick, setTimeout, setImmediate的使用场景和区别

Promise.nextTick

process.nextTick 是一个独立于 eventLoop 的任务队列。

在每一个 eventLoop 阶段完成后会去检查 nextTick 队列,如果里面有任务,会让这部分任务优先于微任务执行。

是所有异步任务中最快执行的。


setTimeout:

setTimeout()方法是定义一个回调,并且希望这个回调在我们所指定的时间间隔后第一时间去执行。


setImmediate:

setImmediate()方法从意义上将是立刻执行的意思,但是实际上它却是在一个固定的阶段才会执行回调,即poll阶段之后。


经典题目分析

一. 下面代码输出什么

async function async1() {

   console.log('async1 start');

   await async2();

   console.log('async1 end');

}

async function async2() {

   console.log('async2');

}

console.log('script start');

setTimeout(function() {

   console.log('setTimeout');

}, 0)

async1();

new Promise(function(resolve) {

   console.log('promise1');

   resolve();

}).then(function() {

   console.log('promise2');

});

console.log('script end');

先执行宏任务(当前代码块也算是宏任务),然后执行当前宏任务产生的微任务,然后接着执行宏任务


从上往下执行代码,先执行同步代码,输出 script start

遇到setTimeout,现把 setTimeout 的代码放到宏任务队列中

执行 async1(),输出 async1 start, 然后执行 async2(), 输出 async2,把 async2() 后面的代码 console.log('async1 end')放到微任务队列中

接着往下执行,输出 promise1,把 .then()放到微任务队列中;注意Promise本身是同步的立即执行函数,.then是异步执行函数

接着往下执行, 输出 script end。同步代码(同时也是宏任务)执行完成,接下来开始执行刚才放到微任务中的代码

依次执行微任务中的代码,依次输出 async1 end、 promise2, 微任务中的代码执行完成后,开始执行宏任务中的代码,输出 setTimeout

最后的执行结果如下


script start

async1 start

async2

promise1

script end

async1 end

promise2

setTimeout

二. 下面代码输出什么

console.log('start');

setTimeout(() => {

   console.log('children2');

   Promise.resolve().then(() => {

       console.log('children3');

   })

}, 0);


new Promise(function(resolve, reject) {

   console.log('children4');

   setTimeout(function() {

       console.log('children5');

       resolve('children6')

   }, 0)

}).then((res) => {

   console.log('children7');

   setTimeout(() => {

       console.log(res);

   }, 0)

})

这道题跟上面题目不同之处在于,执行代码会产生很多个宏任务,每个宏任务中又会产生微任务


从上往下执行代码,先执行同步代码,输出 start

遇到setTimeout,先把 setTimeout 的代码放到宏任务队列①中

接着往下执行,输出 children4, 遇到setTimeout,先把 setTimeout 的代码放到宏任务队列②中,此时.then并不会被放到微任务队列中,因为 resolve是放到 setTimeout中执行的

代码执行完成之后,会查找微任务队列中的事件,发现并没有,于是开始执行宏任务①,即第一个 setTimeout, 输出 children2,此时,会把 Promise.resolve().then放到微任务队列中。

宏任务①中的代码执行完成后,会查找微任务队列,于是输出 children3;然后开始执行宏任务②,即第二个 setTimeout,输出 children5,此时将.then放到微任务队列中。

宏任务②中的代码执行完成后,会查找微任务队列,于是输出 children7,遇到 setTimeout,放到宏任务队列中。此时微任务执行完成,开始执行宏任务,输出 children6;

最后的执行结果如下


start

children4

children2

children3

children5

children7

children6

三. 下面代码输出什么

const p = function() {

   return new Promise((resolve, reject) => {

       const p1 = new Promise((resolve, reject) => {

           setTimeout(() => {

               resolve(1)

           }, 0)

           resolve(2)

       })

       p1.then((res) => {

           console.log(res);

       })

       console.log(3);

       resolve(4);

   })

}



p().then((res) => {

   console.log(res);

})

console.log('end');

执行代码,Promise本身是同步的立即执行函数,.then是异步执行函数。遇到setTimeout,先把其放入宏任务队列中,遇到p1.then会先放到微任务队列中,接着往下执行,输出 3

遇到 p().then 会先放到微任务队列中,接着往下执行,输出 end

同步代码块执行完成后,开始执行微任务队列中的任务,首先执行 p1.then,输出 2, 接着执行p().then, 输出 4

微任务执行完成后,开始执行宏任务,setTimeout, resolve(1),但是此时 p1.then已经执行完成,此时 1不会输出。

最后的执行结果如下


3

end

2

4

你可以将上述代码中的 resolve(2)注释掉, 此时 1才会输出,输出结果为 3 end 4 1。


const p = function() {

   return new Promise((resolve, reject) => {

       const p1 = new Promise((resolve, reject) => {

           setTimeout(() => {

               resolve(1)

           }, 0)

       })

       p1.then((res) => {

           console.log(res);

       })

       console.log(3);

       resolve(4);

   })

}



p().then((res) => {

   console.log(res);

})

console.log('end');

3

end

4

1

最后强烈推荐几个非常好的讲解 event loop 的视频:


What the heck is the event loop anyway? | Philip Roberts | JSConf EU

Jake Archibald: In The Loop - JSConf.Asia

了不起的 tsconfig.json 指南

seo达人

在 TypeScript 开发中,tsconfig.json 是个不可或缺的配置文件,它是我们在 TS 项目中最常见的配置文件,那么你真的了解这个文件吗?它里面都有哪些优秀配置?如何配置一个合理的 tsconfig.json 文件?本文将全面带大家一起详细了解 tsconfig.json 的各项配置。



本文将从以下几个方面全面介绍 tsconfig.json 文件:

了不起的 tsconfig.json 指南.png




水平有限,欢迎各位大佬指点~~


一、tsconfig.json 简介


1. 什么是 tsconfig.json

TypeScript 使用 tsconfig.json 文件作为其配置文件,当一个目录中存在 tsconfig.json 文件,则认为该目录为 TypeScript 项目的根目录。

通常 tsconfig.json 文件主要包含两部分内容:指定待编译文件和定义编译选项。



从《TypeScript编译器的配置文件的JSON模式》可知,目前 tsconfig.json 文件有以下几个顶层属性:


compileOnSave

compilerOptions

exclude

extends

files

include

references

typeAcquisition


文章后面会详细介绍一些常用属性配置。



2. 为什么使用 tsconfig.json

通常我们可以使用 tsc 命令来编译少量 TypeScript 文件:


/*

 参数介绍:

 --outFile // 编译后生成的文件名称

 --target  // 指定ECMAScript目标版本

 --module  // 指定生成哪个模块系统代码

 index.ts  // 源文件

*/

$ tsc --outFile leo.js --target es3 --module amd index.ts

但如果实际开发的项目,很少是只有单个文件,当我们需要编译整个项目时,就可以使用 tsconfig.json 文件,将需要使用到的配置都写进 tsconfig.json 文件,这样就不用每次编译都手动输入配置,另外也方便团队协作开发。



二、使用 tsconfig.json

目前使用 tsconfig.json 有2种操作:


1. 初始化 tsconfig.json

在初始化操作,也有 2 种方式:


手动在项目根目录(或其他)创建 tsconfig.json 文件并填写配置;

通过 tsc --init 初始化 tsconfig.json 文件。


2. 指定需要编译的目录

在不指定输入文件的情况下执行 tsc 命令,默认从当前目录开始编译,编译所有 .ts 文件,并且从当前目录开始查找 tsconfig.json 文件,并逐级向上级目录搜索。


$ tsc

另外也可以为 tsc 命令指定参数 --project 或 -p 指定需要编译的目录,该目录需要包含一个 tsconfig.json 文件,如:


/*

 文件目录:

 ├─src/

 │  ├─index.ts

 │  └─tsconfig.json

 ├─package.json

*/

$ tsc --project src

注意,tsc 的命令行选项具有优先级,会覆盖 tsconfig.json 中的同名选项。



更多 tsc 编译选项,可查看《编译选项》章节。



三、使用示例

这个章节,我们将通过本地一个小项目 learnTsconfig 来学着实现一个简单配置。

当前开发环境:windows / node 10.15.1 / TypeScript3.9



1. 初始化 learnTsconfig 项目

执行下面命令:


$ mkdir learnTsconfig

$ cd .\learnTsconfig\

$ mkdir src

$ new-item index.ts

并且我们为 index.ts 文件写一些简单代码:


// 返回当前版本号

function getVersion(version:string = "1.0.0"): string{

   return version;

}


console.log(getVersion("1.0.1"))

我们将获得这么一个目录结构:


 └─src/

    └─index.ts


2. 初始化 tsconfig.json 文件

在 learnTsconfig 根目录执行:


$ tsc --init


3. 修改 tsconfig.json 文件

我们设置几个常见配置项:


{

 "compilerOptions": {

   "target": "ES5",             // 目标语言的版本

   "module": "commonjs",        // 指定生成代码的模板标准

   "noImplicitAny": true,       // 不允许隐式的 any 类型

   "removeComments": true,      // 删除注释

   "preserveConstEnums": true,  // 保留 const 和 enum 声明

   "sourceMap": true            // 生成目标文件的sourceMap文件

 },

 "files": [   // 指定待编译文件

   "./src/index.ts"  

 ]

}

其中需要注意一点:

files 配置项值是一个数组,用来指定了待编译文件,即入口文件。

当入口文件依赖其他文件时,不需要将被依赖文件也指定到 files 中,因为编译器会自动将所有的依赖文件归纳为编译对象,即 index.ts 依赖 user.ts 时,不需要在 files 中指定 user.ts , user.ts 会自动纳入待编译文件。



4. 执行编译

配置完成后,我们可以在命令行执行 tsc 命令,执行编译完成后,我们可以得到一个 index.js 文件和一个 index.js.map 文件,证明我们编译成功,其中 index.js 文件内容如下:


function getVersion(version) {

   if (version === void 0) { version = "1.0.0"; }

   return version;

}

console.log(getVersion("1.0.1"));

//# sourceMappingURL=index.js.map

可以看出,tsconfig.json 中的 removeComments 配置生效了,将我们添加的注释代码移除了。



到这一步,就完成了这个简单的示例,接下来会基于这个示例代码,讲解《七、常见配置示例》。



四、tsconfig.json 文件结构介绍


1. 按顶层属性分类

在 tsconfig.json 文件中按照顶层属性,分为以下几类:

tsconfig.json 文件结构(顶层属性).png


了不起的 tsconfig.json 指南.png



2. 按功能分类

tsconfig.json 文件结构(功能).png




五、tsconfig.json 配置介绍


1. compileOnSave

compileOnSave 属性作用是设置保存文件的时候自动编译,但需要编译器支持。


{

   // ...

 "compileOnSave": false,

}


2. compilerOptions

compilerOptions 属性作用是配置编译选项。

若 compilerOptions 属性被忽略,则编译器会使用默认值,可以查看《官方完整的编译选项列表》。

编译选项配置非常繁杂,有很多配置,这里只列出常用的配置。


{

 // ...

 "compilerOptions": {

   "incremental": true, // TS编译器在第一次编译之后会生成一个存储编译信息的文件,第二次编译会在第一次的基础上进行增量编译,可以提高编译的速度

   "tsBuildInfoFile": "./buildFile", // 增量编译文件的存储位置

   "diagnostics": true, // 打印诊断信息

   "target": "ES5", // 目标语言的版本

   "module": "CommonJS", // 生成代码的模板标准

   "outFile": "./app.js", // 将多个相互依赖的文件生成一个文件,可以用在AMD模块中,即开启时应设置"module": "AMD",

   "lib": ["DOM", "ES2015", "ScriptHost", "ES2019.Array"], // TS需要引用的库,即声明文件,es5 默认引用dom、es5、scripthost,如需要使用es的高级版本特性,通常都需要配置,如es8的数组新特性需要引入"ES2019.Array",

   "allowJS": true, // 允许编译器编译JS,JSX文件

   "checkJs": true, // 允许在JS文件中报错,通常与allowJS一起使用

   "outDir": "./dist", // 指定输出目录

   "rootDir": "./", // 指定输出文件目录(用于输出),用于控制输出目录结构

   "declaration": true, // 生成声明文件,开启后会自动生成声明文件

   "declarationDir": "./file", // 指定生成声明文件存放目录

   "emitDeclarationOnly": true, // 只生成声明文件,而不会生成js文件

   "sourceMap": true, // 生成目标文件的sourceMap文件

   "inlineSourceMap": true, // 生成目标文件的inline SourceMap,inline SourceMap会包含在生成的js文件中

   "declarationMap": true, // 为声明文件生成sourceMap

   "typeRoots": [], // 声明文件目录,默认时node_modules/@types

   "types": [], // 加载的声明文件包

   "removeComments":true, // 删除注释

   "noEmit": true, // 不输出文件,即编译后不会生成任何js文件

   "noEmitOnError": true, // 发送错误时不输出任何文件

   "noEmitHelpers": true, // 不生成helper函数,减小体积,需要额外安装,常配合importHelpers一起使用

   "importHelpers": true, // 通过tslib引入helper函数,文件必须是模块

   "downlevelIteration": true, // 降级遍历器实现,如果目标源是es3/5,那么遍历器会有降级的实现

   "strict": true, // 开启所有严格的类型检查

   "alwaysStrict": true, // 在代码中注入'use strict'

   "noImplicitAny": true, // 不允许隐式的any类型

   "strictNullChecks": true, // 不允许把null、undefined赋值给其他类型的变量

   "strictFunctionTypes": true, // 不允许函数参数双向协变

   "strictPropertyInitialization": true, // 类的实例属性必须初始化

   "strictBindCallApply": true, // 严格的bind/call/apply检查

   "noImplicitThis": true, // 不允许this有隐式的any类型

   "noUnusedLocals": true, // 检查只声明、未使用的局部变量(只提示不报错)

   "noUnusedParameters": true, // 检查未使用的函数参数(只提示不报错)

   "noFallthroughCasesInSwitch": true, // 防止switch语句贯穿(即如果没有break语句后面不会执行)

   "noImplicitReturns": true, //每个分支都会有返回值

   "esModuleInterop": true, // 允许export=导出,由import from 导入

   "allowUmdGlobalAccess": true, // 允许在模块中全局变量的方式访问umd模块

   "moduleResolution": "node", // 模块解析策略,ts默认用node的解析策略,即相对的方式导入

   "baseUrl": "./", // 解析非相对模块的基地址,默认是当前目录

   "paths": { // 路径映射,相对于baseUrl

     // 如使用jq时不想使用默认版本,而需要手动指定版本,可进行如下配置

     "jquery": ["node_modules/jquery/dist/jquery.min.js"]

   },

   "rootDirs": ["src","out"], // 将多个目录放在一个虚拟目录下,用于运行时,即编译后引入文件的位置可能发生变化,这也设置可以虚拟src和out在同一个目录下,不用再去改变路径也不会报错

   "listEmittedFiles": true, // 打印输出文件

   "listFiles": true// 打印编译的文件(包括引用的声明文件)

 }

}


3. exclude

exclude 属性作用是指定编译器需要排除的文件或文件夹。

默认排除 node_modules 文件夹下文件。


{

   // ...

 "exclude": [

   "src/lib" // 排除src目录下的lib文件夹下的文件不会编译

 ]

}

和 include 属性一样,支持 glob 通配符:


* 匹配0或多个字符(不包括目录分隔符)

? 匹配一个任意字符(不包括目录分隔符)

**/ 递归匹配任意子目录


4. extends

extends 属性作用是引入其他配置文件,继承配置。

默认包含当前目录和子目录下所有 TypeScript 文件。


{

   // ...

 // 把基础配置抽离成tsconfig.base.json文件,然后引入

   "extends": "./tsconfig.base.json"

}


5. files

files 属性作用是指定需要编译的单个文件列表。

默认包含当前目录和子目录下所有 TypeScript 文件。


{

   // ...

 "files": [

   // 指定编译文件是src目录下的leo.ts文件

   "scr/leo.ts"

 ]

}


6. include

include 属性作用是指定编译需要编译的文件或目录。


{

   // ...

 "include": [

   // "scr" // 会编译src目录下的所有文件,包括子目录

   // "scr/*" // 只会编译scr一级目录下的文件

   "scr/*/*" // 只会编译scr二级目录下的文件

 ]

}


7. references

references 属性作用是指定工程引用依赖。

在项目开发中,有时候我们为了方便将前端项目和后端node项目放在同一个目录下开发,两个项目依赖同一个配置文件和通用文件,但我们希望前后端项目进行灵活的分别打包,那么我们可以进行如下配置:


{

   // ...

 "references": [ // 指定依赖的工程

    {"path": "./common"}

 ]

}


8. typeAcquisition

typeAcquisition 属性作用是设置自动引入库类型定义文件(.d.ts)相关。

包含 3 个子属性:


enable  : 布尔类型,是否开启自动引入库类型定义文件(.d.ts),默认为 false;

include  : 数组类型,允许自动引入的库名,如:["jquery", "lodash"];

exculde  : 数组类型,排除的库名。

{

   // ...

 "typeAcquisition": {

   "enable": false,

   "exclude": ["jquery"],

   "include": ["jest"]

 }

}


六、常见配置示例

本部分内容中,我们找了几个实际开发中比较常见的配置,当然,还有很多配置需要自己摸索哟~~



1. 移除代码中注释

tsconfig.json:


{

 "compilerOptions": {

   "removeComments": true,

 }

}

编译前:


// 返回当前版本号

function getVersion(version:string = "1.0.0"): string{

   return version;

}

console.log(getVersion("1.0.1"))

编译结果:


function getVersion(version) {

   if (version === void 0) { version = "1.0.0"; }

   return version;

}

console.log(getVersion("1.0.1"));


2. 开启null、undefined检测

tsconfig.json:


{

   "compilerOptions": {

       "strictNullChecks": true

   },

}

修改 index.ts 文件内容:


const leo;

leo = new Pingan('leo','hello');


这时候编辑器也会提示错误信息,执行 tsc 后,控制台报错:


src/index.ts:9:11 - error TS2304: Cannot find name 'Pingan'.


9 leo = new Pingan('leo','hello');


Found 1 error.


3. 配置复用

通过 extends 属性实现配置复用,即一个配置文件可以继承另一个文件的配置属性。

比如,建立一个基础的配置文件 configs/base.json :


{

 "compilerOptions": {

   "noImplicitAny": true,

   "strictNullChecks": true

 }

}

在tsconfig.json 就可以引用这个文件的配置了:


{

 "extends": "./configs/base",

 "files": [

   "main.ts",

   "supplemental.ts"

 ]

}


4. 生成枚举的映射代码

在默认情况下,使用 const 修饰符后,枚举不会生成映射代码。

如下,我们可以看出:使用 const 修饰符后,编译器不会生成任何 RequestMethod 枚举的任何映射代码,在其他地方使用时,内联每个成员的值,节省很大开销。


const enum RequestMethod {

 Get,

 Post,

 Put,

 Delete

}


let methods = [

 RequestMethod.Get,

 RequestMethod.Post

]

编译结果:


"use strict";

let methods = [

   0 /* Get */,

   1 /* Post */

];

当然,我们希望生成映射代码时,也可以设置 tsconfig.json 中的配置,设置 preserveConstEnums 编译器选项为 true :


{

 "compilerOptions": {

   "target": "es5",

   "preserveConstEnums": true

 }

}


最后编译结果变成:


"use strict";

var RequestMethod;

(function (RequestMethod) {

   RequestMethod[RequestMethod["Get"] = 0] = "Get";

   RequestMethod[RequestMethod["Post"] = 1] = "Post";

   RequestMethod[RequestMethod["Put"] = 2] = "Put";

   RequestMethod[RequestMethod["Delete"] = 3] = "Delete";

})(RequestMethod || (RequestMethod = {}));

let methods = [

   0 /* Get */,

   1 /* Post */

];


5. 关闭 this 类型注解提示

通过下面代码编译后会报错:


const button = document.querySelector("button");

button?.addEventListener("click", handleClick);

function handleClick(this) {

console.log("Clicked!");

this.removeEventListener("click", handleClick);

}


报错内容:


src/index.ts:10:22 - error TS7006: Parameter 'this' implicitly has an 'any' type.

10 function handleClick(this) {

Found 1 error.


这是因为 this 隐式具有 any 类型,如果没有指定类型注解,编译器会提示“"this" 隐式具有类型 "any",因为它没有类型注释。”。



解决方法有2种:


指定 this 类型,如本代码中为 HTMLElement 类型:

HTMLElement 接口表示所有的 HTML 元素。一些HTML元素直接实现了 HTMLElement 接口,其它的间接实现HTMLElement接口。

关于 HTMLElement 可查看详细。


使用 --noImplicitThis 配置项:


在 TS2.0 还增加一个新的编译选项: --noImplicitThis,表示当 this 表达式值为 any 类型时生成一个错误信息。我们设置为 true 后就能正常编译。


{

 "compilerOptions": {

   "noImplicitThis": true

 }

}


七、Webpack/React 中使用示例


1. 配置编译 ES6 代码,JSX 文件

创建测试项目 webpack-demo,结构如下:


webpack-demo/

 |- package.json

 |- tsconfig.json

 |- webpack.config.js

 |- /dist

   |- bundle.js

   |- index.html

 |- /src

   |- index.js

   |- index.ts

 |- /node_modules

安装 TypeScript 和 ts-loader:


$ npm install --save-dev typescript ts-loader

配置 tsconfig.json,支持 JSX,并将 TypeScript 编译为 ES5:


{

 "compilerOptions": {

   "outDir": "./dist/",

   "noImplicitAny": true,

+   "module": "es6",

+   "target": "es5",

+   "jsx": "react",

   "allowJs": true

 }

}

还需要配置 webpack.config.js,使其能够处理 TypeScript 代码,这里主要在 rules 中添加 ts-loader :


const path = require('path');


module.exports = {

 entry: './src/index.ts',

 module: {

   rules: [

     {

       test: /\.tsx?$/,

       use: 'ts-loader',

       exclude: /node_modules/

     }

   ]

 },

 resolve: {

   extensions: [ '.tsx', '.ts', '.js' ]

 },

 output: {

   filename: 'bundle.js',

   path: path.resolve(__dirname, 'dist')

 }

};


2. 配置 source map

想要启用 source map,我们必须配置 TypeScript,以将内联的 source map 输出到编译后的 JavaScript 文件中。

只需要在 tsconfig.json 中配置 sourceMap 属性:


 {

   "compilerOptions": {

     "outDir": "./dist/",

+     "sourceMap": true,

     "noImplicitAny": true,

     "module": "commonjs",

     "target": "es5",

     "jsx": "react",

     "allowJs": true

   }

 }

然后配置 webpack.config.js 文件,让 webpack 提取 source map,并内联到最终的 bundle 中:


 const path = require('path');


 module.exports = {

   entry: './src/index.ts',

+   devtool: 'inline-source-map',

   module: {

     rules: [

       {

         test: /\.tsx?$/,

         use: 'ts-loader',

         exclude: /node_modules/

       }

     ]

   },

   resolve: {

     extensions: [ '.tsx', '.ts', '.js' ]

   },

   output: {

     filename: 'bundle.js',

     path: path.resolve(__dirname, 'dist')

   }

 };


八、总结

本文较全面介绍了 tsconfig.json 文件的知识,从“什么是 tsconfig.js 文件”开始,一步步带领大家全面认识 tsconfig.json 文件。

文中通过一个简单 learnTsconfig 项目,让大家知道项目中如何使用 tsconfig.json 文件。在后续文章中,我们将这么多的配置项进行分类学习。最后通过几个常见配置示例,解决我们开发中遇到的几个常见问题。

vue.js路由与vuex数据模型设计

seo达人

路由设计

本则路由考虑验证进入登录页面,完成登录操作进入首页。


import Vue from "vue";

import Router from "vue-router";

Vue.use(Router);


import store from "@/store/store";


// (延迟加载)

const Login = () => import("@/views/login");

const Home = () => import("@/views/home");


const HomeRoute = {

 path: "/",

 name: "首页",

 component: Home

};


export { HomeRoute };


const router = new Router({

 base: process.env.BASE_URL,

 routes: [

   {

     path: "/login",

     name: "登录",

     component: Login

   },

   HomeRoute

 ]

});


router.beforeEach((to, from, next) => {

 let loginName = store.state.user.loginName;

 if (to.path === "/" && loginName == "") {

   next("/login");

 } else {

   next();

 }

});


export default router;

数据模型

const state = {

 loginName: ""

};

const mutations = {

 SET_LOGINNAME(state, loginName) {

   state.loginName = loginName;

 }

};

const actions = {

 login({ commit }, userInfo) {

   return new Promise((res, ret) => {

     commit("SET_LOGINNAME", userInfo);

     res();

   });

 },

 logout({ commit }) {

   return new Promise((res, ret) => {

     commit("SET_LOGINNAME", "");

     res();

   });

 }

};

export default {

 namespaced: true,

 state,

 mutations,

 actions

};

import Vue from "vue";

import Vuex from "vuex";

Vue.use(Vuex);


import user from "./modules/user";


const store = new Vuex.Store({

 modules: {

   user

 }

});


export default store;

组件

<div class="modify">

 <input

   type="text"

   @keydown.enter.prevent="handleKeydown"

   v-model="currentVal"

   placeholder="使用enter键切换频道"

 />

 <button @click="reset" style="margin-left:5px;outline:none;cursor:pointer;">复位</button>

</div>

import { mapState, mapMutations, mapActions } from "vuex";

export default {

 name: "login",

 data() {

   return {

     currentVal: "",

     list: ["咨询服务", "音悦台", "体育台", "财经频道", "时尚资讯"],

     index: 0

   };

 },

 computed: {

   ...mapState({

     loginName: state => state.user.loginName

   })

 },

 methods: {

   ...mapActions({

     login: "user/login"

   }),

   handleToHome() {

     let userInfo = "user";

     this.login(userInfo);

     this.$router.push({

       path: "/"

     });

   },

RN和React路由详解及对比

seo达人

前言

在平时H5或者RN开发时,我们业务场景中大部分都不是单页面的需求,那这时我们就能使用路由在进行多页面的切换。下面会对比一下react路由和RN路由的本质区别和使用方法。


路由(routing)是指分组从源到目的地时,决定端到端路径的网络范围的进程

React路由

简介

使用React构建的单页面应用,要想实现页面间的跳转,首先想到的就是使用路由。在React中,常用的有两个包可以实现这个需求,那就是react-router和react-router-dom。本文主要针对react-router-dom进行说明


在根组件上配置路由,引用react-router-dom结构{ HashRouter as Router, Route ,Link ,Redirect ,Switch },HashRouter组件是路由的根组件,定义属性和方法传递给子组件。Router组件进行路由,指定每个路由跳转到相应的组件。Link组件指定跳转链接。Redirect组件路由重定向,不管什么情况下,都会跳转当前指定的路径,和switch组件联合起来一起调用,当路径匹配到路由,不在往下匹配


两类路由

HashRouter:利用监听hash变化(有一个事件hashchange)实现路由切换,它是路由容器,

渲染子组件,并向下层子组件传递(Context上下文传递)loaction,history等路由信息


BrowserHistory:利用H5Api实现路由切换,是路由容器,渲染子组件,

并向子组件传递loaction,history等路由信息

路由配置

image-20200601110809995


路由实现原理

HashRouter只是一个容器,本身并没有DOM结构

它渲染的就是它的子组件,并向下层传递location

组件挂载完成之后根据hash改变pathname的值,如果没有hash值就默认展示根组件

需要跳转路由页面时我们使用link或者push去赋值hash的pathname 如this.props.history.push({ pathname: preview, param: { pic, index } });

当hash值发生变化的时候会通过hashchange捕获变化,并给pathname重新赋值

拿到上下文中传过来的location,然后取出pathname。再对它的子组件进行遍历,如果子组件的path属性和当前上下文中传过来的pathname属性相匹配就进行渲染,若不匹配就返回null。

总结

React路由是实质就是,根据遍历识别路由的pathname,来切换router路由容器中component组件的加载渲染。每次更改pathname就都是组件的重新渲染流程,页面也都会呈现出刷新的效果。


RN路由

简介

RN把导航和路由都集中到了react-navigation库里面

组件使用堆栈式的页面导航来实现各个页面跳转

构造函数:StackNavigator(RouteConfigs, StackNavigatorConfig)

RouteConfigs:页面路由配置

StackNavigatorConfig:路由参数配置

路由配置

image-20200601111333107


参数详解

navigationOptions:配置StackNavigator的一些属性。


   title:标题,如果设置了这个导航栏和标签栏的title就会变成一样的,不推荐使用

   header:可以设置一些导航的属性,如果隐藏顶部导航栏只要将这个属性设置为null

   headerTitle:设置导航栏标题,推荐

   headerBackTitle:设置跳转页面左侧返回箭头后面的文字,默认是上一个页面的标题。可以自定义,也可以设置为null

   headerTruncatedBackTitle:设置当上个页面标题不符合返回箭头后的文字时,默认改成"返回"

   headerRight:设置导航条右侧。可以是按钮或者其他视图控件

   headerLeft:设置导航条左侧。可以是按钮或者其他视图控件

   headerStyle:设置导航条的样式。背景色,宽高等

   headerTitleStyle:设置导航栏文字样式

   headerBackTitleStyle:设置导航栏‘返回’文字样式

   headerTintColor:设置导航栏颜色

   headerPressColorAndroid:安卓独有的设置颜色纹理,需要安卓版本大于5.0

   gesturesEnabled:是否支持滑动返回手势,iOS默认支持,安卓默认关闭



screen:对应界面名称,需要填入import之后的页面


mode:定义跳转风格


  card:使用iOS和安卓默认的风格


  modal:iOS独有的使屏幕从底部画出。类似iOS的present效果


headerMode:返回上级页面时动画效果


  float:iOS默认的效果


  screen:滑动过程中,整个页面都会返回


  none:无动画


cardStyle:自定义设置跳转效果


  transitionConfig: 自定义设置滑动返回的配置


  onTransitionStart:当转换动画即将开始时被调用的功能


  onTransitionEnd:当转换动画完成,将被调用的功能


path:路由中设置的路径的覆盖映射配置


initialRouteName:设置默认的页面组件,必须是上面已注册的页面组件


initialRouteParams:初始路由参数

路由首页

react:


image-20200601111638902


在react中初始化时没有指定hash值,route会匹配路由表里面的根组件”/”


RN:


image-20200601111722749


RN 需要在StackNavigatorConfig里面指定首页


RN路由使用

image-20200601112012191


在入口路由列表注册完成之后 在导航器中的每一个页面,都有 navigation 属性 通过提供的navigate方法来提供跳转


navigation

在导航器中每一个页面都有navigation属性,该属性有以下几个属性/方法

navigate 跳转到其他页面 常用参数如下

routeName 导航器中配置的路由名称

params 传递到下一个页面的参数

state:state 里面包含有传递过来的参数 params 、 key 、路由名称 routeName

setParams 更改当前页面路由参数(后面详细介绍)

goBack: 回退可穿参数

navigate



setParams




在Vue中创建可重用的 Transition

seo达人

原始transition组件和CSS

定义transition的最简单方法是使用transition·或transition-group 组件。这需要为transition定义一个name`和一些CSS。


<template>

 <div id="app">

   <button v-on:click="show = !show">

     Toggle

   </button>

   <transition name="fade">

     <p v-if="show">hello</p>

   </transition>

 </div>

</template>

<script>

export default {

 name: "App",

 data() {

   return {

     show: true

   };

 }

};

</script>

<style>

.fade-enter-active,

.fade-leave-active {

 transition: opacity 0.3s;

}

.fade-enter,

.fade-leave-to {

 opacity: 0;

}

</style>

图片描述


看起来容易,对吧?然而,这种方法有一个问题。我们不能在另一个项目中真正重用这个transition。


封装transition组件

如果我们将前面的逻辑封装到一个组件中,并将其用作一个组件,结果会怎样呢?


// FadeTransition.vue

<template>

 <transition name="fade">

   <slot></slot>

 </transition>

</template>

<script>

export default {

 

};

</script>

<style>

.fade-enter-active,

.fade-leave-active {

 transition: opacity 0.3s;

}

.fade-enter,

.fade-leave-to {

 opacity: 0;

}

</style>


// App.vue


<template>

 <div id="app">

   <button v-on:click="show = !show">

     Toggle transition

   </button>

   <fade-transition>

     <div v-if="show" class="box"></div>

   </fade-transition>

 </div>

</template>

<script>...</script>

<style>...</style>

图片描述


通过在transition组件中提供一个slot,我们几乎可以像使用基本transition组件一样使用它。这比前面的例子稍微好一点,但是如果我们想要传递其他特定于transition的prop,比如mode或者一些hook,该怎么办呢


封装的包装器transition组件

幸运的是,Vue 中有一个功能,使我们可以将用户指定的所有额外props和监听器传递给我们的内部标签/组件。 如果你还不知道,则可以通过$attrs访问额外传递的 props,并将它们与v-bind结合使用以将它们绑定为props。 这同样适用于通过$listeners进行的事件,并通过v-on对其进行应用。


// FadeTransition.vue


<template>

 <transition name="fade" v-bind="$attrs" v-on="$listeners">

   <slot></slot>

 </transition>

</template>

<script>

export default {};

</script>

<style>

.fade-enter-active,

.fade-leave-active {

 transition: opacity 0.3s;

}

.fade-enter,

.fade-leave-to {

 opacity: 0;

}

</style>


// App.vue


...


<fade-transition mode="out-in">

 <div key="blue" v-if="show" class="box"></div>

 <div key="red" v-else class="red-box"></div>

</fade-transition>


...

图片描述


完整事例地址:https://codesandbox.io/s/yjl1...


现在,我们可以传递普通transition组件可以接受的任何事件和支持,这使得我们的组件更加可重用。但为什么不更进一步,增加通过 prop 轻松定制持续时间的可能性。


显式持续时间 prop

Vue 为transition组件提供了一个duration prop,然而,它是为更复杂的动画链接而设计的,它帮助 Vue 正确地将它们链接在一起。


在我们的案例中,我们真正需要的是通过组件prop控制CSS animation/transition。 我们可以通过不在CSS中指定显式的CSS动画持续时间,而是将其作为样式来实现。 我们可以借助transition hook来做到这一点,该transition hook与组件生命周期 hook 非常相似,但是它们在过渡所需元素之前和之后被调用。 让我们看看效果如何。


// FadeTransition.vue


<template>

 <transition name="fade"

             enter-active-class="fadeIn"

             leave-active-class="fadeOut"

             v-bind="$attrs"

             v-on="hooks">

     <slot></slot>

 </transition>

</template>

<script>

export default {

 props: {

   duration: {

     type: Number,

     default: 300

   }

 },

 computed: {

   hooks() {

     return {

       beforeEnter: this.setDuration,

       afterEnter: this.cleanUpDuration,

       beforeLeave: this.setDuration,

       afterLeave: this.cleanUpDuration,

       ...this.$listeners

     };

   }

 },

 methods: {

   setDuration(el) {

     el.style.animationDuration = `${this.duration}ms`;

   },

   cleanUpDuration(el) {

     el.style.animationDuration = "";

   }

 }

};

</script>

<style>

@keyframes fadeIn {

 from {

   opacity: 0;

 }

 to {

   opacity: 1;

 }

}

.fadeIn {

 animation-name: fadeIn;

}

@keyframes fadeOut {

 from {

   opacity: 1;

 }

 to {

   opacity: 0;

 }

}

.fadeOut {

 animation-name: fadeOut;

}

</style>

图片描述


完整事例地址:https://codesandbox.io/s/j4qn...


现在,我们可以控制实际的可见过渡时间,这使我们可重用的过渡变得灵活且易于使用。 但是,如何过渡多个元素(如列表项)呢?


Transition group 支持

你想到的最直接的方法可能是创建一个新组件,比如fade-transition-group,然后将当前transition标签替换为transition-group标签,以实现 group transition。如果我们可以在相同的组件中这样做,并公开一个将切换到transition-group实现的group prop,那会怎么样呢?幸运的是,我们可以通过render函数或component和is属性来实现这一点。


// FadeTransition.vue


<template>

 <component :is="type"

            :tag="tag"

            enter-active-class="fadeIn"

            leave-active-class="fadeOut"

            move-class="fade-move"

            v-bind="$attrs"

            v-on="hooks">

     <slot></slot>

 </component>

</template>

<script>

export default {

 props: {

   duration: {

     type: Number,

     default: 300

   },

   group: {

     type: Boolean,

     default: false

   },

   tag: {

     type: String,

     default: "div"

   }

 },

 computed: {

   type() {

     return this.group ? "transition-group" : "transition";

   },

   hooks() {

     return {

       beforeEnter: this.setDuration,

       afterEnter: this.cleanUpDuration,

       beforeLeave: this.setDuration,

       afterLeave: this.cleanUpDuration,

       leave: this.setAbsolutePosition,

       ...this.$listeners

     };

   }

 },

 methods: {

   setDuration(el) {

     el.style.animationDuration = `${this.duration}ms`;

   },

   cleanUpDuration(el) {

     el.style.animationDuration = "";

   },

   setAbsolutePosition(el) {

     if (this.group) {

       el.style.position = "absolute";

     }

   }

 }

};

</script>

<style>

@keyframes fadeIn {

 from {

   opacity: 0;

 }

 to {

   opacity: 1;

 }

}

.fadeIn {

 animation-name: fadeIn;

}

@keyframes fadeOut {

 from {

   opacity: 1;

 }

 to {

   opacity: 0;

 }

}

.fadeOut {

 animation-name: fadeOut;

}

.fade-move {

 transition: transform 0.3s ease-out;

}

</style>


// App.vue


...


<div class="box-wrapper">

 <fade-transition group :duration="300">

   <div class="box"

        v-for="(item, index) in list"

        @click="remove(index)"

        :key="item"

    >

   </div>

 </fade-transition>

</div>


...

图片描述


完整事例地址:https://codesandbox.io/s/pk9r...


文档中介绍了一个带有transition-group元素的警告。 我们基本上必须在元素离开时将每个项目的定位设置为absolute,以实现其他项目的平滑移动动画。 我们也必须添加一个move-class并手动指定过渡持续时间,因为没有用于移动的 JS hook。我们将这些调整添加到我们的上一个示例中。


再做一些调整,通过在mixin中提取 JS 逻辑,我们可以将其应用于轻松创建新的transition组件,只需将其放入下一个项目中即可。


Vue Transition

在此之前描述的所有内容基本上都是这个小型 transition 集合所包含的内容。它有 10 个封装的transition组件,每个约1kb(缩小)。我认为它非常方便,可以轻松地在不同的项目中使用。你可以试一试:)


总结

我们从一个基本的过渡示例开始,并最终通过可调整的持续时间和transition-group支持来创建可重用的过渡组件。 我们可以使用这些技巧根据并根据自身的需求创建自己的过渡组件。 希望读者从本文中学到了一些知识,并且可以帮助你们建立功能更好的过渡组件。

web安全之XSS实例解析

seo达人

XSS

跨站脚本攻击(Cross Site Script),本来缩写是 CSS, 但是为了和层叠样式表(Cascading Style Sheet, CSS)有所区分,所以安全领域叫做 “XSS”;


XSS攻击,通常是指攻击者通过 “HTML注入”篡改了网页,插入了恶意的脚本,从而在用户浏览网页时,对用户的浏览器进行控制或者获取用户的敏感信息(Cookie, SessionID等)的一种攻击方式。


页面被注入了恶意JavaScript脚本,浏览器无法判断区分这些脚本是被恶意注入的,还是正常的页面内容,所以恶意注入Javascript脚本也拥有了所有的脚本权限。如果页面被注入了恶意 JavaScript脚本,它可以做哪些事情呢?


可以窃取 cookie信息。恶意 JavaScript可以通过 ”doccument.cookie“获取cookie信息,然后通过 XMLHttpRequest或者Fetch加上CORS功能将数据发送给恶意服务器;恶意服务器拿到用户的cookie信息之后,就可以在其他电脑上模拟用户的登陆,然后进行转账操作。

可以监听用户行为。恶意JavaScript可以使用 "addEventListener"接口来监听键盘事件,比如可以获取用户输入的银行卡等信息,又可以做很多违法的事情。

可以修改DOM 伪造假的登陆窗口,用来欺骗用户输入用户名和密码等信息。

还可以在页面内生成浮窗广告,这些广告会严重影响用户体验。

XSS攻击可以分为三类:反射型,存储型,基于DOM型(DOM based XSS)


反射型

恶意脚本作为网络请求的一部分。


const Koa = require("koa");

const app = new Koa();


app.use(async ctx => {

   // ctx.body 即服务端响应的数据

   ctx.body = '<script>alert("反射型 XSS 攻击")</script>';

})


app.listen(3000, () => {

   console.log('启动成功');

});

访问 http://127.0.0.1:3000/ 可以看到 alert执行


反射型XSS1


举一个常见的场景,我们通过页面的url的一个参数来控制页面的展示内容,比如我们把上面的一部分代码改成下面这样


app.use(async ctx => {

   // ctx.body 即服务端响应的数据

   ctx.body = ctx.query.userName;

})

此时访问 http://127.0.0.1:3000?userName=xiaoming 可以看到页面上展示了xiaoming,此时我们访问 http://127.0.0.1:3000?userName=<script>alert("反射型 XSS 攻击")</script>, 可以看到页面弹出 alert。


反射型XSS2


通过这个操作,我们会发现用户将一段含有恶意代码的请求提交给服务器,服务器在接收到请求时,又将恶意代码反射给浏览器端,这就是反射型XSS攻击。另外一点需要注意的是,Web 服务器不会存储反射型 XSS 攻击的恶意脚本,这是和存储型 XSS 攻击不同的地方。


在实际的开发过程中,我们会碰到这样的场景,在页面A中点击某个操作,这个按钮操作是需要登录权限的,所以需要跳转到登录页面,登录完成之后再跳转会A页面,我们是这么处理的,跳转登录页面的时候,会加一个参数 returnUrl,表示登录完成之后需要跳转到哪个页面,即这个地址是这样的 http://xxx.com/login?returnUrl=http://xxx.com/A,假如这个时候把returnUrl改成一个script脚本,而你在登录完成之后,如果没有对returnUrl进行合法性判断,而直接通过window.location.href=returnUrl,这个时候这个恶意脚本就会执行。


存储型

存储型会把用户输入的数据“存储”在服务器。


比较常见的一个场景就是,攻击者在社区或论坛写下一篇包含恶意 JavaScript代码的博客文章或评论,文章或评论发表后,所有访问该博客文章或评论的用户,都会在他们的浏览器中执行这段恶意的JavaScript代码。


存储型攻击大致需要经历以下几个步骤


首先攻击者利用站点漏洞将一段恶意JavaScript代码提交到网站数据库中

然后用户向网站请求包含了恶意 JavaScript脚本的页面

当用户浏览该页面的时候,恶意脚本就会将用户的cookie信息等数据上传到服务器

存储型XSS


举一个简单的例子,一个登陆页面,点击登陆的时候,把数据存储在后端,登陆完成之后跳转到首页,首页请求一个接口将当前的用户名显示到页面


客户端代码


<!DOCTYPE html>

<html lang="en">


<head>

   <meta charset="UTF-8">

   <meta name="viewport" content="width=device-width, initial-scale=1.0">

   <meta http-equiv="X-UA-Compatible" content="ie=edge">

   <title>XSS-demo</title>

   <style>

       .login-wrap {

           height: 180px;

           width: 300px;

           border: 1px solid #ccc;

           padding: 20px;

           margin-bottom: 20px;

       }

       input {

           width: 300px;

       }

   </style>

</head>


<body>

   <div class="login-wrap">

       <input type="text" placeholder="用户名" class="userName">

       <br>

       <input type="password" placeholder="密码" class="password">

       <br>

       <br>

       <button class="btn">登陆</button>

   </div>

</body>

<script>

   var btn = document.querySelector('.btn');

   

   btn.onclick = function () {

       var userName = document.querySelector('.userName').value;

       var password = document.querySelector('.password').value;

       

       fetch('http://localhost:3200/login', {

           method: 'POST',

           body: JSON.stringify({

               userName,

               password

           }),

           headers:{

               'Content-Type': 'application/json'

           },

           mode: 'cors'

       })

           .then(function (response) {

               return response.json();

           })

           .then(function (res) {

               alert(res.msg);

               window.location.href= "http://localhost:3200/home";

           })

           .catch(err => {

               message.error(`本地测试错误 ${err.message}`);

               console.error('本地测试错误', err);

           });

   }

</script>


</html>

服务端代码


const Koa = require("koa");

const app = new Koa();

const route = require('koa-route');

var bodyParser = require('koa-bodyparser');

const cors = require('@koa/cors');


// 临时用一个变量来存储,实际应该存在数据库中

let currentUserName = '';


app.use(bodyParser()); // 处理post请求的参数


const login = ctx => {

   const req = ctx.request.body;

   const userName = req.userName;

   currentUserName = userName;


   ctx.response.body = {

       msg: '登陆成功'

   };

}


const home = ctx => {

   ctx.body = currentUserName;

}

app.use(cors());

app.use(route.post('/login', login));

app.use(route.get('/home', home));

app.listen(3200, () => {

   console.log('启动成功');

});

点击登陆将输入信息提交大服务端,服务端使用变量 currentUserName来存储当前的输入内容,登陆成功后,跳转到 首页, 服务端会返回当前的用户名。如果用户输入了恶意脚本内容,则恶意脚本就会在浏览器端执行。


在用户名的输入框输入 <script>alert('存储型 XSS 攻击')</script>,执行结果如下


存储型XSS


基于DOM(DOM based XSS)

通过恶意脚本修改页面的DOM节点,是发生在前端的攻击


基于DOM攻击大致需要经历以下几个步骤


攻击者构造出特殊的URL,其中包含恶意代码

用户打开带有恶意代码的URL

用户浏览器接受到响应后执行解析,前端JavaScript取出URL中的恶意代码并执行

恶意代码窃取用户数据并发送到攻击者的网站,冒充用户行为,调用目标网站接口执行攻击者指定的操作。

举个例子:


<body>

   <div class="login-wrap">

       <input type="text" placeholder="输入url" class="url">

       <br>

       <br>

       <button class="btn">提交</button>

       <div class="content"></div>

   </div>

</body>

<script>

   var btn = document.querySelector('.btn');

   var content = document.querySelector('.content');

   

   btn.onclick = function () {

       var url = document.querySelector('.url').value;

       content.innerHTML = `<a href=${url}>跳转到输入的url</a>`

   }

</script>

点击提交按钮,会在当前页面插入一个超链接,其地址为文本框的内容。


在输入框输入 如下内容


'' onclick=alert('哈哈,你被攻击了')

执行结果如下


基于DOM型XSS


首先用两个单引号闭合调 href属性,然后插入一个onclick事件。点击这个新生成的链接,脚本将被执行。


上面的代码是通过执行 执行 alert来演示的攻击类型,同样你可以把上面的脚本代码修改为任何你想执行的代码,比如获取 用户的 cookie等信息,<script>alert(document.cookie)</script>,同样也是可以的.

防御XSS

HttpOnly

由于很多XSS攻击都是来盗用Cookie的,因此可以通过 使用HttpOnly属性来防止直接通过 document.cookie 来获取 cookie。


一个Cookie的使用过程如下


浏览器向服务器发起请求,这时候没有 Cookie

服务器返回时设置 Set-Cookie 头,向客户端浏览器写入Cookie

在该 Cookie 到期前,浏览器访问该域下的所有页面,都将发送该Cookie

HttpOnly是在 Set-Cookie时标记的:


通常服务器可以将某些 Cookie 设置为 HttpOnly 标志,HttpOnly 是服务器通过 HTTP 响应头来设置的。


const login = ctx => {

   // 简单设置一个cookie

   ctx.cookies.set(

       'cid',

       'hello world',

       {

         domain: 'localhost',  // 写cookie所在的域名

         path: '/home',       // 写cookie所在的路径

         maxAge: 10 * 60 * 1000, // cookie有效时长

         expires: new Date('2021-02-15'),  // cookie失效时间

         httpOnly: true,  // 是否只用于http请求中获取

         overwrite: false  // 是否允许重写

       }

     )

}

HttpOnly


需要注意的一点是:HttpOnly 并非阻止 XSS 攻击,而是能阻止 XSS 攻击后的 Cookie 劫持攻击。


输入和输出的检查

永远不要相信用户的输入。


输入检查一般是检查用户输入的数据是都包含一些特殊字符,如 <、>, '及"等。如果发现特殊字符,则将这些字符过滤或编码。这种可以称为 “XSS Filter”。


安全的编码函数


针对HTML代码的编码方式是 HtmlEncode(是一种函数实现,将字符串转成 HTMLEntrities)


& --> &amp;

< --> &lt;

> --> &gt;

" --> &quot;

相应的, JavaScript的编码方式可以使用 JavascriptEncode。


假如说用户输入了 <script>alert("你被攻击了")</script>,我们要对用户输入的内容进行过滤(如果包含了 <script> 等敏感字符,就过滤掉)或者对其编码,如果是恶意的脚本,则会变成下面这样


&lt;script&gt;alert("你被攻击了");&lt;/script&gt;

经过转码之后的内容,如 <script>标签被转换为 &lt;script&gt;,即使这段脚本返回给页面,页面也不会指向这段代码。


防御 DOM Based XSS

我们可以回看一下上面的例子


btn.onclick = function () {

   var url = document.querySelector('.url').value;

   content.innerHTML = `<a href=${url}>跳转到输入的url</a>`

}

事实上,DOM Based XSS 是从 JavaScript中输出数据到HTML页面里。


用户输入 '' onclick=alert('哈哈,你被攻击了'),然后通过 innerHTML 修改DOM的内容,就变成了 <a href='' onclick=alert('哈哈,你被攻击了')>跳转到输入的url</a>, XSS因此产生。


那么正确的防御方法是什么呢?

从JavaScript输出到HTML页面,相当于一次 XSS输出的过程,需要根据不同场景进行不同的编码处理


变量输出到 <script>,执行一次 JavascriptEncode

通过JS输出到HTML页面


输出事件或者脚本,做 JavascriptEncode 处理

输出 HTML内容或者属性,做 HtmlEncode 处理

会触发 DOM Based XSS的地方有很多,比如


xxx.interHTML

xxx.outerHTML

document.write

页面中所有的inputs框

XMLHttpRequest返回的数据

...


项目中如果用到,一定要避免在字符串中拼接不可信的数据。


利用CSP

CSP (Content Security Policy) 即内容安全策略,是一种可信白名单机制,可以在服务端配置浏览器哪些外部资源可以加载和执行。我们只需要配置规则,如何拦截是由浏览器自己实现的。我们可以通过这种方式来尽量减少 XSS 攻击。


通常可以通过两种方式来开启 CSP:


设置 HTTP Header 的 Content-Security-Policy

Content-Security-Policy: default-src 'self'; // 只允许加载本站资源

Content-Security-Policy: img-src https://*  // 只允许加载 HTTPS 协议图片

Content-Security-Policy: child-src 'none'    // 允许加载任何来源框架

设置 meta 标签的方式

<meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src https://*; child-src 'none';">

这些 CSS 伪类,你可能还不知道,可以用起来了!

seo达人

css 伪类是用于向某些选择器添加特殊的效果,是动态的,指当前元素所处的状态或者特性。只有一个元素达到一个特定状态时,它可能得到一个伪类的样式;当状态改变时,它又会失去这个样式。


这篇文章在一定程度上鼓励你在构建UI时使用更简单的CSS和更少的 JS。熟悉 CSS 所提供的一切是实现这一目标的一种方法,另一种方法是实现最佳实践并尽可能多地重用代码。


接下介绍一些大家可能还不熟悉的一些伪类及其用例,希望对大家日后有所帮助。


::first-line | 选择文本的第一行

::first-line 伪元素在某块级元素的第一行应用样式。第一行的长度取决于很多因素,包括元素宽度,文档宽度和文本的文字大小。


::first-line 伪元素只能在块容器中,所以,::first-line伪元素只能在一个display值为block, inline-block, table-cell 或者 table-caption中有用。在其他的类型中,::first-line 是不起作用的。


用法如下:


p:first-line {

 color: lightcoral;

}

::first-letter | 选择这一行的第一字

CSS 伪元素 ::first-letter会选中某块级元素第一行的第一个字母。用法如下:


<style>

   p::first-letter{

     color: red;

     font-size: 2em;

   }

</style>


<p>前端小智,不断努,终身学习者!</p>

clipboard.png


::selection| 被用户高亮的部分

::selection 伪元素应用于文档中被用户高亮的部分(比如使用鼠标或其他选择设备选中的部分)。


div::selection {

     color: #409EFF;

}

clipboard.png


:root | 根元素

:root 伪类匹配文档树的根元素。对于 HTML 来说,:root 表示 <html> 元素,除了优先级更高之外,与 html 选择器相同。


在声明全局 CSS 变量时 :root 会很有用:


:root {

 --main-color: hotpink;

 --pane-padding: 5px 42px;

}

:empty | 仅当子项为空时才有作用

:empty 伪类代表没有子元素的元素。子元素只可以是元素节点或文本(包括空格),注释或处理指令都不会产生影响。


div:empty {

 border: 2px solid orange;

 margin-bottom: 10px;

}


<div></div>

<div></div>

<div>

</div>

clipboard.png


只有第一个和第二个div有作用,因为它们确实是空的,第三个 div 没有作用,因为它有一个换行。


:only-child | 只有一个子元素才有作用

:only-child 匹配没有任何兄弟元素的元素.等效的选择器还可以写成 :first-child:last-child或者:nth-child(1):nth-last-child(1),当然,前者的权重会低一点。


p:only-child{

 background: #409EFF;

}


<div>

 <p>第一个没有任何兄弟元素的元素</p>

</div>

<div>

 <p>第二个</p>

 <p>第二个</p>

</div>

clipboard.png


:first-of-type | 选择指定类型的第一个子元素

:first-of-type表示一组兄弟元素中其类型的第一个元素。


.innerDiv p:first-of-type {

 color: orangered;

}

上面表示将 .innerDiv 内的第一个元素为 p 的颜色设置为橘色。


<div class="innerDiv">

   <div>Div1</div>

   <p>These are the necessary steps</p>

   <p>hiya</p>

   

   <p>

       Do <em>not</em> push the brake at the same time as the accelerator.

   </p>

   <div>Div2</div>

</div>

clipboard.png


:last-of-type | 选择指定类型的最后一个子元素

:last-of-type CSS 伪类 表示了在(它父元素的)子元素列表中,最后一个给定类型的元素。当代码类似Parent tagName:last-of-type的作用区域包含父元素的所有子元素中的最后一个选定元素,也包括子元素的最后一个子元素并以此类推。


.innerDiv p:last-of-type {

   color: orangered;

}

上面表示将 .innerDiv 内的的最后一个元素为 p 的颜色设置为橘色。


clipboard.png


nth-of-type() | 选择指定类型的子元素

:nth-of-type() 这个 CSS 伪类是针对具有一组兄弟节点的标签, 用 n 来筛选出在一组兄弟节点的位置。


.innerDiv p:nth-of-type(1) {

   color: orangered;

}


<div class="innerDiv">

 <div>Div1</div>

 <p>These are the necessary steps</p>

 <p>hiya</p>

 

 <p>

     Do <em>not</em> push the brake at the same time as the accelerator.

 </p>

 <div>Div2</div>

</div>

clipboard.png


:nth-last-of-type() | 在列表末尾选择类型的子元素

:nth-last-of-type(an+b) 这个 CSS 伪类 匹配那些在它之后有 an+b-1 个相同类型兄弟节点的元素,其中 n 为正值或零值。它基本上和 :nth-of-type 一样,只是它从结尾处反序计数,而不是从开头处。


.innerDiv p:nth-last-of-type(1) {

   color: orangered;

}

这会选择innerDiv元素中包含的类型为p元素的列表中的最后一个子元素。


<div class="innerDiv">

   <p>These are the necessary steps</p>

   <p>hiya</p>

   <div>Div1</div>

   <p>

       Do the same.

   </p>

   <div>Div2</div>

</div>

clipboard.png


:link | 选择一个未访问的超链接

:link伪类选择器是用来选中元素当中的链接。它将会选中所有尚未访问的链接,包括那些已经给定了其他伪类选择器的链接(例如:hover选择器,:active选择器,:visited选择器)。


为了可以正确地渲染链接元素的样式,:link伪类选择器应当放在其他伪类选择器的前面,并且遵循LVHA的先后顺序,即::link — :visited — :hover — :active。:focus伪类选择器常伴随在:hover伪类选择器左右,需要根据你想要实现的效果确定它们的顺序。


a:link {

   color: orangered;

}

<a href="/login">Login<a>

clipboard.png


:checked | 选择一个选中的复选框

:checked CSS 伪类选择器表示任何处于选中状态的radio(<input type="radio">), checkbox (<input type="checkbox">) 或("select") 元素中的option HTML元素("option")。


input:checked {

 box-shadow: 0 0 0 3px hotpink;

}


<input type="checkbox" />

clipboard.png


大家都说简历没项目写,我就帮大家找了一个项目,还附赠【搭建教程】。


:valid | 选择一个有效的元素

:valid CSS 伪类表示内容验证正确的<input> 或其他 <form> 元素。这能简单地将校验字段展示为一种能让用户辨别出其输入数据的正确性的样式。


input:valid {

 box-shadow: 0 0 0 3px hotpink;

}

clipboard.png


:invalid | 选择一个无效的元素

:invalid CSS 伪类 表示任意内容未通过验证的 <input> 或其他 <form> 元素。


input[type="text"]:invalid {

   border-color: red;

}

:lang() | 通过指定的lang值选择一个元素

:lang() CSS 伪类基于元素语言来匹配页面元素。


/* 选取任意的英文(en)段落 */

p:lang(en) {

 quotes: '\201C' '\201D' '\2018' '\2019';

}

:not() | 用来匹配不符合一组选择器的元素

CSS 伪类 :not() 用来匹配不符合一组选择器的元素。由于它的作用是防止特定的元素被选中,它也被称为反选伪类(negation pseudo-class)。


来看一个例子:


.innerDiv :not(p) {

   color: lightcoral;

}

<div class="innerDiv">

   <p>Paragraph 1</p>

   <p>Paragraph 2</p>

   <div>Div 1</div>

   <p>Paragraph 3</p>

   <div>Div 2</div>

</div>

clipboard.png


Div 1 和 Div 2会被选中,p 不会被选 中。


原文:https://blog.bitsrc.io/css-ps...


代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug。



日历

链接

blogger

蓝蓝 http://www.lanlanwork.com

存档