疯狂的技术宅

疯狂的技术宅 查看完整档案

北京编辑  |  填写毕业院校  |  填写所在公司/组织 blog.yidengxuetang.com/ 编辑
编辑

资深技术宅,爱好广泛,兴趣多变。博览群书,喜欢扯淡。十八种语言样样稀松。想要了解更多,请关注微信公众号:充实的脑洞

个人动态

疯狂的技术宅 发布了文章 · 4月7日

在JavaScript中实现队列

作为一个优秀的程序猿需要具有知识的广度。首先是要了解你选择的编程语言。如果你正在阅读这篇文章,最有可能使用 JavaScript。

然而在熟悉了编程语言之后,你还必须了解如何根据任务轻松且有效地操纵数据。这就是数据结构的用武之地。

在本文中,我将描述队列数据这个结构:它都有哪些操作以及在 JavaScript 中怎样实现。

1. 队列数据结构

如果你喜欢四处旅行,肯定在火车站经历过检票这道手续。如果有很多人要坐火车,那么很自然地会形成一个队列。刚进入车站的人加入队列。另一边刚刚通过检票的人从队列中走出。这就是队列的一个例子,与队列数据结构的操作方式相同。

队列是一种遵循先入先出(FIFO)规则的数据结构。第一个进入队列中的项目(输入)是第一个出队(输出)的。

队列有2个指针:队首和队尾。最先进入队列进行排队的项目位于队首,而最后进入队列的项目位于队尾

回顾车站的例子,第一个检票的是在队列的队首。刚进入队列的人在队尾。

image.png

从更高的层面来看,队列是一种允许你按照先后顺序处理项目的数据结构。

2. 队列的操作

队列支持 2 个主要操作:入队(enqueue)出队(dequeue),另外还有 peek 和 length 操作。

2.1 入队操作

入队操作在队列的尾部插入项目,使其成为队列的队尾。

image.png

上图中的入队操作在队尾插入了 8,之后 8 成为队列的队尾。

queue.enqueue(8);

2.2 出队操作

出队操作取出队列中第一个项目,此时队列中的下一个项目成为队首。

image.png

在上图中,出队操作返回项目7并从队列中删除。 出队之后之后,项目 2 成为新的队首。

queue.dequeue(); // => 7

2.3 Peek 操作

Peek 操作读取队首的项目,但是不改变队列。

image.png

上图中 7 是队首。 peek 操作只需返回队首 7 但是不修改队列。

queue.peek(); // => 7

2.4 length

length 操作返回队列中包含项目的数量。

image.png

上图中的队列有 4 项:462 和。7。结果队列长度为 4

queue.length; // => 4

2.5 队列操作的时间复杂度

关于队列所有操作的重点:enqueue,dequeue,peek 和 length 必须以常数时间复杂度 O(1) 执行。

常数时间复杂度 O(1) 意味着无论队列大小如何(不管是有 10 个还是 100 万个项目),这些操作都必须在相对一致的时间内执行。

3. 用 JavaScript 实现队列

来看一下怎样在保证所有操作必须以常数时间复杂度O(1) 要求实现队列这种数据结构。

class Queue {
  constructor() {
    this.items = {};
    this.headIndex = 0;
    this.tailIndex = 0;
  }

  enqueue(item) {
    this.items[this.tailIndex] = item;
    this.tailIndex++;
  }

  dequeue() {
    const item = this.items[this.headIndex];
    delete this.items[this.headIndex];
    this.headIndex++;
    return item;
  }

  peek() {
    return this.items[this.headIndex];
  }

  get length() {
    return this.tailIndex - this.headIndex;
  }
}

const queue = new Queue();

queue.enqueue(7);
queue.enqueue(2);
queue.enqueue(6);
queue.enqueue(4);

queue.dequeue(); // => 7

queue.peek();    // => 2

queue.length;    // => 3

const queue = new Queue() 是创建队列的实例。

queue.enqueue(7) 方法将 7 存入队列中。

queue.dequeue() 从队列中取出一个头部项目,而 queue.peek() 只读队首项。

最后的 Queue.Length 显示队列中还有多少个项目。

关于实现:在 Queue 类中,普通对象 this.Items 将队列的项目通过数值索引保持。 队首项的索引由 Where.HeadInex 跟踪,队尾项由 this.tailIndex 跟踪。

队列方法的复杂度

Queuequeue()dequeue()peek()length() 方法中存在:

  • 属性访问器(如:this.items[this.headIndex]),
  • 执行算数操作(如:this.headidex++

这些方法的时间复杂度是恒定的时间 O(1)

4. 总结

队列是一种遵循先入先出(FIFO)规则的的数据结构。

队列有 2 个主要操作:入队和出队。 另外,队列可以有辅助操作,例如 peek 和 length。

所有队列操作都必须以常数时间 O(1) 执行。

挑战一下:改进 dequeue()peek() 方法,当在空队列上执行时会抛出错误。

173382ede7319973.gif


本文首发微信公众号:前端先锋

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章


欢迎继续阅读本专栏其它高赞文章:


查看原文

赞 7 收藏 5 评论 0

疯狂的技术宅 收藏了文章 · 4月6日

一个能够确定 this 值的算法

每个 JavaScript 程序猿,包括我自己,都一直在努力了解 this 关键字在代码中的真正身份。

我设计了一个通用算法,可以帮你在任何情况下确定 this 关键字的值。虽然我尽可能的使算法容易看懂,但还是建议你多看几遍并理解相关术语。

另外还用了几个例子展示怎样用这个算法一步一步的对 this 进行评估,最后你自己亲自试一试。

1. this 算法

把算法定义为 ThisValueOfFunction(func, invocationType) ,返回值为在以 invocationtype 方式调用函数 func 时的 this 值:

ThisValueOfFunction(func, invocationType):

  1. 如果 func 是一个箭头函数,那么

    1. 如果 func 是在最外面的作用域 中定义的,那么返回 globalObject
    2. 否则

      1. SuffeFuncFunc外部函数
      2. 返回 ThisValueOfFunction(outerFunc, outerInvocationType)
  2. 如果 funcoriginFunc 函数的绑定函数,那么

    1. thisArgFunc = OriginFunc.bind(thisarg) 的参数
    2. 返回 thisArg
  3. 如果 funcsomeclass 中的 constructor() 方法,那么

    1. instanceinstance = new SomeClass() 的实例
    2. 返回 instance
  4. 如果 func 是一个常规函数,那么

    1. 如果 invocationtype作为构造函数,那么

      1. newObject 是新构造的对象 newObject = new func()
      2. 返回 newObject
    2. 如果 invocationtype是间接调用的,那么

      1. thisArgfunc.call(thisArg)func.apply(thisArg) 的参数
      2. 返回 thisArg
    3. 如果 invocationtype方法,那么

      1. object 是在 object.func() 上调用 func 的对象
      2. 返回 object
    4. 如果 invocationtype常规的,那么

      1. 如果启用了严格的模式,那么返回 undefined
      2. 否则返回 globalObject

1.1 算法中使用的术语

这个算法使用了大量的 JavaScript 术语。如果你不熟悉某些东西,先看下面的解释。

  • 箭头函数

    箭头函数是使用粗箭头语法 => 定义的函数。 箭头函数示例:

    const sum = (number1, number2) => {
      return number1 + number2;
    }
  • 绑定函数

    绑定函数是通过在函数上调用方法 myFunc.bind(thisArg, arg1, ..., argN)创建的函数。 绑定函数的示例:

    function originalFunction() {
      // ...
    }
    
    const boundFunction = originalFunction.bind({ prop: 'Value' });
  • 常规函数

    常规函数是用 function 关键字或在对象上定义的简单 JavaScript 函数。 常规函数的示例:

    function regularFunction(who) {
      return `Hello, ${who}!`;
    }
    
    const object = {
      anotherRegularFunction(who) {
        return `Good bye, ${who}!`
      }
    };
  • constructor()

    constructor()class 内部的一种特殊方法,用于初始化类实例。

    class SomeClass() {
      constructor(prop) {
        this.prop = prop;
      }
    }
  • 最外部的作用域

    最外部的作用域是没有外部作用域的最顶级作用域。

    // 最外部的作用域
    let a = 1;
    
    function someFunction() {
      // someFunction() 的作用域
      // 这里不是最外部的作用域
      let b = 1;
    }
  • 外部函数
    外部函数在其作用域内包含另一个函数。

    // outerFunction() 是 myFunction() 的外部函数
    function outerFunction() {
      function myFunction() {
        //...
      }
    }
  • 全局对象

    全局对象是在全局作用域内始终存在的对象。 window 是浏览器环境中的全局对象,在 Node 环境中是 global

  • 调用
    函数的调用只是使用一些参数来调用该函数。

    function sum(number1, number2) {
      return number1 + number2;
    }
    sum(1, 3);           // 调用
    sum.call({}, 3, 4);  // 调用
    sum.apply({}, 5, 9); // 调用
    
    const obj = {
      method() {
        return 'Some method';
      }
    };
    obj.method(); // 调用
    
    class SomeClass {
      constructor(prop) {
        this.prop = prop;
      } 
    }
    const instance = new SomeClass('Value'); // 调用
  • 构造函数调用
    使用 new 关键字调用函数或类时,将发生构造函数调用。

    function MyCat(name) {
      this.name = name;
    }
    const fluffy = new MyCat('Fluffy'); // 构造函数调用
    
    class MyDog {
      constructor(name) {
        this.name = name;
      }
    }
    const rex = new MyDog('Rex'); // 构造函数调用
  • 间接调用
    使用 func.call(thisArg, ...)func.apply(thisArg, ...) 方法调用函数时,会发生间接调用。

    function sum(number1, number2) {
      return number1 + number2;
    }
    
    sum.call({}, 1, 2);  // 间接调用
    sum.apply({}, 3, 5); // 间接调用
  • 方法调用
    当在属性访问器表达式 object.method() 中调用函数时,将发生方法调用。

    const object = {
      greeting(who) {
        return `Hello, ${who}!`
      }
    };
    
    object.greeting('World');    // 方法调用
    object['greeting']('World'); // 方法调用
  • 常规调用
    只用函数参数变量调用 func(...) 时,会发生常规调用。

    function sum(number1, number2) {
      return number1 + number2;
    }
    
    sum(1, 4); // 常规调用
  • 严格模式
    严格模式是对运行 JavaScript 代码有特殊限制的一种特殊模式。 通过在脚本的开头或函数作用域的顶部添加 use strict 指令来启用严格模式。

2.例子

例 1

const myFunc = () => {
  console.log(this); // logs `window`};

myFunc();

ThisValueOfFunction(myFunc, “常规的”)

myfunc 是箭头函数:从而在算法中匹配情况 1。同时 myFunc 在最外面的作用域内定义,匹配情况 1.1

算法 1.1 中返回 globalObject 意思是 myFunc 中的 this 值为全局对象 window(在浏览器环境中)。

例 2

const object = {
  method() {
    console.log(this); // logs { method() {...} }  } 
};

object.method();

ThisValueOfFunction(object.method, “作为方法调用”)

method() 同时是 object 的属性,是常规函数。与算法的情况 4 匹配。

object.method() 是一种方法调用,因为是属性访问的,送一因此与 4.3 匹配。

然后,根据 4.3method() 方法中的 this 等于方法的拥有者 (object.method()) — object

例 3

function MyCat(name) {
  this.name = name;

  const getName = () => {
    console.log(this); // logs { name: 'Fluffy', getName() {...} }    return this.name;
  }

  this.getName = getName;
}

const fluffy = new MyCat('Fluffy');
fluffy.getName();

ThisValueOfFunction(getName, “作为方法调用”)

getName() 是一个箭头函数,所以符合算法的情况 1;因为 mycatgetName()的外部函数,然后与 1.2 匹配。

分支 1.2.2thisgetName() 箭头函数内部的值等于外部函数的值 MyCat

所以让我们在 MyCat 函数上运行算法 ThisValueOfFunction(MyCat, "做为构造函数")

ThisValueOfFunction(MyCat, “作为构造函数”)

MyCat 是常规函数,所以跳转到算法的分支 4

因为 MyCat 做为构造函数调用 new MyCat('Fluffy'),符合分支 4.1。最后根据 4.1.14.1.2thisMyCat 中等于构造的对象:fluffy

然后,返回箭头函数后符合 1.2.2,在 getname() 中的 this 等于 mycatthis,最终结果为 fluffy

3. 练习

要理解这个算法,最好自己亲自试试。下面是 3 个练习。

练习 1

const myRegularFunc = function() {
  console.log(this); // logs ???};

myRegularFunc();

如何确定 myRegularFunc() 中的 this 值?写出你的判断步骤。

练习 2

class MyCat {
  constructor(name) {
    this.name = name;
    console.log(this); // logs ???  }
}

const myCat = new MyCat('Lucy');

如何确定 new MyCat('Lucy') 中的 this 值?写出你的判断步骤。

练习3

const object = {
  name: 'Batman',

  getName() {
    const arrow = () => {
      console.log(this); // logs ???      return this.name;
    };

    return arrow();
  };
}

object.getName();

如何确定 arrow() 中的 this 值?写出你的判断步骤。

173382ede7319973.gif


本文首发微信公众号:前端先锋

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章


欢迎继续阅读本专栏其它高赞文章:


查看原文

疯狂的技术宅 收藏了文章 · 4月6日

ES2018 中 4 个有用的功能

ES2018 规范引入了四个新功能。这些功能包括异步迭代,rest/spread 属性,Promise.prototype.finally() 和正则表达式改进。本问将帮你了解这些 ES2018 功能的工作原理及使用方法。

异步迭代

异步迭代是讨论的比较少 ES2018 功能之一。虽然还有很多关于 ES2018 其他功能的讨论,但几乎没有关于异步迭代这方面的内容。通过异步迭代,我们可以得到异步的可迭代对象和迭代器。

这意味着你可以把 await 关键字与 for…of 循环放在一起使用。你可以用这些循环对可迭代对象进行迭代。可迭代对象的包括数组、map、set,NodeList,函数的 arguments 参数,TypedArray 等。

在 ES2018 之前,for...of 循环是同步的。如果你试着迭代涉及异步操作的可迭代对象并 await,则无法正常工作。循环本身会保持同步,基本上忽略 await ,并在其内部的异步操作可以完成之前完成迭代。

// 下面的代码在 ES2018 之前不起作用,因为循环保持同步。
// 创建一个异步函数:
async function processResponses(someIterable) {
  // 对可迭代对象进行迭代
  for (let item of someIterable) {
    // 通过异步操作处理项目,例如promise:
    await processItem(item)
  }
}

同时 for...of 循环也可以与异步代码一起使用。也就是说可以在遍历可迭代对象时执行一些异步操作。for...of 循环将会是异步的,让你能够等待异步操作完成。

需要记住的是在哪里使用 await 关键字。不需要把它放进循环体中,应该将其放在for...of关键字中 for 的后面。现在当你用 next() 方法获取异步迭代器的下个值时,将会得到一个 Promise。如果你想了解更多信息,可以在 GitHub 上去看看(https://github.com/tc39/propo...)。

// 创建一个异步函数:
async function processResponses(someIterable) {
  //遍历可迭代对象并等待异步操作的结果
  for await (let item of someIterable) {
    processItem(item)
  }
}

Rest/Spread 属性

restspread 并不是真正的新功能。两者都是在 ES6 中作为新的运算符引入的,它们很快就开始流行起来。可以说 JavaScript 程序员喜欢它们。唯一的问题是它们只能用在数组和参数上,不过 ES2018 把这两个功能引入了对象中。

restspread 运算符的语法都非常简单,由三个点(...)组成。这些点后面是要在其上使用 restspread 运算符的对象。接下来简单的讨论一下两者的工作原理。

对象的 rest 运算符

rest 运算符使你可以将对象的所有剩余对象属性属性提取到新对象上。要注意这些属性必须是可枚举的。如果你已经对某些属性使用了分解,那么 rest 运算符会只提取剩余的属性。

// Rest example:

const daysObj = {
  one: 'Monday',
  two: 'Tuesday',
  three: 'Wednesday',
  four: 'Thursday',
  five: 'Friday'
}

//使用解构将变量的前两个属性分配给变量。
//然后,使用rest将其余属性分配给第三个变量。
const { one, two, ...restOfDays } = daysObj
// rest 仅提取 "three", "four" 和 "five" 
// 因为我们已经提取了 "one" 和 "two" 

console.log(one)
// Output:
// 'Monday'

console.log(two)
// Output:
// 'Tuesday'

console.log(restOfDays)
// Output:
// { three: 'Wednesday', four: 'Thursday', five: 'Friday' }

如果要对对象使用 rest 运算符,需要记住两点:首先,只能用一次,除非把它用在嵌套对象上。其次,必须在最后使用。这就是为什么在上面的例子中,在解构前两个属性之后而不是之前看到它的原因。

// 这行代码不起作用,因为把 rest 运算符用在了最前面:
const { ...all, one, two } = { one: 1, two: 2, three: 3 }

//这行能起作用:
const { one, two, ...all } = { one: 1, two: 2, three: 3 }

// 这行不起作用,因为同一级别上有多个 rest 运算符:
const { one, ...some, ...end } = { /* some properties */ }

// 这行能起作用,在多个级别上的多个 rest 运算符:
const { one, {...secondLevel }, ...firstLevel } = { /* some properties */ }

对象的 spread 运算符

spread 运算符的作用是可以通过插入另一个对象的所有属性来创建新对象。 Spread 运算符还允许你从多个对象插入属性。也可以把这个运算符与添加新属性结合使用。

// Spread example:
const myOriginalObj = { name: 'Joe Doe', age: 33 }
// 用 spread 运算符创建新对象:
const myNewObj = { ...myOriginalObj }

console.log(myNewObj)
// Output:
// { name: 'Joe Doe', age: 33 }


// 添加属性的例子:
const myOriginalObj = { name: 'Caesar' }
// 用 spread 运算符创建新对象
// 并添加新的属性“genre”:
const myNewObj = { ...myOriginalObj, genre: 'Strategy' }

console.log(myNewObj)
// Output:
// {
//   name: 'Caesar',
//   genre: 'Strategy'
// }


// Spread 运算符并合并两个对象:
const myObjOne = { title: 'Eloquent JavaScript' }
const myObjTwo = { author: 'Marijn Haverbeke' }

const myNewObj = { ...myObjOne, ...myObjTwo }

console.log(myNewObj)
// Output:
// {
//   title: 'Eloquent JavaScript',
//   author: 'Marijn Haverbeke'
// }

当从多个对象插入属性并添加新属性时,顺序很重要

我来解释一下,假设你要用 spread 运算符基于两个现有对象创建一个新对象。第一个已有对象中包含具有某些值的属性 title。第二个对象也包含属性 title,但是值不一样。最终到底取哪个 title

答案是最后一个。如果对第一个对象使用 spread 运算符,然后再对第二个对象使用,则第二个 title 会生效。如果你将 spread 运算符永在第二个对象上,则第一个 title 会生效。

// Spread 运算符并合并两个对象:
const myObjOne = {
  title: 'Eloquent JavaScript',
  author: 'Marijn Haverbeke',
}

const myObjTwo = {
  title: 'You Don\'t Know JS Yet',
  language: 'English'
}

// 用 spread 运算符通过组合 “myObjOne” 和 “myObjTwo” 创建新对象
// 注意:“myObjTwo” 中的 “title” 会将覆盖 “myObjTwo” 的 “title”
// 因为“ myObjTwo”排在最后。
const myNewObj = { ...myObjOne, ...myObjTwo }

console.log(myNewObj)
// Output:
// {
//   title: "You Don't Know JS Yet",
//   author: 'Marijn Haverbeke',
//   language: 'English'
// }


// 注意:“myObjOne” 中的 “title” 将覆盖 “myObjTwo” 的 “title”
const myNewObj = { ...myObjTwo, ...myObjOne }

console.log(myNewObj)
// Output:
// {
//   title: 'Eloquent JavaScript',
//   language: 'English',
//   author: 'Marijn Haverbeke'
// }

Promise.prototype.finally()

一开始有两个用于 Promise 的回调函数。其中一个是 then(),在实现诺 Promise 执行。第二个是catch(),在 promise 被拒绝或 then() 抛出异常时执行。 ES2018 增加了用于 Promise 的第三个回调函数 finally()

每次完成 promise 时,都会执行 finally() 回调,不管 promise 是否完成。这个回调的一般用于执行应始终发生的操作。例如关闭模态对话框、关闭数据库连接或进行某些清理。

// finally() example:
fetch()
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.log(error))
  //最后做点什么:
  .finally(() => console.log('Operation done.'))

对正则表达式的改进

ES2018 还对正则表达式功能进行了的一些改进。这些改进包括 s(dotAll) 标志,后行断言,命名捕获组和 unicode 属性转义。

s(dotAll)

首先是 s(dotAll) 。与点(.)不同,s(dotAll) 允许对换行符及表情符号进行匹配。

// s(dotAll) example:
/hello.world/.test('hello\nworld')
// Output:
// false

/hello.world/s.test('hello\nworld')
// Output:
// true

后行断言

在ES2018之前,JavaScript仅支持先行断言。先行断言用于基于其后的文本来匹配模式。在 ES2018 中增加了对后行断言的支持。通过它可以基于模式之前的文本模式来进行匹配。后行断言的语法为 ?<=

// 后行断言例子:
/(?<=green) apple/.test('One red apple is on the table.')
// Output:
// false

/(?<=green) apple/.test('One green apple is on the table.')
// Output:
// true

断言后面也有一个反向的回溯。仅当子字符串之前没有断言时,此断言才与模式匹配。对后行断言取反操作的语法是 ?<!

/(?<!green) apple/.test('One red apple is on the table.')
// Output:
// true

/(?<!green) apple/.test('One green apple is on the table.')
// Output:
// false

命名捕获组

另一个被 ES2018 引入到正则表达式的好功能是命名捕获组。命名捕获组的语法为 ?<some_name>

const date_pattern = /(?<day>\d{2})\/(?<month>\d{2})\/(?<year>\d{4})/
const result = date_pattern.exec('11/12/2021')

console.log(result)
// Output:
// [
//   '11/12/2021',
//   '11',
//   '12',
//   '2021',
//   index: 0,
//   input: '11/12/2021',
//   groups: [Object: null prototype] { day: '11', month: '12', year: '2021' }
// ]

console.log(result.groups.day)
// Output:
// '11'

console.log(result.groups.month)
// Output:
// '12'

console.log(result.groups.year)
// Output:
// '2021'

Unicode 属性转义

每个 unicode 字符都有许多属性。例如:空白字符,大小写,字母,ASCII,表情符号等。现在你可以在正则表达式中访问这些属性了。

要使用这个功能需要做两件事。首先必须使用 /u 标志。这个标志告诉 JavaScript 你的字符串是一系列 Unicode 代码点。第二是使用 \p{}。你要检查的属性位于大括号之间,反之则用 \P{}

// 用俄语创建一个字符串(西里尔字母):
const myStrCyr = 'Доброе утро'

//创建英文字符串(拉丁字母):
const myStrLat = 'Good morning'

//测试“ myStrCyr”是否包含西里尔字符:
/\p{Script=Cyrillic}/u.test(myStrCyr) // true

//测试“ myStrLat”是否包含西里尔字符:
/\p{Script=Cyrillic}/u.test(myStrLat) // false

// 测试“myStrLat” 是否包含西里尔字符:
/\p{Script=Latin}/u.test(myStrCyr) // false

// 测试“myStrLat” 是否包含拉丁语字符:
/\p{Script=Latin}/u.test(myStrLat) // true

173382ede7319973.gif


本文首发微信公众号:前端先锋

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章


欢迎继续阅读本专栏其它高赞文章:


查看原文

疯狂的技术宅 收藏了文章 · 4月6日

10分钟用Hugo打造一个静态网站

什么是静态网站生成器?

简简单的说,静态网站生成器会获取你的内容,并将其应用于模板,然后生成基于 HTML 的静态网站。非常适合个人博客。

好处:

  • 快速部署
  • 安全(无动态内容)
  • 快速迅速
  • 使用简单
  • 能够进行版本控制

那么,都有哪些流行的静态网站生成器呢?

  • Gatsby (React/JS)
  • Hugo (Go)
  • Next.js (React/JS)
  • Jekyll (Ruby)
  • Gridsome (Vue/JS)

这些项目在 GitHub 上的知名度非常高。

Hugo 是什么?

其官方网站号称 Hugo 是世界上最快的静态网站引擎。

image.png

Hugo 是用 Go 语言编写的,它还有非常丰富的主题系统。

安装 Hugo

Mac:

brew install hugo

Linux:

sudo apt-get install hugo
或者
sudo pacman -Syu hugo

然后执行下面的命令检查是否安装成功:

hugo version

使用Hugo

创建一个新项目:

hugo new site my-project

下载一个主题。可以在 https://themes.gohugo.io/ 找到更多你喜欢的主题。

cd my-project
git init
git submodule add https://github.com/budparr/gohugo-theme-ananke.git themes/ananke

将主题添加到配置文件。

echo 'theme = "ananke"' >> config.toml

添加一篇文章。

hugo new posts/my-first-post.md

它看上去应该像这样:

---
title: "My First Post"
date: 2021-03-10T18:37:11+08:00
draft: true
---

Hello World!

可以在这里给文章添加添加更多属性配置(标签,描述,类别,作者)。

可以在 https://gohugo.io/content-man... 了解更多的配置项。

看看效果:

hugo server -D

在浏览器中打开 http://localhost:1313 就能看到你的网站了。

Hugo 的目录结构

.
├── archetypes
├── assets (not created by default)
├── config.toml
├── content
├── data
├── layouts
├── static
└── themes
  • archetypes:Archetypes 是内容模板文件,其中包含预配置的首选项(日期、标题、草稿等)。可以用自定义的预配置前端字段创建新的原型。
  • assets:Assets 文件夹存储所有文件,这些文件由 Hugo Pipes 处理(例如 CSS/Sass 文件)。默认不创建这个目录。
  • config.toml:Hugo 使用 config.tomlconfig.yamlconfig.json(可以在网站根目录中找到)作为默认网站配置文件。除了单独的配置文件之外,你还可以使用 config directory 来分隔不同的环境。
  • content:所有内容文件放在这里。顶级文件夹计为内容部分。如果你有 devopsnodejs 部分,那么你需要有 content/devops/first-post.mdcontent/nodejs/second-post.md 目录。
  • data:这里用来存储配置文件,Hugo 会在生成你网站时用到。
  • layouts:以 .html 文件的形式存储模板。有关更多信息,请参见 Styling 部分。
  • static:存储所有静态内容:图片、CSS、JavaScript 等。当 Hugo 创建你的网站时,static 目录中的所有资源均按原样复制。
  • themes:你所选择的 Hugo 主题。

修改静态网站的样式

我们在之前应用了一个主题。现在,如果我们检查 themes 文件夹,可以看到样式文件。

但是要当心!

千万不要直接编辑这些文件!

应该将主题目录结构复制到 layouts 文件夹。

假设我要将自定义 CSS 应用于主题。

主题有一个 themes/theme-name/layouts/partials 文件夹,可以在其中找到一些HTML模板(header.htmlfooter.html)。现在我们将编辑 header.html 模板,将内容从这个文件复制到 layouts/partials/header.html 中,并注意在主题 layouts 根目录中创建与主题相同的目录结构。

layouts/partials/header.htmlss
themes/theme-name/layouts/partials/header.html

创建一个自定义CSS文件: static/css/custom-style.css,然后把自定义 CSS 文件添加到 config.toml 中:

[params]
 custom_css = ["css/custom-style.css"]

打开 layouts/partials/header.html

将这段代码添加到 <head> 标签内:

{{ range .Site.Params.custom_css -}}
   <link rel="stylesheet" href="{{ . | absURL }}">
{{- end }}

现在,就可以覆盖主题中所应用的 CSS 类。

构建静态网站

在项目的根目录下执行 hugo 命令:

>>> hugo
                   | EN  
-------------------+-----
  Pages            | 14  
  Paginator pages  |  0  
  Non-page files   |  0  
  Static files     |  1  
  Processed images |  0  
  Aliases          |  6  
  Sitemaps         |  1  
  Cleaned          |  0  

Total in 74 ms

执行成功后,会生成一个public 目录,这个目录中的内容就是我们静态网站的所有内容。

然后就可以托管到 GitHub 或 OSS 中了。

Hugo 还提供了更多的内容,可以到官方文档查看:https://gohugo.io/documentation/

173382ede7319973.gif


本文首发微信公众号:前端先锋

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章


欢迎继续阅读本专栏其它高赞文章:


查看原文

疯狂的技术宅 发布了文章 · 4月5日

ES2018 中 4 个有用的功能

ES2018 规范引入了四个新功能。这些功能包括异步迭代,rest/spread 属性,Promise.prototype.finally() 和正则表达式改进。本问将帮你了解这些 ES2018 功能的工作原理及使用方法。

异步迭代

异步迭代是讨论的比较少 ES2018 功能之一。虽然还有很多关于 ES2018 其他功能的讨论,但几乎没有关于异步迭代这方面的内容。通过异步迭代,我们可以得到异步的可迭代对象和迭代器。

这意味着你可以把 await 关键字与 for…of 循环放在一起使用。你可以用这些循环对可迭代对象进行迭代。可迭代对象的包括数组、map、set,NodeList,函数的 arguments 参数,TypedArray 等。

在 ES2018 之前,for...of 循环是同步的。如果你试着迭代涉及异步操作的可迭代对象并 await,则无法正常工作。循环本身会保持同步,基本上忽略 await ,并在其内部的异步操作可以完成之前完成迭代。

// 下面的代码在 ES2018 之前不起作用,因为循环保持同步。
// 创建一个异步函数:
async function processResponses(someIterable) {
  // 对可迭代对象进行迭代
  for (let item of someIterable) {
    // 通过异步操作处理项目,例如promise:
    await processItem(item)
  }
}

同时 for...of 循环也可以与异步代码一起使用。也就是说可以在遍历可迭代对象时执行一些异步操作。for...of 循环将会是异步的,让你能够等待异步操作完成。

需要记住的是在哪里使用 await 关键字。不需要把它放进循环体中,应该将其放在for...of关键字中 for 的后面。现在当你用 next() 方法获取异步迭代器的下个值时,将会得到一个 Promise。如果你想了解更多信息,可以在 GitHub 上去看看(https://github.com/tc39/propo...)。

// 创建一个异步函数:
async function processResponses(someIterable) {
  //遍历可迭代对象并等待异步操作的结果
  for await (let item of someIterable) {
    processItem(item)
  }
}

Rest/Spread 属性

restspread 并不是真正的新功能。两者都是在 ES6 中作为新的运算符引入的,它们很快就开始流行起来。可以说 JavaScript 程序员喜欢它们。唯一的问题是它们只能用在数组和参数上,不过 ES2018 把这两个功能引入了对象中。

restspread 运算符的语法都非常简单,由三个点(...)组成。这些点后面是要在其上使用 restspread 运算符的对象。接下来简单的讨论一下两者的工作原理。

对象的 rest 运算符

rest 运算符使你可以将对象的所有剩余对象属性属性提取到新对象上。要注意这些属性必须是可枚举的。如果你已经对某些属性使用了分解,那么 rest 运算符会只提取剩余的属性。

// Rest example:

const daysObj = {
  one: 'Monday',
  two: 'Tuesday',
  three: 'Wednesday',
  four: 'Thursday',
  five: 'Friday'
}

//使用解构将变量的前两个属性分配给变量。
//然后,使用rest将其余属性分配给第三个变量。
const { one, two, ...restOfDays } = daysObj
// rest 仅提取 "three", "four" 和 "five" 
// 因为我们已经提取了 "one" 和 "two" 

console.log(one)
// Output:
// 'Monday'

console.log(two)
// Output:
// 'Tuesday'

console.log(restOfDays)
// Output:
// { three: 'Wednesday', four: 'Thursday', five: 'Friday' }

如果要对对象使用 rest 运算符,需要记住两点:首先,只能用一次,除非把它用在嵌套对象上。其次,必须在最后使用。这就是为什么在上面的例子中,在解构前两个属性之后而不是之前看到它的原因。

// 这行代码不起作用,因为把 rest 运算符用在了最前面:
const { ...all, one, two } = { one: 1, two: 2, three: 3 }

//这行能起作用:
const { one, two, ...all } = { one: 1, two: 2, three: 3 }

// 这行不起作用,因为同一级别上有多个 rest 运算符:
const { one, ...some, ...end } = { /* some properties */ }

// 这行能起作用,在多个级别上的多个 rest 运算符:
const { one, {...secondLevel }, ...firstLevel } = { /* some properties */ }

对象的 spread 运算符

spread 运算符的作用是可以通过插入另一个对象的所有属性来创建新对象。 Spread 运算符还允许你从多个对象插入属性。也可以把这个运算符与添加新属性结合使用。

// Spread example:
const myOriginalObj = { name: 'Joe Doe', age: 33 }
// 用 spread 运算符创建新对象:
const myNewObj = { ...myOriginalObj }

console.log(myNewObj)
// Output:
// { name: 'Joe Doe', age: 33 }


// 添加属性的例子:
const myOriginalObj = { name: 'Caesar' }
// 用 spread 运算符创建新对象
// 并添加新的属性“genre”:
const myNewObj = { ...myOriginalObj, genre: 'Strategy' }

console.log(myNewObj)
// Output:
// {
//   name: 'Caesar',
//   genre: 'Strategy'
// }


// Spread 运算符并合并两个对象:
const myObjOne = { title: 'Eloquent JavaScript' }
const myObjTwo = { author: 'Marijn Haverbeke' }

const myNewObj = { ...myObjOne, ...myObjTwo }

console.log(myNewObj)
// Output:
// {
//   title: 'Eloquent JavaScript',
//   author: 'Marijn Haverbeke'
// }

当从多个对象插入属性并添加新属性时,顺序很重要

我来解释一下,假设你要用 spread 运算符基于两个现有对象创建一个新对象。第一个已有对象中包含具有某些值的属性 title。第二个对象也包含属性 title,但是值不一样。最终到底取哪个 title

答案是最后一个。如果对第一个对象使用 spread 运算符,然后再对第二个对象使用,则第二个 title 会生效。如果你将 spread 运算符永在第二个对象上,则第一个 title 会生效。

// Spread 运算符并合并两个对象:
const myObjOne = {
  title: 'Eloquent JavaScript',
  author: 'Marijn Haverbeke',
}

const myObjTwo = {
  title: 'You Don\'t Know JS Yet',
  language: 'English'
}

// 用 spread 运算符通过组合 “myObjOne” 和 “myObjTwo” 创建新对象
// 注意:“myObjTwo” 中的 “title” 会将覆盖 “myObjTwo” 的 “title”
// 因为“ myObjTwo”排在最后。
const myNewObj = { ...myObjOne, ...myObjTwo }

console.log(myNewObj)
// Output:
// {
//   title: "You Don't Know JS Yet",
//   author: 'Marijn Haverbeke',
//   language: 'English'
// }


// 注意:“myObjOne” 中的 “title” 将覆盖 “myObjTwo” 的 “title”
const myNewObj = { ...myObjTwo, ...myObjOne }

console.log(myNewObj)
// Output:
// {
//   title: 'Eloquent JavaScript',
//   language: 'English',
//   author: 'Marijn Haverbeke'
// }

Promise.prototype.finally()

一开始有两个用于 Promise 的回调函数。其中一个是 then(),在实现诺 Promise 执行。第二个是catch(),在 promise 被拒绝或 then() 抛出异常时执行。 ES2018 增加了用于 Promise 的第三个回调函数 finally()

每次完成 promise 时,都会执行 finally() 回调,不管 promise 是否完成。这个回调的一般用于执行应始终发生的操作。例如关闭模态对话框、关闭数据库连接或进行某些清理。

// finally() example:
fetch()
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.log(error))
  //最后做点什么:
  .finally(() => console.log('Operation done.'))

对正则表达式的改进

ES2018 还对正则表达式功能进行了的一些改进。这些改进包括 s(dotAll) 标志,后行断言,命名捕获组和 unicode 属性转义。

s(dotAll)

首先是 s(dotAll) 。与点(.)不同,s(dotAll) 允许对换行符及表情符号进行匹配。

// s(dotAll) example:
/hello.world/.test('hello\nworld')
// Output:
// false

/hello.world/s.test('hello\nworld')
// Output:
// true

后行断言

在ES2018之前,JavaScript仅支持先行断言。先行断言用于基于其后的文本来匹配模式。在 ES2018 中增加了对后行断言的支持。通过它可以基于模式之前的文本模式来进行匹配。后行断言的语法为 ?<=

// 后行断言例子:
/(?<=green) apple/.test('One red apple is on the table.')
// Output:
// false

/(?<=green) apple/.test('One green apple is on the table.')
// Output:
// true

断言后面也有一个反向的回溯。仅当子字符串之前没有断言时,此断言才与模式匹配。对后行断言取反操作的语法是 ?<!

/(?<!green) apple/.test('One red apple is on the table.')
// Output:
// true

/(?<!green) apple/.test('One green apple is on the table.')
// Output:
// false

命名捕获组

另一个被 ES2018 引入到正则表达式的好功能是命名捕获组。命名捕获组的语法为 ?<some_name>

const date_pattern = /(?<day>\d{2})\/(?<month>\d{2})\/(?<year>\d{4})/
const result = date_pattern.exec('11/12/2021')

console.log(result)
// Output:
// [
//   '11/12/2021',
//   '11',
//   '12',
//   '2021',
//   index: 0,
//   input: '11/12/2021',
//   groups: [Object: null prototype] { day: '11', month: '12', year: '2021' }
// ]

console.log(result.groups.day)
// Output:
// '11'

console.log(result.groups.month)
// Output:
// '12'

console.log(result.groups.year)
// Output:
// '2021'

Unicode 属性转义

每个 unicode 字符都有许多属性。例如:空白字符,大小写,字母,ASCII,表情符号等。现在你可以在正则表达式中访问这些属性了。

要使用这个功能需要做两件事。首先必须使用 /u 标志。这个标志告诉 JavaScript 你的字符串是一系列 Unicode 代码点。第二是使用 \p{}。你要检查的属性位于大括号之间,反之则用 \P{}

// 用俄语创建一个字符串(西里尔字母):
const myStrCyr = 'Доброе утро'

//创建英文字符串(拉丁字母):
const myStrLat = 'Good morning'

//测试“ myStrCyr”是否包含西里尔字符:
/\p{Script=Cyrillic}/u.test(myStrCyr) // true

//测试“ myStrLat”是否包含西里尔字符:
/\p{Script=Cyrillic}/u.test(myStrLat) // false

// 测试“myStrLat” 是否包含西里尔字符:
/\p{Script=Latin}/u.test(myStrCyr) // false

// 测试“myStrLat” 是否包含拉丁语字符:
/\p{Script=Latin}/u.test(myStrLat) // true

173382ede7319973.gif


本文首发微信公众号:前端先锋

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章


欢迎继续阅读本专栏其它高赞文章:


查看原文

赞 5 收藏 5 评论 0

疯狂的技术宅 发布了文章 · 4月2日

10分钟用Hugo打造一个静态网站

什么是静态网站生成器?

简简单的说,静态网站生成器会获取你的内容,并将其应用于模板,然后生成基于 HTML 的静态网站。非常适合个人博客。

好处:

  • 快速部署
  • 安全(无动态内容)
  • 快速迅速
  • 使用简单
  • 能够进行版本控制

那么,都有哪些流行的静态网站生成器呢?

  • Gatsby (React/JS)
  • Hugo (Go)
  • Next.js (React/JS)
  • Jekyll (Ruby)
  • Gridsome (Vue/JS)

这些项目在 GitHub 上的知名度非常高。

Hugo 是什么?

其官方网站号称 Hugo 是世界上最快的静态网站引擎。

image.png

Hugo 是用 Go 语言编写的,它还有非常丰富的主题系统。

安装 Hugo

Mac:

brew install hugo

Linux:

sudo apt-get install hugo
或者
sudo pacman -Syu hugo

然后执行下面的命令检查是否安装成功:

hugo version

使用Hugo

创建一个新项目:

hugo new site my-project

下载一个主题。可以在 https://themes.gohugo.io/ 找到更多你喜欢的主题。

cd my-project
git init
git submodule add https://github.com/budparr/gohugo-theme-ananke.git themes/ananke

将主题添加到配置文件。

echo 'theme = "ananke"' >> config.toml

添加一篇文章。

hugo new posts/my-first-post.md

它看上去应该像这样:

---
title: "My First Post"
date: 2021-03-10T18:37:11+08:00
draft: true
---

Hello World!

可以在这里给文章添加添加更多属性配置(标签,描述,类别,作者)。

可以在 https://gohugo.io/content-man... 了解更多的配置项。

看看效果:

hugo server -D

在浏览器中打开 http://localhost:1313 就能看到你的网站了。

Hugo 的目录结构

.
├── archetypes
├── assets (not created by default)
├── config.toml
├── content
├── data
├── layouts
├── static
└── themes
  • archetypes:Archetypes 是内容模板文件,其中包含预配置的首选项(日期、标题、草稿等)。可以用自定义的预配置前端字段创建新的原型。
  • assets:Assets 文件夹存储所有文件,这些文件由 Hugo Pipes 处理(例如 CSS/Sass 文件)。默认不创建这个目录。
  • config.toml:Hugo 使用 config.tomlconfig.yamlconfig.json(可以在网站根目录中找到)作为默认网站配置文件。除了单独的配置文件之外,你还可以使用 config directory 来分隔不同的环境。
  • content:所有内容文件放在这里。顶级文件夹计为内容部分。如果你有 devopsnodejs 部分,那么你需要有 content/devops/first-post.mdcontent/nodejs/second-post.md 目录。
  • data:这里用来存储配置文件,Hugo 会在生成你网站时用到。
  • layouts:以 .html 文件的形式存储模板。有关更多信息,请参见 Styling 部分。
  • static:存储所有静态内容:图片、CSS、JavaScript 等。当 Hugo 创建你的网站时,static 目录中的所有资源均按原样复制。
  • themes:你所选择的 Hugo 主题。

修改静态网站的样式

我们在之前应用了一个主题。现在,如果我们检查 themes 文件夹,可以看到样式文件。

但是要当心!

千万不要直接编辑这些文件!

应该将主题目录结构复制到 layouts 文件夹。

假设我要将自定义 CSS 应用于主题。

主题有一个 themes/theme-name/layouts/partials 文件夹,可以在其中找到一些HTML模板(header.htmlfooter.html)。现在我们将编辑 header.html 模板,将内容从这个文件复制到 layouts/partials/header.html 中,并注意在主题 layouts 根目录中创建与主题相同的目录结构。

layouts/partials/header.htmlss
themes/theme-name/layouts/partials/header.html

创建一个自定义CSS文件: static/css/custom-style.css,然后把自定义 CSS 文件添加到 config.toml 中:

[params]
 custom_css = ["css/custom-style.css"]

打开 layouts/partials/header.html

将这段代码添加到 <head> 标签内:

{{ range .Site.Params.custom_css -}}
   <link rel="stylesheet" href="{{ . | absURL }}">
{{- end }}

现在,就可以覆盖主题中所应用的 CSS 类。

构建静态网站

在项目的根目录下执行 hugo 命令:

>>> hugo
                   | EN  
-------------------+-----
  Pages            | 14  
  Paginator pages  |  0  
  Non-page files   |  0  
  Static files     |  1  
  Processed images |  0  
  Aliases          |  6  
  Sitemaps         |  1  
  Cleaned          |  0  

Total in 74 ms

执行成功后,会生成一个public 目录,这个目录中的内容就是我们静态网站的所有内容。

然后就可以托管到 GitHub 或 OSS 中了。

Hugo 还提供了更多的内容,可以到官方文档查看:https://gohugo.io/documentation/

173382ede7319973.gif


本文首发微信公众号:前端先锋

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章


欢迎继续阅读本专栏其它高赞文章:


查看原文

赞 11 收藏 10 评论 0

疯狂的技术宅 收藏了文章 · 4月1日

对网站进行测试的 9 个要点

网站或网页的效率在很大程度上取决于测试,并且涉及对网站或相关软件的细致检查。那么我们如何该测试 Web 程序呢?在探究 Web 程序测试服务的更多细节之前,先讨论一下测试为何如此重要。

网站测试或 Web 应用测试应该是每个团队都要关注的问题。随着网站复杂性的日益增加,我们更要关注这个问题,这样才能确保在各种设备、浏览器和操作系统平台上得到最好的用户体验和性能。

以下是测试网站的十个要点,它们可以帮你用更短的时间得出更加准确可信的测试结果。

1. 将探索性测试与传统技术结合

用于网站测试最有效的技术之一是探索性测试。探索性测试有助于减少测试时间并发现其他缺陷。在探索性测试中,测试人员必须发挥创造力编写并运行测试用例。

更重要的是,可以通过将探索性测试与各种黑帽和白帽软件测试技术集成,从而解决探索性测试的缺点。这样做的另一个好处是,能够以更少的时间来生成有着更高的可靠性的测试结果。

2. 在测试中包括第三方应用、扩展和插件

每个 Web 应用都使用了很多第三方扩展、应用和插件。这些资源对于提高 Web 应用的性能和功能至关重要。但是每个插件、应用或扩展的质量差异可能会很大。

所以必须验证插件和扩展与 Web 应用的兼容性。另外还应该确定插件或扩展中的漏洞,这些漏洞可能会对网站的性能产生负面影响。这种测试对实时聊天、RSS、社交媒体、搜索或嵌入式视频功能的影响非常重要。

3. 跨浏览器兼容性测试

网站应该是针对移动设备友好的,因为通常用户喜欢在他们的移动设备上访问。许多公司通过响应式 Web 设计来确保他们的网站在有特定代码库的计算机以及移动设备上能够高效的运行。

开发人员应该使用开放技术(例如CSS3、JavaScript 和。HTML5)创建自适应网站。对于测试而言,进行跨浏览器兼容性测试至关重要。

4. 与开发团队沟通合作

许多公司选择 DevOps 来统一软件开发和测试。所以 QA 不仅应该在整个软件开发生命周期中测试 Web 应用,还要确保与开发人员、客户和需求分析师的协作。

5. 永远不要低估完整性测试

许多测试人员认为完整性测试是回归测试的辅助元素,从而破坏了完整性测试的重要性。但是,健全性测试有助于在有限的时间内评估 Web 应用中的功能。另外完整性测试还可以在代码更改或错误修复后帮助检查 Web 应用的功能。在测试时保持理智无疑是减少测试时间的可靠工具。

6. 站在黑客的角度思考

网站测试还必须侧重于最终用户的仿真,以此获得对用户体验的精确估计。但是,测试人​​员无法通过模拟最终用户来准确地评估网站的安全性。相反,通过模仿网络犯罪分子和黑客的行为来取得更好的测试结果非常重要。

7. 选择可用性测试的理想参数

在网站测试的众多方面中,可用性测试始终占据头把交椅。在进行测试时,应该始终评估网站的用户界面和用户体验。可用性测试中一些值得注意的参数是UI设计、内容可读性、速度、可访问性和可导航性。

8.确保 URL 字符串不被篡改

网站的安全功能无疑是测试的关键之一。应该评估 Web 应用在维护用户数据和企业数据安全性方面的效率。但是黑客可以通过篡改 URL 将用户重定向到恶意网站或访问敏感数据。某些恶意网站可以在用户的​​系统上安装恶意软件,或迫使它们共享敏感数据。

9. 进行持续的负载测试

压力测试在网站测试中是一个令人望而生畏的领域,它有助于评估网站在正常压力和峰值压力条件下的性能。可以利用复杂而全面的自动化测试工具来加速负载测试。

不过重要的是要通过不断的执行压力测试,来验证不同负载条件下 Web 应用的性能。应该通过逐渐增加压力的方式来确保增量执行负载测试。用户负载的缓慢而逐步的增加可以帮助我们找到由于压力而崩溃的确切位置。

查看原文

疯狂的技术宅 收藏了文章 · 4月1日

Sass / SCSS简明入门

Sass / SCSS简介

Sass 是 CSS3 的扩展,添加了嵌套规则、变量、mixins、选择器继承等。可以用命令行工具或网络框架插件将其转换为格式良好的标准 CSS。

Sass(Syntactically Awesome StyleSheets) 是 CSS 的一种扩展,是 CSS的 超集(通过编译生成浏览器可以处理传统 CSS)。Sass 的出现是为了解决在大型项目中传统 CSS 会遇到的重复、可维护性差等问题(添加了嵌套的规则、变量、mixins、选择器继承等特性)。让开发者可以编写简洁、富语意(expressive )、可复用、可维护性和可延展性性佳的 CSS 代码。

Sass 的语法分为新的 SCSS(Sassy CSSSass 3,文件名称 *.scss)和旧的 SASS( Haml 风格,由于不用大括号格式,使用了缩紧,不能直接用 CSS 语法,学习曲线较高等特性,文件名称为*.sass)。由于新的 SCSS 语法是 CSS3 的超集,所以把传统的 CSS3 直接复制过来也不会出错,学习曲线相对比较平缓,所以我们将使用SCSS语法。

SASS 初体验

在开始介绍 SASS 特性之前先来学习如何将 Sass 转译成 CSS。

首先,先按照官网先安装Sass,然后在项目文件夹建立一个 main.scss 文件,文件内容如下:

// 引用
@import url(https://fonts.googleapis.com/css?family=Pacifico);
//Add variables here:

h1 {
  font-family: Roboto, sans-serif;
  text-align: center;
}

.banner {
  font-family: 'Pacifico', cursive;
  height: 400px;
  background-image: url("lemonade.jpg");
}

.container {
  text-align: center;
  font-family: 'Pacifico', cursive;
}

在终端下用以下命令进行转译:

sass main.scss main.css

这时你就会看到文件夹中多了 main.cssmain.css.map 两个文件,前者是转译过后的 CSS 文件,大部分是方便使用浏览器调试工具在进行调试时连结原文件和转译文件1⃣️方便调试。

1. 变量:变量可以用来储存值,方便重复利用

在 Sass 中用 $ 来表示变量,变量的数据型态可以是数字、字符串、布尔值、null值、甚至可以使用 List 和 Map。

变量的使用:

$translucent-white: rgba(255,255,255,0.3);
p {
    background-color: $translucent-white;
}

List 可以用空格或逗号分隔属性值:

$font-style-2: Helvetica, Arial, sans-serif;
$standard-border: 4px solid black;

p {
    border: $standard-border;
}

// maps key:value
$font-style-2: (key1: value1, key2: value2);


2. 嵌套:降低父元素重复性

转译前:

.parent {
    color: blue;
    .child {
        font-size: 12px;
    }
}



轉譯後:

转译后:

.parent {
    color: blue;
}

.parent .child {
    font-size: 12px;
}
```



在 Nesting 中不僅只有 child selectors 可以使用,還可以使用在相同的 Properties 上:

在Nesting中且只有子选择器可以使用,还可以使用在相同的

.parent {
font : {
    family: Roboto, sans-serif;
    size: 12px;
    decoration: none;
  }
}

转译后:

.parent {
    font-family: Roboto, sans-serif;
    font-size: 12px;
    font-decoration: none;
}

3. Mixins:减少编写伪元素时的重复

如:::before:: after::hover,在 Sass 中使用 代表父元素

转译前:

.notecard{ 
    &:hover{
        @include transform (rotatey(-180deg));  
    }
}

转译后:

.notecard:hover {
    transform: rotatey(-180deg);
}

重用群组的 CSS,例如跨浏览器的前缀,使用 @include 加入群组:

转译前:

@mixin backface-visibility {
    backface-visibility: hidden;
    -webkit-backface-visibility: hidden;
    -moz-backface-visibility: hidden;
    -ms-backface-visibility: hidden;
    -o-backface-visibility: hidden;
}
.notecard {
    .front, .back {
        width: 100%;
        height: 100%;
        position: absolute;
        @include backface_visibility;
    }
}

转译后:

.notecard .front, .notecard .back {
    width: 100%;
    height: 100%;
    position: absolute;

    backface-visibility: hidden;
    -webkit-backface-visibility: hidden; 
    -moz-backface-visibility: hidden;
    -ms-backface-visibility: hidden;
    -o-backface-visibility: hidden;
}

@mixin 也可以通过 @include 使用参数,也可以使用默认值:

@mixin backface-visibility($visibility:hidden) { //Add an argument
    backface-visibility: $visibility;
    -webkit-backface-visibility: $visibility;
    -moz-backface-visibility: $visibility;
    -ms-backface-visibility: $visibility;
    -o-backface-visibility: $visibility;
}

.front, .back {
    @include backface-visibility(hidden);
}

有时我们也需要处理一些复杂的参数:

@mixin stripes($direction, $width-percent, $stripe-color, $stripe-background: #FFF) {
    background: repeating-linear-gradient(
        $direction,
        $stripe-background,
        $stripe-background ($width-percent - 1),
        $stripe-color 1%,
        $stripe-background $width-percent
    );
}

用 Map 传入变量:

$college-ruled-style: ( 
    direction: to bottom,
    width-percent: 15%,
    stripe-color: blue,
    stripe-background: white
);

变量用 ... 进行传递:

.definition {
    width: 100%;
    height: 100%;
    @include stripes($college-ruled-style...);
}

还有种情况是字符串:

转译前:

// 使用 #{$file} 接收
@mixin photo-content($file) {
    content: url(#{$file}.jpg); //string interpolation
    object-fit: cover;
}

.photo { 
    @include photo-content('titanosaur');
    width: 60%;
    margin: 0px auto; 
}

转译后:

.photo { 
    content: url(titanosaur.jpg);
    width: 60%;
    margin: 0px auto; 
}
更可以搭配 Nesting 使用:

还可以搭配 Nesting :

@mixin hover-color($color) {
    &:hover {
        color: $color;
    }
 }

 .word {
    @include hover-color(red);
 }

4. 函数

在 Sass 中也可以通过内置函数简单设定颜色、渐变等,例如:adjust-hue($color,$degrees)fade-out

$lagoon-blue: fade-out(#62fdca, 0.5);

更多的内建函数可以参考高压博文文档(http://sass-lang.com/document...

5. 操作符:通过加减乘除和求余数等运算符方便的计算所需的属性值

颜色加法:

$color: #010203 + #040506;
/*
01 + 04 = 05
02 + 05 = 07
03 + 06 = 09
color: #050709;
*/

在使用 / 时需要注意:

width: $variable/6; // 除法
line-height: (600px)/9; // 除法
margin-left: 20-10 px/ 2; // 除法
font-size: 10px/8px; //无法计算

也可以使用 @each 语法循环 list 内容:

$list: (orange, purple, teal);

@each $item in $list {
    .#{$item} {
          background: $item;
    }
}

使用 @for 循环,并加入条件判断:

@for $i from 1 through $total {
    .ray:nth-child(#{$i}) {
        background: adjust-hue(blue, $i * $step);
        // 
        width: if($i % 2 == 0, 300px, 350px);
        margin-left: if($i % 2 == 0, 0px, 50px);
    }
}

6. @include 引用:用于引入其他 Sass、SCSS 文件:

我們通常使用 `Partials` 去處理特定功能,方便管理和維護。以下是引用 `_variables.scss` 檔案範例,其中檔名前的 `_` 表示引用前要先 compile:

我们通常用 Partials 去处理特定功能,方便管理和维护。以下是引用 _variables.scss 文件,其中文件名前的 _ 表示引用前要先编译:

@import "variables";

7. @extend :

编译前:

.lemonade {
    border: 1px yellow;
    background-color: #fdd;
}
.strawberry {
    @extend .lemonade;
    border-color: pink;
}

转译后:

.lemonade, .strawberry {
    border: 1px yellow;
    background-color: #fdd;
}

.strawberry {
    @extend .lemonade;
    border-color: pink;
}

搭配占位符使用:

转译前:

a%drink {
    font-size: 2em;
    background-color: $lemon-yellow;
}

.lemonade {
    @extend %drink;
    //more rules
}

转译后

a.lemonade {
  font-size: 2em;
  background-color: $lemon-yellow;
}

.lemonade {
 //more rules
}

8.@mixin@extend 的比较

转译前:

@mixin no-variable {
    font-size: 12px;
    color: #FFF;
    opacity: .9;
}

%placeholder {
    font-size: 12px;
    color: #FFF;
    opacity: .9;
}

span {
    @extend %placeholder;
}

div {
    @extend %placeholder;
}

p {
    @include no-variable;
}

h1 {
    @include no-variable;
}

转译后:

span, div{
font-size: 12px;
color: #FFF;
opacity: .9;
}

p {
font-size: 12px;
color: #FFF;
opacity: .9;
//rules specific to ps
}

h1 {
font-size: 12px;
color: #FFF;
opacity: .9;
//rules specific to ps
}

9. Sass文件夹结构

sass/
    components/
        _buttons.scss
    helpers/
        _variables.scss
        _functions.scss
        _mixins.scss
    layout/
        _grid.scss
        _header.scss
        _footer.scss
    pages/
        _home.scss
        _contact.scss

总结

以上是 Sass/SCSS 简明入门,在这篇文章中我们大致上介绍了 Sass 使用语法。除了 Sass 外上还有很多 CSS 的变形,包括语法比较容易学的 LESS、具有组件化思想的 CSS in JS,主要解决全局问题和模块引用的 CSS Modules,取经于 JavaScript Task Runner 的 PostCSS,网格样式表单 GSS 等,这些最终都是要解决传统 CSS 不易维护,重用性差的问题。实际上有些人觉得使用预处理器更好维护,也有些人认为进行编译很麻烦,到于要不要用,用哪种类型的 CSS 预处理器,必须要在团队内部进行讨论和规范。

173382ede7319973.gif


本文首发微信公众号:前端先锋

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章


欢迎继续阅读本专栏其它高赞文章:


查看原文

疯狂的技术宅 发布了文章 · 4月1日

Sass / SCSS简明入门

Sass / SCSS简介

Sass 是 CSS3 的扩展,添加了嵌套规则、变量、mixins、选择器继承等。可以用命令行工具或网络框架插件将其转换为格式良好的标准 CSS。

Sass(Syntactically Awesome StyleSheets) 是 CSS 的一种扩展,是 CSS的 超集(通过编译生成浏览器可以处理传统 CSS)。Sass 的出现是为了解决在大型项目中传统 CSS 会遇到的重复、可维护性差等问题(添加了嵌套的规则、变量、mixins、选择器继承等特性)。让开发者可以编写简洁、富语意(expressive )、可复用、可维护性和可延展性性佳的 CSS 代码。

Sass 的语法分为新的 SCSS(Sassy CSSSass 3,文件名称 *.scss)和旧的 SASS( Haml 风格,由于不用大括号格式,使用了缩紧,不能直接用 CSS 语法,学习曲线较高等特性,文件名称为*.sass)。由于新的 SCSS 语法是 CSS3 的超集,所以把传统的 CSS3 直接复制过来也不会出错,学习曲线相对比较平缓,所以我们将使用SCSS语法。

SASS 初体验

在开始介绍 SASS 特性之前先来学习如何将 Sass 转译成 CSS。

首先,先按照官网先安装Sass,然后在项目文件夹建立一个 main.scss 文件,文件内容如下:

// 引用
@import url(https://fonts.googleapis.com/css?family=Pacifico);
//Add variables here:

h1 {
  font-family: Roboto, sans-serif;
  text-align: center;
}

.banner {
  font-family: 'Pacifico', cursive;
  height: 400px;
  background-image: url("lemonade.jpg");
}

.container {
  text-align: center;
  font-family: 'Pacifico', cursive;
}

在终端下用以下命令进行转译:

sass main.scss main.css

这时你就会看到文件夹中多了 main.cssmain.css.map 两个文件,前者是转译过后的 CSS 文件,大部分是方便使用浏览器调试工具在进行调试时连结原文件和转译文件1⃣️方便调试。

1. 变量:变量可以用来储存值,方便重复利用

在 Sass 中用 $ 来表示变量,变量的数据型态可以是数字、字符串、布尔值、null值、甚至可以使用 List 和 Map。

变量的使用:

$translucent-white: rgba(255,255,255,0.3);
p {
    background-color: $translucent-white;
}

List 可以用空格或逗号分隔属性值:

$font-style-2: Helvetica, Arial, sans-serif;
$standard-border: 4px solid black;

p {
    border: $standard-border;
}

// maps key:value
$font-style-2: (key1: value1, key2: value2);


2. 嵌套:降低父元素重复性

转译前:

.parent {
    color: blue;
    .child {
        font-size: 12px;
    }
}



轉譯後:

转译后:

.parent {
    color: blue;
}

.parent .child {
    font-size: 12px;
}
```



在 Nesting 中不僅只有 child selectors 可以使用,還可以使用在相同的 Properties 上:

在Nesting中且只有子选择器可以使用,还可以使用在相同的

.parent {
font : {
    family: Roboto, sans-serif;
    size: 12px;
    decoration: none;
  }
}

转译后:

.parent {
    font-family: Roboto, sans-serif;
    font-size: 12px;
    font-decoration: none;
}

3. Mixins:减少编写伪元素时的重复

如:::before:: after::hover,在 Sass 中使用 代表父元素

转译前:

.notecard{ 
    &:hover{
        @include transform (rotatey(-180deg));  
    }
}

转译后:

.notecard:hover {
    transform: rotatey(-180deg);
}

重用群组的 CSS,例如跨浏览器的前缀,使用 @include 加入群组:

转译前:

@mixin backface-visibility {
    backface-visibility: hidden;
    -webkit-backface-visibility: hidden;
    -moz-backface-visibility: hidden;
    -ms-backface-visibility: hidden;
    -o-backface-visibility: hidden;
}
.notecard {
    .front, .back {
        width: 100%;
        height: 100%;
        position: absolute;
        @include backface_visibility;
    }
}

转译后:

.notecard .front, .notecard .back {
    width: 100%;
    height: 100%;
    position: absolute;

    backface-visibility: hidden;
    -webkit-backface-visibility: hidden; 
    -moz-backface-visibility: hidden;
    -ms-backface-visibility: hidden;
    -o-backface-visibility: hidden;
}

@mixin 也可以通过 @include 使用参数,也可以使用默认值:

@mixin backface-visibility($visibility:hidden) { //Add an argument
    backface-visibility: $visibility;
    -webkit-backface-visibility: $visibility;
    -moz-backface-visibility: $visibility;
    -ms-backface-visibility: $visibility;
    -o-backface-visibility: $visibility;
}

.front, .back {
    @include backface-visibility(hidden);
}

有时我们也需要处理一些复杂的参数:

@mixin stripes($direction, $width-percent, $stripe-color, $stripe-background: #FFF) {
    background: repeating-linear-gradient(
        $direction,
        $stripe-background,
        $stripe-background ($width-percent - 1),
        $stripe-color 1%,
        $stripe-background $width-percent
    );
}

用 Map 传入变量:

$college-ruled-style: ( 
    direction: to bottom,
    width-percent: 15%,
    stripe-color: blue,
    stripe-background: white
);

变量用 ... 进行传递:

.definition {
    width: 100%;
    height: 100%;
    @include stripes($college-ruled-style...);
}

还有种情况是字符串:

转译前:

// 使用 #{$file} 接收
@mixin photo-content($file) {
    content: url(#{$file}.jpg); //string interpolation
    object-fit: cover;
}

.photo { 
    @include photo-content('titanosaur');
    width: 60%;
    margin: 0px auto; 
}

转译后:

.photo { 
    content: url(titanosaur.jpg);
    width: 60%;
    margin: 0px auto; 
}
更可以搭配 Nesting 使用:

还可以搭配 Nesting :

@mixin hover-color($color) {
    &:hover {
        color: $color;
    }
 }

 .word {
    @include hover-color(red);
 }

4. 函数

在 Sass 中也可以通过内置函数简单设定颜色、渐变等,例如:adjust-hue($color,$degrees)fade-out

$lagoon-blue: fade-out(#62fdca, 0.5);

更多的内建函数可以参考高压博文文档(http://sass-lang.com/document...

5. 操作符:通过加减乘除和求余数等运算符方便的计算所需的属性值

颜色加法:

$color: #010203 + #040506;
/*
01 + 04 = 05
02 + 05 = 07
03 + 06 = 09
color: #050709;
*/

在使用 / 时需要注意:

width: $variable/6; // 除法
line-height: (600px)/9; // 除法
margin-left: 20-10 px/ 2; // 除法
font-size: 10px/8px; //无法计算

也可以使用 @each 语法循环 list 内容:

$list: (orange, purple, teal);

@each $item in $list {
    .#{$item} {
          background: $item;
    }
}

使用 @for 循环,并加入条件判断:

@for $i from 1 through $total {
    .ray:nth-child(#{$i}) {
        background: adjust-hue(blue, $i * $step);
        // 
        width: if($i % 2 == 0, 300px, 350px);
        margin-left: if($i % 2 == 0, 0px, 50px);
    }
}

6. @include 引用:用于引入其他 Sass、SCSS 文件:

我們通常使用 `Partials` 去處理特定功能,方便管理和維護。以下是引用 `_variables.scss` 檔案範例,其中檔名前的 `_` 表示引用前要先 compile:

我们通常用 Partials 去处理特定功能,方便管理和维护。以下是引用 _variables.scss 文件,其中文件名前的 _ 表示引用前要先编译:

@import "variables";

7. @extend :

编译前:

.lemonade {
    border: 1px yellow;
    background-color: #fdd;
}
.strawberry {
    @extend .lemonade;
    border-color: pink;
}

转译后:

.lemonade, .strawberry {
    border: 1px yellow;
    background-color: #fdd;
}

.strawberry {
    @extend .lemonade;
    border-color: pink;
}

搭配占位符使用:

转译前:

a%drink {
    font-size: 2em;
    background-color: $lemon-yellow;
}

.lemonade {
    @extend %drink;
    //more rules
}

转译后

a.lemonade {
  font-size: 2em;
  background-color: $lemon-yellow;
}

.lemonade {
 //more rules
}

8.@mixin@extend 的比较

转译前:

@mixin no-variable {
    font-size: 12px;
    color: #FFF;
    opacity: .9;
}

%placeholder {
    font-size: 12px;
    color: #FFF;
    opacity: .9;
}

span {
    @extend %placeholder;
}

div {
    @extend %placeholder;
}

p {
    @include no-variable;
}

h1 {
    @include no-variable;
}

转译后:

span, div{
font-size: 12px;
color: #FFF;
opacity: .9;
}

p {
font-size: 12px;
color: #FFF;
opacity: .9;
//rules specific to ps
}

h1 {
font-size: 12px;
color: #FFF;
opacity: .9;
//rules specific to ps
}

9. Sass文件夹结构

sass/
    components/
        _buttons.scss
    helpers/
        _variables.scss
        _functions.scss
        _mixins.scss
    layout/
        _grid.scss
        _header.scss
        _footer.scss
    pages/
        _home.scss
        _contact.scss

总结

以上是 Sass/SCSS 简明入门,在这篇文章中我们大致上介绍了 Sass 使用语法。除了 Sass 外上还有很多 CSS 的变形,包括语法比较容易学的 LESS、具有组件化思想的 CSS in JS,主要解决全局问题和模块引用的 CSS Modules,取经于 JavaScript Task Runner 的 PostCSS,网格样式表单 GSS 等,这些最终都是要解决传统 CSS 不易维护,重用性差的问题。实际上有些人觉得使用预处理器更好维护,也有些人认为进行编译很麻烦,到于要不要用,用哪种类型的 CSS 预处理器,必须要在团队内部进行讨论和规范。

173382ede7319973.gif


本文首发微信公众号:前端先锋

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章


欢迎继续阅读本专栏其它高赞文章:


查看原文

赞 5 收藏 4 评论 0

疯狂的技术宅 发布了文章 · 3月31日

对网站进行测试的 9 个要点

网站或网页的效率在很大程度上取决于测试,并且涉及对网站或相关软件的细致检查。那么我们如何该测试 Web 程序呢?在探究 Web 程序测试服务的更多细节之前,先讨论一下测试为何如此重要。

网站测试或 Web 应用测试应该是每个团队都要关注的问题。随着网站复杂性的日益增加,我们更要关注这个问题,这样才能确保在各种设备、浏览器和操作系统平台上得到最好的用户体验和性能。

以下是测试网站的十个要点,它们可以帮你用更短的时间得出更加准确可信的测试结果。

1. 将探索性测试与传统技术结合

用于网站测试最有效的技术之一是探索性测试。探索性测试有助于减少测试时间并发现其他缺陷。在探索性测试中,测试人员必须发挥创造力编写并运行测试用例。

更重要的是,可以通过将探索性测试与各种黑帽和白帽软件测试技术集成,从而解决探索性测试的缺点。这样做的另一个好处是,能够以更少的时间来生成有着更高的可靠性的测试结果。

2. 在测试中包括第三方应用、扩展和插件

每个 Web 应用都使用了很多第三方扩展、应用和插件。这些资源对于提高 Web 应用的性能和功能至关重要。但是每个插件、应用或扩展的质量差异可能会很大。

所以必须验证插件和扩展与 Web 应用的兼容性。另外还应该确定插件或扩展中的漏洞,这些漏洞可能会对网站的性能产生负面影响。这种测试对实时聊天、RSS、社交媒体、搜索或嵌入式视频功能的影响非常重要。

3. 跨浏览器兼容性测试

网站应该是针对移动设备友好的,因为通常用户喜欢在他们的移动设备上访问。许多公司通过响应式 Web 设计来确保他们的网站在有特定代码库的计算机以及移动设备上能够高效的运行。

开发人员应该使用开放技术(例如CSS3、JavaScript 和。HTML5)创建自适应网站。对于测试而言,进行跨浏览器兼容性测试至关重要。

4. 与开发团队沟通合作

许多公司选择 DevOps 来统一软件开发和测试。所以 QA 不仅应该在整个软件开发生命周期中测试 Web 应用,还要确保与开发人员、客户和需求分析师的协作。

5. 永远不要低估完整性测试

许多测试人员认为完整性测试是回归测试的辅助元素,从而破坏了完整性测试的重要性。但是,健全性测试有助于在有限的时间内评估 Web 应用中的功能。另外完整性测试还可以在代码更改或错误修复后帮助检查 Web 应用的功能。在测试时保持理智无疑是减少测试时间的可靠工具。

6. 站在黑客的角度思考

网站测试还必须侧重于最终用户的仿真,以此获得对用户体验的精确估计。但是,测试人​​员无法通过模拟最终用户来准确地评估网站的安全性。相反,通过模仿网络犯罪分子和黑客的行为来取得更好的测试结果非常重要。

7. 选择可用性测试的理想参数

在网站测试的众多方面中,可用性测试始终占据头把交椅。在进行测试时,应该始终评估网站的用户界面和用户体验。可用性测试中一些值得注意的参数是UI设计、内容可读性、速度、可访问性和可导航性。

8.确保 URL 字符串不被篡改

网站的安全功能无疑是测试的关键之一。应该评估 Web 应用在维护用户数据和企业数据安全性方面的效率。但是黑客可以通过篡改 URL 将用户重定向到恶意网站或访问敏感数据。某些恶意网站可以在用户的​​系统上安装恶意软件,或迫使它们共享敏感数据。

9. 进行持续的负载测试

压力测试在网站测试中是一个令人望而生畏的领域,它有助于评估网站在正常压力和峰值压力条件下的性能。可以利用复杂而全面的自动化测试工具来加速负载测试。

不过重要的是要通过不断的执行压力测试,来验证不同负载条件下 Web 应用的性能。应该通过逐渐增加压力的方式来确保增量执行负载测试。用户负载的缓慢而逐步的增加可以帮助我们找到由于压力而崩溃的确切位置。

查看原文

赞 4 收藏 4 评论 0

疯狂的技术宅 收藏了文章 · 3月30日

用 WebGL 创建一个在线画廊

作者:Luis Henrique Bizarro

翻译:疯狂的技术宅

原文:https://tympanus.net/codrops/...

下载源代码: https://github.com/lhbizarro/...

在本文中,我们将基于 WebGL 与 OGL 来实现一个无限循环画廊。

本文中所用到的大多数套路也可以用在其他 WebGL 库中,例如 Three.jsBabylon.js 中,但是需要一些小小的调整。

创建 OGL 3D 环境

首先要确保你正确设置了创建 3D 环境所需的所有渲染逻辑。

通常我们需要:一台照相机,一个场景和一个渲染器,它将把所有内容输出到一个 canvas 元素中。然后在 requestAnimationFrame 循环中用相机在渲染器中渲染场景。以下是原始代码段:

import { Renderer, Camera, Transform } from 'ogl'
 
export default class App {
  constructor () {
    this.createRenderer()
    this.createCamera()
    this.createScene()
 
    this.onResize()
 
    this.update()
 
    this.addEventListeners()
  }
 
  createRenderer () {
    this.renderer = new Renderer()
 
    this.gl = this.renderer.gl
    this.gl.clearColor(0.79607843137, 0.79215686274, 0.74117647058, 1)
 
    document.body.appendChild(this.gl.canvas)
  }
 
  createCamera () {
    this.camera = new Camera(this.gl)
    this.camera.fov = 45
    this.camera.position.z = 20
  }
 
  createScene () {
    this.scene = new Transform()
  }
 
  /**
   * Events.
   */
  onTouchDown (event) {
      
  }
 
  onTouchMove (event) {
      
  }
 
  onTouchUp (event) {
      
  }
 
  onWheel (event) {
      
  }
 
  /**
   * Resize.
   */
  onResize () {
    this.screen = {
      height: window.innerHeight,
      width: window.innerWidth
    }
 
    this.renderer.setSize(this.screen.width, this.screen.height)
 
    this.camera.perspective({
      aspect: this.gl.canvas.width / this.gl.canvas.height
    })
 
    const fov = this.camera.fov * (Math.PI / 180)
    const height = 2 * Math.tan(fov / 2) * this.camera.position.z
    const width = height * this.camera.aspect
 
    this.viewport = {
      height,
      width
    }
  }
 
  /**
   * Update.
   */
  update () {
    this.renderer.render({
      scene: this.scene,
      camera: this.camera
    })
    
    window.requestAnimationFrame(this.update.bind(this))
  }
 
  /**
   * Listeners.
   */
  addEventListeners () {
    window.addEventListener('resize', this.onResize.bind(this))
 
    window.addEventListener('mousewheel', this.onWheel.bind(this))
    window.addEventListener('wheel', this.onWheel.bind(this))
 
    window.addEventListener('mousedown', this.onTouchDown.bind(this))
    window.addEventListener('mousemove', this.onTouchMove.bind(this))
    window.addEventListener('mouseup', this.onTouchUp.bind(this))
 
    window.addEventListener('touchstart', this.onTouchDown.bind(this))
    window.addEventListener('touchmove', this.onTouchMove.bind(this))
    window.addEventListener('touchend', this.onTouchUp.bind(this))
  }
}
 
new App()

解释 App 类的设置

createRenderer 方法中,通过调用 this.gl.clearColor 来初始化有着固定颜色背景的渲染器。然后将 GL 上下文(this.renderer.gl`)引用存储在 `this.gl` 变量中,并将 `<canvas>this.gl.canvas)元素附加到 document.body 中。

createCamera 方法中,我们要创建一个 new Camera() 实例并设置其一些属性:fov 和它的 z 位置。 FOV是摄像机的视野,我们通过它来看到最终的画面。 z 是相机在 z 轴上的位置。

createScene 方法中使用的是 Transform 类,它是一个新场景的表示,包含所有表示 WebGL 环境中图像的平面。

onResize 方法是初始化设置中最重要的部分,负责三件事:

  1. 确保我们能够始终用正确的视口大小调整 <canvas> 元素的大小。
  2. 更新 this.camera 透视图,以划分视口的 widthheight
  3. 将变量值 this.viewport 存储在变量 this.viewport 中,这个值表示将通过使用摄像机的 fov 将像素转换为 3D 环境尺寸。

使用 camera.fov 在 3D 环境尺寸下转换像素的方法在众多的 WebGL 实现中非常常用。基本上它的工作是确保能够执行以下操作:this.mesh.scale.x = this.viewport.width; 这会使我们的网格适合整个屏幕宽度,其表现为 width: 100% ,不过是在 3D 空间中。

最后在更新中,我们设置了 requestAnimationFrame 循环,并确保能够持续渲染场景。

另外代码中还包含了 wheeltouchstarttouchmovetouchendmousedownmousemovemouseup 事件,它们用于处理用户与我们程序的交互。

创建可重用的几何实例

不管你用的是哪种 WebGL 库,总是要通过重复使用相同的几何图形引用来保持较低的内存使用量,这是一种很好的做法。为了表示所有图像,我们将使用平面几何图形,所以要创建一个新方法并将新几何图形存储在 this.planeGeometry 变量中。

import { Renderer, Camera, Transform, Plane } from 'ogl'
 
createGeometry () {
  this.planeGeometry = new Plane(this.gl, {
    heightSegments: 50,
    widthSegments: 100
  })
}

在这些值中之所以包含 heightSegmentswidthSegments ,是因为能够通过它们操纵顶点,以使 Plane 的行为像空气中的纸一样。

用 Webpack 导入图像

接下来就要将图像导入我们的程序了。在这里我们使用 Webpack,需要获取图像的操作只需要简单的使用 import 就够了:

import Image1 from 'images/1.jpg'
import Image2 from 'images/2.jpg'
import Image3 from 'images/3.jpg'
import Image4 from 'images/4.jpg'
import Image5 from 'images/5.jpg'
import Image6 from 'images/6.jpg'
import Image7 from 'images/7.jpg'
import Image8 from 'images/8.jpg'
import Image9 from 'images/9.jpg'
import Image10 from 'images/10.jpg'
import Image11 from 'images/11.jpg'
import Image12 from 'images/12.jpg'

现在创建要在轮播滑块中使用的图像数组,并在 createMedia 方法中调用上面的变量。用 .map 创建 Media 类的新实例(new Media()),它将用来表示画廊程序中每个图片。

createMedias () {
  this.mediasImages = [
    { image: Image1, text: 'New Synagogue' },
    { image: Image2, text: 'Paro Taktsang' },
    { image: Image3, text: 'Petra' },
    { image: Image4, text: 'Gooderham Building' },
    { image: Image5, text: 'Catherine Palace' },
    { image: Image6, text: 'Sheikh Zayed Mosque' },
    { image: Image7, text: 'Madonna Corona' },
    { image: Image8, text: 'Plaza de Espana' },
    { image: Image9, text: 'Saint Martin' },
    { image: Image10, text: 'Tugela Falls' },
    { image: Image11, text: 'Sintra-Cascais' },
    { image: Image12, text: 'The Prophet\'s Mosque' },
    { image: Image1, text: 'New Synagogue' },
    { image: Image2, text: 'Paro Taktsang' },
    { image: Image3, text: 'Petra' },
    { image: Image4, text: 'Gooderham Building' },
    { image: Image5, text: 'Catherine Palace' },
    { image: Image6, text: 'Sheikh Zayed Mosque' },
    { image: Image7, text: 'Madonna Corona' },
    { image: Image8, text: 'Plaza de Espana' },
    { image: Image9, text: 'Saint Martin' },
    { image: Image10, text: 'Tugela Falls' },
    { image: Image11, text: 'Sintra-Cascais' },
    { image: Image12, text: 'The Prophet\'s Mosque' },
  ]
 
 
  this.medias = this.mediasImages.map(({ image, text }, index) => {
    const media = new Media({
      geometry: this.planeGeometry,
      gl: this.gl,
      image,
      index,
      length: this.mediasImages.length,
      scene: this.scene,
      screen: this.screen,
      text,
      viewport: this.viewport
    })
 
    return media
  })
}

你可能注意到了,我们把一堆参数传递给了 Media 类,在下一小节讲到设置类时,会解释为什么需要这样。另外还将复制图片数量,以免在非常宽的屏幕上无限循环时出现图片不足的问题。

this.medias 数组的 onResize update 方法中包括一些特定的调用,因为我们希望图像能够响应:

onResize () {
  if (this.medias) {
    this.medias.forEach(media => media.onResize({
      screen: this.screen,
      viewport: this.viewport
    }))
  }
}

并在 requestAnimationFrame 内部执行一些实时操作:

update () {
  this.medias.forEach(media => media.update(this.scroll, this.direction))
}

设置 Media

Media 类中用 OGL 中的 MeshProgramTexture 类来创建 3D 平面并赋予纹理,在例子中,这个平面会成为我们的图像。

在构造函数中存储所需的所有变量,这些变量是从 index.jsnew Media() 初始化时传递的:

export default class {
  constructor ({ geometry, gl, image, index, length, renderer, scene, screen, text, viewport }) {
    this.geometry = geometry
    this.gl = gl
    this.image = image
    this.index = index
    this.length = length
    this.scene = scene
    this.screen = screen
    this.text = text
    this.viewport = viewport
 
    this.createShader()
    this.createMesh()
 
    this.onResize()
  }
}

解释一下其中的参数, geometry 是要应用于 Mesh 类的几何图形。 this.gl 是 GL 上下文,用于在类中继续进行 WebGL 操作。 this.image 是图像的 URL。 this.indexthis.length 都将用于进行网格的位置计算。 this.scene 是要将网格附加到的组。this.screenthis.viewport 是视口和环境的大小。

接下来用 createShader 方法创建要应用于 Mesh 的着色器,在 OGL 着色器中是通过 Program 创建的:

createShader () {
  const texture = new Texture(this.gl, {
    generateMipmaps: false
  })
 
  this.program = new Program(this.gl, {
    fragment,
    vertex,
    uniforms: {
      tMap: { value: texture },
      uPlaneSizes: { value: [0, 0] },
      uImageSizes: { value: [0, 0] },
      uViewportSizes: { value: [this.viewport.width, this.viewport.height] }
      },
    transparent: true
  })
 
  const image = new Image()
 
  image.src = this.image
  image.onload = _ => {
    texture.image = image
 
    this.program.uniforms.uImageSizes.value = [image.naturalWidth, image.naturalHeight]
  }
}

在上面的代码段中,创建了一个 new Texture() 实例,并把 generateMipmaps 设置为 false ,以便保留图像的质量。然后创建一个 new Program() 实例,该实例代表由 fragmentvertex 组成的着色器,并带有一些用于操纵它的 uniforms

代码中将创建了一个 new Image() 实例,用于在 texture.image 之前预加载图像。并且还要更新 this.program.uniforms.uImageSizes.value,它用于保留图像的长宽比。

现在创建片段和顶点着色器,先创建两个新文件:fragment.glslvertex.glsl

precision highp float;
 
uniform vec2 uImageSizes;
uniform vec2 uPlaneSizes;
uniform sampler2D tMap;
 
varying vec2 vUv;
 
void main() {
  vec2 ratio = vec2(
    min((uPlaneSizes.x / uPlaneSizes.y) / (uImageSizes.x / uImageSizes.y), 1.0),
    min((uPlaneSizes.y / uPlaneSizes.x) / (uImageSizes.y / uImageSizes.x), 1.0)
  );
 
  vec2 uv = vec2(
    vUv.x * ratio.x + (1.0 - ratio.x) * 0.5,
    vUv.y * ratio.y + (1.0 - ratio.y) * 0.5
  );
 
  gl_FragColor.rgb = texture2D(tMap, uv).rgb;
  gl_FragColor.a = 1.0;
}
precision highp float;
 
attribute vec3 position;
attribute vec2 uv;
 
uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;
 
varying vec2 vUv;
 
void main() {
  vUv = uv;
 
  vec3 p = position;
 
  gl_Position = projectionMatrix * modelViewMatrix * vec4(p, 1.0);
}

并用 WebpackMedia.js 开头中导入它们:

import fragment from './fragment.glsl'
import vertex from './vertex.glsl'

之后在 createMesh 方法中创建 new Mesh() 实例,将几何图形和着色器合并在一起。

createMesh () {
  this.plane = new Mesh(this.gl, {
    geometry: this.geometry,
    program: this.program
  })
 
  this.plane.setParent(this.scene)
}

Mesh 实例存储在 this.plane 变量中,以便在 onResizeupdate 方法中重用,然后作为 this.scene 组的子代附加。

现在屏幕上出现了带有图像的简单正方形:

image.png

接着实现 onResize 方法,确保我们能够渲染矩形:

onResize ({ screen, viewport } = {}) {
  if (screen) {
    this.screen = screen
  }
 
  if (viewport) {
    this.viewport = viewport
 
    this.plane.program.uniforms.uViewportSizes.value = [this.viewport.width, this.viewport.height]
  }
 
  this.scale = this.screen.height / 1500
 
  this.plane.scale.y = this.viewport.height * (900 * this.scale) / this.screen.height
  this.plane.scale.x = this.viewport.width * (700 * this.scale) / this.screen.width
 
  this.plane.program.uniforms.uPlaneSizes.value = [this.plane.scale.x, this.plane.scale.y]
}

scale.yscale.x 调用负责正确缩放元素,根据缩放比例将先前的正方形转换为 700×900 大小的矩形。

uViewportSizesuPlaneSizes 统一值更新可以使图像正确显示。这就为了使图片具有 background-size: cover; 行为。

image.png

现在我们需要在 x 轴上放置所有矩形,确保它们之间有一个很小的间隙。用 this.plane.scale.xthis.paddingthis.index 变量来进行移动它们所需的计算:

this.padding = 2
 
this.width = this.plane.scale.x + this.padding
this.widthTotal = this.width * this.length
 
this.x = this.width * this.index

update 方法中将 this.plane.position 设置为以下变量:

update () {
  this.plane.position.x = this.x
}

现在已经设置好了 Media 的所有初始代码,其结果如下图所示:

image.png

添加无限滚动逻辑

现在添加滚动逻辑,所以当用户滚动浏览你的页面时,会有一个无限旋转的画廊。在 index.js 中添加一下代码。

首先在构造函数中包含一个名为 this.scroll 的新对象,其中包含我们将要进行平滑滚动的所有变量:

this.scroll = {
  ease: 0.05,
  current: 0,
  target: 0,
  last: 0
}

下面添加触摸和滚轮事件,以便用户与画布交互时他将能够移动东西:

onTouchDown (event) {
  this.isDown = true
 
  this.scroll.position = this.scroll.current
  this.start = event.touches ? event.touches[0].clientX : event.clientX
}
 
onTouchMove (event) {
  if (!this.isDown) return
 
  const x = event.touches ? event.touches[0].clientX : event.clientX
  const distance = (this.start - x) * 0.01
 
  this.scroll.target = this.scroll.position + distance
}
 
onTouchUp (event) {
  this.isDown = false
}

然后在 onWheel 事件中包含 NormalizeWheel 库,这样当用户滚动时,在所有浏览器上能得到有相同的值:

onWheel (event) {
  const normalized = NormalizeWheel(event)
  const speed = normalized.pixelY
 
  this.scroll.target += speed * 0.005
}

在带有 requestAnimationFrameupdate 方法中,我们将使用 this.scroll.target对this.scroll.current 进行平滑处理,然后将其传递给所有 media:

update () {
  this.scroll.current = lerp(this.scroll.current, this.scroll.target, this.scroll.ease)
 
  if (this.medias) {
    this.medias.forEach(media => media.update(this.scroll))
  }
 
  this.scroll.last = this.scroll.current
 
  window.requestAnimationFrame(this.update.bind(this))
}

现在我们只是更新 Media 文件,用当前滚动值将 Mesh 移到新的滚动位置:

update (scroll) {
  this.plane.position.x = this.x - scroll.current * 0.1
}

下面是目前的成果:

https://i7x7p5b7.stackpathcdn...

现在它还不能无限滚动,要实现这一点还需要添加一些代码。第一步是将滚动的方向包含在来自 index.jsupdate方法中:

update () {
  this.scroll.current = lerp(this.scroll.current, this.scroll.target, this.scroll.ease)
 
  if (this.scroll.current > this.scroll.last) {
    this.direction = 'right'
  } else {
    this.direction = 'left'
  }
 
  if (this.medias) {
    this.medias.forEach(media => media.update(this.scroll, this.direction))
  }
 
  this.scroll.last = this.scroll.current
}

Media 类的造函数中包含一个名为 this.extra 的变量,并对它进行一些操作,当元素位于屏幕外部时求出图库的总宽度。

constructor ({ geometry, gl, image, index, length, renderer, scene, screen, text, viewport }) {
  this.extra = 0
}

update (scroll) {
  this.plane.position.x = this.x - scroll.current * 0.1 - this.extra
    
  const planeOffset = this.plane.scale.x / 2
  const viewportOffset = this.viewport.width
 
  this.isBefore = this.plane.position.x + planeOffset < -viewportOffset
  this.isAfter = this.plane.position.x - planeOffset > viewportOffset
 
  if (direction === 'right' && this.isBefore) {
    this.extra -= this.widthTotal
 
    this.isBefore = false
    this.isAfter = false
  }
 
  if (direction === 'left' && this.isAfter) {
    this.extra += this.widthTotal
 
    this.isBefore = false
    this.isAfter = false
  }
}

现在可以无限滚动了。

https://i7x7p5b7.stackpathcdn...

加入圆周旋转

首先让它根据位置平滑旋转。 map 方法是一种基于另一个特定范围提供值的方法,例如 map(0.5, 0, 1, -500, 500); 将返回 0,因为它是在 -500500 之间的中间位置。一般来说第一个参数控制 min2max2 的输出:

export function map (num, min1, max1, min2, max2, round = false) {
  const num1 = (num - min1) / (max1 - min1)
  const num2 = (num1 * (max2 - min2)) + min2
 
  if (round) return Math.round(num2)
 
  return num2
}

让我们通过在 Media 类中添加以下类似的代码来观察它的作用:

this.plane.rotation.z = map(this.plane.position.x, -this.widthTotal, this.widthTotal, Math.PI, -Math.PI)

这是目前的结果。你可以看到旋转根据平面位置而变化:

https://i7x7p5b7.stackpathcdn...

接下来要让它看起来像圆形。只需要用 Math.costhis.plane.position.x/this.widthTotal 做一个简单的计算即可:

this.plane.position.y = Math.cos((this.plane.position.x / this.widthTotal) * Math.PI) * 75 - 75

只需根据位置在环境空间中将其移动 75 即可,结果如下所示:

https://i7x7p5b7.stackpathcdn...

捕捉到最接近的项目

现在添加在用户停止滚动时简单地捕捉到最近的项目。创建一个名为 onCheck 的方法,该方法将在用户释放滚动时进行一些计算:

onCheck () {
  const { width } = this.medias[0]
  const itemIndex = Math.round(Math.abs(this.scroll.target) / width)
  const item = width * itemIndex
 
  if (this.scroll.target < 0) {
    this.scroll.target = -item
  } else {
    this.scroll.target = item
  }
}

item 变量的结果始终是图库中元素之一的中心,这会将用户锁定到相应的位置。

对于滚动事件,还需要一个去抖动的版本 onCheckDebounce ,可以通过导入 lodash/debounce 将其添加到构造函数中:

import debounce from 'lodash/debounce'
 
constructor ({ camera, color, gl, renderer, scene, screen, url, viewport }) {
  this.onCheckDebounce = debounce(this.onCheck, 200)
}
 
onWheel (event) {
  this.onCheckDebounce()
}

现在画廊总是能够被捕捉到正确的条目:

https://i7x7p5b7.stackpathcdn...

编写着色器

最后是最有意思的部分,通过滚动速度和使网格的顶点变形来稍微增强着色器。

第一步是在 Media 类的 this.program 声明中包括两个新的 uniform:uSpeeduTime

this.program = new Program(this.gl, {
  fragment,
  vertex,
  uniforms: {
    tMap: { value: texture },
    uPlaneSizes: { value: [0, 0] },
    uImageSizes: { value: [0, 0] },
    uViewportSizes: { value: [this.viewport.width, this.viewport.height] },
    uSpeed: { value: 0 },
    uTime: { value: 0 }
  },
  transparent: true
})

现在编写一些着色器代码,使图像弯曲和变形。在你的 vertex.glsl 文件中,应该添加新的 uniform :uniform float uTimeuniform float uSpeed

uniform float uTime;
uniform float uSpeed;

然后在着色器的 void main() 内部,可以用这两个值以及在 p 中存储的 position 变量来操纵 z 轴上的顶点。可以用 sincos 像平面一样弯曲我们的顶点,添加下面的代码:

p.z = (sin(p.x * 4.0 + uTime) * 1.5 + cos(p.y * 2.0 + uTime) * 1.5);

同样不要忘记在 Mediaupdate() 方法中包含 uTime 增量:

this.program.uniforms.uTime.value += 0.04

下面是产生的纸张效果动画:

https://i7x7p5b7.stackpathcdn...

用MSDF字体在WebGL中包含文本

现在把文本用 WebGL 显示出来,首先用 msdf-bmfont 来生成文件,安装 npm 依赖项并运行以下命令:

msdf-bmfont -f json -m 1024,1024 -d 4 --pot --smart-size freight.otf

运行之后,在当前目录中会有一个 .png.json 文件,这些是将在 OGL 中的 MSDF 实现中使用的文件。

创建一个名为 Title 的新文件,在其中创建 class 并在着色器和文件中使用 import

import AutoBind from 'auto-bind'
import { Color, Geometry, Mesh, Program, Text, Texture } from 'ogl'
 
import fragment from 'shaders/text-fragment.glsl'
import vertex from 'shaders/text-vertex.glsl'
 
import font from 'fonts/freight.json'
import src from 'fonts/freight.png'
 
export default class {
  constructor ({ gl, plane, renderer, text }) {
    AutoBind(this)
 
    this.gl = gl
    this.plane = plane
    this.renderer = renderer
    this.text = text
 
    this.createShader()
    this.createMesh()
  }
}

现在开始在 createShader() 方法中设置 MSDF 实现代码。首先创建一个新的 Texture() 实例,并加载存储在 src 中的 fonts/freight.png

createShader () {
  const texture = new Texture(this.gl, { generateMipmaps: false })
  const textureImage = new Image()
 
  textureImage.src = src
  textureImage.onload = _ => texture.image = textureImage
}

然后设置用于渲染 MSDF 文本的片段着色器,因为可以在 WebGL 2.0 中优化 MSDF,所以使用 OGL 中的 this.renderer.isWebgl2 来检查是否支持,并基于它声明不同的着色器,我们将使用 vertex300fragment300vertex100fragment100

createShader () {
  const vertex100 = `${vertex}`
 
  const fragment100 = `
    #extension GL_OES_standard_derivatives : enable
 
    precision highp float;
 
    ${fragment}
  `
 
  const vertex300 = `#version 300 es
 
    #define attribute in
    #define varying out
 
    ${vertex}
  `
 
  const fragment300 = `#version 300 es
 
    precision highp float;
 
    #define varying in
    #define texture2D texture
    #define gl_FragColor FragColor
 
    out vec4 FragColor;
 
    ${fragment}
  `
 
  let fragmentShader = fragment100
  let vertexShader = vertex100
 
  if (this.renderer.isWebgl2) {
    fragmentShader = fragment300
    vertexShader = vertex300
  }
 
  this.program = new Program(this.gl, {
    cullFace: null,
    depthTest: false,
    depthWrite: false,
    transparent: true,
    fragment: fragmentShader,
    vertex: vertexShader,
    uniforms: {
      uColor: { value: new Color('#545050') },
      tMap: { value: texture }
    }
  })
}

你可能已经注意到,我们在 fragmentvertex 之前添加了基于渲染器 WebG L版本的不同设置,接着创建了text-fragment.glsltext-vertex.glsl 文件:

uniform vec3 uColor;
uniform sampler2D tMap;
 
varying vec2 vUv;
 
void main() {
  vec3 color = texture2D(tMap, vUv).rgb;
 
  float signed = max(min(color.r, color.g), min(max(color.r, color.g), color.b)) - 0.5;
  float d = fwidth(signed);
  float alpha = smoothstep(-d, d, signed);
 
  if (alpha < 0.02) discard;
 
  gl_FragColor = vec4(uColor, alpha);
}
attribute vec2 uv;
attribute vec3 position;
 
uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;
 
varying vec2 vUv;
 
void main() {
  vUv = uv;
 
  gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}

最后在 createMesh() 方法中创建 MSDF 字体实现的几何,使用OGL的 new Text() 实例,然后将由此生成的缓冲区应用于 new Text() 实例:

createMesh () {
  const text = new Text({
    align: 'center',
    font,
    letterSpacing: -0.05,
    size: 0.08,
    text: this.text,
    wordSpacing: 0,
  })
 
  const geometry = new Geometry(this.gl, {
    position: { size: 3, data: text.buffers.position },
    uv: { size: 2, data: text.buffers.uv },
    id: { size: 1, data: text.buffers.id },
    index: { data: text.buffers.index }
  })
 
  geometry.computeBoundingBox()
 
  this.mesh = new Mesh(this.gl, { geometry, program: this.program })
  this.mesh.position.y = -this.plane.scale.y * 0.5 - 0.085
  this.mesh.setParent(this.plane)
}

接下来在 Media 类中应用新的标题,创建一个名为 createTilte() 的新方法,并在 constructor 中调用:

constructor ({ geometry, gl, image, index, length, renderer, scene, screen, text, viewport }) {
  this.createTitle()
}

createTitle () {
  this.title = new Title({
    gl: this.gl,
    plane: this.plane,
    renderer: this.renderer,
    text: this.text,
  })
}

将输出以下结果:

image.png

就这个程序而言,我们还实现了一个 new Number() 类,负责显示用户正在查看的当前索引。你可以检查它在源代码中的实现方式,但是它基本上与 Title 类的实现相同,唯一的区别是它加载了不同的字体样式:

image.png

引入背景块

最后还需要在后台实现一些将在 x 和 y 轴上移动的块,以增强其深度效果:

https://i7x7p5b7.stackpathcdn...

为了达到这种效果,需要创建一个新的 Background 类,并在其内部通过更改 scale 来在一个带有随机大小和位置的 new Mesh() 中初始化一些 new Plane() 几何形状。

import { Color, Mesh, Plane, Program } from 'ogl'
 
import fragment from 'shaders/background-fragment.glsl'
import vertex from 'shaders/background-vertex.glsl'
 
import { random } from 'utils/math'
 
export default class {
  constructor ({ gl, scene, viewport }) {
    this.gl = gl
    this.scene = scene
    this.viewport = viewport
 
    const geometry = new Plane(this.gl)
    const program = new Program(this.gl, {
      vertex,
      fragment,
      uniforms: {
        uColor: { value: new Color('#c4c3b6') }
      },
      transparent: true
    })
 
    this.meshes = []
 
    for (let i = 0; i < 50; i++) {
      let mesh = new Mesh(this.gl, {
        geometry,
        program,
      })
 
      const scale = random(0.75, 1)
 
      mesh.scale.x = 1.6 * scale
      mesh.scale.y = 0.9 * scale
 
      mesh.speed = random(0.75, 1)
 
      mesh.xExtra = 0
 
      mesh.x = mesh.position.x = random(-this.viewport.width * 0.5, this.viewport.width * 0.5)
      mesh.y = mesh.position.y = random(-this.viewport.height * 0.5, this.viewport.height * 0.5)
 
      this.meshes.push(mesh)
 
      this.scene.addChild(mesh)
    }
  }
}

然后只需要对它们应用无限滚动逻辑,并遵循与 Media 类中相同的方向进行验证:

update (scroll, direction) {
  this.meshes.forEach(mesh => {
    mesh.position.x = mesh.x - scroll.current * mesh.speed - mesh.xExtra
 
    const viewportOffset = this.viewport.width * 0.5
    const widthTotal = this.viewport.width + mesh.scale.x
 
    mesh.isBefore = mesh.position.x < -viewportOffset
    mesh.isAfter = mesh.position.x > viewportOffset
 
    if (direction === 'right' && mesh.isBefore) {
      mesh.xExtra -= widthTotal
 
      mesh.isBefore = false
      mesh.isAfter = false
    }
 
    if (direction === 'left' && mesh.isAfter) {
      mesh.xExtra += widthTotal
 
      mesh.isBefore = false
      mesh.isAfter = false
    }
 
    mesh.position.y += 0.05 * mesh.speed
 
    if (mesh.position.y > this.viewport.height * 0.5 + mesh.scale.y) {
      mesh.position.y -= this.viewport.height + mesh.scale.y
    }
  })
}

就这么简单,现在我们的代码终于完成了。

173382ede7319973.gif


本文首发微信公众号:前端先锋

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章


欢迎继续阅读本专栏其它高赞文章:


查看原文

疯狂的技术宅 发布了文章 · 3月30日

用 WebGL 创建一个在线画廊

作者:Luis Henrique Bizarro

翻译:疯狂的技术宅

原文:https://tympanus.net/codrops/...

下载源代码: https://github.com/lhbizarro/...

在本文中,我们将基于 WebGL 与 OGL 来实现一个无限循环画廊。

本文中所用到的大多数套路也可以用在其他 WebGL 库中,例如 Three.jsBabylon.js 中,但是需要一些小小的调整。

创建 OGL 3D 环境

首先要确保你正确设置了创建 3D 环境所需的所有渲染逻辑。

通常我们需要:一台照相机,一个场景和一个渲染器,它将把所有内容输出到一个 canvas 元素中。然后在 requestAnimationFrame 循环中用相机在渲染器中渲染场景。以下是原始代码段:

import { Renderer, Camera, Transform } from 'ogl'
 
export default class App {
  constructor () {
    this.createRenderer()
    this.createCamera()
    this.createScene()
 
    this.onResize()
 
    this.update()
 
    this.addEventListeners()
  }
 
  createRenderer () {
    this.renderer = new Renderer()
 
    this.gl = this.renderer.gl
    this.gl.clearColor(0.79607843137, 0.79215686274, 0.74117647058, 1)
 
    document.body.appendChild(this.gl.canvas)
  }
 
  createCamera () {
    this.camera = new Camera(this.gl)
    this.camera.fov = 45
    this.camera.position.z = 20
  }
 
  createScene () {
    this.scene = new Transform()
  }
 
  /**
   * Events.
   */
  onTouchDown (event) {
      
  }
 
  onTouchMove (event) {
      
  }
 
  onTouchUp (event) {
      
  }
 
  onWheel (event) {
      
  }
 
  /**
   * Resize.
   */
  onResize () {
    this.screen = {
      height: window.innerHeight,
      width: window.innerWidth
    }
 
    this.renderer.setSize(this.screen.width, this.screen.height)
 
    this.camera.perspective({
      aspect: this.gl.canvas.width / this.gl.canvas.height
    })
 
    const fov = this.camera.fov * (Math.PI / 180)
    const height = 2 * Math.tan(fov / 2) * this.camera.position.z
    const width = height * this.camera.aspect
 
    this.viewport = {
      height,
      width
    }
  }
 
  /**
   * Update.
   */
  update () {
    this.renderer.render({
      scene: this.scene,
      camera: this.camera
    })
    
    window.requestAnimationFrame(this.update.bind(this))
  }
 
  /**
   * Listeners.
   */
  addEventListeners () {
    window.addEventListener('resize', this.onResize.bind(this))
 
    window.addEventListener('mousewheel', this.onWheel.bind(this))
    window.addEventListener('wheel', this.onWheel.bind(this))
 
    window.addEventListener('mousedown', this.onTouchDown.bind(this))
    window.addEventListener('mousemove', this.onTouchMove.bind(this))
    window.addEventListener('mouseup', this.onTouchUp.bind(this))
 
    window.addEventListener('touchstart', this.onTouchDown.bind(this))
    window.addEventListener('touchmove', this.onTouchMove.bind(this))
    window.addEventListener('touchend', this.onTouchUp.bind(this))
  }
}
 
new App()

解释 App 类的设置

createRenderer 方法中,通过调用 this.gl.clearColor 来初始化有着固定颜色背景的渲染器。然后将 GL 上下文(this.renderer.gl`)引用存储在 `this.gl` 变量中,并将 `<canvas>this.gl.canvas)元素附加到 document.body 中。

createCamera 方法中,我们要创建一个 new Camera() 实例并设置其一些属性:fov 和它的 z 位置。 FOV是摄像机的视野,我们通过它来看到最终的画面。 z 是相机在 z 轴上的位置。

createScene 方法中使用的是 Transform 类,它是一个新场景的表示,包含所有表示 WebGL 环境中图像的平面。

onResize 方法是初始化设置中最重要的部分,负责三件事:

  1. 确保我们能够始终用正确的视口大小调整 <canvas> 元素的大小。
  2. 更新 this.camera 透视图,以划分视口的 widthheight
  3. 将变量值 this.viewport 存储在变量 this.viewport 中,这个值表示将通过使用摄像机的 fov 将像素转换为 3D 环境尺寸。

使用 camera.fov 在 3D 环境尺寸下转换像素的方法在众多的 WebGL 实现中非常常用。基本上它的工作是确保能够执行以下操作:this.mesh.scale.x = this.viewport.width; 这会使我们的网格适合整个屏幕宽度,其表现为 width: 100% ,不过是在 3D 空间中。

最后在更新中,我们设置了 requestAnimationFrame 循环,并确保能够持续渲染场景。

另外代码中还包含了 wheeltouchstarttouchmovetouchendmousedownmousemovemouseup 事件,它们用于处理用户与我们程序的交互。

创建可重用的几何实例

不管你用的是哪种 WebGL 库,总是要通过重复使用相同的几何图形引用来保持较低的内存使用量,这是一种很好的做法。为了表示所有图像,我们将使用平面几何图形,所以要创建一个新方法并将新几何图形存储在 this.planeGeometry 变量中。

import { Renderer, Camera, Transform, Plane } from 'ogl'
 
createGeometry () {
  this.planeGeometry = new Plane(this.gl, {
    heightSegments: 50,
    widthSegments: 100
  })
}

在这些值中之所以包含 heightSegmentswidthSegments ,是因为能够通过它们操纵顶点,以使 Plane 的行为像空气中的纸一样。

用 Webpack 导入图像

接下来就要将图像导入我们的程序了。在这里我们使用 Webpack,需要获取图像的操作只需要简单的使用 import 就够了:

import Image1 from 'images/1.jpg'
import Image2 from 'images/2.jpg'
import Image3 from 'images/3.jpg'
import Image4 from 'images/4.jpg'
import Image5 from 'images/5.jpg'
import Image6 from 'images/6.jpg'
import Image7 from 'images/7.jpg'
import Image8 from 'images/8.jpg'
import Image9 from 'images/9.jpg'
import Image10 from 'images/10.jpg'
import Image11 from 'images/11.jpg'
import Image12 from 'images/12.jpg'

现在创建要在轮播滑块中使用的图像数组,并在 createMedia 方法中调用上面的变量。用 .map 创建 Media 类的新实例(new Media()),它将用来表示画廊程序中每个图片。

createMedias () {
  this.mediasImages = [
    { image: Image1, text: 'New Synagogue' },
    { image: Image2, text: 'Paro Taktsang' },
    { image: Image3, text: 'Petra' },
    { image: Image4, text: 'Gooderham Building' },
    { image: Image5, text: 'Catherine Palace' },
    { image: Image6, text: 'Sheikh Zayed Mosque' },
    { image: Image7, text: 'Madonna Corona' },
    { image: Image8, text: 'Plaza de Espana' },
    { image: Image9, text: 'Saint Martin' },
    { image: Image10, text: 'Tugela Falls' },
    { image: Image11, text: 'Sintra-Cascais' },
    { image: Image12, text: 'The Prophet\'s Mosque' },
    { image: Image1, text: 'New Synagogue' },
    { image: Image2, text: 'Paro Taktsang' },
    { image: Image3, text: 'Petra' },
    { image: Image4, text: 'Gooderham Building' },
    { image: Image5, text: 'Catherine Palace' },
    { image: Image6, text: 'Sheikh Zayed Mosque' },
    { image: Image7, text: 'Madonna Corona' },
    { image: Image8, text: 'Plaza de Espana' },
    { image: Image9, text: 'Saint Martin' },
    { image: Image10, text: 'Tugela Falls' },
    { image: Image11, text: 'Sintra-Cascais' },
    { image: Image12, text: 'The Prophet\'s Mosque' },
  ]
 
 
  this.medias = this.mediasImages.map(({ image, text }, index) => {
    const media = new Media({
      geometry: this.planeGeometry,
      gl: this.gl,
      image,
      index,
      length: this.mediasImages.length,
      scene: this.scene,
      screen: this.screen,
      text,
      viewport: this.viewport
    })
 
    return media
  })
}

你可能注意到了,我们把一堆参数传递给了 Media 类,在下一小节讲到设置类时,会解释为什么需要这样。另外还将复制图片数量,以免在非常宽的屏幕上无限循环时出现图片不足的问题。

this.medias 数组的 onResize update 方法中包括一些特定的调用,因为我们希望图像能够响应:

onResize () {
  if (this.medias) {
    this.medias.forEach(media => media.onResize({
      screen: this.screen,
      viewport: this.viewport
    }))
  }
}

并在 requestAnimationFrame 内部执行一些实时操作:

update () {
  this.medias.forEach(media => media.update(this.scroll, this.direction))
}

设置 Media

Media 类中用 OGL 中的 MeshProgramTexture 类来创建 3D 平面并赋予纹理,在例子中,这个平面会成为我们的图像。

在构造函数中存储所需的所有变量,这些变量是从 index.jsnew Media() 初始化时传递的:

export default class {
  constructor ({ geometry, gl, image, index, length, renderer, scene, screen, text, viewport }) {
    this.geometry = geometry
    this.gl = gl
    this.image = image
    this.index = index
    this.length = length
    this.scene = scene
    this.screen = screen
    this.text = text
    this.viewport = viewport
 
    this.createShader()
    this.createMesh()
 
    this.onResize()
  }
}

解释一下其中的参数, geometry 是要应用于 Mesh 类的几何图形。 this.gl 是 GL 上下文,用于在类中继续进行 WebGL 操作。 this.image 是图像的 URL。 this.indexthis.length 都将用于进行网格的位置计算。 this.scene 是要将网格附加到的组。this.screenthis.viewport 是视口和环境的大小。

接下来用 createShader 方法创建要应用于 Mesh 的着色器,在 OGL 着色器中是通过 Program 创建的:

createShader () {
  const texture = new Texture(this.gl, {
    generateMipmaps: false
  })
 
  this.program = new Program(this.gl, {
    fragment,
    vertex,
    uniforms: {
      tMap: { value: texture },
      uPlaneSizes: { value: [0, 0] },
      uImageSizes: { value: [0, 0] },
      uViewportSizes: { value: [this.viewport.width, this.viewport.height] }
      },
    transparent: true
  })
 
  const image = new Image()
 
  image.src = this.image
  image.onload = _ => {
    texture.image = image
 
    this.program.uniforms.uImageSizes.value = [image.naturalWidth, image.naturalHeight]
  }
}

在上面的代码段中,创建了一个 new Texture() 实例,并把 generateMipmaps 设置为 false ,以便保留图像的质量。然后创建一个 new Program() 实例,该实例代表由 fragmentvertex 组成的着色器,并带有一些用于操纵它的 uniforms

代码中将创建了一个 new Image() 实例,用于在 texture.image 之前预加载图像。并且还要更新 this.program.uniforms.uImageSizes.value,它用于保留图像的长宽比。

现在创建片段和顶点着色器,先创建两个新文件:fragment.glslvertex.glsl

precision highp float;
 
uniform vec2 uImageSizes;
uniform vec2 uPlaneSizes;
uniform sampler2D tMap;
 
varying vec2 vUv;
 
void main() {
  vec2 ratio = vec2(
    min((uPlaneSizes.x / uPlaneSizes.y) / (uImageSizes.x / uImageSizes.y), 1.0),
    min((uPlaneSizes.y / uPlaneSizes.x) / (uImageSizes.y / uImageSizes.x), 1.0)
  );
 
  vec2 uv = vec2(
    vUv.x * ratio.x + (1.0 - ratio.x) * 0.5,
    vUv.y * ratio.y + (1.0 - ratio.y) * 0.5
  );
 
  gl_FragColor.rgb = texture2D(tMap, uv).rgb;
  gl_FragColor.a = 1.0;
}
precision highp float;
 
attribute vec3 position;
attribute vec2 uv;
 
uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;
 
varying vec2 vUv;
 
void main() {
  vUv = uv;
 
  vec3 p = position;
 
  gl_Position = projectionMatrix * modelViewMatrix * vec4(p, 1.0);
}

并用 WebpackMedia.js 开头中导入它们:

import fragment from './fragment.glsl'
import vertex from './vertex.glsl'

之后在 createMesh 方法中创建 new Mesh() 实例,将几何图形和着色器合并在一起。

createMesh () {
  this.plane = new Mesh(this.gl, {
    geometry: this.geometry,
    program: this.program
  })
 
  this.plane.setParent(this.scene)
}

Mesh 实例存储在 this.plane 变量中,以便在 onResizeupdate 方法中重用,然后作为 this.scene 组的子代附加。

现在屏幕上出现了带有图像的简单正方形:

image.png

接着实现 onResize 方法,确保我们能够渲染矩形:

onResize ({ screen, viewport } = {}) {
  if (screen) {
    this.screen = screen
  }
 
  if (viewport) {
    this.viewport = viewport
 
    this.plane.program.uniforms.uViewportSizes.value = [this.viewport.width, this.viewport.height]
  }
 
  this.scale = this.screen.height / 1500
 
  this.plane.scale.y = this.viewport.height * (900 * this.scale) / this.screen.height
  this.plane.scale.x = this.viewport.width * (700 * this.scale) / this.screen.width
 
  this.plane.program.uniforms.uPlaneSizes.value = [this.plane.scale.x, this.plane.scale.y]
}

scale.yscale.x 调用负责正确缩放元素,根据缩放比例将先前的正方形转换为 700×900 大小的矩形。

uViewportSizesuPlaneSizes 统一值更新可以使图像正确显示。这就为了使图片具有 background-size: cover; 行为。

image.png

现在我们需要在 x 轴上放置所有矩形,确保它们之间有一个很小的间隙。用 this.plane.scale.xthis.paddingthis.index 变量来进行移动它们所需的计算:

this.padding = 2
 
this.width = this.plane.scale.x + this.padding
this.widthTotal = this.width * this.length
 
this.x = this.width * this.index

update 方法中将 this.plane.position 设置为以下变量:

update () {
  this.plane.position.x = this.x
}

现在已经设置好了 Media 的所有初始代码,其结果如下图所示:

image.png

添加无限滚动逻辑

现在添加滚动逻辑,所以当用户滚动浏览你的页面时,会有一个无限旋转的画廊。在 index.js 中添加一下代码。

首先在构造函数中包含一个名为 this.scroll 的新对象,其中包含我们将要进行平滑滚动的所有变量:

this.scroll = {
  ease: 0.05,
  current: 0,
  target: 0,
  last: 0
}

下面添加触摸和滚轮事件,以便用户与画布交互时他将能够移动东西:

onTouchDown (event) {
  this.isDown = true
 
  this.scroll.position = this.scroll.current
  this.start = event.touches ? event.touches[0].clientX : event.clientX
}
 
onTouchMove (event) {
  if (!this.isDown) return
 
  const x = event.touches ? event.touches[0].clientX : event.clientX
  const distance = (this.start - x) * 0.01
 
  this.scroll.target = this.scroll.position + distance
}
 
onTouchUp (event) {
  this.isDown = false
}

然后在 onWheel 事件中包含 NormalizeWheel 库,这样当用户滚动时,在所有浏览器上能得到有相同的值:

onWheel (event) {
  const normalized = NormalizeWheel(event)
  const speed = normalized.pixelY
 
  this.scroll.target += speed * 0.005
}

在带有 requestAnimationFrameupdate 方法中,我们将使用 this.scroll.target对this.scroll.current 进行平滑处理,然后将其传递给所有 media:

update () {
  this.scroll.current = lerp(this.scroll.current, this.scroll.target, this.scroll.ease)
 
  if (this.medias) {
    this.medias.forEach(media => media.update(this.scroll))
  }
 
  this.scroll.last = this.scroll.current
 
  window.requestAnimationFrame(this.update.bind(this))
}

现在我们只是更新 Media 文件,用当前滚动值将 Mesh 移到新的滚动位置:

update (scroll) {
  this.plane.position.x = this.x - scroll.current * 0.1
}

下面是目前的成果:

https://i7x7p5b7.stackpathcdn...

现在它还不能无限滚动,要实现这一点还需要添加一些代码。第一步是将滚动的方向包含在来自 index.jsupdate方法中:

update () {
  this.scroll.current = lerp(this.scroll.current, this.scroll.target, this.scroll.ease)
 
  if (this.scroll.current > this.scroll.last) {
    this.direction = 'right'
  } else {
    this.direction = 'left'
  }
 
  if (this.medias) {
    this.medias.forEach(media => media.update(this.scroll, this.direction))
  }
 
  this.scroll.last = this.scroll.current
}

Media 类的造函数中包含一个名为 this.extra 的变量,并对它进行一些操作,当元素位于屏幕外部时求出图库的总宽度。

constructor ({ geometry, gl, image, index, length, renderer, scene, screen, text, viewport }) {
  this.extra = 0
}

update (scroll) {
  this.plane.position.x = this.x - scroll.current * 0.1 - this.extra
    
  const planeOffset = this.plane.scale.x / 2
  const viewportOffset = this.viewport.width
 
  this.isBefore = this.plane.position.x + planeOffset < -viewportOffset
  this.isAfter = this.plane.position.x - planeOffset > viewportOffset
 
  if (direction === 'right' && this.isBefore) {
    this.extra -= this.widthTotal
 
    this.isBefore = false
    this.isAfter = false
  }
 
  if (direction === 'left' && this.isAfter) {
    this.extra += this.widthTotal
 
    this.isBefore = false
    this.isAfter = false
  }
}

现在可以无限滚动了。

https://i7x7p5b7.stackpathcdn...

加入圆周旋转

首先让它根据位置平滑旋转。 map 方法是一种基于另一个特定范围提供值的方法,例如 map(0.5, 0, 1, -500, 500); 将返回 0,因为它是在 -500500 之间的中间位置。一般来说第一个参数控制 min2max2 的输出:

export function map (num, min1, max1, min2, max2, round = false) {
  const num1 = (num - min1) / (max1 - min1)
  const num2 = (num1 * (max2 - min2)) + min2
 
  if (round) return Math.round(num2)
 
  return num2
}

让我们通过在 Media 类中添加以下类似的代码来观察它的作用:

this.plane.rotation.z = map(this.plane.position.x, -this.widthTotal, this.widthTotal, Math.PI, -Math.PI)

这是目前的结果。你可以看到旋转根据平面位置而变化:

https://i7x7p5b7.stackpathcdn...

接下来要让它看起来像圆形。只需要用 Math.costhis.plane.position.x/this.widthTotal 做一个简单的计算即可:

this.plane.position.y = Math.cos((this.plane.position.x / this.widthTotal) * Math.PI) * 75 - 75

只需根据位置在环境空间中将其移动 75 即可,结果如下所示:

https://i7x7p5b7.stackpathcdn...

捕捉到最接近的项目

现在添加在用户停止滚动时简单地捕捉到最近的项目。创建一个名为 onCheck 的方法,该方法将在用户释放滚动时进行一些计算:

onCheck () {
  const { width } = this.medias[0]
  const itemIndex = Math.round(Math.abs(this.scroll.target) / width)
  const item = width * itemIndex
 
  if (this.scroll.target < 0) {
    this.scroll.target = -item
  } else {
    this.scroll.target = item
  }
}

item 变量的结果始终是图库中元素之一的中心,这会将用户锁定到相应的位置。

对于滚动事件,还需要一个去抖动的版本 onCheckDebounce ,可以通过导入 lodash/debounce 将其添加到构造函数中:

import debounce from 'lodash/debounce'
 
constructor ({ camera, color, gl, renderer, scene, screen, url, viewport }) {
  this.onCheckDebounce = debounce(this.onCheck, 200)
}
 
onWheel (event) {
  this.onCheckDebounce()
}

现在画廊总是能够被捕捉到正确的条目:

https://i7x7p5b7.stackpathcdn...

编写着色器

最后是最有意思的部分,通过滚动速度和使网格的顶点变形来稍微增强着色器。

第一步是在 Media 类的 this.program 声明中包括两个新的 uniform:uSpeeduTime

this.program = new Program(this.gl, {
  fragment,
  vertex,
  uniforms: {
    tMap: { value: texture },
    uPlaneSizes: { value: [0, 0] },
    uImageSizes: { value: [0, 0] },
    uViewportSizes: { value: [this.viewport.width, this.viewport.height] },
    uSpeed: { value: 0 },
    uTime: { value: 0 }
  },
  transparent: true
})

现在编写一些着色器代码,使图像弯曲和变形。在你的 vertex.glsl 文件中,应该添加新的 uniform :uniform float uTimeuniform float uSpeed

uniform float uTime;
uniform float uSpeed;

然后在着色器的 void main() 内部,可以用这两个值以及在 p 中存储的 position 变量来操纵 z 轴上的顶点。可以用 sincos 像平面一样弯曲我们的顶点,添加下面的代码:

p.z = (sin(p.x * 4.0 + uTime) * 1.5 + cos(p.y * 2.0 + uTime) * 1.5);

同样不要忘记在 Mediaupdate() 方法中包含 uTime 增量:

this.program.uniforms.uTime.value += 0.04

下面是产生的纸张效果动画:

https://i7x7p5b7.stackpathcdn...

用MSDF字体在WebGL中包含文本

现在把文本用 WebGL 显示出来,首先用 msdf-bmfont 来生成文件,安装 npm 依赖项并运行以下命令:

msdf-bmfont -f json -m 1024,1024 -d 4 --pot --smart-size freight.otf

运行之后,在当前目录中会有一个 .png.json 文件,这些是将在 OGL 中的 MSDF 实现中使用的文件。

创建一个名为 Title 的新文件,在其中创建 class 并在着色器和文件中使用 import

import AutoBind from 'auto-bind'
import { Color, Geometry, Mesh, Program, Text, Texture } from 'ogl'
 
import fragment from 'shaders/text-fragment.glsl'
import vertex from 'shaders/text-vertex.glsl'
 
import font from 'fonts/freight.json'
import src from 'fonts/freight.png'
 
export default class {
  constructor ({ gl, plane, renderer, text }) {
    AutoBind(this)
 
    this.gl = gl
    this.plane = plane
    this.renderer = renderer
    this.text = text
 
    this.createShader()
    this.createMesh()
  }
}

现在开始在 createShader() 方法中设置 MSDF 实现代码。首先创建一个新的 Texture() 实例,并加载存储在 src 中的 fonts/freight.png

createShader () {
  const texture = new Texture(this.gl, { generateMipmaps: false })
  const textureImage = new Image()
 
  textureImage.src = src
  textureImage.onload = _ => texture.image = textureImage
}

然后设置用于渲染 MSDF 文本的片段着色器,因为可以在 WebGL 2.0 中优化 MSDF,所以使用 OGL 中的 this.renderer.isWebgl2 来检查是否支持,并基于它声明不同的着色器,我们将使用 vertex300fragment300vertex100fragment100

createShader () {
  const vertex100 = `${vertex}`
 
  const fragment100 = `
    #extension GL_OES_standard_derivatives : enable
 
    precision highp float;
 
    ${fragment}
  `
 
  const vertex300 = `#version 300 es
 
    #define attribute in
    #define varying out
 
    ${vertex}
  `
 
  const fragment300 = `#version 300 es
 
    precision highp float;
 
    #define varying in
    #define texture2D texture
    #define gl_FragColor FragColor
 
    out vec4 FragColor;
 
    ${fragment}
  `
 
  let fragmentShader = fragment100
  let vertexShader = vertex100
 
  if (this.renderer.isWebgl2) {
    fragmentShader = fragment300
    vertexShader = vertex300
  }
 
  this.program = new Program(this.gl, {
    cullFace: null,
    depthTest: false,
    depthWrite: false,
    transparent: true,
    fragment: fragmentShader,
    vertex: vertexShader,
    uniforms: {
      uColor: { value: new Color('#545050') },
      tMap: { value: texture }
    }
  })
}

你可能已经注意到,我们在 fragmentvertex 之前添加了基于渲染器 WebG L版本的不同设置,接着创建了text-fragment.glsltext-vertex.glsl 文件:

uniform vec3 uColor;
uniform sampler2D tMap;
 
varying vec2 vUv;
 
void main() {
  vec3 color = texture2D(tMap, vUv).rgb;
 
  float signed = max(min(color.r, color.g), min(max(color.r, color.g), color.b)) - 0.5;
  float d = fwidth(signed);
  float alpha = smoothstep(-d, d, signed);
 
  if (alpha < 0.02) discard;
 
  gl_FragColor = vec4(uColor, alpha);
}
attribute vec2 uv;
attribute vec3 position;
 
uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;
 
varying vec2 vUv;
 
void main() {
  vUv = uv;
 
  gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}

最后在 createMesh() 方法中创建 MSDF 字体实现的几何,使用OGL的 new Text() 实例,然后将由此生成的缓冲区应用于 new Text() 实例:

createMesh () {
  const text = new Text({
    align: 'center',
    font,
    letterSpacing: -0.05,
    size: 0.08,
    text: this.text,
    wordSpacing: 0,
  })
 
  const geometry = new Geometry(this.gl, {
    position: { size: 3, data: text.buffers.position },
    uv: { size: 2, data: text.buffers.uv },
    id: { size: 1, data: text.buffers.id },
    index: { data: text.buffers.index }
  })
 
  geometry.computeBoundingBox()
 
  this.mesh = new Mesh(this.gl, { geometry, program: this.program })
  this.mesh.position.y = -this.plane.scale.y * 0.5 - 0.085
  this.mesh.setParent(this.plane)
}

接下来在 Media 类中应用新的标题,创建一个名为 createTilte() 的新方法,并在 constructor 中调用:

constructor ({ geometry, gl, image, index, length, renderer, scene, screen, text, viewport }) {
  this.createTitle()
}

createTitle () {
  this.title = new Title({
    gl: this.gl,
    plane: this.plane,
    renderer: this.renderer,
    text: this.text,
  })
}

将输出以下结果:

image.png

就这个程序而言,我们还实现了一个 new Number() 类,负责显示用户正在查看的当前索引。你可以检查它在源代码中的实现方式,但是它基本上与 Title 类的实现相同,唯一的区别是它加载了不同的字体样式:

image.png

引入背景块

最后还需要在后台实现一些将在 x 和 y 轴上移动的块,以增强其深度效果:

https://i7x7p5b7.stackpathcdn...

为了达到这种效果,需要创建一个新的 Background 类,并在其内部通过更改 scale 来在一个带有随机大小和位置的 new Mesh() 中初始化一些 new Plane() 几何形状。

import { Color, Mesh, Plane, Program } from 'ogl'
 
import fragment from 'shaders/background-fragment.glsl'
import vertex from 'shaders/background-vertex.glsl'
 
import { random } from 'utils/math'
 
export default class {
  constructor ({ gl, scene, viewport }) {
    this.gl = gl
    this.scene = scene
    this.viewport = viewport
 
    const geometry = new Plane(this.gl)
    const program = new Program(this.gl, {
      vertex,
      fragment,
      uniforms: {
        uColor: { value: new Color('#c4c3b6') }
      },
      transparent: true
    })
 
    this.meshes = []
 
    for (let i = 0; i < 50; i++) {
      let mesh = new Mesh(this.gl, {
        geometry,
        program,
      })
 
      const scale = random(0.75, 1)
 
      mesh.scale.x = 1.6 * scale
      mesh.scale.y = 0.9 * scale
 
      mesh.speed = random(0.75, 1)
 
      mesh.xExtra = 0
 
      mesh.x = mesh.position.x = random(-this.viewport.width * 0.5, this.viewport.width * 0.5)
      mesh.y = mesh.position.y = random(-this.viewport.height * 0.5, this.viewport.height * 0.5)
 
      this.meshes.push(mesh)
 
      this.scene.addChild(mesh)
    }
  }
}

然后只需要对它们应用无限滚动逻辑,并遵循与 Media 类中相同的方向进行验证:

update (scroll, direction) {
  this.meshes.forEach(mesh => {
    mesh.position.x = mesh.x - scroll.current * mesh.speed - mesh.xExtra
 
    const viewportOffset = this.viewport.width * 0.5
    const widthTotal = this.viewport.width + mesh.scale.x
 
    mesh.isBefore = mesh.position.x < -viewportOffset
    mesh.isAfter = mesh.position.x > viewportOffset
 
    if (direction === 'right' && mesh.isBefore) {
      mesh.xExtra -= widthTotal
 
      mesh.isBefore = false
      mesh.isAfter = false
    }
 
    if (direction === 'left' && mesh.isAfter) {
      mesh.xExtra += widthTotal
 
      mesh.isBefore = false
      mesh.isAfter = false
    }
 
    mesh.position.y += 0.05 * mesh.speed
 
    if (mesh.position.y > this.viewport.height * 0.5 + mesh.scale.y) {
      mesh.position.y -= this.viewport.height + mesh.scale.y
    }
  })
}

就这么简单,现在我们的代码终于完成了。

173382ede7319973.gif


本文首发微信公众号:前端先锋

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章


欢迎继续阅读本专栏其它高赞文章:


查看原文

赞 5 收藏 4 评论 0

疯狂的技术宅 收藏了文章 · 3月29日

弄死 Node.js 进程有几种方法

有几个原因会导致 Node.js 进程终止。其中一些是可以避免的,例如抛出错误时,而另一些是无法防止的,例如内存不足。全局 process 是一个 Event Emitter 实例,当执行正常退出时,将发出一个 exit 事件。然后程序代码可以通过侦听这个事件来执行最后的同步清理工作。

下面是可以主动触发进程终止的一些方法:

操作例子
手动流程退出process.exit(1)
未捕获的异常throw new Error()
未兑现的 promisePromise.reject()
忽略的错误事件EventEmitter#emit('error')
未处理的信号$ kill <PROCESS_ID>

其中有许多是属于偶然被触发的,例如未捕获的错误或未处理的 promise,但是其中也有为了直接使进程终止而创建的。

进程退出

使用 process.exit(code) 来终止进程是最直接的方法。这在当你知道自己的过程已经到了生命周期的尽头时非常有用。 code 值是可选的,默认值为0,最大可以设为 255。0 表示进程运行成功,而任何非零的数字都表示发生了问题。这些值可以被许多不同的外部工具使用。例如当测试套件运行时,非零值表示测试失败。

直接调用 process.exit() 时,不会向控制台写入任何隐式文本。如果你编写了以错误表示形式调用此方法的代码,则你的代码应该用户输出错误来帮助他们解决问题。例如运行以下代码:

$ node -e "process.exit(42)"
$ echo $?

在这种情况下,单行的 Node.js 程序不会输出任何信息,尽管 shell 程序确实会打印退出状态。遇到这样的进程退出,用户将无法理解究竟发生了什么事情。所以要参考下面这段程序配置错误时会执行的代码:

function checkConfig(config) {
  if (!config.host) {
    console.error("Configuration is missing 'host' parameter!");
    process.exit(1);
  }
}

在这种情况下,用户没会很清楚发生了什么。他们运行这个程序,将错误输出到控制台上,并且他们能够纠正这个问题。

process.exit() 方法非常强大。尽管它在程序代码中有自己的用途,但实际上绝对不应该将其引入可重用的库中。如果在库中确实发生了错误,则应抛出这个错误,以便程序可以决定应该如何处理它。

exceprion、rejection 和发出的 Error

虽然 process.exit() 很有用,但对于运行时错误,你需要使用其他工具。例如当程序正在处理 HTTP 请求时,一般来说错误不应该终止进程,而是仅返回错误响应。发生错误的位置信息也很有用,这正是应该抛出 Error 对象的地方。

Error 类的实例包含对导致错误的原因有用的元数据,例如栈跟踪信息和消息字符串。从 Error 扩展你自己的错误类是很常见的操作。单独实例化 Error 不会有太多副作用,如果发生错误则必须抛出。

在使用 throw 关键字或发生某些逻辑错误时,将引发 Error。发生这种情况时,当前栈将会“展开”,这意味着每个函数都会退出,直到一个调用函数将调用包装在 try/catch 语句中为止。遇到此语句后,将调用 catch 分支。如果错误没有被包含在 try/catch 中,则该错误被视为未捕获。

虽然你应该使用带有 Errorthrow 关键字,例如 throw new Error('foo'),但从技术上讲,你可以抛出任何东西。一旦抛出了什么东西,它就被认为是一个例外。抛出 Error 实例非常重要,因为捕获这些错误的代码很可能会期望得到错误属性。

Node.js 内部库中常用的另一种模式是提供一个 .code 属性,该属性是一个字符串值,在发行版之间应保持一致。比如错误的 .code 值是 ERR_INVALID_URI,即使是供人类可读的 .message 属性可能会更改,但这个 code 值也不应被更改。

可悲的是,一种更常用的区分错误的模式是检查 .message 属性,这个属性通常是动态的,因为可能回需要修改拼写错误。这种方法是很冒险的,也是容易出错的。 Node.js 生态中没有完美的解决方案来区分所有库中的错误。

当引发未捕获的错误时,控制台中将打印栈跟踪信息,并且进程将回以退出状态 1 终止。这是此类异常的例子:

/tmp/foo.js:1
throw new TypeError('invalid foo');
^
Error: invalid foo
    at Object.<anonymous> (/tmp/foo.js:2:11)
    ... TRUNCATED ...
    at internal/main/run_main_module.js:17:47

上面的栈跟踪片段表明错误发生在名为 foo.js 的文件的第 2 行第 11 列。

全局的 process 是一个事件发射器,可以通过侦听 uncaughtException 事件来拦截未捕获的错误。下面是一个使用它的例子,在退出前拦截错误以发送异步消息:

const logger = require('./lib/logger.js');
process.on('uncaughtException', (error) => {
  logger.send("An uncaught exception has occured", error, () => {
    console.error(error);
    process.exit(1);
  });
});

Promise 拒绝与抛出错误非常相似。如果 Promise 中的 reject() 方法被调用,或者在异步函数中引发了错误,则 Promise 可以拒绝。在这方面,以下两个例子大致相同:

Promise.reject(new Error('oh no'));

(async () => {
  throw new Error('oh no');
})();

这是输出到控制台的消息:

(node:52298) UnhandledPromiseRejectionWarning: Error: oh no
    at Object.<anonymous> (/tmp/reject.js:1:16)
    ... TRUNCATED ...
    at internal/main/run_main_module.js:17:47
(node:52298) UnhandledPromiseRejectionWarning: Unhandled promise
  rejection. This error originated either by throwing inside of an
  async function without a catch block, or by rejecting a promise
  which was not handled with .catch().

与未捕获的异常不同,从 Node.js v14 开始,这些 rejection 不会使进程崩溃。在未来的 Node.js 版本中,会使当前进程崩溃。当这些未处理的 rejection 发生时,你还可以拦截事件,侦听 process 对象上的另一个事件:

process.on('unhandledRejection', (reason, promise) => {});

事件发射器是 Node.js 中的常见模式,许多对象实例都从这个基类扩展而来,并在库和程序中使用。它们非常欢迎,值得和 error 与 rejection 放在一起讨论。

当事件发射器发出没有侦听器的 error 事件时,将会抛出所发出的参数。然后将抛出出一个错误并导致进程退出:

events.js:306
    throw err; // Unhandled 'error' event
    ^
Error [ERR_UNHANDLED_ERROR]: Unhandled error. (undefined)
    at EventEmitter.emit (events.js:304:17)
    at Object.<anonymous> (/tmp/foo.js:1:40)
    ... TRUNCATED ...
    at internal/main/run_main_module.js:17:47 {
  code: 'ERR_UNHANDLED_ERROR',
  context: undefined
}

确保在你使用的事件发射器实例中侦听 error 事件,以便你的程序可以正常处理事件而不会崩溃。

信号

信号是操作系统提供的机制,用于把用数字表示的消息从一个程序发送到另一个程序。这些数字通常用等价的常量字符串来表示。例如,信号 SIGKILL 代表数字信号 9。信号可以有不同的用途,但通常用于终止程序。

不同的操作系统可以定义不同的信号,但是下面列表中的信号一般是通用的:

名称编号可处理Node.js 默认信号用途
SIGHUP1终止父终端已关闭
SIGINT2终止终端试图中断,按下 Ctrl + C
SIGQUIT3终止终端试图退出,按下 Ctrl + D
SIGKILL9终止进程被强行杀死
SIGUSR110启动调试器用户定义的信号1
SIGUSR212终止用户定义的信号2
SIGTERM12终止代表优雅的终止
SIGSTOP19终止进程被强行停止

如果程序可以选择实现信号处理程序,则 Handleable 一列则为。为的两个信号无法处理。 Node.js 默认 这一列告诉你在收到信号时,Node.js 程序的默认操作是什么。最后一个信号用途指出了信号对应的作用。

在 Node.js 程序中处理这些信号可以通过侦听 process 对象上的更多事件来完成:

#!/usr/bin/env node
console.log(`Process ID: ${process.pid}`);
process.on('SIGHUP', () => console.log('Received: SIGHUP'));
process.on('SIGINT', () => console.log('Received: SIGINT'));
setTimeout(() => {}, 5 * 60 * 1000); // keep process alive

在终端窗口中运行这个程序,然后按 Ctrl + C,这个进程不会被终止。它将会声明已接收到 SIGINT 信号。切换到另一个终端窗口,并根据输出的进程 ID 值执行以下命令:

$ kill -s SIGHUP <PROCESS_ID>

这演示了一个程序怎样向另一个程序发送信号,并且在第一个终端中运行的 Node.js 程序中输出它所接收到的 SIGHUP 信号。

你可能已经猜到了,Node.js 也能把命令发送到其他程序。可以用下面的命令以把信号从临时的 Node.js 进程发送到你现有的进程:

$ node -e "process.kill(<PROCESS_ID>, 'SIGHUP')"

这还会在你的第一个程序中显示 SIGHUP 消息。现在,如果你想终止第一个进程,要运行下面的命令向其发送不能处理的 SIGKILL 信号:

$ kill -9 <PROCESS_ID>

这时程序应该结束。

这些信号在 Node.js 程序中经常用于处理正常的关闭事件。例如,当 Kubernetes Pod 终止时,它将向程序发送 SIGTERM 信号,之后启动 30 秒计时器。然后程序可以在这 30 秒内正常关闭自己,关闭连接并保存数据。如果该进程在此计时器后仍保持活动状态,则 Kubernetes 将向其发送一个 SIGKILL

173382ede7319973.gif


本文首发微信公众号:前端先锋

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章


欢迎继续阅读本专栏其它高赞文章:


查看原文

疯狂的技术宅 发布了文章 · 3月29日

弄死 Node.js 进程有几种方法

有几个原因会导致 Node.js 进程终止。其中一些是可以避免的,例如抛出错误时,而另一些是无法防止的,例如内存不足。全局 process 是一个 Event Emitter 实例,当执行正常退出时,将发出一个 exit 事件。然后程序代码可以通过侦听这个事件来执行最后的同步清理工作。

下面是可以主动触发进程终止的一些方法:

操作例子
手动流程退出process.exit(1)
未捕获的异常throw new Error()
未兑现的 promisePromise.reject()
忽略的错误事件EventEmitter#emit('error')
未处理的信号$ kill <PROCESS_ID>

其中有许多是属于偶然被触发的,例如未捕获的错误或未处理的 promise,但是其中也有为了直接使进程终止而创建的。

进程退出

使用 process.exit(code) 来终止进程是最直接的方法。这在当你知道自己的过程已经到了生命周期的尽头时非常有用。 code 值是可选的,默认值为0,最大可以设为 255。0 表示进程运行成功,而任何非零的数字都表示发生了问题。这些值可以被许多不同的外部工具使用。例如当测试套件运行时,非零值表示测试失败。

直接调用 process.exit() 时,不会向控制台写入任何隐式文本。如果你编写了以错误表示形式调用此方法的代码,则你的代码应该用户输出错误来帮助他们解决问题。例如运行以下代码:

$ node -e "process.exit(42)"
$ echo $?

在这种情况下,单行的 Node.js 程序不会输出任何信息,尽管 shell 程序确实会打印退出状态。遇到这样的进程退出,用户将无法理解究竟发生了什么事情。所以要参考下面这段程序配置错误时会执行的代码:

function checkConfig(config) {
  if (!config.host) {
    console.error("Configuration is missing 'host' parameter!");
    process.exit(1);
  }
}

在这种情况下,用户没会很清楚发生了什么。他们运行这个程序,将错误输出到控制台上,并且他们能够纠正这个问题。

process.exit() 方法非常强大。尽管它在程序代码中有自己的用途,但实际上绝对不应该将其引入可重用的库中。如果在库中确实发生了错误,则应抛出这个错误,以便程序可以决定应该如何处理它。

exceprion、rejection 和发出的 Error

虽然 process.exit() 很有用,但对于运行时错误,你需要使用其他工具。例如当程序正在处理 HTTP 请求时,一般来说错误不应该终止进程,而是仅返回错误响应。发生错误的位置信息也很有用,这正是应该抛出 Error 对象的地方。

Error 类的实例包含对导致错误的原因有用的元数据,例如栈跟踪信息和消息字符串。从 Error 扩展你自己的错误类是很常见的操作。单独实例化 Error 不会有太多副作用,如果发生错误则必须抛出。

在使用 throw 关键字或发生某些逻辑错误时,将引发 Error。发生这种情况时,当前栈将会“展开”,这意味着每个函数都会退出,直到一个调用函数将调用包装在 try/catch 语句中为止。遇到此语句后,将调用 catch 分支。如果错误没有被包含在 try/catch 中,则该错误被视为未捕获。

虽然你应该使用带有 Errorthrow 关键字,例如 throw new Error('foo'),但从技术上讲,你可以抛出任何东西。一旦抛出了什么东西,它就被认为是一个例外。抛出 Error 实例非常重要,因为捕获这些错误的代码很可能会期望得到错误属性。

Node.js 内部库中常用的另一种模式是提供一个 .code 属性,该属性是一个字符串值,在发行版之间应保持一致。比如错误的 .code 值是 ERR_INVALID_URI,即使是供人类可读的 .message 属性可能会更改,但这个 code 值也不应被更改。

可悲的是,一种更常用的区分错误的模式是检查 .message 属性,这个属性通常是动态的,因为可能回需要修改拼写错误。这种方法是很冒险的,也是容易出错的。 Node.js 生态中没有完美的解决方案来区分所有库中的错误。

当引发未捕获的错误时,控制台中将打印栈跟踪信息,并且进程将回以退出状态 1 终止。这是此类异常的例子:

/tmp/foo.js:1
throw new TypeError('invalid foo');
^
Error: invalid foo
    at Object.<anonymous> (/tmp/foo.js:2:11)
    ... TRUNCATED ...
    at internal/main/run_main_module.js:17:47

上面的栈跟踪片段表明错误发生在名为 foo.js 的文件的第 2 行第 11 列。

全局的 process 是一个事件发射器,可以通过侦听 uncaughtException 事件来拦截未捕获的错误。下面是一个使用它的例子,在退出前拦截错误以发送异步消息:

const logger = require('./lib/logger.js');
process.on('uncaughtException', (error) => {
  logger.send("An uncaught exception has occured", error, () => {
    console.error(error);
    process.exit(1);
  });
});

Promise 拒绝与抛出错误非常相似。如果 Promise 中的 reject() 方法被调用,或者在异步函数中引发了错误,则 Promise 可以拒绝。在这方面,以下两个例子大致相同:

Promise.reject(new Error('oh no'));

(async () => {
  throw new Error('oh no');
})();

这是输出到控制台的消息:

(node:52298) UnhandledPromiseRejectionWarning: Error: oh no
    at Object.<anonymous> (/tmp/reject.js:1:16)
    ... TRUNCATED ...
    at internal/main/run_main_module.js:17:47
(node:52298) UnhandledPromiseRejectionWarning: Unhandled promise
  rejection. This error originated either by throwing inside of an
  async function without a catch block, or by rejecting a promise
  which was not handled with .catch().

与未捕获的异常不同,从 Node.js v14 开始,这些 rejection 不会使进程崩溃。在未来的 Node.js 版本中,会使当前进程崩溃。当这些未处理的 rejection 发生时,你还可以拦截事件,侦听 process 对象上的另一个事件:

process.on('unhandledRejection', (reason, promise) => {});

事件发射器是 Node.js 中的常见模式,许多对象实例都从这个基类扩展而来,并在库和程序中使用。它们非常欢迎,值得和 error 与 rejection 放在一起讨论。

当事件发射器发出没有侦听器的 error 事件时,将会抛出所发出的参数。然后将抛出出一个错误并导致进程退出:

events.js:306
    throw err; // Unhandled 'error' event
    ^
Error [ERR_UNHANDLED_ERROR]: Unhandled error. (undefined)
    at EventEmitter.emit (events.js:304:17)
    at Object.<anonymous> (/tmp/foo.js:1:40)
    ... TRUNCATED ...
    at internal/main/run_main_module.js:17:47 {
  code: 'ERR_UNHANDLED_ERROR',
  context: undefined
}

确保在你使用的事件发射器实例中侦听 error 事件,以便你的程序可以正常处理事件而不会崩溃。

信号

信号是操作系统提供的机制,用于把用数字表示的消息从一个程序发送到另一个程序。这些数字通常用等价的常量字符串来表示。例如,信号 SIGKILL 代表数字信号 9。信号可以有不同的用途,但通常用于终止程序。

不同的操作系统可以定义不同的信号,但是下面列表中的信号一般是通用的:

名称编号可处理Node.js 默认信号用途
SIGHUP1终止父终端已关闭
SIGINT2终止终端试图中断,按下 Ctrl + C
SIGQUIT3终止终端试图退出,按下 Ctrl + D
SIGKILL9终止进程被强行杀死
SIGUSR110启动调试器用户定义的信号1
SIGUSR212终止用户定义的信号2
SIGTERM12终止代表优雅的终止
SIGSTOP19终止进程被强行停止

如果程序可以选择实现信号处理程序,则 Handleable 一列则为。为的两个信号无法处理。 Node.js 默认 这一列告诉你在收到信号时,Node.js 程序的默认操作是什么。最后一个信号用途指出了信号对应的作用。

在 Node.js 程序中处理这些信号可以通过侦听 process 对象上的更多事件来完成:

#!/usr/bin/env node
console.log(`Process ID: ${process.pid}`);
process.on('SIGHUP', () => console.log('Received: SIGHUP'));
process.on('SIGINT', () => console.log('Received: SIGINT'));
setTimeout(() => {}, 5 * 60 * 1000); // keep process alive

在终端窗口中运行这个程序,然后按 Ctrl + C,这个进程不会被终止。它将会声明已接收到 SIGINT 信号。切换到另一个终端窗口,并根据输出的进程 ID 值执行以下命令:

$ kill -s SIGHUP <PROCESS_ID>

这演示了一个程序怎样向另一个程序发送信号,并且在第一个终端中运行的 Node.js 程序中输出它所接收到的 SIGHUP 信号。

你可能已经猜到了,Node.js 也能把命令发送到其他程序。可以用下面的命令以把信号从临时的 Node.js 进程发送到你现有的进程:

$ node -e "process.kill(<PROCESS_ID>, 'SIGHUP')"

这还会在你的第一个程序中显示 SIGHUP 消息。现在,如果你想终止第一个进程,要运行下面的命令向其发送不能处理的 SIGKILL 信号:

$ kill -9 <PROCESS_ID>

这时程序应该结束。

这些信号在 Node.js 程序中经常用于处理正常的关闭事件。例如,当 Kubernetes Pod 终止时,它将向程序发送 SIGTERM 信号,之后启动 30 秒计时器。然后程序可以在这 30 秒内正常关闭自己,关闭连接并保存数据。如果该进程在此计时器后仍保持活动状态,则 Kubernetes 将向其发送一个 SIGKILL

173382ede7319973.gif


本文首发微信公众号:前端先锋

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章


欢迎继续阅读本专栏其它高赞文章:


查看原文

赞 3 收藏 3 评论 0

疯狂的技术宅 收藏了文章 · 3月26日

Web 安全简明入门指南

Web 安全已经是 Web 开发中一个重要的组成部分,而许多程序猿往往希望专注于程序的实现,而忽略了信息安全的实质。如果没有严谨地考虑到信息安全问题,等出了乱子之后反而会造成更严重的损失。所以要在开发网络应用时更注重 Web 安全,甚至努力成为一个白帽黑客。

常见 Web 信息安全

一般来说 Web 安全需要符合三点安全要素:

  1. 保密性:通过加密等方法确保数据的保密性
  2. 完整性:要求用户取得的资料是完整而不可被篡改的
  3. 可用性:保证网站服务的持续可访问性

以下是常见的影响 Web 安全的攻击手段:

1. SQL注入

使用恶意的 SQL 语法去影响数据库内容:

// “--” 是 SQL 语句的注释符号
/user/profile?id=1";DROP TABLE user--

SELECT * FROM USER WHERE id = "1"; DROP TABLE user--

用户登录:

// password" AND 1=1-- 
SELECT * FROM USER WHERE username = "Mark"; AND 1=1-- AND PASSWORD="1234"

简单的防范手段:

不信任用户输入的数据,确保用户输入必须经过检查,目前许多成熟的 Web 框架都支持ORM 服务,大部分都基本防范了 SQL 注入。

2. XSS(Cross-Site Scripting)

XSS 也很容易将恶意代码植入到网页,让看到网页的用户受到干扰,常见的重灾区包括BBS、留言板等。实际上 XSS 的概念很简单,通过表单输入建立一些恶意网址、恶意图片网址或把 JavsScript 代码注入到 HTML中,当用户浏览页面时就会被触发。

<IMG SRC="" onerror="alert('XSS')">

更多关于XSS资料可以参考 XSS Filter Evasion Cheat Sheethttps://www.owasp.org/index.p...)。另外也有中文版(链接是乌云镜像备份,顺便怀念一下)

简易的防范手段方式:

不信任用户输入的任何资料,将所有输入内容编码并过滤。

3. CSRF

CSRF 跨站请求伪造又被称为 one-click attack 或者 session riding,通常缩写为CSRF 或 XSRF。在已登录的 Web 应用上执行非本意的操作的攻击方法。

举一个例子:假如一家银行执行转帐操作的 URL 地址如下:

http://www.examplebank.com/withdraw?account=AccoutName&amount=10000&for=PayeeName

那么,一个恶意攻击者可以在另一个网站上放置如下代码:

<img data-original="http://www.examplebank.com/withdraw?account=Mark&amount=10000&for=Bob">

如果用户的登录信息尚未过期的话,就会损失 10000 元。

简单的防范手段:

  1. 检查 Referer 头字段

    这是比较基本的验证方式,通常 HTTP 头中有一个 Referer 字段,它的值应该和请求位置在同一个域下,因此可以通过验证网址是否相同来验证是不是恶意请求,但是有被伪造的可能。

  2. 添加验证 token

    现在许多 Web 框架都提供在表单加入由服务器生成的随机验证 CSRF 的代码,可以辅助防止 CSRF 攻击。

4. DoS

DoS 攻击具体可以分为两种形式:带宽消耗型以及资源消耗型,它们都是通过大量合法或伪造的请求大量消耗网络以及硬件资源,以达到瘫痪网络和系统的目的。

带宽消耗型又分为分布式拒绝服务攻击和放大攻击:分布式拒绝服务攻击的特点是利用僵尸网络向受害者的服务器发送大量流量,目的在于占用其带宽。放大攻击和分布式拒绝服务攻击类似,是通过恶意放大流量限制受害者系统的带宽;其特点是利用僵尸网络通过伪造的源 IP(即攻击目标)向某些存在漏洞的服务器发送请求,服务器在处理请求后向伪造的源 IP 传送应答包,由于这些服务的特殊性导致应答包比请求包更大,因此只使用少量的带宽就可以使服务器器传送大量的响应到目标主机上。

资源消耗型又分为协议分析攻击(SYN Flood)、LAND攻击、CC攻击、僵尸网络攻击,应用程序级洪水攻击(Application level floods)等。

简易的防范手段:

  1. 防火墙

设定规则阻挡简单攻击

  1. 交换机

大多交换机都有限制访问的控制功能

  1. 路由器
    大多交换机都有限制访问的控制功能
  2. 黑洞启动
    将请求转发到不存在的位置

5.文件上传漏洞

许多 web 应用都允许用户把文件上传到服务器,由于我们不知道用户会上传什么类型的文件,如果不加注意的话就会引发很大的问题。

简单的防范手段:

  1. 阻止非法文件上传

    • 设定文件名白名单
    • 判断文件标头
  2. 阻止非法文件执行

    • 存储目录与 Web 应用分离
    • 存储目录无执行权限
    • 文件重命名
    • 图片压缩
  3. 加密安全

6. 加密安全

大多数网站都会提供会员注册的功能,要注意不要将密码的明文存入数据库。要如果你所登陆的网站在你忘记密码时,取回口令的功能会把密码明文发到你的邮箱,那么这个网站十有八九是没有做加密或者是用的是可逆加密算法。这时你的密码很容易会出现在“我的密码没加密”(http://plainpass.com/)这个网站上。不过即使将密码加密也未必安全,网上存在大量的破解网站(http://www.cmd5.com/),使用彩虹表就可以破解加密的密码。所以一般会针对不同用户使用随机产生的 salt 字符串加盐只后再进行加密的方式来提高密码的强健性。

sha3(salt + gap + password)

简单的入侵流程

  1. 侦查(Reconnaissance)

攻击者准备攻击之前进行的调查行为,使用搜索引擎或社工手段寻找目标的相关信息,方便之后的攻击

  1. 扫描(Scanning)
    扫描目标主机的弱点,获取服务器操作系统、服务和运行状况等相关信息
  2. 获取权限(Gaining Access)
    利用系统弱点获得服务器权限
  3. 维护权限(Maintaining Access)
    维护当前获取到的权限,以便日后再次操作
  4. 清除痕迹(Clearing Tracks)
    清除入侵的痕迹

总结

随着网络技术的发展,信息安全会越来越重要,当有系统上线时,最好按照《信息安全检查清单》(https://github.com/FallibleIn...)检查一下有哪些安全事项是被你忽略掉的。

173382ede7319973.gif


本文首发微信公众号:前端先锋

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章


欢迎继续阅读本专栏其它高赞文章:


查看原文

疯狂的技术宅 发布了文章 · 3月26日

Web 安全简明入门指南

Web 安全已经是 Web 开发中一个重要的组成部分,而许多程序猿往往希望专注于程序的实现,而忽略了信息安全的实质。如果没有严谨地考虑到信息安全问题,等出了乱子之后反而会造成更严重的损失。所以要在开发网络应用时更注重 Web 安全,甚至努力成为一个白帽黑客。

常见 Web 信息安全

一般来说 Web 安全需要符合三点安全要素:

  1. 保密性:通过加密等方法确保数据的保密性
  2. 完整性:要求用户取得的资料是完整而不可被篡改的
  3. 可用性:保证网站服务的持续可访问性

以下是常见的影响 Web 安全的攻击手段:

1. SQL注入

使用恶意的 SQL 语法去影响数据库内容:

// “--” 是 SQL 语句的注释符号
/user/profile?id=1";DROP TABLE user--

SELECT * FROM USER WHERE id = "1"; DROP TABLE user--

用户登录:

// password" AND 1=1-- 
SELECT * FROM USER WHERE username = "Mark"; AND 1=1-- AND PASSWORD="1234"

简单的防范手段:

不信任用户输入的数据,确保用户输入必须经过检查,目前许多成熟的 Web 框架都支持ORM 服务,大部分都基本防范了 SQL 注入。

2. XSS(Cross-Site Scripting)

XSS 也很容易将恶意代码植入到网页,让看到网页的用户受到干扰,常见的重灾区包括BBS、留言板等。实际上 XSS 的概念很简单,通过表单输入建立一些恶意网址、恶意图片网址或把 JavsScript 代码注入到 HTML中,当用户浏览页面时就会被触发。

<IMG SRC="" onerror="alert('XSS')">

更多关于XSS资料可以参考 XSS Filter Evasion Cheat Sheethttps://www.owasp.org/index.p...)。另外也有中文版(链接是乌云镜像备份,顺便怀念一下)

简易的防范手段方式:

不信任用户输入的任何资料,将所有输入内容编码并过滤。

3. CSRF

CSRF 跨站请求伪造又被称为 one-click attack 或者 session riding,通常缩写为CSRF 或 XSRF。在已登录的 Web 应用上执行非本意的操作的攻击方法。

举一个例子:假如一家银行执行转帐操作的 URL 地址如下:

http://www.examplebank.com/withdraw?account=AccoutName&amount=10000&for=PayeeName

那么,一个恶意攻击者可以在另一个网站上放置如下代码:

<img data-original="http://www.examplebank.com/withdraw?account=Mark&amount=10000&for=Bob">

如果用户的登录信息尚未过期的话,就会损失 10000 元。

简单的防范手段:

  1. 检查 Referer 头字段

    这是比较基本的验证方式,通常 HTTP 头中有一个 Referer 字段,它的值应该和请求位置在同一个域下,因此可以通过验证网址是否相同来验证是不是恶意请求,但是有被伪造的可能。

  2. 添加验证 token

    现在许多 Web 框架都提供在表单加入由服务器生成的随机验证 CSRF 的代码,可以辅助防止 CSRF 攻击。

4. DoS

DoS 攻击具体可以分为两种形式:带宽消耗型以及资源消耗型,它们都是通过大量合法或伪造的请求大量消耗网络以及硬件资源,以达到瘫痪网络和系统的目的。

带宽消耗型又分为分布式拒绝服务攻击和放大攻击:分布式拒绝服务攻击的特点是利用僵尸网络向受害者的服务器发送大量流量,目的在于占用其带宽。放大攻击和分布式拒绝服务攻击类似,是通过恶意放大流量限制受害者系统的带宽;其特点是利用僵尸网络通过伪造的源 IP(即攻击目标)向某些存在漏洞的服务器发送请求,服务器在处理请求后向伪造的源 IP 传送应答包,由于这些服务的特殊性导致应答包比请求包更大,因此只使用少量的带宽就可以使服务器器传送大量的响应到目标主机上。

资源消耗型又分为协议分析攻击(SYN Flood)、LAND攻击、CC攻击、僵尸网络攻击,应用程序级洪水攻击(Application level floods)等。

简易的防范手段:

  1. 防火墙

设定规则阻挡简单攻击

  1. 交换机

大多交换机都有限制访问的控制功能

  1. 路由器
    大多交换机都有限制访问的控制功能
  2. 黑洞启动
    将请求转发到不存在的位置

5.文件上传漏洞

许多 web 应用都允许用户把文件上传到服务器,由于我们不知道用户会上传什么类型的文件,如果不加注意的话就会引发很大的问题。

简单的防范手段:

  1. 阻止非法文件上传

    • 设定文件名白名单
    • 判断文件标头
  2. 阻止非法文件执行

    • 存储目录与 Web 应用分离
    • 存储目录无执行权限
    • 文件重命名
    • 图片压缩
  3. 加密安全

6. 加密安全

大多数网站都会提供会员注册的功能,要注意不要将密码的明文存入数据库。要如果你所登陆的网站在你忘记密码时,取回口令的功能会把密码明文发到你的邮箱,那么这个网站十有八九是没有做加密或者是用的是可逆加密算法。这时你的密码很容易会出现在“我的密码没加密”(http://plainpass.com/)这个网站上。不过即使将密码加密也未必安全,网上存在大量的破解网站(http://www.cmd5.com/),使用彩虹表就可以破解加密的密码。所以一般会针对不同用户使用随机产生的 salt 字符串加盐只后再进行加密的方式来提高密码的强健性。

sha3(salt + gap + password)

简单的入侵流程

  1. 侦查(Reconnaissance)

攻击者准备攻击之前进行的调查行为,使用搜索引擎或社工手段寻找目标的相关信息,方便之后的攻击

  1. 扫描(Scanning)
    扫描目标主机的弱点,获取服务器操作系统、服务和运行状况等相关信息
  2. 获取权限(Gaining Access)
    利用系统弱点获得服务器权限
  3. 维护权限(Maintaining Access)
    维护当前获取到的权限,以便日后再次操作
  4. 清除痕迹(Clearing Tracks)
    清除入侵的痕迹

总结

随着网络技术的发展,信息安全会越来越重要,当有系统上线时,最好按照《信息安全检查清单》(https://github.com/FallibleIn...)检查一下有哪些安全事项是被你忽略掉的。

173382ede7319973.gif


本文首发微信公众号:前端先锋

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章


欢迎继续阅读本专栏其它高赞文章:


查看原文

赞 12 收藏 11 评论 0

疯狂的技术宅 收藏了文章 · 3月25日

js 引擎是怎样将 var 转换为 JIT 的

作者:Kevin S

翻译:疯狂的技术宅

原文:https://alistapart.com/articl...

绑定到字节码

对于现代 Web 程序,浏览器首先看到的 JavaScript 通常不是前端程序员写的。相反,它很可能是由 webpack 之类的工具产生的捆绑包,而且可能是一个相当大的捆绑包,其中包含 UI 框架,例如 React,各种 polyfills(在较旧的浏览器中模拟新平台功能的库),以及在 npm 上找到的各种软件包。浏览器的 JavaScript 引擎面临的第一个挑战是将一大堆文本转换为可以在虚拟机上执行的指令。正是由于它需要解析代码,而且用户正在等待 JavaScript 进行交互,所以它的执行速度必须很快才行。

在高级方面,JavaScript 引擎像其他语言编译器一样去解析代码。首先,输入的文本流被分解为名为 token 的块。每个 token 代表语法结构中的一个有意义的单元,类似于自然语言中的单词和标点符号。然后,将这些 token 输入到自上而下的解析器中,生成生成表示程序的树结构。语言设计师和编译器工程师喜欢把这种树结构称为 AST(抽象语法树)。然后就可以通过分析生成的 AST 来生成称为字节码的虚拟机指令列表。

image.png

生成 AST 的过程是 JavaScript 引擎更直接的工作之一。不过它也可能很慢。还记得本文开始时所提到的一大堆代码吗? JavaScript 引擎必须在用户能够开始与站点进行交互之前解析整个捆绑包并构建语法树。对于初始页面加载,其中大部分代码可能是不必要的,甚至根本无法执行其中的某些代码!

不过好在编译器工程师发明了各种技巧来加快处理速度。首先,某些引擎在后台线程中解析代码,从而释放主 UI 线程用于其他计算。其次,现代引擎将通过使用名为“延迟解析”或“延迟编译”的技术,尽可能地延迟内存中语法树的创建。

它的工作方式是这样的:如果引擎看到一个可能在一段时间内不执行的函数定义,它会对函数体进行快速的“丢弃”解析。这种一次性分析能够发现可能隐藏在代码内的所有语法错误,但不会生成 AST。稍后,当第一次调用该函数时,会再次解析这段代码。这次,引擎将生成执行所需的完整 AST 和字节码。在 JavaScript 的世界中,执行两次有时比执行一次更快!

但是,最好的优化是使我们完全绕开所有耗时处理的优化。对于 JavaScript 编译,这意味着完全跳过了解析步骤。一些 JavaScript 引擎会尝试缓存生成的字节码,以备以后用户再次访问该网站时进行重用。这并不那么简单。随着网站的更新,JavaScript 包可能会经常发生改变,浏览器必须仔细权衡序列化字节码的成本与缓存带来的性能提升之间的关系。

运行时的字节码

现在有了字节码,就可以开始执行了。在当今的 JavaScript 引擎中,在解析过程中生成的字节码首先被送到名为解释器的虚拟机中。解释器有点像用软件实现的 CPU。它一次查看一条字节码指令,然后决定要执行的实际机器指令以及下一步要执行的指令。

JavaScript 编程语言的结构和行为在名为 ECMA-262 的文档中进行了定义。其中结构部分被称为“语法”,行为部分为“语义”。编程语言的语义几乎都是由伪代码编写的算法定义的。假设我们是编译器工程师,正在实现带符号的右移运算符>>)以下则是规格说明(以下脚本引自 ECMA-262 ):

ShiftExpression : ShiftExpression >> AdditiveExpression

  1. Let lref be the result of evaluating ShiftExpression.
  2. Let lval be ? GetValue(lref).
  3. Let rref be the result of evaluating AdditiveExpression.
  4. Let rval be ? GetValue(rref).
  5. Let lnum be ? ToInt32(lval).
  6. Let rnum be ? ToUint32(rval).
  7. Let shiftCount be the result of masking out all but the least significant 5 bits of rnum, that is, compute rnum & 0x1F.
  8. Return the result of performing a sign-extending right shift of lnum by shiftCount bits. The most significant bit is propagated. The result is a signed 32-bit integer.

前六个步骤将操作数( >> 两侧的值)转换为32位整数,然后执行实际的移位操作。

但是如果真的完全按照规范中的描述去实现算法,那么做出来的解释器会很慢。下面以从 JavaScript 对象获取属性值的简单操作为例。

从概念上讲,JavaScript 中的对象就像字典一样。每个属性均以字符串名作为关键字。对象也可以有原型对象

image.png

如果某个对象没有给定字符关键字的条目,那么就需要在原型中寻找该键。不断重复这个操作,直到找到所需的关键字或到达原型链的末尾为止。

这就导致了每次想从对象中获取属性值时,可能要做很多工作。

JavaScript 引擎中用于加速动态属性查找的策略称为内联缓存。内联缓存最早是在 1980 年代为 Smalltalk 语言所开发的。其基本思想是,先前属性查找操作的结果可以直接存储在生成的字节码指令中。

为了了解它的工作原理,让我们闭上眼睛,想象 JavaScript 引擎是一座充满魔法的大型图书馆。当我们走进去时,会注意到里面塞满了到处飞来飞去的书(即对象)。每个对象都有一个可识别的形状,这个形状便确定了其属性的存储位置。

假设我们正在按照书单上所记录的一系列字节码指令执行程序。下一条指令告诉我们从某个对象获取名为 x 的属性的值。你抓住该对象,找出 x 的存储位置,然后发现它已存储在该对象的第二个数据插槽中。

你会发现,具有相同形状的所有对象在其第二个数据插槽中都有 x 属性。拿出你的笔,在字节码书单上做一个注释,标记出对象的形状和 x 属性的位置。下次再看到这个标记时,只需检查对象的形状就行了。如果形状与你在字节码注释中所标记的形状匹配,不需要检查对象就可以准确知道数据的位置。这样你就实现了单态内联缓存

但是,如果对象的形状与我们的字节码注释不匹配怎么办?这时可以通过制作一张小表格,并把看到的每种形状作为一行记录的方式来解决这个问题。当每看到一个新形状时,就把它作为一行添加到表中。这样就实现了一个多态内联缓存。它的速度不如单态缓存快,并且在书单上会占用更多的空间,但是如果行数不多,效果会非常好。

如果最后生成的表太大,就要把它删除掉,并做个注释来提醒自己不要再纠结这个指令的内联缓存了。用编译器的术语来说,实现了一个复态调用点(megamorphic callsite)

一般来说单态代码非常快,多态代码差不多一样快,而复态代码则往往很慢。

  • 单态:快如疾风
  • 多态:动若脱兔
  • 复态:慢似乌龟

即时编译( JIT)

解释器的优点在于可以快速开始执行代码,对于仅运行一两次的代码,这种“软件 CPU”的执行速度还是可以接受的。但是对于“热代码”(运行数百、上千甚至几数百万次的函数)来说,我们真正想要的是直接在实际硬件上执行机器指令。这时就需要即时(JIT)编译了。

当 JavaScript 函数由解释器执行时,会收集关于这个函数被调用的频率以及调用参数的各种统计信息。如果函数经常使用相同类型的参数执行,那么引擎可能会将函数的字节码转换为机器代码。

下面再次进入前面想象出来的 JavaScript 引擎,也就是那个充满魔法的图书馆。当程序开始执行时,你应该从贴有标签的架子拿出字节码书单。对于每个函数,大约有一行。按照每行上的说明进行操作时,你可以记录执行每一行的次数。另外还要注意在执行说明时所遇到的对象的形状。这时你就是分析解释器(profiling interpreter)

当你看到下一个字节码行时,会注意到该字节码“很热”,因为你已经执行了几十次,并且认为加快它的运行速度。你有两个助手可以随时为你翻译。第一个助手可以将字节码快速转换为机器代码。他生成的代码质量很好,简洁明了,但效率却不如预期。第二个助手工作更加细心,尽管会花费更长的时间,但是产生的代码经过了高度优化,使速度尽可能的更快。

在编译器方面,我们将这些不同的助手称为 JIT编译层。不同的引擎有不同的层数,这取决于它们要进行的权衡和取舍。

你决定将字节码发送到第一个助手哪里有。经过一段时间的处理后,通过用仔细记录的笔记,他会产生一个包含机器指令的新书单,并将其与原始字节码版本一起放在正确的书架上。下次需要执行该函数时,可以用这个更快的指令集。

但问题是,助手在翻译我们的书单时做出了很多假设。也许他认为变量将始终包含一个整数。如果这些假设无效会导致什么结果?

这时就必须进行所谓的 bailout 操作。拿出出原始的字节码书单,并弄清楚应该从哪条指令开始执行。机器代码书单会送到第二个助手那里,然后再次开始前面的过程。

超越无限

当今的高性能 JavaScript 引擎已经远远超越了 1990 年代 Netscape Navigator 和 Internet Explorer 中的相对简单的解释器,而且还在继续发展。新功能正在被逐渐添加到 JavaScript 语言中。常见的编码模式已得到优化。 WebAssembly 也已经成熟,正在开发更丰富的标准模块库。作为开发人员,我们可以期望现代 JavaScript 引擎能够快速、高效的执行,只要控制捆绑包的大小,并且能确保不要让对性能至关重要的代码过于动态化。

173382ede7319973.gif


本文首发微信公众号:前端先锋

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章


欢迎继续阅读本专栏其它高赞文章:


查看原文

疯狂的技术宅 收藏了文章 · 3月25日

写TypeScript代码的10种坏习惯

作者:Daniel Bartholomae

翻译:疯狂的技术宅

原文:https://startup-cto.net/10-ba...

近几年 TypeScript 和 JavaScript 一直在稳步发展。我们在过去写代码时养成了一些习惯,而有些习惯却没有什么意义。以下是我们都应该改正的 10 个坏习惯。

1.不使用 strict 模式

这种习惯看起来是什么样的

没有用严格模式编写 tsconfig.json

{
  "compilerOptions": {
    "target": "ES2015",
    "module": "commonjs"
  }
}

应该怎样

只需启用 strict 模式即可:

{
  "compilerOptions": {
    "target": "ES2015",
    "module": "commonjs",
    "strict": true
  }
}

为什么会有这种坏习惯

在现有代码库中引入更严格的规则需要花费时间。

为什么不该这样做

更严格的规则使将来维护代码时更加容易,使你节省大量的时间。

2. 用 || 定义默认值

这种习惯看起来是什么样的

使用旧的 || 处理后备的默认值:

function createBlogPost (text: string, author: string, date?: Date) {
  return {
    text: text,
    author: author,
    date: date || new Date()
  }
}

应该怎样

使用新的 ?? 运算符,或者在参数重定义默认值。

function createBlogPost (text: string, author: string, date: Date = new Date())
  return {
    text: text,
    author: author,
    date: date
  }
}

为什么会有这种坏习惯

?? 运算符是去年才引入的,当在长函数中使用值时,可能很难将其设置为参数默认值。

为什么不该这样做

??|| 不同,?? 仅针对 nullundefined,并不适用于所有虚值。

3. 随意使用 any 类型

这种习惯看起来是什么样的

当你不确定结构时,可以用 any 类型。

async function loadProducts(): Promise<Product[]> {
  const response = await fetch('https://api.mysite.com/products')
  const products: any = await response.json()
  return products
}

应该怎样

把你代码中任何一个使用 any 的地方都改为 unknown

async function loadProducts(): Promise<Product[]> {
  const response = await fetch('https://api.mysite.com/products')
  const products: unknown = await response.json()
  return products as Product[]
}

为什么会有这种坏习惯

any 是很方便的,因为它基本上禁用了所有的类型检查。通常,甚至在官方提供的类型中都使用了 any。例如,TypeScript 团队将上面例子中的 response.json() 的类型设置为 Promise <any>

为什么不该这样做

它基本上禁用所有类型检查。任何通过 any 进来的东西将完全放弃所有类型检查。这将会使错误很难被捕获到。

4. val as SomeType

这种习惯看起来是什么样的

强行告诉编译器无法推断的类型。

async function loadProducts(): Promise<Product[]> {
  const response = await fetch('https://api.mysite.com/products')
  const products: unknown = await response.json()
  return products as Product[]
}

应该怎样

这正是 Type Guard 的用武之地。

function isArrayOfProducts (obj: unknown): obj is Product[] {
  return Array.isArray(obj) && obj.every(isProduct)
}

function isProduct (obj: unknown): obj is Product {
  return obj != null
    && typeof (obj as Product).id === 'string'
}

async function loadProducts(): Promise<Product[]> {
  const response = await fetch('https://api.mysite.com/products')
  const products: unknown = await response.json()
  if (!isArrayOfProducts(products)) {
    throw new TypeError('Received malformed products API response')
  }
  return products
}

为什么会有这种坏习惯

从 JavaScript 转到 TypeScript 时,现有的代码库通常会对 TypeScript 编译器无法自动推断出的类型进行假设。在这时,通过 as SomeOtherType 可以加快转换速度,而不必修改 tsconfig 中的设置。

为什么不该这样做

Type Guard 会确保所有检查都是明确的。

5. 测试中的 as any

这种习惯看起来是什么样的

编写测试时创建不完整的用例。

interface User {
  id: string
  firstName: string
  lastName: string
  email: string
}

test('createEmailText returns text that greats the user by first name', () => {
  const user: User = {
    firstName: 'John'
  } as any
  
  expect(createEmailText(user)).toContain(user.firstName)
}

应该怎样

如果你需要模拟测试数据,请将模拟逻辑移到要模拟的对象旁边,并使其可重用。

interface User {
  id: string
  firstName: string
  lastName: string
  email: string
}

class MockUser implements User {
  id = 'id'
  firstName = 'John'
  lastName = 'Doe'
  email = 'john@doe.com'
}

test('createEmailText returns text that greats the user by first name', () => {
  const user = new MockUser()

  expect(createEmailText(user)).toContain(user.firstName)
}

为什么会有这种坏习惯

在给尚不具备广泛测试覆盖条件的代码编写测试时,通常会存在复杂的大数据结构,但要测试的特定功能仅需要其中的一部分。短期内不必关心其他属性。

为什么不该这样做

在某些情况下,被测代码依赖于我们之前认为不重要的属性,然后需要更新针对该功能的所有测试。

6. 可选属性

这种习惯看起来是什么样的

将属性标记为可选属性,即便这些属性有时不存在。

interface Product {
  id: string
  type: 'digital' | 'physical'
  weightInKg?: number
  sizeInMb?: number
}

应该怎样

明确哪些组合存在,哪些不存在。

interface Product {
  id: string
  type: 'digital' | 'physical'
}

interface DigitalProduct extends Product {
  type: 'digital'
  sizeInMb: number
}

interface PhysicalProduct extends Product {
  type: 'physical'
  weightInKg: number
}

为什么会有这种坏习惯

将属性标记为可选而不是拆分类型更容易,并且产生的代码更少。它还需要对正在构建的产品有更深入的了解,并且如果对产品的设计有所修改,可能会限制代码的使用。

为什么不该这样做

类型系统的最大好处是可以用编译时检查代替运行时检查。通过更显式的类型,能够对可能不被注意的错误进行编译时检查,例如确保每个 DigitalProduct 都有一个 sizeInMb

7. 用一个字母通行天下

这种习惯看起来是什么样的

用一个字母命名泛型

function head<T> (arr: T[]): T | undefined {
  return arr[0]
}

应该怎样

提供完整的描述性类型名称。

function head<Element> (arr: Element[]): Element | undefined {
  return arr[0]
}

为什么会有这种坏习惯

这种写法最早来源于C++的范型库,即使是 TS 的官方文档也在用一个字母的名称。它也可以更快地输入,只需要简单的敲下一个字母 T 就可以代替写全名。

为什么不该这样做

通用类型变量也是变量,就像其他变量一样。当 IDE 开始向我们展示变量的类型细节时,我们已经慢慢放弃了用它们的名称描述来变量类型的想法。例如我们现在写代码用 const name ='Daniel',而不是 const strName ='Daniel'。同样,一个字母的变量名通常会令人费解,因为不看声明就很难理解它们的含义。

8. 对非布尔类型的值进行布尔检查

这种习惯看起来是什么样的

通过直接将值传给 if 语句来检查是否定义了值。

function createNewMessagesResponse (countOfNewMessages?: number) {
  if (countOfNewMessages) {
    return `You have ${countOfNewMessages} new messages`
  }
  return 'Error: Could not retrieve number of new messages'
}

应该怎样

明确检查我们所关心的状况。

function createNewMessagesResponse (countOfNewMessages?: number) {
  if (countOfNewMessages !== undefined) {
    return `You have ${countOfNewMessages} new messages`
  }
  return 'Error: Could not retrieve number of new messages'
}

为什么会有这种坏习惯

编写简短的检测代码看起来更加简洁,使我们能够避免思考实际想要检测的内容。

为什么不该这样做

也许我们应该考虑一下实际要检查的内容。例如上面的例子以不同的方式处理 countOfNewMessages0 的情况。

9. ”棒棒“运算符

这种习惯看起来是什么样的

将非布尔值转换为布尔值。

function createNewMessagesResponse (countOfNewMessages?: number) {
  if (!!countOfNewMessages) {
    return `You have ${countOfNewMessages} new messages`
  }
  return 'Error: Could not retrieve number of new messages'
}

应该怎样

明确检查我们所关心的状况。

function createNewMessagesResponse (countOfNewMessages?: number) {
  if (countOfNewMessages !== undefined) {
    return `You have ${countOfNewMessages} new messages`
  }
  return 'Error: Could not retrieve number of new messages'
}

为什么会有这种坏习惯

对某些人而言,理解 !! 就像是进入 JavaScript 世界的入门仪式。它看起来简短而简洁,如果你对它已经非常习惯了,就会知道它的含义。这是将任意值转换为布尔值的便捷方式。尤其是在如果虚值之间没有明确的语义界限时,例如 nullundefined''

为什么不该这样做

与很多编码时的便捷方式一样,使用 !! 实际上是混淆了代码的真实含义。这使得新开发人员很难理解代码,无论是对一般开发人员来说还是对 JavaScript 来说都是新手。也很容易引入细微的错误。在对“非布尔类型的值”进行布尔检查时 countOfNewMessages0 的问题在使用 !! 时仍然会存在。

10. != null

这种习惯看起来是什么样的

棒棒运算符的小弟 ! = null使我们能同时检查 nullundefined

function createNewMessagesResponse (countOfNewMessages?: number) {
  if (countOfNewMessages != null) {
    return `You have ${countOfNewMessages} new messages`
  }
  return 'Error: Could not retrieve number of new messages'
}

应该怎样

明确检查我们所关心的状况。

function createNewMessagesResponse (countOfNewMessages?: number) {
  if (countOfNewMessages !== undefined) {
    return `You have ${countOfNewMessages} new messages`
  }
  return 'Error: Could not retrieve number of new messages'
}

为什么会有这种坏习惯

如果你的代码在 nullundefined 之间没有明显的区别,那么 != null 有助于简化对这两种可能性的检查。

为什么不该这样做

尽管 null 在 JavaScript早期很麻烦,但 TypeScript 处于 strict 模式时,它却可以成为这种语言中宝贵的工具。一种常见模式是将 null 值定义为不存在的事物,将 undefined 定义为未知的事物,例如 user.firstName === null 可能意味着用户实际上没有名字,而 user.firstName === undefined 只是意味着我们尚未询问该用户(而 user.firstName === 的意思是字面意思是 ''

173382ede7319973.gif


本文首发微信公众号:前端先锋

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章


欢迎继续阅读本专栏其它高赞文章:


查看原文

疯狂的技术宅 收藏了文章 · 3月25日

ES2019 中 8 个非常有用的功能

ES2019 规范是对 JavaScript的小规模扩展,但仍带来了一些有趣的功能。本文向你展示八个 ES2019 的功能,这些功能可以使你的开发变得更轻松。

String.prototype.trimStart() 和 String.prototype.trimEnd()

有时我们在处理字符串时需要处理多余的空格。ES2020 增加了两个功能:.trimStart()trimEnd() 方法可以帮你处理这些琐事。

它们都可以帮助你修剪或删除给定字符串中的空格。 trimStart() 删除字符串开头的所有空格。trimEnd()将删除字符串末尾的所有空格。不过要是想去除两边的空格呢?

有两个选择。第一种是同时使用这两个 ES2019 功能。第二个是使用另一个字符串方法 trim()。两种方式都能给你想要的结果。

// String.prototype.trimStart() 例子:
// 处理不带空格的字符串:
'JavaScript'.trimStart()
// Output:
//'JavaScript'

// 处理以空格开头的字符串:
' JavaScript'.trimStart()
// Output:
//'JavaScript'

// 两边都留有空格的字符串
' JavaScript '.trimStart()
// Output:
//'JavaScript '

// 以空格结尾的字符串
'JavaScript '.trimStart()
// Output:
//'JavaScript '


// String.prototype.trimEnd() 例子:
// 处理不带空格的字符串:
'JavaScript'.trimEnd()
// Output:
//'JavaScript'

// 处理以空格开头的字符串:
' JavaScript'.trimEnd()
// Output:
//' JavaScript'

// 两边都留有空格的字符串
' JavaScript '.trimEnd()
// Output:
//' JavaScript'

// 以空格结尾的字符串
'JavaScript '.trimEnd()
// Output:
//'JavaScript'

Function.prototype.toString()

函数的 toString() 方法已经存在了一段时间。它的作用是使你可以打印函数的代码。 ES2019 的不同之处在于它处理注释和特殊字符(例如空格)的方式。

过去,toString() 方法删除了注释和空格。所以该函数的打印版本可能看起来与原始代码不一样。 ES2019 的不会再发生这种情况。它返回的值将会与原始值匹配,包括注释和特殊字符。

// ES2019 之前:
function myFunc/* is this really a good name? */() {
  /* Now, what to do? */
}

myFunc.toString()
// Output:
// "function myFunc() {}"


// ES2019:
function myFunc/* is this really a good name? */() {
  /* Now, what to do? */
}

myFunc.toString()
// Output:
// "function myFunc/* is this really a good name? */() {
//   /* Now, what to do? */
// }"

Array.prototype.flat() 和 Array.prototype.flatMap()

数组是 JavaScript 的基本组成部分之一。它们有时会引起很多问题。当你必须要处理多维数组时尤其如此。甚至将多维数组转换为一维这样看似简单的任务也可能很困难。

好消息是,ES2019 的两个功能使这种操作变得更容易。第一个是 flat() 方法。在多维数组上使用时,它将转换为一维。默认情况下,flat()只会将数组展平一级。

但是页可以指定级数,并在调用时作为参数传递。如果不确定需要多少级,也可以使用 Infinity

// 创建一个数组:
const myArray = ['JavaScript', ['C', 'C++', ['Assembly', ['Bytecode']]]]

// 展平一级:
let myFlatArray = myArray.flat(1)

// 输出:
console.log(myFlatArray)
// Output:
// [ 'JavaScript', 'C', 'C++', [ 'Assembly', [ 'Bytecode' ] ] ]

// 用参数 Infinity 展平:
let myInfiniteFlatArray = myArray.flat(Infinity)

// 输出:
console.log(myInfiniteFlatArray)
// Output:
// [ 'JavaScript', 'C', 'C++', 'Assembly', 'Bytecode' ]

Array.prototype.flatMap()

除了 flat() 方法之外,还有 flatMap()。可以把它看作是 flat() 的高级版本。区别在于 flatMap() 方法把 flat()map() 结合了起来。在展平数组时,可以调用回调函数。

这样就可以在展平过程中使用原始数组中的每个元素。当在对数组进行展平操作的同时又要修改内容时很方便。

// 创建数组:
const myArray = ['One word', 'Two words', 'Three words']

// 用 map() 将数组中的所有字符串拆分为单词:
// 注意:这将会创建多维数组。
const myMappedWordArray = myArray.map(str => str.split(' '))

console.log(myMappedWordArray)
// Output:
// [ [ 'One', 'word' ], [ 'Two', 'words' ], [ 'Three', 'words' ] ]


// flatMap() 的例子:
const myArray = ['One word', 'Two words', 'Three words']

// 用 map() 将数组中的所有字符串拆分为单词:
// 注意:这将会创建多维数组。
const myFlatWordArray = myArray.flatMap(str => str.split(' '))

console.log(myFlatWordArray)
// Output:
// [ 'One', 'word', 'Two', 'words', 'Three', 'words' ]

Object.fromEntries()

当需要把某个对象转换为数组时,可以用 entries() 来完成。但是想要反向操作的话就困难了。ES2019 提供了 fromEntries() 来轻松解决这个问题。

这个方法的作用很简单。它需要键值对的可迭代形式,例如数组或 Map,然后将其转换为对象。

// 把数组转换为对象:
// 创建数组:
const myArray = [['name', 'Joe'], ['age', 33], ['favoriteLanguage', 'JavaScript']]
const myObj = Object.fromEntries(myArray)
console.log(myObj)
// Output:
// {
//   name: 'Joe',
//   age: 33,
//   favoriteLanguage: 'JavaScript'
// }


// 把 Map 转换为对象:
// 创建 map:
const myMap = new Map(
  [['name', 'Spike'], ['species', 'dog'], ['age', 3]]
)
const myObj = Object.fromEntries(myMap)
console.log(myObj)
// Output:
// {
//   name: 'Spike',
//   species: 'dog',
//   age: 3
// }

可选的 catch 绑定

以前使用 try ... catch 时,还必须使用绑定。即使没有使用该异常,你也必须将其作为参数传递。 在 ES2019 种,如果不想使用该异常,则可以使用不带参数的 catch 块。

// ES2019 之前:
try {
  // Do something.
} catch (e) {
    //忽略必需的e参数
       //如果你不想用它,也应该保留。
}

// ES2019:
try {
  // Do something.
} catch {
  // 不需要添加任何参数
}

格式正确的 JSON.stringify()

过去,当对包含特定字符的东西使用 JSON.stringify() 时,会得到格式不正确的 Unicode 字符串。从 U+D800到 U+DFFF 的编码段会变成 “�”。更糟的是没办法把这些错误的字符变回原样。

ES2019 修复了 JSON.stringify() 方法。现在能够对那些有问题的代码段进行分类,并且可以将它们转换回其原始表示形式。

Symbol.prototype.description

符号是在 ES2015(ES6)中引入的新数据类型。它们通常用于标识对象属性。 ES2019 增加了 description 属性。这个属性是只读的,无法更改它的值。它用来返回给定符号的描述。

要牢记两点。首先,创建符号时描述不是必须的,而是可选的。所以当你尝试访问 description 时,可能会得到除 undefined 之外的任何信息。如果你尝试访问不带描述的符号描述,则会得到 undefined(未定义)信息。

第二点是 description 是对符号本身的描述。它不是符号的标识符。这意味着你不能使用现有的描述(即 description 属性的值)来访问现有的符号。它只是为了更容易识别正在你正在使用的符号。

说明:创建新的符号时,可以通过将一些字符串作为参数传递给 Symbol() 对象来添加描述。如果留空,description 将会是 undefined

// 创建带有描述的 Symbol:
// 创建 Symbol 并添加描述:
//注意:描述是"My first symbol."
const mySymbol = Symbol('My first symbol.')

// 输出 description 属性的值:
console.log(mySymbol.description)
// Output:
// 'My first symbol.'


// 读取不存在的 Symbol:
console.log(Symbol().description)
// Output:
// undefined


// 读取定义为空字符串的描述:
console.log(Symbol('').description)
// Output:
// ''

Symbol.prototype.toString()

toString() 方法提供了另一种读取符号描述的方式。它的缺点是在返回的字符串中还包含 Symbol()。另一个区别是 toString() 方法永远不会返回不存在的undefined 描述。

使用 description 的另一个原因是:如果你有一个没有说明的 Symbol 并用了 toString() 方法,仍将得到 Symbol() 部分。如果描述为空字符串,也将获得此信息。这样就基本上不可能区分不存在的描述和用作描述的空字符串。

// 创建带有描述的 Symbol:
const mySymbol = Symbol('REAMDE.')

// 输出 description 属性的值:
console.log(mySymbol.toString())
// Output:
// 'Symbol(REAMDE.)'

// 读取不存在的 Symbol:
console.log(Symbol().toString())
// Output:
// 'Symbol()'


// 读取定义为空字符串的描述:
console.log(Symbol('').toString())
// Output:
// 'Symbol()'

173382ede7319973.gif


本文首发微信公众号:前端先锋

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章


欢迎继续阅读本专栏其它高赞文章:


查看原文

疯狂的技术宅 发布了文章 · 3月25日

js 引擎是怎样将 var 转换为 JIT 的

作者:Kevin S

翻译:疯狂的技术宅

原文:https://alistapart.com/articl...

绑定到字节码

对于现代 Web 程序,浏览器首先看到的 JavaScript 通常不是前端程序员写的。相反,它很可能是由 webpack 之类的工具产生的捆绑包,而且可能是一个相当大的捆绑包,其中包含 UI 框架,例如 React,各种 polyfills(在较旧的浏览器中模拟新平台功能的库),以及在 npm 上找到的各种软件包。浏览器的 JavaScript 引擎面临的第一个挑战是将一大堆文本转换为可以在虚拟机上执行的指令。正是由于它需要解析代码,而且用户正在等待 JavaScript 进行交互,所以它的执行速度必须很快才行。

在高级方面,JavaScript 引擎像其他语言编译器一样去解析代码。首先,输入的文本流被分解为名为 token 的块。每个 token 代表语法结构中的一个有意义的单元,类似于自然语言中的单词和标点符号。然后,将这些 token 输入到自上而下的解析器中,生成生成表示程序的树结构。语言设计师和编译器工程师喜欢把这种树结构称为 AST(抽象语法树)。然后就可以通过分析生成的 AST 来生成称为字节码的虚拟机指令列表。

image.png

生成 AST 的过程是 JavaScript 引擎更直接的工作之一。不过它也可能很慢。还记得本文开始时所提到的一大堆代码吗? JavaScript 引擎必须在用户能够开始与站点进行交互之前解析整个捆绑包并构建语法树。对于初始页面加载,其中大部分代码可能是不必要的,甚至根本无法执行其中的某些代码!

不过好在编译器工程师发明了各种技巧来加快处理速度。首先,某些引擎在后台线程中解析代码,从而释放主 UI 线程用于其他计算。其次,现代引擎将通过使用名为“延迟解析”或“延迟编译”的技术,尽可能地延迟内存中语法树的创建。

它的工作方式是这样的:如果引擎看到一个可能在一段时间内不执行的函数定义,它会对函数体进行快速的“丢弃”解析。这种一次性分析能够发现可能隐藏在代码内的所有语法错误,但不会生成 AST。稍后,当第一次调用该函数时,会再次解析这段代码。这次,引擎将生成执行所需的完整 AST 和字节码。在 JavaScript 的世界中,执行两次有时比执行一次更快!

但是,最好的优化是使我们完全绕开所有耗时处理的优化。对于 JavaScript 编译,这意味着完全跳过了解析步骤。一些 JavaScript 引擎会尝试缓存生成的字节码,以备以后用户再次访问该网站时进行重用。这并不那么简单。随着网站的更新,JavaScript 包可能会经常发生改变,浏览器必须仔细权衡序列化字节码的成本与缓存带来的性能提升之间的关系。

运行时的字节码

现在有了字节码,就可以开始执行了。在当今的 JavaScript 引擎中,在解析过程中生成的字节码首先被送到名为解释器的虚拟机中。解释器有点像用软件实现的 CPU。它一次查看一条字节码指令,然后决定要执行的实际机器指令以及下一步要执行的指令。

JavaScript 编程语言的结构和行为在名为 ECMA-262 的文档中进行了定义。其中结构部分被称为“语法”,行为部分为“语义”。编程语言的语义几乎都是由伪代码编写的算法定义的。假设我们是编译器工程师,正在实现带符号的右移运算符>>)以下则是规格说明(以下脚本引自 ECMA-262 ):

ShiftExpression : ShiftExpression >> AdditiveExpression

  1. Let lref be the result of evaluating ShiftExpression.
  2. Let lval be ? GetValue(lref).
  3. Let rref be the result of evaluating AdditiveExpression.
  4. Let rval be ? GetValue(rref).
  5. Let lnum be ? ToInt32(lval).
  6. Let rnum be ? ToUint32(rval).
  7. Let shiftCount be the result of masking out all but the least significant 5 bits of rnum, that is, compute rnum & 0x1F.
  8. Return the result of performing a sign-extending right shift of lnum by shiftCount bits. The most significant bit is propagated. The result is a signed 32-bit integer.

前六个步骤将操作数( >> 两侧的值)转换为32位整数,然后执行实际的移位操作。

但是如果真的完全按照规范中的描述去实现算法,那么做出来的解释器会很慢。下面以从 JavaScript 对象获取属性值的简单操作为例。

从概念上讲,JavaScript 中的对象就像字典一样。每个属性均以字符串名作为关键字。对象也可以有原型对象

image.png

如果某个对象没有给定字符关键字的条目,那么就需要在原型中寻找该键。不断重复这个操作,直到找到所需的关键字或到达原型链的末尾为止。

这就导致了每次想从对象中获取属性值时,可能要做很多工作。

JavaScript 引擎中用于加速动态属性查找的策略称为内联缓存。内联缓存最早是在 1980 年代为 Smalltalk 语言所开发的。其基本思想是,先前属性查找操作的结果可以直接存储在生成的字节码指令中。

为了了解它的工作原理,让我们闭上眼睛,想象 JavaScript 引擎是一座充满魔法的大型图书馆。当我们走进去时,会注意到里面塞满了到处飞来飞去的书(即对象)。每个对象都有一个可识别的形状,这个形状便确定了其属性的存储位置。

假设我们正在按照书单上所记录的一系列字节码指令执行程序。下一条指令告诉我们从某个对象获取名为 x 的属性的值。你抓住该对象,找出 x 的存储位置,然后发现它已存储在该对象的第二个数据插槽中。

你会发现,具有相同形状的所有对象在其第二个数据插槽中都有 x 属性。拿出你的笔,在字节码书单上做一个注释,标记出对象的形状和 x 属性的位置。下次再看到这个标记时,只需检查对象的形状就行了。如果形状与你在字节码注释中所标记的形状匹配,不需要检查对象就可以准确知道数据的位置。这样你就实现了单态内联缓存

但是,如果对象的形状与我们的字节码注释不匹配怎么办?这时可以通过制作一张小表格,并把看到的每种形状作为一行记录的方式来解决这个问题。当每看到一个新形状时,就把它作为一行添加到表中。这样就实现了一个多态内联缓存。它的速度不如单态缓存快,并且在书单上会占用更多的空间,但是如果行数不多,效果会非常好。

如果最后生成的表太大,就要把它删除掉,并做个注释来提醒自己不要再纠结这个指令的内联缓存了。用编译器的术语来说,实现了一个复态调用点(megamorphic callsite)

一般来说单态代码非常快,多态代码差不多一样快,而复态代码则往往很慢。

  • 单态:快如疾风
  • 多态:动若脱兔
  • 复态:慢似乌龟

即时编译( JIT)

解释器的优点在于可以快速开始执行代码,对于仅运行一两次的代码,这种“软件 CPU”的执行速度还是可以接受的。但是对于“热代码”(运行数百、上千甚至几数百万次的函数)来说,我们真正想要的是直接在实际硬件上执行机器指令。这时就需要即时(JIT)编译了。

当 JavaScript 函数由解释器执行时,会收集关于这个函数被调用的频率以及调用参数的各种统计信息。如果函数经常使用相同类型的参数执行,那么引擎可能会将函数的字节码转换为机器代码。

下面再次进入前面想象出来的 JavaScript 引擎,也就是那个充满魔法的图书馆。当程序开始执行时,你应该从贴有标签的架子拿出字节码书单。对于每个函数,大约有一行。按照每行上的说明进行操作时,你可以记录执行每一行的次数。另外还要注意在执行说明时所遇到的对象的形状。这时你就是分析解释器(profiling interpreter)

当你看到下一个字节码行时,会注意到该字节码“很热”,因为你已经执行了几十次,并且认为加快它的运行速度。你有两个助手可以随时为你翻译。第一个助手可以将字节码快速转换为机器代码。他生成的代码质量很好,简洁明了,但效率却不如预期。第二个助手工作更加细心,尽管会花费更长的时间,但是产生的代码经过了高度优化,使速度尽可能的更快。

在编译器方面,我们将这些不同的助手称为 JIT编译层。不同的引擎有不同的层数,这取决于它们要进行的权衡和取舍。

你决定将字节码发送到第一个助手哪里有。经过一段时间的处理后,通过用仔细记录的笔记,他会产生一个包含机器指令的新书单,并将其与原始字节码版本一起放在正确的书架上。下次需要执行该函数时,可以用这个更快的指令集。

但问题是,助手在翻译我们的书单时做出了很多假设。也许他认为变量将始终包含一个整数。如果这些假设无效会导致什么结果?

这时就必须进行所谓的 bailout 操作。拿出出原始的字节码书单,并弄清楚应该从哪条指令开始执行。机器代码书单会送到第二个助手那里,然后再次开始前面的过程。

超越无限

当今的高性能 JavaScript 引擎已经远远超越了 1990 年代 Netscape Navigator 和 Internet Explorer 中的相对简单的解释器,而且还在继续发展。新功能正在被逐渐添加到 JavaScript 语言中。常见的编码模式已得到优化。 WebAssembly 也已经成熟,正在开发更丰富的标准模块库。作为开发人员,我们可以期望现代 JavaScript 引擎能够快速、高效的执行,只要控制捆绑包的大小,并且能确保不要让对性能至关重要的代码过于动态化。

173382ede7319973.gif


本文首发微信公众号:前端先锋

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章


欢迎继续阅读本专栏其它高赞文章:


查看原文

赞 4 收藏 4 评论 0