Webpack Plugin插件机制

Webpack Plugin插件机制

plugin机制出现原因

前面我们已经知道了,loader机制让webpack拥有了处理除js类型文件以外的能力。

那如果我们需要在项目中实现打包前自动清理上次打包生成的文件将一些文件复制到打包目录中自动生成html文件将打包产物自动上传至服务器将打包后代码进行压缩、拆分等一系列定制化功能,此时就必须借助webpack的plugin机制去实现了。

没错,webpack的plugin机制让webpack有了定制化的能力。

plugin原理

那具体如何通过plugin机制去实现这些定制化功能呢?

其实是webpack在打包过程中的不同阶段(配置文件读取完成后、打包开始前、打包完成后等阶段)会触发不同的钩子,我们只需要明确要实现的功能应该在哪个阶段,然后将具体实现代码注册为对应钩子的事件即可。

webpack运行原理

我们在了解这些钩子之前,必须要知道webpack的运行原理。

这是一个简化版的webpack打包过程,当我们执行 webpack build 命令后,webpack会先读取配置文件,然后根据配置文件中的配置项去初始化,创建一个 compiler 对象,然后调用 compiler 对象的 run 方法,初始化一个 compilation 对象,执行 compilation 中的 build 方法进行编译,编译完成后,触发 compiler 对象的 done 钩子,完成打包。

image

1
2
3
4
5
6
7
8
9
10
11
//第一步:搭建结构,读取配置参数,这里接受的是webpack.config.js中的参数
function webpack(webpackOptions) {
//第二步:用配置参数对象初始化 `Compiler` 对象
const compiler = new Compiler(webpackOptions);
//第三步:挂载配置文件中的插件
const { plugins } = webpackOptions;
for (let plugin of plugins) {
plugin.apply(compiler);
}
return compiler;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
//Compiler其实是一个类,它是整个编译过程的大管家,而且是单例模式
class Compiler {
constructor(webpackOptions) {
//省略
}

// 第五步:创建compilation对象
compile(callback){
//虽然webpack只有一个Compiler,但是每次编译都会产出一个新的Compilation,
//这里主要是为了考虑到watch模式,它会在启动时先编译一次,然后监听文件变化,如果发生变化会重新开始编译
//每次编译都会产出一个新的Compilation,代表每次的编译结果
let compilation = new Compilation(this.options);
compilation.build(callback); //执行compilation的build方法进行编译,编译成功之后执行回调
}

//第四步:执行`Compiler`对象的`run`方法开始执行编译
run(callback) {
this.hooks.run.call(); //在编译前触发run钩子执行,表示开始启动编译了
const onCompiled = () => {
// 第七步:当编译成功后会触发done这个钩子执行
this.hooks.done.call();
};
this.compile(onCompiled); //开始编译,成功之后调用onCompiled
}
}


class Compilation {
constructor(webpackOptions) {
this.options = webpackOptions;
this.modules = []; //本次编译所有生成出来的模块
this.chunks = []; //本次编译产出的所有代码块,入口模块和依赖的模块打包在一起为代码块
this.assets = {}; //本次编译产出的资源文件
this.fileDependencies = []; //本次打包涉及到的文件,这里主要是为了实现watch模式下监听文件的变化,文件发生变化后会重新编译
}

//第六步:执行compilation的build方法进行编译
build(callback) {
//这里开始做编译工作,编译成功执行callback

// ... 编译过程代码省略

// 编译完成后,触发callback回调
callback()
}
}

Read More

前端面试题汇总

Event Loop

Event Loop即事件循环,是指浏览器或者Nodejs解决javascript单线程运行时异步逻辑不会阻塞的一种机制。

Event Loop是一个执行模型,不同的运行环境有不同的实现,浏览器和nodejs基于不同的技术实现自己的event loop。

  • 浏览器的Event Loop是在HTML5规范中明确定义。
  • Nodejs的Event Loop是libuv实现的。
  • libuv已经对Event Loop作出了实现,HTML5规范中只是定义的浏览器中Event Loop的模型,具体的实现交给了浏览器厂商。

宏队列和微队列

在javascript中,任务被分为两种,一种为宏任务(macrotask),也称为task,一种为微任务(microtask),也称为jobs。

宏任务主要包括:

  • script全部代码
  • setTimeout
  • setInterval
  • setImmediate (Nodejs独有,浏览器暂时不支持,只有IE10支持)
  • requestAnimationFrame (浏览器独有)
  • I/O
  • UI rendering (浏览器独有)

微任务主要包括:

  • process.nextTick (Nodejs独有)
  • Promise
  • Object.observe (废弃)
  • MutationObserver

浏览器中的Event Loop

Javascript 有一个主线程 main thread 和 一个调用栈(执行栈) call-stack,所有任务都会被放到调用栈等待主线程的执行。

JS调用栈采用的是后进先出的规则,当函数执行时,会被添加到调用栈的顶部,当执行栈执行完后,就会从栈顶移除,直到栈内被清空。

Javascript单线程任务可以分为同步任务和异步任务,同步任务会在调用栈内按照顺序依次被主线程执行,异步任务会在异步任务有了结果后,将注册的回调函数放入任务队列中等待主线程空闲的时候(调用栈被清空的时候),被读取到调用栈内等待主线程的执行

任务队列 Task Queue, 是先进先出的数据结构。

浏览器事件循环的进程模型

浏览器Event Loop的具体流程:

  1. 执行全局Javascript的同步代码,可能包含一些同步语句,也可以是异步语句(setTimeout语句不执行回调函数里面的,Promise中.then之前的语句)
  2. 全局Javascript执行完毕后,调用栈call-stack会被清空
  3. 从微队列microtask queue中取出位于首部的回调函数,放入到调用栈call-stack中执行,执行完毕后从调用栈中删除,microtask queue的长度减1。
  4. 继续从微队列microtask queue的队首取出任务,放到调用栈中执行,依次循环往复,直至微任务队列microtask queue中的任务都被调用栈执行完毕。特别注意,如果在执行微任务microtask过程中,又产生了微任务microtask,新产生的微任务也会追加到微任务队列microtask queue的尾部,新生成的微任务也会在当前周期中被执行完毕。
  5. microtask queue中的任务都被执行完毕后,microtask queue为空队列,调用栈也处于空闲阶段
  6. 执行UI rendering
  7. 从宏队列macrotask queue的队首取出宏任务,放入调用栈中执行。
  8. 执行完后,调用栈为空闲状态
  9. 重复 3 - 8 的步骤,直至宏任务队列的任务都被执行完毕。

浏览器Event Loop的3个重点:

  1. 宏队列macrotask queue每次只从中取出一个任务放到调用栈中执行,执行完后去执行微任务队列中的所有任务
  2. 微任务队列中的所有任务都会依次取出来执行,只是微任务队列中的任务清空
  3. UI rendering 的执行节点在微任务队列执行完毕后,宏任务队列中取出任务执行之前执行

NodeJs中的Event Loop

libuv结构

libuv的事件循环模型

NodeJs中的宏任务队列和微任务队列

NodeJs的Event Loop中,执行宏任务队列的回调有6个阶段

NodeJS中的宏队列执行回调的6个阶段

Node的Event Loop可以分为6个阶段,各个阶段执行的任务如下所示:

  • timers: 执行setTimeout和setInterval中到期的callback。
  • I/O callbacks: 执行几乎所有的回调,除了close callbacks以及timers调度的回调和setImmediate()调度的回调。
  • idle, prepare: 仅在内部使用。
  • poll: 最重要的阶段,检索新的I/O事件,在适当的情况下回阻塞在该阶段。
  • check: 执行setImmediate的callback(setImmediate()会将事件回调插入到事件队列的尾部,主线程和事件队列的任务执行完毕后会立即执行setImmediate中传入的回调函数)。
  • close callbacks: 执行close事件的callback,例如socket.on(‘close’, fn)或则http.server.on(‘close’, fn)等。

NodeJs中的宏任务队列可以分为下列4个:

  1. Timers Queue
  2. I/O Callbacks Queue
  3. Check Queue
  4. Close Callbacks Queue

在浏览器中只有一个宏任务队列,所有宏任务都会放入到宏任务队列中等待放入执行栈中被主线程执行,NodeJs中有4个宏任务队列,不同类型的宏任务会被放入到不同的宏任务队列中。

NodeJs中的微任务队列可以分为下列2个:

  1. Next Tick Queue: 放置process.nextTick(callback)的回调函数
  2. Other Micro Queue: 其他microtask,例如Promise等

在浏览器中只有一个微任务队列,所有微任务都会放入到微任务队列中等待放入执行栈中被主线程执行,NodeJs中有2个微任务队列,不同类型的微任务会被放入到不同的微任务队列中。

NodeJs事件循环

NodeJs的Event Loop的具体流程:

  1. 执行全局Javascript的同步代码,可能包含一些同步语句,也可以是异步语句(setTimeout语句不执行回调函数里面的,Promise中.then之前的语句)。
  2. 执行微任务队列中的微任务,先执行Next Tick Queue队列中的所有的所有任务,再执行Other Micro Queue队列中的所有任务。
  3. 开始执行宏任务队列中的任务,共6个阶段,从第1个阶段开始执行每个阶段对应宏任务队列中的所有任务,注意,这里执行的是该阶段宏任务队列中的所有的任务,浏览器Event Loop每次只会中宏任务队列中取出队首的任务执行,执行完后开始执行微任务队列中的任务,NodeJs的Event Loop会执行完该阶段中宏任务队列中的所有任务后,才开始执行微任务队列中的任务,也就是步骤2
  4. Timers Queue -> 步骤2 -> I/O Callbacks Queue -> 步骤2 -> Check Queue -> 步骤2 -> Close Callback Queue -> 步骤2 -> Timers Queue -> ……

特别注意:

  • 上述的第三步,当 NodeJs 版本小于11时,NodeJs的Event Loop会执行完该阶段中宏任务队列中的所有任务
  • 当 NodeJS 版本大于等于11时,在timer阶段的setTimeout,setInterval…和在check阶段的setImmediate都在node11里面都修改为一旦执行一个阶段里的一个任务就立刻执行微任务队列。为了和浏览器更加趋同。

NodeJs的Event Loop的microtask queue和macrotask queue的执行顺序详情

NodeJS中的微任务队列执行顺序

NodeJS中的宏任务队列执行顺序

当setTimeout(fn, 0)和setImmediate(fn)放在同一同步代码中执行时,可能会出现下面两种情况:

  1. 第一种情况: 同步代码执行完后,timer还没到期,setImmediate中注册的回调函数先放入到Check Queue的宏任务队列中,先执行微任务队列,然后开始执行宏任务队列,先从Timers Queue开始,由于在Timer Queue中未发现任何的回调函数,往下阶段走,直到Check Queue中发现setImmediate中注册的回调函数,先执行,然后timer到期,setTimeout注册的回调函数会放入到Timers Queue的宏任务队列中,下一轮后再次执行到Timers Queue阶段时,才会再Timers Queue中发现了setTimeout注册的回调函数,于是执行该timer的回调,所以,setImmediate(fn)注册的回调函数会早于setTimeout(fn, 0)注册的回调函数执行

  2. 第二种情况: 同步代码执行完之前,timer已经到期,setTimeout注册的回调函数会放入到Timers Queue的宏任务队列中,执行同步代码到setImmediate时,将其回调函数注册到Check Queue中,同步代码执行完后,先执行微任务队列,然后开始执行宏任务队列,先从Timers Queue开始,在Timers Queue发现了timer中注册的回调函数,取出执行,往下阶段走,到Check Queue中发现setImmediate中注册的回调函数,又执行,所以这种情况时,setTimeout(fn, 0)注册的回调函数会早于setImmediate(fn)注册的回调函数执行

  3. 在同步代码中同时调setTimeout(fn, 0)和setImmediate执行顺序情况是不确定的,但是如果把他们放在一个IO的回调,比如readFile(‘xx’, function () {// ….})回调中,那么IO回调是在I/O Callbacks Queue中,setTimeout到期回调注册到Timers Queue,setImmediate回调注册到Check Queue,I/O Callbacks Queue执行完到Check Queue,Timers Queue得到下个循环周期,所以setImmediate回调这种情况下肯定比setTimeout(fn, 0)回调先执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
setImmediate(function A() {
console.log(1);
setImmediate(function B(){console.log(2);});
});

setTimeout(function timeout() {
console.log('TIMEOUT FIRED');
}, 0);

// 执行结果: 会存在下面两种情况
// 第一种情况:
// 1
// TIMEOUT FIRED
// 2

// 第二种情况:
// TIMEOUT FIRED
// 1
// 2

注:

  • setImmediate中如果又存在setImmediate语句,内部的setImmediate语句注册的回调函数会在下一个check阶段来执行,并不在当前的check阶段来执行。

poll 阶段详解:

poll 阶段主要又两个功能:

  1. 当timers到达指定的时间后,执行指定的timer的回调(Executing scripts for timers whose threshold has elapsed, then)。
  2. 处理poll队列的事件(Processing events in the poll queue)。

当进入到poll阶段,并且没有timers被调用的时候,会出现下面的情况:

  • 如果poll队列不为空,Event Loop将同步执行poll queue中的任务,直到poll queue队列为空或者执行的callback达到上限。
  • 如果poll队列为空,会发生下面的情况:
    • 如果脚本执行过setImmediate代码,Event Loop会结束poll阶段,直接进入check阶段,执行Check Queue中调用setImmediate注册的回调函数。
    • 如果脚本没有执行过setImmediate代码,poll阶段将等待callback被添加到队列中,然后立即执行。

当进入到poll阶段,并且调用了timers的话,会发生下面的情况:

  • 一旦poll queue为空,Event Loop会检测Timers Queue中是否存在任务,如果存在任务的话,Event Loop会回到timer阶段并执行Timers Queue中timers注册的回调函数。执行完后是进入check阶段,还是又重新进入I/O callbacks阶段?

setTimeout 对比 setImmediate

  • setTimeout(fn, 0)在timers阶段执行,并且是在poll阶段进行判断是否达到指定的timer时间才会执行
  • setImmediate(fn)在check阶段执行

两者的执行顺序要根据当前的执行环境才能确定:

  • 如果两者都在主模块(main module)调用,那么执行先后取决于进程性能,顺序随机
  • 如果两者都不在主模块调用,即在一个I/O Circle中调用,那么setImmediate的回调永远先执行,因为会先到Check阶段

setImmediate 对比 process.nextTick

  • setImmediate(fn)的回调任务会插入到宏队列Check Queue中
  • process.nextTick(fn)的回调任务会插入到微队列Next Tick Queue中
  • process.nextTick(fn)调用深度有限制,上限是1000,而setImmediate则没有

Read More

NextJs 获取 Github Action 部署的环境变量

NextJs 获取 Github Action 部署的环境变量

设置变量

项目中需要某些私有密钥,不能直接暴露在仓库中,在编译Next.js的时候,需要将该密钥通过环境变量的形式注入到Next.js项目中,所以第一步,我们需要将这个密钥储存到当前仓库的Settings/Secrets里面,具体操作如下图所示:

image

配置变量

Next.js中获取GitHub Actions环境变量,你可以使用process.env对象来访问在GitHub Actions中设置的环境变量。以下是一个如何在Next.js中获取GitHub Actions环境变量的例子:

首先,在GitHub Actions的工作流文件中设置环境变量,例如在 .github/workflows/ci.yml中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: 14
- name: Install Dependencies
run: npm install
- name: Build Next.js App
env:
MY_ENV_VAR: ${{ secrets.MY_SECRET_ENV_VAR }}
run: npm run build

在这个例子中,MY_ENV_VAR是一个环境变量,它可以是一个秘密值,通过GitHub Actions的秘密(secrets)功能来安全地设置。

然后,需要将环境变量添加到 Next 项目的配置文件中,在 next.config.js 中:

1
2
3
4
5
module.exports = {
env: {
MY_ENV_VAR: process.env.MY_ENV_VAR,
}
}

获取变量

然后,在Next.js的应用代码中,你可以这样获取这个环境变量:

1
2
3
4
5
6
7
8
9
// pages/index.js
export default function Home() {
const myEnvVar = process.env.MY_ENV_VAR;
return (
<div>
<p>The environment variable is: {myEnvVar || 'undefined'}</p>
</div>
);
}

在这段代码中,process.env.MY_ENV_VAR 将会获取在 GitHub Actions 中设置的环境变量MY_ENV_VAR的值。如果环境变量存在,它将被显示在页面上;如果不存在,则会显示'undefined'

使用 Github Pages 和 Issues 搭建博客

Github Pages 简介

自己维护博客服务器,需要购买服务器,配置域名等一些列操作,比较繁琐,索性直接找个现成的稳定的博客平台,github pages成为首选,这里有几个思路

  • 使用github pages,编写静态网页
  • 使用github pages 与静态网页生成工具,需要本地电脑编写文章
  • 使用github pages 与自建后端服务器,需要另外维护一个后端服务器
  • 使用issues 写文章,issues页面就是一个博客网站
  • 使用github pages 与 github api,使用issues写文章

使用github pages,编写静态网页

有很多文档网站是使用这种方式搭建,一般是作为demo页面或者单页文档页面,否则页面稍微一复杂,就变得非常难以维护

使用github pages 与 静态网页生成工具

有很多博客网站与文档网站都是使用这种方式,这是一种最简单最方便的方式,现在流行的工具有 hexoJekyll 等,这种方式还要在本地维护一个仓库,生成静态页面后再上传,操作过于繁琐,而且每个页面之间切换都需要重新载入所有资源,网页数据传输量较大

具体工具如下所示:

使用github pages 与 自建后端服务器

在github pages编写一个单页面应用,数据通过跨域请求来自于自建的服务器,这种方式有三大难点:前端、后端、服务器

前端

需要编写一个单页面应用,这对技术需要一定的水平,基本很少有开源工具

后端

需要后端服务,这可以使用现成工具提供api,比如wordpress、drupal、ghost等

使用issues 写文章

这种方式简单粗暴,直接在issues写文章,评论、标签、提醒神马的都有了,现在其实很流行这种方式,看看这几个博客,都几千个star了

要说它的缺点嘛,就是人人都可以往你博客提交文章,界面千篇一律,而且也不怎么好看

服务器

这个问题很严重,问题来了,你都有自建的服务器了,还要用github pages干嘛呢,哪天自建服务器挂了,博客照样挂。

这种方式可以使用域名来提升逼格。

使用github pages 与 github api

这种方案与上一种对比起来其实没多大区别,唯一的区别就是自建服务换成了github的另一个服务,就是说,github帮我们建好了。
github api:https://developer.github.com/v3/

Read More

thread-loader

thread-loader

thread-loader 是一种在 worker 池中运行以下 loader 的工具。

开始使用

通过 npm、yarn 或 pnpm 安装:

1
npm install --save-dev thread-loader

或者

1
yarn add -D thread-loader

或者

1
pnpm add -D thread-loader

将其放在其他 loader 的前面,以下 loader 将在 worker 池中运行。

worker 池中运行的 loader 有一些限制。例如:

  • loader 不能发射文件。
  • loader 不能使用自定义 loader API(即插件)。
  • loader 不能访问 webpack 配置项。

每个 worker 是一个单独的 node.js 进程,启动一个 worker 大约需要 600ms 的时间。此外,进程间通信也存在一定的开销。

因此,仅将此 loader 用于计算密集型操作!

示例

webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
module.exports = {
module: {
rules: [
{
test: /\.js$/,
include: path.resolve('src'),
use: [
'thread-loader',
// 你的计算密集型 loader(例如 babel-loader)
],
},
],
},
};

带有选项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
use: [
{
loader: 'thread-loader',
// 选项相同的 loader 将共享 worker 池
options: {
// 生成的 worker 数量,默认为(CPU 核心数 - 1)或当 require('os').cpus() 未定义时回退到 1
workers: 2,

// 一个 worker 并行处理的 job 数量
// 默认为 20
workerParallelJobs: 50,

// 额外的 node.js 参数
workerNodeArgs: ['--max-old-space-size=1024'],

// 允许重新生成已死亡的 worker 池
// 重新生成会减慢整个编译速度
// 在开发环境中应将其设置为 false
poolRespawn: false,

// 当空闲时杀死 worker 进程的超时时间
// 默认为 500 (ms)
// 对于保持 worker 存活的监视构建,可以设置为 Infinity
poolTimeout: 2000,

// 池分配给 worker 的工作数量
// 默认为 200
// 降低这个数值会降低总体的效率,但是会提升工作分布更均一
poolParallelJobs: 50,

// 池的名称
// 可以修改名称来创建其余选项都一样的池
name: "my-pool"
},
},
// 耗时的 loader(例如 babel-loader)
];

预热

为了防止在启动 worker 时出现高延迟,可以预热 worker 池。

这将启动池中的最大 worker 数量,并将指定的模块加载到 node.js 模块缓存中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const threadLoader = require('thread-loader');

threadLoader.warmup(
{
// 池选项,像传递给 loader 选项那样
// 必须匹配 loader 选项以启动正确的池
},
[
// 要加载的模块
// 可以是任何模块,例如
'babel-loader',
'babel-preset-es2015',
'sass-loader',
]
);

翻译: thread-loader

cache-loader

cache-loader

cache-loader 允许在磁盘(默认)或数据库中缓存后续加载器的结果。

开始使用

要开始使用,您需要安装 cache-loader

1
npm install --save-dev cache-loader

将此加载器添加到其他(耗时的)加载器前面,以便在磁盘上缓存结果。

webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
module.exports = {
module: {
rules: [
{
test: /\.ext$/,
use: ['cache-loader', ...loaders],
include: path.resolve('src'),
},
],
},
};

⚠️ 请注意,保存和读取缓存文件会有一定的开销,因此只将此加载器用于缓存耗时的加载器。

选项

名称 类型 n 默认值 描述
cacheContext {String} undefined 允许您覆盖默认的缓存上下文,以便相对于某个路径生成缓存。默认情况下,它将使用绝对路径。
cacheKey {Function(options, request) -> {String}} undefined 允许您覆盖默认的缓存键生成器。
cacheDirectory {String} findCacheDir({ name: 'cache-loader' }) or os.tmpdir() 提供一个缓存目录,其中应存储缓存项(用于默认的读写实现)。
cacheIdentifier {String} cache-loader:{version} {process.env.NODE_ENV} 提供一个无效标识符,用于生成哈希值。您可以将其用于加载器的额外依赖项(用于默认的读写实现)
compare {Function(stats, dep) -> {Boolean}} undefined 允许您覆盖缓存的依赖项与正在读取的依赖项之间的默认比较函数。返回 true 以使用缓存的资源。
precision {Number} 0 在将这些参数传递给比较函数之前,使用此毫秒数对statsdepmtime进行四舍五入。
read {Function(cacheKey, callback) -> {void}} undefined 允许您覆盖从文件中读取默认缓存数据的函数。
readOnly {Boolean} false 允许您覆盖默认值,并使缓存只读(在某些环境中很有用,您不希望更新缓存,而只是从中读取)。
write {Function(cacheKey, data, callback) -> {void}} undefined 允许您覆盖将默认缓存数据写入文件(例如Redis,memcached)的函数。

示例

基础

webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: ['cache-loader', 'babel-loader'],
include: path.resolve('src'),
},
],
},
};

在基础示例中,cache-loader 被用于缓存对 .js 文件的处理结果。当文件内容发生变化时,缓存将被无效化,并且会重新处理文件。这种缓存机制可以显著提高构建速度,特别是当处理大量未更改的文件时。

数据库集成

webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
// 或者使用其他数据库客户端 - memcached, mongodb, ...
const redis = require('redis');
const crypto = require('crypto');

// ...
// 连接到客户端
// ...

const BUILD_CACHE_TIMEOUT = 24 * 3600; // 1天

function digest(str) {
return crypto
.createHash('md5')
.update(str)
.digest('hex');
}

// 生成自定义缓存键
function cacheKey(options, request) {
return `build:cache:${digest(request)}`;
}

// 从数据库读取数据并解析
function read(key, callback) {
client.get(key, (err, result) => {
if (err) {
return callback(err);
}

if (!result) {
return callback(new Error(`Key ${key} not found`));
}

try {
let data = JSON.parse(result);
callback(null, data);
} catch (e) {
callback(e);
}
});
}

// 在cacheKey下将数据写入数据库
function write(key, data, callback) {
client.set(key, JSON.stringify(data), 'EX', BUILD_CACHE_TIMEOUT, callback);
}

module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: [
{
loader: 'cache-loader',
options: {
cacheKey,
read,
write,
},
},
'babel-loader',
],
include: path.resolve('src'),
},
],
},
};

ache-loader 被配置为使用自定义的读取和写入函数,这些函数将数据存储在 Redis 数据库中。通过 cacheKey 函数,可以为每个请求生成一个唯一的缓存键。read 函数从数据库中读取缓存数据,并将其解析为 JavaScript 对象。write 函数将处理后的数据写入数据库,并设置了一个过期时间(在这种情况下为 1 天)。

通过这种方式,缓存数据可以在构建过程中跨多个运行实例共享,并且可以持久化存储,即使 webpack 构建进程重启也不会丢失。这可以进一步提高构建速度,特别是在大型项目中,并且可以在多个构建任务之间共享缓存数据。

翻译: cache-loader

OffscreenCanvas 离屏Canvas — 使用Web Worker提高你的Canvas运行速度

OffscreenCanvas 离屏Canvas — 使用Web Worker提高你的Canvas运行速度

OffscreenCanvas提供了一个可以脱离屏幕渲染的canvas对象。

有了离屏Canvas,你可以不用在你的主线程中绘制图像了!

Canvas 是一个非常受欢迎的表现方式,同时也是WebGL的入口。它能绘制图形,图片,展示动画,甚至是处理视频内容。它经常被用来在富媒体web应用中创建炫酷的用户界面或者是制作在线(web)游戏。

它是非常灵活的,这意味着绘制在Canvas的内容可以被编程。JavaScript就提供了Canvas的系列API。这些给了Canvas非常好的灵活度。

但同时,在一些现代化的web站点,脚本解析运行是实现流畅用户反馈的最大的问题之一。因为Canvas计算和渲染和用户操作响应都发生在同一个线程中,在动画中(有时候很耗时)的计算操作将会导致App卡顿,降低用户体验。

幸运的是, OffscreenCanvas 离屏Canvas可以非常棒的解决这个麻烦!

到目前为止,Canvas的绘制功能都与<canvas>标签绑定在一起,这意味着Canvas API和DOM是耦合的。而OffscreenCanvas,正如它的名字一样,通过将Canvas移出屏幕来解耦了DOM和Canvas API。

由于这种解耦,OffscreenCanvas的渲染与DOM完全分离了开来,并且比普通Canvas速度提升了一些,而这只是因为两者(Canvas和DOM)之间没有同步。但更重要的是,将两者分离后,Canvas将可以在Web Worker中使用,即使在Web Worker中没有DOM。这给Canvas提供了更多的可能性。

兼容性

这是一个实验中的功能
此功能某些浏览器尚在开发中,请参考浏览器兼容性表格以得到在不同浏览器中适合使用的前缀。由于该功能对应的标准文档可能被重新修订,所以在未来版本的浏览器中该功能的语法和行为可能随之改变。

支持浏览器如下图所示:

OffscreenCanvas兼容性

在Worker中使用OffscreenCanvas

它在窗口环境和web worker环境均有效。

Workers 是一个Web版的线程——它允许你在幕后运行你的代码。将你的一部分代码放到Worker中可以给你的主线程更多的空闲时间,这可以提高你的用户体验度。就像其没有DOM一样,直到现在,在Worker中都没有Canvas API。

而OffscreenCanvas并不依赖DOM,所以在Worker中Canvas API可以被某种方法来代替。下面是我在Worker中用OffscreenCanvas来计算渐变颜色的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// file: worker.js

function getGradientColor(percent) {
const canvas = new OffscreenCanvas(100, 1);
const ctx = canvas.getContext('2d');
const gradient = ctx.createLinearGradient(0, 0, canvas.width, 0);
gradient.addColorStop(0, 'red');
gradient.addColorStop(1, 'blue');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, ctx.canvas.width, 1);
const imgd = ctx.getImageData(0, 0, ctx.canvas.width, 1);
const colors = imgd.data.slice(percent * 4, percent * 4 + 4);
return `rgba(${colors[0]}, ${colors[1]}, ${colors[2]}, ${colors[3]})`;
}

getGradientColor(40); // rgba(152, 0, 104, 255)

不要阻塞主线程

当我们将大量的计算移到Worker中运行时,可以释放主线程上的资源,这很有意思。我们可以使用transferControlToOffscreen 方法将常规的Canvas映射到OffscreenCanvas实例上。之后所有应用于OffscreenCanvas的操作将自动呈现在在源Canvas上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
<!DOCTYPE html>
<html>
<body>
<canvas id="myCanvas" width="600" height="500" style="border:1px solid #d3d3d3;">
Your browser does not support the HTML5 canvas tag.
</canvas>
<script>
var canvas = document.getElementById("myCanvas");
// var context = canvas.getContext("2d");

// // 画线
// context.moveTo(100, 100);
// context.lineTo(300, 100);
// context.lineTo(300, 200);

// // 画第二条线
// // 画第二条线
// context.moveTo(100, 300);
// context.lineTo(300, 300);

// // 最后要描边才会出效果
// context.stroke();

// // 创建一张新的玻璃纸
// context.beginPath();
// // 画第三条线
// context.moveTo(400, 100);
// context.lineTo(400, 300);
// context.lineTo(500, 300);
// context.lineTo(500, 200);

// // 只要执行stroke,都会玻璃纸上的图形重复印刷一次
// context.stroke();

// // 填充
// context.fill();
// context.fillStyle = "gray";

// // 设置描边色
// context.strokeStyle = "red"; // 颜色的写法和css写法是一样的
// context.stroke();

// //填充
// //设置填充色
// context.fillStyle = "yellowgreen";
// context.fill();

// //把路径闭合
// context.closePath();

// //设置线条的粗细, 不需要加px
// context.lineWidth = 15;
// //线条的头部的设置
// context.lineCap = "round"; //默认是butt, 记住round

// 注: 如果将canvas转化成离屏canvas时,就不能使用原canvas的cantext来绘制图案,否则会报错,已经绘制了的canvas不同通过transferControlToOffscreen转换成OffscreenCanvas
// Uncaught DOMException: Failed to execute 'transferControlToOffscreen' on 'HTMLCanvasElement': Cannot transfer control from a canvas that has a rendering context.
const offscreen = canvas.transferControlToOffscreen();
const worker = new Worker('worker.js');
worker.postMessage({ canvas: offscreen }, [offscreen]);
</script>
</body>
</html>

OffscreenCanvas 是可转移的,除了将其指定为传递信息中的字段之一以外,还需要将其作为postMessage(传递信息给Worker的方法)中的第二个参数传递出去,以便可以在Worker线程的context(上下文)中使用它。

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

self.onmessage = function (event) {
// 获取传送过来的离屏Canvas(OffscreenCanvas)
var canvas = event.data.canvas;
var context = canvas.getContext('2d');

// 画一个曲径球体
var c1 = {x: 240, y: 160, r: 0};
var c2 = {x: 300, y: 200, r: 120};

var gradient = context.createRadialGradient(c1.x, c1.y, c1.r, c2.x, c2.y, c2.r);
gradient.addColorStop(1, "gray");
gradient.addColorStop(0, "lightgray");

//2. 将渐变对象设为填充色
context.fillStyle = gradient;

//3. 画圆并填充
context.arc(c2.x, c2.y, c2.r, 0, 2*Math.PI);
context.fill();
}

效果如下所示:

WebWorker中OffscreenCanvas绘制径向渐变画球

任务繁忙的主线程也不会影响在Worker上运行的动画。所以即使主线程非常繁忙,你也可以通过此功能来避免掉帧并保证流畅的动画

WebRTC的YUV媒体流数据的离屏渲染

从 WebRTC 中拿到的是 YUV 的原始视频流,将原始的 YUV 视频帧直接转发过来,通过第三方库直接在 Cavans 上渲染。

可以使用yuv-canvasyuv-buffer第三方库来渲染YUV的原始视频流。

主进程render.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
"use strict";
exports.__esModule = true;
var isEqual = require('lodash.isequal');
var YUVBuffer = require('yuv-buffer');
var YUVCanvas = require('yuv-canvas');
var Renderer = /** @class */ (function () {
function Renderer(workSource) {
var _this = this;
this._sendCanvas = function () {
_this.canvasSent = true;
_this.worker && _this.worker.postMessage({
type: 'constructor',
data: {
canvas: _this.offCanvas,
id: (_this.element && _this.element.id) || (Math.random().toString(16).slice(2) + Math.random().toString(16).slice(2))
}
}, [_this.offCanvas]);
};
/**
* 判断使用渲染的方式
*/
this._checkRendererWay = function () {
if (_this.workerReady && _this.worker && _this.offCanvas && _this.enableWorker) {
return 'worker';
}
else {
return 'software';
}
};
// workerCanvas渲染
this._workDrawFrame = function (width, height, yUint8Array, uUint8Array, vUint8Array) {
if (_this.canvasWrapper && _this.canvasWrapper.style.display !== 'none') {
_this.canvasWrapper.style.display = 'none';
}
if (_this.workerCanvasWrapper && _this.workerCanvasWrapper.style.display === 'none') {
_this.workerCanvasWrapper.style.display = 'flex';
}
_this.worker && _this.worker.postMessage({
type: 'drawFrame',
data: {
width: width,
height: height,
yUint8Array: yUint8Array,
uUint8Array: uUint8Array,
vUint8Array: vUint8Array
}
}, [yUint8Array, uUint8Array, vUint8Array]);
};
// 实际渲染Canvas
this._softwareDrawFrame = function (width, height, yUint8Array, uUint8Array, vUint8Array) {
if (_this.workerCanvasWrapper && _this.workerCanvasWrapper.style.display !== 'none') {
_this.workerCanvasWrapper.style.display = 'none';
}
if (_this.canvasWrapper && _this.canvasWrapper.style.display === 'none') {
_this.canvasWrapper.style.display = 'flex';
}
var format = YUVBuffer.format({
width: width,
height: height,
chromaWidth: width / 2,
chromaHeight: height / 2
});
var y = YUVBuffer.lumaPlane(format, yUint8Array);
var u = YUVBuffer.chromaPlane(format, uUint8Array);
var v = YUVBuffer.chromaPlane(format, vUint8Array);
var frame = YUVBuffer.frame(format, y, u, v);
_this.yuv.drawFrame(frame);
};
this.cacheCanvasOpts = {};
this.yuv = {};
this.ready = false;
this.contentMode = 0;
this.container = {};
this.canvasWrapper;
this.canvas = {};
this.element = {};
this.offCanvas = {};
this.enableWorker = !!workSource;
if (this.enableWorker) {
this.worker = new Worker(workSource);
this.workerReady = false;
this.canvasSent = false;
this.worker.onerror = function (evt) {
console.error('[WorkerRenderer]: the renderer worker catch error: ', evt);
_this.workerReady = false;
_this.enableWorker = false;
};
this.worker.onmessage = function (evt) {
var data = evt.data;
switch (data.type) {
case 'ready': {
console.log('[WorkerRenderer]: the renderer worker was ready');
_this.workerReady = true;
if (_this.offCanvas) {
_this._sendCanvas();
}
break;
}
case 'exited': {
console.log('[WorkerRenderer]: the renderer worker was exited');
_this.workerReady = false;
_this.enableWorker = false;
break;
}
}
};
}
}
Renderer.prototype._calcZoom = function (vertical, contentMode, width, height, clientWidth, clientHeight) {
if (vertical === void 0) { vertical = false; }
if (contentMode === void 0) { contentMode = 0; }
var localRatio = clientWidth / clientHeight;
var tempRatio = width / height;
if (isNaN(localRatio) || isNaN(tempRatio)) {
return 1;
}
if (!contentMode) {
if (vertical) {
return localRatio > tempRatio ?
clientHeight / height : clientWidth / width;
}
else {
return localRatio < tempRatio ?
clientHeight / height : clientWidth / width;
}
}
else {
if (vertical) {
return localRatio < tempRatio ?
clientHeight / height : clientWidth / width;
}
else {
return localRatio > tempRatio ?
clientHeight / height : clientWidth / width;
}
}
};
Renderer.prototype.getBindingElement = function () {
return this.element;
};
Renderer.prototype.bind = function (element) {
// record element
this.element = element;
// create container
var container = document.createElement('div');
container.className += ' video-canvas-container';
Object.assign(container.style, {
width: '100%',
height: '100%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
position: 'relative'
});
this.container = container;
element && element.appendChild(this.container);
// 创建两个canvas,一个在主线程中渲染,如果web worker中的离屏canvas渲染进程出错了,还可以切换到主进程的canvas进行渲染
var canvasWrapper = document.createElement('div');
canvasWrapper.className += ' video-canvas-wrapper canvas-renderer';
Object.assign(canvasWrapper.style, {
width: '100%',
height: '100%',
justifyContent: 'center',
alignItems: 'center',
position: 'absolute',
left: '0px',
right: '0px',
display: 'none'
});
this.canvasWrapper = canvasWrapper;
this.container.appendChild(this.canvasWrapper);
var workerCanvasWrapper = document.createElement('div');
workerCanvasWrapper.className += ' video-canvas-wrapper webworker-renderer';
Object.assign(workerCanvasWrapper.style, {
width: '100%',
height: '100%',
justifyContent: 'center',
alignItems: 'center',
position: 'absolute',
left: '0px',
right: '0px',
display: 'none'
});
this.workerCanvasWrapper = workerCanvasWrapper;
this.container.appendChild(this.workerCanvasWrapper);
// create canvas
this.canvas = document.createElement('canvas');
this.workerCanvas = document.createElement('canvas');
this.canvasWrapper.appendChild(this.canvas);
this.workerCanvasWrapper.appendChild(this.workerCanvas);
// 创建 OffscreenCanvas 对象
this.offCanvas = this.workerCanvas.transferControlToOffscreen();
if (!this.canvasSent && this.offCanvas && this.worker && this.workerReady) {
this._sendCanvas();
}
this.yuv = YUVCanvas.attach(this.canvas, { webGL: false });
};
Renderer.prototype.unbind = function () {
this.canvasWrapper && this.canvasWrapper.removeChild(this.canvas);
this.workerCanvasWrapper && this.workerCanvasWrapper.removeChild(this.workerCanvas);
this.container && this.container.removeChild(this.canvasWrapper);
this.container && this.container.removeChild(this.workerCanvasWrapper);
this.element && this.element.removeChild(this.container);
this.worker && this.worker.terminate();
this.workerReady = false;
this.canvasSent = false;
this.yuv = null;
this.container = null;
this.workerCanvasWrapper = null;
this.canvasWrapper = null;
this.element = null;
this.canvas = null;
this.workerCanvas = null;
this.offCanvas = null;
this.worker = null;
};
Renderer.prototype.refreshCanvas = function () {
// Not implemented for software renderer
};
Renderer.prototype.updateCanvas = function (options) {
if (options === void 0) { options = {
width: 0,
height: 0,
rotation: 0,
mirrorView: false,
contentMode: 0,
clientWidth: 0,
clientHeight: 0
}; }
// check if display options changed
if (isEqual(this.cacheCanvasOpts, options)) {
return;
}
this.cacheCanvasOpts = Object.assign({}, options);
// check for rotation
if (options.rotation === 0 || options.rotation === 180) {
this.canvas.width = options.width;
this.canvas.height = options.height;
// canvas 调用 transferControlToOffscreen 方法后无法修改canvas的宽度和高度,只允许修改canvas的style属性
this.workerCanvas.style.width = options.width + "px";
this.workerCanvas.style.height = options.height + "px";
}
else if (options.rotation === 90 || options.rotation === 270) {
this.canvas.height = options.width;
this.canvas.width = options.height;
this.workerCanvas.style.height = options.width + "px";
this.workerCanvas.style.width = options.height + "px";
}
else {
throw new Error('Invalid value for rotation. Only support 0, 90, 180, 270');
}
var transformItems = [];
transformItems.push("rotateZ(" + options.rotation + "deg)");
var scale = this._calcZoom(options.rotation === 90 || options.rotation === 270, options.contentMode, options.width, options.height, options.clientWidth, options.clientHeight);
// transformItems.push(`scale(${scale})`)
this.canvas.style.zoom = scale;
this.workerCanvas.style.zoom = scale;
// check for mirror
if (options.mirrorView) {
// this.canvas.style.transform = 'rotateY(180deg)';
transformItems.push('rotateY(180deg)');
}
if (transformItems.length > 0) {
var transform = "" + transformItems.join(' ');
this.canvas.style.transform = transform;
this.workerCanvas.style.transform = transform;
}
};
Renderer.prototype.drawFrame = function (imageData) {
if (!this.ready) {
this.ready = true;
}
var dv = new DataView(imageData.header);
// let format = dv.getUint8(0);
var mirror = dv.getUint8(1);
var contentWidth = dv.getUint16(2);
var contentHeight = dv.getUint16(4);
var left = dv.getUint16(6);
var top = dv.getUint16(8);
var right = dv.getUint16(10);
var bottom = dv.getUint16(12);
var rotation = dv.getUint16(14);
// let ts = dv.getUint32(16);
var width = contentWidth + left + right;
var height = contentHeight + top + bottom;
this.updateCanvas({
width: width, height: height, rotation: rotation,
mirrorView: !!mirror,
contentMode: this.contentMode,
clientWidth: this.container && this.container.clientWidth,
clientHeight: this.container && this.container.clientHeight
});
if (this._checkRendererWay() === 'software') {
// 实际渲染canvas
this._softwareDrawFrame(width, height, imageData.yUint8Array, imageData.uUint8Array, imageData.vUint8Array);
}
else {
this._workDrawFrame(width, height, imageData.yUint8Array, imageData.uUint8Array, imageData.vUint8Array);
}
};
/**
* 清空整个Canvas面板
*
* @memberof Renderer
*/
Renderer.prototype.clearFrame = function () {
if (this._checkRendererWay() === 'software') {
this.yuv && this.yuv.clear();
}
else {
this.worker && this.worker.postMessage({
type: 'clearFrame'
});
}
};
Renderer.prototype.setContentMode = function (mode) {
if (mode === void 0) { mode = 0; }
this.contentMode = mode;
};
return Renderer;
}());

exports["default"] = Renderer;

渲染 WebWorker

具体代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
// render worker

(function() {
const dateFormat = function(date, formatter = 'YYYY-MM-DD hh:mm:ss SSS') {
if (!date) {
return date;
}

let time;

try {
time = new Date(date);
} catch (e) {
return date;
}

const oDate = {
Y: time.getFullYear(),
M: time.getMonth() + 1,
D: time.getDate(),
h: time.getHours(),
m: time.getMinutes(),
s: time.getSeconds(),
S: time.getMilliseconds()
};

return formatter.replace(/(Y|M|D|h|m|s|S)+/g, (res, key) => {
let len = 2;

switch (res.length) {
case 1:
len = res.slice(1, 0) === 'Y' ? 4 : 2;
break;
case 2:
len = 2;
break;
case 3:
len = 3;
break;
case 4:
len = 4;
break;
default:
len = 2;
}
return (`0${oDate[key]}`).slice(-len);
});
}

let yuv;

try {
importScripts('./yuv-buffer/yuv-buffer.js');
importScripts('./yuv-canvas/shaders.js');
importScripts('./yuv-canvas/depower.js');
importScripts('./yuv-canvas/YCbCr.js');
importScripts('./yuv-canvas/FrameSink.js');
importScripts('./yuv-canvas/SoftwareFrameSink.js');
importScripts('./yuv-canvas/WebGLFrameSink.js');
importScripts('./yuv-canvas/yuv-canvas.js');

self.addEventListener('message', function (e) {
const data = e.data;
switch (data.type) {
case 'constructor':
console.log(`${dateFormat(new Date())} RENDER_WORKER [INFO]: received canvas: `, data.data.canvas, data.data.id);
yuv = YUVCanvas.attach(data.data.canvas, { webGL: false });
break;
case 'drawFrame':
// 考虑是否使用requestAnimationFrame进行渲染,控制每一帧显示的频率
const width = data.data.width;
const height = data.data.height;
const yUint8Array = data.data.yUint8Array;
const uUint8Array = data.data.uUint8Array;
const vUint8Array = data.data.vUint8Array;
const format = YUVBuffer.format({
width: width,
height: height,
chromaWidth: width / 2,
chromaHeight: height / 2
});
const y = YUVBuffer.lumaPlane(format, yUint8Array);
const u = YUVBuffer.chromaPlane(format, uUint8Array);
const v = YUVBuffer.chromaPlane(format, vUint8Array);
const frame = YUVBuffer.frame(format, y, u, v);
yuv && yuv.drawFrame(frame);
break;
case 'clearFrame': {
yuv && yuv.clear(frame);
break;
}
default:
console.log(`${dateFormat(new Date())} RENDER_WORKER [INFO]: [RendererWorker]: Unknown message: `, data);
};
}, false);

self.postMessage({
type: 'ready',
});
} catch (error) {
self.postMessage({
type: 'exited',
});

console.log(`${dateFormat(new Date())} RENDER_WORKER [INFO]: [RendererWorker]: catch error`, error);
}
})();

总结

如果你对图像绘画使用得非常多,OffscreenCanvas可以有效的提高你APP的性能。它使得Worker可以处理canvas的渲染绘制,让你的APP更好地利用了多核系统。

OffscreenCanvas在Chrome 69中已经不需要开启flag(实验性功能)就可以使用了。它也正在被 Firefox 实现。由于其API与普通canvas元素非常相似,所以你可以轻松地对其进行特征检测并循序渐进地使用它,而不会破坏现有的APP或库的运行逻辑。OffscreenCanvas在任何涉及到图形计算以及动画表现且与DOM关系并不密切(即依赖DOM API不多)的情况下,它都具有性能优势。

WebWorker 和 postMessage 使用指南

WebWorker 和 postMessage 使用指南

概述

在网页开发中,我们通常使用 JavaScript 来处理和响应用户的各种操作。然而,随着网页变得越来越复杂,我们需要执行的任务也变得越来越繁重。这些任务可能包括大量的计算、数据处理、文件读取等。如果我们直接在主线程中执行这些任务,可能会导致页面卡顿、响应变慢,甚至阻塞用户的其他操作。为了解决这个问题,Web Worker 应运而生。

JavaScript 语言采用的是单线程模型,也就是说,所有任务只能在一个线程上完成,一次只能做一件事。前面的任务没做完,后面的任务只能等着。随着电脑计算能力的增强,尤其是多核 CPU 的出现,单线程带来很大的不便,无法充分发挥计算机的计算能力。

Web Worker 的作用,就是为 JavaScript 创造多线程环境,允许主线程创建 Worker 线程,将一些任务分配给后者运行。在主线程运行的同时,Worker 线程在后台运行,两者互不干扰。等到 Worker 线程完成计算任务,再把结果返回给主线程。这样的好处是,一些计算密集型或高延迟的任务,被 Worker 线程负担了,主线程(通常负责 UI 交互)就会很流畅,不会被阻塞或拖慢。

Web Worker 是一种在浏览器后台运行的 JavaScript 线程,Worker 线程一旦新建成功,就会始终运行,它独立于主线程运行,不会阻塞页面的渲染和用户交互,不会被主线程上的活动(比如用户点击按钮、提交表单)打断。这样有利于随时响应主线程的通信。但是,这也造成了 Worker 比较耗费资源,不应该过度使用,而且一旦使用完毕,就应该关闭。

Web Worker 有以下几个使用注意点。

(1)同源限制

分配给 Worker 线程运行的脚本文件,必须与主线程的脚本文件同源。

(2)DOM 限制

Worker 线程所在的全局对象,与主线程不一样,无法读取主线程所在网页的 DOM 对象,也无法使用documentwindowparent这些对象。但是,Worker 线程可以navigator对象和location对象。

(3)通信联系

Worker 线程和主线程不在同一个上下文环境,它们不能直接通信,必须通过消息完成。(postMessage)

(4)脚本限制

Worker 线程不能执行alert()方法和confirm()方法,但可以使用 XMLHttpRequest 对象发出 AJAX 请求。

(5)文件限制

Worker 线程无法读取本地文件,即不能打开本机的文件系统(file://),它所加载的脚本,必须来自网络。

基本用法

主线程

主线程采用new命令,调用Worker()构造函数,新建一个 Worker 线程。

1
var worker = new Worker('work.js');

Worker()构造函数的参数是一个脚本文件,该文件就是 Worker 线程所要执行的任务。由于 Worker 不能读取本地文件,所以这个脚本必须来自网络。如果下载没有成功(比如404错误),Worker 就会默默地失败。

然后,主线程调用worker.postMessage()方法,向 Worker 发消息。

1
2
3
worker.postMessage('Hello World');
worker.postMessage({method: 'echo', args: ['Work']});
// worker.postMessage() 方法的参数,就是主线程传给 Worker 的数据。它可以是各种数据类型,包括二进制数据。

接着,主线程通过worker.onmessage指定监听函数,接收子线程发回来的消息。

1
2
3
4
5
6
7
8
9
worker.onmessage = function (event) {
console.log('Received message ' + event.data);
doSomething();
}

function doSomething() {
// 执行任务
worker.postMessage('Work done!');
}

上面代码中,事件对象的data属性可以获取 Worker 发来的数据。

Worker 完成任务以后,主线程就可以把它关掉。

1
worker.terminate();

Worker 线程

Worker 线程内部需要有一个监听函数,监听message事件。

1
2
3
self.addEventListener('message', function (e) {
self.postMessage('You said: ' + e.data);
}, false);

上面代码中,self代表子线程自身,即子线程的全局对象。因此,等同于下面两种写法。

1
2
3
4
5
6
7
8
9
// 写法一
this.addEventListener('message', function (e) {
this.postMessage('You said: ' + e.data);
}, false);

// 写法二
addEventListener('message', function (e) {
postMessage('You said: ' + e.data);
}, false);

除了使用self.addEventListener()指定监听函数,也可以使用self.onmessage指定。监听函数的参数是一个事件对象,它的data属性包含主线程发来的数据。self.postMessage()方法用来向主线程发送消息。

根据主线程发来的数据,Worker 线程可以调用不同的方法,下面是一个例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
self.addEventListener('message', function (e) {
var data = e.data;
switch (data.cmd) {
case 'start':
self.postMessage('WORKER STARTED: ' + data.msg);
break;
case 'stop':
self.postMessage('WORKER STOPPED: ' + data.msg);
self.close(); // Terminates the worker.
break;
default:
self.postMessage('Unknown command: ' + data.msg);
};
}, false);

上面代码中,self.close()用于在 Worker 内部关闭自身。

Worker 加载脚本

Worker 内部如果要加载其他脚本,有一个专门的方法importScripts()

1
importScripts('script1.js');

该方法可以同时加载多个脚本。

1
importScripts('script1.js', 'script2.js');

Worker 错误处理

主线程可以监听 Worker 是否发生错误。如果发生错误,Worker 会触发主线程的error事件。

1
2
3
4
5
6
7
8
9
10
worker.onerror(function (event) {
console.log([
'ERROR: Line ', e.lineno, ' in ', e.filename, ': ', e.message
].join(''));
});

// 或者
worker.addEventListener('error', function (event) {
// ...
});

Worker 内部也可以监听error事件。

关闭 Worker

使用完毕,为了节省系统资源,必须关闭 Worker

1
2
3
4
5
// 主线程
worker.terminate();

// Worker 线程
self.close();

数据通信

前面说过,主线程与 Worker 之间的通信内容,可以是文本,也可以是对象。需要注意的是,这种通信是拷贝关系,即是传值而不是传址,Worker 对通信内容的修改,不会影响到主线程。事实上,浏览器内部的运行机制是,先将通信内容串行化,然后把串行化后的字符串发给 Worker,后者再将它还原。

主线程与 Worker 之间也可以交换二进制数据,比如 FileBlobArrayBuffer 等类型,也可以在线程之间发送。下面是一个例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 主线程
var uInt8Array = new Uint8Array(new ArrayBuffer(10));
for (var i = 0; i < uInt8Array.length; ++i) {
uInt8Array[i] = i * 2; // [0, 2, 4, 6, 8,...]
}
worker.postMessage(uInt8Array);

// Worker 线程
self.onmessage = function (e) {
var uInt8Array = e.data;
postMessage('Inside worker.js: uInt8Array.toString() = ' + uInt8Array.toString());
postMessage('Inside worker.js: uInt8Array.byteLength = ' + uInt8Array.byteLength);
};

但是,拷贝方式发送二进制数据,会造成性能问题。比如,主线程向 Worker 发送一个 500MB 文件,默认情况下浏览器会生成一个原文件的拷贝。为了解决这个问题,JavaScript 允许主线程把二进制数据直接转移给子线程,但是一旦转移,主线程就无法再使用这些二进制数据了,这是为了防止出现多个线程同时修改数据的麻烦局面。这种转移数据的方法,叫做Transferable Objects。这使得主线程可以快速把数据交给 Worker,对于影像处理、声音处理、3D 运算等就非常方便了,不会产生性能负担。

如果要直接转移数据的控制权,就要使用下面的写法。

1
2
3
4
5
6
// Transferable Objects 格式
worker.postMessage(arrayBuffer, [arrayBuffer]);

// 例子
var ab = new ArrayBuffer(1);
worker.postMessage(ab, [ab]);

同页面的 Web Worker

通常情况下,Worker 载入的是一个单独的 JavaScript 脚本文件,但是也可以载入与主线程在同一个网页的代码。

1
2
3
4
5
6
7
8
9
<!DOCTYPE html>
<body>
<script id="worker" type="app/worker">
addEventListener('message', function () {
postMessage('some message');
}, false);
</script>
</body>
</html>

上面是一段嵌入网页的脚本,注意必须指定<script>标签的type属性是一个浏览器不认识的值,上例是app/worker

然后,读取这一段嵌入页面的脚本,用 Worker 来处理。

1
2
3
4
5
6
7
var blob = new Blob([document.querySelector('#worker').textContent]);
var url = window.URL.createObjectURL(blob);
var worker = new Worker(url);

worker.onmessage = function (e) {
// e.data === 'some message'
};

上面代码中,先将嵌入网页的脚本代码,转成一个二进制对象,然后为这个二进制对象生成 URL,再让 Worker 加载这个 URL。这样就做到了,主线程和 Worker 的代码都在同一个网页上面。

Worker 线程完成轮询

有时,浏览器需要轮询服务器状态,以便第一时间得知状态改变。这个工作可以放在 Worker 里面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function createWorker(f) {
var blob = new Blob(['(' + f.toString() +')()']);
var url = window.URL.createObjectURL(blob);
var worker = new Worker(url);
return worker;
}

var pollingWorker = createWorker(function (e) {
var cache;

function compare(new, old) { ... };

setInterval(function () {
fetch('/my-api-endpoint').then(function (res) {
var data = res.json();

if (!compare(data, cache)) {
cache = data;
self.postMessage(data);
}
})
}, 1000)
});

pollingWorker.onmessage = function () {
// render data
}

pollingWorker.postMessage('init');

上面代码中,Worker 每秒钟轮询一次数据,然后跟缓存做比较。如果不一致,就说明服务端有了新的变化,因此就要通知主线程。

Worker 新建 Worker

Worker 线程内部还能再新建 Worker 线程(目前只有 Firefox 浏览器支持)。下面的例子是将一个计算密集的任务,分配到10个 Worker

主线程代码如下。

1
2
3
4
var worker = new Worker('worker.js');
worker.onmessage = function (event) {
document.getElementById('result').textContent = event.data;
};

Worker 线程代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// worker.js

// settings
var num_workers = 10;
var items_per_worker = 1000000;

// start the workers
var result = 0;
var pending_workers = num_workers;
for (var i = 0; i < num_workers; i += 1) {
var worker = new Worker('core.js');
worker.postMessage(i * items_per_worker);
worker.postMessage((i + 1) * items_per_worker);
worker.onmessage = storeResult;
}

// handle the results
function storeResult(event) {
result += event.data;
pending_workers -= 1;
if (pending_workers <= 0)
postMessage(result); // finished!
}

上面代码中,Worker 线程内部新建了10个 Worker 线程,并且依次向这10个 Worker 发送消息,告知了计算的起点和终点。计算任务脚本的代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// core.js
var start;
onmessage = getStart;
function getStart(event) {
start = event.data;
onmessage = getEnd;
}

var end;
function getEnd(event) {
end = event.data;
onmessage = null;
work();
}

function work() {
var result = 0;
for (var i = start; i < end; i += 1) {
// perform some complex calculation here
result += 1;
}
postMessage(result);
close();
}

API

主线程

浏览器原生提供Worker()构造函数,用来供主线程生成 Worker 线程。

1
var myWorker = new Worker(jsUrl, options);

Worker()构造函数,可以接受两个参数。第一个参数是脚本的网址(必须遵守同源政策),该参数是必需的,且只能加载 JS 脚本,否则会报错。第二个参数是配置对象,该对象可选。它的一个作用就是指定 Worker 的名称,用来区分多个 Worker 线程。

1
2
3
4
5
// 主线程
var myWorker = new Worker('worker.js', { name : 'myWorker' });

// Worker 线程
self.name // myWorker

Worker()构造函数返回一个 Worker 线程对象,用来供主线程操作 WorkerWorker 线程对象的属性和方法如下。

  • Worker.onerror:指定 error 事件的监听函数。
  • Worker.onmessage:指定 message 事件的监听函数,发送过来的数据在Event.data属性中。
  • Worker.onmessageerror:指定 messageerror 事件的监听函数。发送的数据无法序列化成字符串时,会触发这个事件。
  • Worker.postMessage():Worker 线程发送消息。
  • Worker.terminate():立即终止 Worker 线程。

Worker 线程

Web Worker 有自己的全局对象,不是主线程的window,而是一个专门为 Worker 定制的全局对象。因此定义在window上面的对象和方法不是全部都可以使用。

Worker 线程有一些自己的全局属性和方法。

  • self.name:Worker 的名字。该属性只读,由构造函数指定。
  • self.onmessage:指定 message 事件的监听函数。
  • self.onmessageerror:指定 messageerror 事件的监听函数。发送的数据无法序列化成字符串时,会触发这个事件。
  • self.close():关闭 Worker 线程。
  • self.postMessage():向产生这个 Worker 线程发送消息。
  • self.importScripts():加载 JS 脚本。

应用场景

Web Workers适用于以下场景:

  1. 大量数据处理:如表格、图表等需要处理大量数据的场景,可以将数据处理任务放在 Web Workers 中执行,避免阻塞主线程。
  2. 复杂计算:如图像处理、机器学习等需要执行复杂计算的场景,可以利用 Web Workers 实现并行计算,提高性能。
  3. 文件读写:如读取大文件、处理文件数据等场景,可以使用 Web Workers 在后台进行文件读写操作,避免阻塞主线程。

总之,Web Workers 为网页开发提供了一种有效的手段来优化性能和用户体验。通过合理利用 Web Workers,我们可以实现真正的并行处理,使网页更加流畅、响应更快。

来源: https://www.ruanyifeng.com/blog/2018/07/web-worker.html

Nginx常用基础配置详解

Nginx常用基础配置详解

介绍

Nginx 是一个高性能的开源 Web 服务器,它不仅可以作为 HTTP 服务器使用,还可以用作反向代理服务器、负载均衡器、缓存服务器等。在实际应用中,正确配置 Nginx 是确保网站稳定性和性能的重要步骤。本文将详细介绍一些常用的 Nginx 基础配置,包括虚拟主机配置、HTTP 重定向、反向代理等内容,并提供详细的示例说明。

虚拟主机配置

虚拟主机是 Nginx 中非常重要的概念,它允许您在一台服务器上托管多个网站。下面是一个简单的虚拟主机配置示例:

1
2
3
4
5
6
7
8
9
10
11
server {
listen 80;
server_name example.com www.example.com;

root /var/www/example;
index index.html index.htm;

location / {
try_files $uri $uri/ /index.html;
}
}
  • **listen 80;**:指定 Nginx 监听的端口号。
  • **server_name example.com www.example.com;**:指定虚拟主机的域名。
  • **root /var/www/example;**:指定网站的根目录。
  • **index index.html index.htm;**:指定默认的索引文件。
  • **location /**:配置请求的 URL 路径。
  • **try_files $uri $uri/ /index.html;**:尝试寻找与请求 URI 匹配的文件,如果找不到则返回 index.html

HTTP 重定向

HTTP 重定向是将一个 URL 请求重定向到另一个 URL 的过程。下面是一个简单的 HTTP 重定向配置示例:

1
2
3
4
5
6
server {
listen 80;
server_name www.example.com;

return 301 http://example.com$request_uri;
}

这个配置会将所有访问 www.example.com 的请求重定向到 example.com,保证网站的访问统一性。

反向代理

Nginx 反向代理是将客户端的请求转发给后端服务器的过程,常用于负载均衡和隐藏后端服务器。下面是一个简单的反向代理配置示例:

1
2
3
4
5
6
7
8
9
10
11
12
server {
listen 80;
server_name www.example.com;

location / {
proxy_pass http://backend_server;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
  • **proxy_pass http://backend_server;**:指定后端服务器的地址。
  • **proxy_set_header**:设置代理请求的头部信息,如 HostX-Real-IPX-Forwarded-For 等。

SSL/TLS 配置

SSL/TLS 是保护网站安全的重要手段,Nginx 提供了丰富的 SSL/TLS 配置选项。下面是一个简单的 SSL/TLS 配置示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
server {
listen 80;
server_name www.example.com;
# 将 http 重定向转移到 https
return 301 https://$server_name$request_uri;
}

server {
listen 443 ssl;
server_name www.example.com;
ssl_certificate /etc/nginx/ssl/www.example.com.pem;
ssl_certificate_key /etc/nginx/ssl/www.example.com.key;
ssl_session_timeout 10m;
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;

location / {
root /var/nginx/html;
index index.html index.htm index.md;
try_files $uri $uri/ /index.html;
}
}

  • **listen 443 ssl;**:指定监听的端口号,并开启 SSL。
  • **ssl_certificate 和 ssl_certificate_key**:指定 SSL 证书和私钥的路径。
  • **ssl_protocols**:指定允许的 SSL/TLS 协议版本。
  • **ssl_prefer_server_ciphers on;**:优先使用服务器端的加密算法。
  • **ssl_ciphers**:指定允许的加密算法。

隐藏 Nginx 版本信息

要隐藏 Nginx 版本信息,您可以通过在配置文件中进行相应的设置来实现。具体来说,您需要修改 nginx.conf 文件,使用 server_tokens off; 指令来关闭 Nginx 的版本号显示。以下是如何进行配置的示例:

1
2
3
http {
server_tokens off;
}

将上述配置添加到 nginx.conf 文件的 http 块中即可禁用 Nginx 版本号显示。

禁止 ip 直接访问 80 端口

要禁止直接通过 IP 地址访问 80 端口,您可以通过 Nginx 配置文件进行相应的设置。具体来说,您可以配置一个默认的 server 块,用于捕获所有请求,并返回一个错误页面或者重定向到其他地址。以下是一个示例配置:

1
2
3
4
5
6
server {
listen 80 default_server;
server_name _;

return 444;
}

在这个配置中:

  • **listen 80 default_server;**:指定 Nginx 监听默认的 HTTP 端口,并将此 server 块标记为默认服务器。
  • **server_name _;**: 表示该 server 块将匹配所有请求。
  • **return 444;**: 是一个特殊的 Nginx 返回指令,它会立即关闭客户端连接,相当于不做任何响应。

通过这样的配置,当有请求通过 IP 地址直接访问 80 端口时,Nginx 将返回一个 444 错误,不会提供任何内容,从而实现了禁止直接访问 80 端口的目的。

启动 web 服务 (react 项目为例)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
server {
# 项目启动端口
listen 80;
# 域名(localhost)
server_name _;
# 禁止 iframe 嵌套
add_header X-Frame-Options SAMEORIGIN;

# 访问地址 根路径配置
location / {
# 项目目录
root /var/nginx/html;
# 默认读取文件
index index.html;
# 配置 history 模式的刷新空白
try_files $uri $uri/ /index.html;
}

# 后缀匹配,解决静态资源找不到问题
location ~* \.(gif|jpg|jpeg|png|css|js|ico)$ {
root /var/nginx/html/static/;
}

# 图片防盗链
location ~/static/.*\.(jpg|jpeg|png|gif|webp)$ {
root /var/nginx/html;
valid_referers *.example.com;
if ($invalid_referer) {
return 403;
}
}

# 访问限制
location /static {
root /var/nginx/html;
# allow 允许
allow 39.xxx.xxx.xxx;
# deny 拒绝
deny all;
}
}

在这个配置中:

  • **listen 80;**: 指定 Nginx 监听的端口号。
  • **server_name _;**: 表示该 server 块将匹配所有请求。
  • **location /**: 配置请求的 · 路径,尝试寻找与请求 URI 匹配的文件,如果找不到则返回 index.html
  • **add_header X-Frame-Options SAMEORIGIN;**:设置响应的头部信息 X-Frame-Options,不允许我们的页面嵌套到第三方网页里面。
  • **root /var/nginx/html;**: 指定网站的根目录,这里是 React 项目构建后的静态文件目录。
  • **index index.html;**: 指定默认的索引文件。
  • **try_files $uri $uri/ /index.html;**:尝试寻找与请求 URI 匹配的文件,如果找不到则返回 index.html

一个web服务,配置多个项目 (location 匹配路由区别)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
server {
listen 80;
server_name _;

# 主应用
location / {
root /var/nginx/html/main;
index index.html;
try_files $uri $uri/ /index.html;
}

# 子应用一
location ^~ /user/ {
proxy_pass http://localhost:8001;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

# 子应用二
location ^~ /product/ {
proxy_pass http://localhost:8002;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

# 静态资源读取不到问题处理
rewrite ^/api/profile/(.*)$ /(替换成正确路径的文件的上一层目录)/$1 last;
}

# 子应用一服务
server {
listen 8001;
server_name _;
location / {
root /var/nginx/html/user;
index index.html;
try_files $uri $uri/ /index.html;
}

location ^~ /user/ {
alias /var/nginx/html/user/;
index index.html index.htm;
try_files $uri /user/index.html;
}

# 接口代理
location /api {
proxy_pass http://localhost:3001;
}
}

# 子应用二服务
server {
listen 8002;
server_name _;

location / {
root /var/nginx/html/product;
index index.html;
try_files $uri $uri/ /index.html;
}

location ^~ /product/ {
alias /var/nginx/html/product/;
index index.html index.htm;
try_files $uri /product/index.html;
}

# 接口代理
location /api {
proxy_pass http://localhost:3002;
}
}

在上述配置中

  • **location ^~ /user/**:使用 ^~ 修饰的 location 块,匹配以 /user/ 开头的 URI。如果请求的 URI/user/ 开头,则 Nginx 将立即停止搜索其他 location 块,而是使用这个 location 块进行处理,location 块里面使用反向代理,指向服务器 http://localhost:8001 的地址。
  • **location ^~ /product/**:同上,匹配以 /product/ 开头的 URI

PC端和移动端使用不同的项目文件映射

要在 Nginx 中根据用户设备类型(例如PC端和移动端)使用不同的项目文件映射,您可以使用 map 指令创建一个变量,根据用户的 User-Agent 头部信息来判断设备类型,并使用if语句根据变量的值来选择不同的文件映射。以下是一个示例配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
map $http_user_agent $is_mobile {
default 0;
~*iphone 1;
~*android 1;
~*mobile 1;
}

server {
listen 80;
server_name example.com;

root /var/www;

location / {
if ($is_mobile) {
alias /var/www/mobile/;
}
alias /var/www/desktop/;
try_files $uri $uri/ /index.html;
}
}

或者

1
2
3
4
5
6
7
8
9
server {
location / {
root /var/www/desktop;
if ($http_user_agent ~* '(mobile|android|iphone|ipad|phone)') {
root /var/www/mobile;
}
index index.html;
}
}

在这个配置中:

  • **map $http_user_agent $is_mobile**:创建一个变量 $is_mobile,根据 $http_user_agent 中的 User-Agent 头部信息判断设备类型。如果 User-Agent 中包含 iphoneandroidmobile 等关键词,则将 $is_mobile 设置为 1,否则设置为 0
  • **location /**:对所有请求进行匹配。
  • **if ($is_mobile)**:使用 if 语句根据 $is_mobile 的值判断设备类型。如果是移动设备,则将请求映射到 /var/www/mobile/ 目录;否则映射到 /var/www/desktop/ 目录。
  • **try_files $uri $uri/ /index.html;**:尝试寻找与请求 URI 匹配的文件,如果找不到则返回 /index.html

需要注意的是,尽管 if 语句在 Nginx 中是有效的,但它可能会导致性能问题,并且在某些情况下可能不起作用。如果您担心性能问题,可以考虑使用更高效的方法,如根据不同的 User-Agent 头部信息设置不同的变量,并使用 map 指令匹配。

配置负载均衡

要在 Nginx 中配置负载均衡,您可以使用 upstream 块定义一组后端服务器,并在 server 块中使用 proxy_pass 指令将请求代理到这组后端服务器。以下是一个示例配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
http {
upstream backend {
server backend1.example.com;
server backend2.example.com;
server backend3.example.com;
}

server {
listen 80;
server_name example.com;

location / {
proxy_pass http://backend;
proxy_set_header Host $proxy_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
}

在这个配置中:

  • **upstream backend**:定义了一个名为 backend 的负载均衡组,其中包含了三个后端服务器:backend1.example.combackend2.example.combackend3.example.com
  • **server块中的location /**:匹配所有请求。
  • **proxy_pass http://backend;**:将请求代理到名为 backend 的负载均衡组中的服务器。Nginx 会自动根据默认的负载均衡算法(轮询)将请求分发到这组后端服务器中的一个。
  • **proxy_set_header**:设置代理请求的头部信息,如真实 IP 地址、转发者 IP 地址和主机地址。

您还可以根据需要添加其他负载均衡配置选项,例如设置负载均衡算法、调整权重等。以下是一个更复杂的负载均衡配置示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
http {
upstream backend {
least_conn; # 使用最少连接数算法
server backend1.example.com weight=3;
server backend2.example.com;
server backend3.example.com;
}

server {
listen 80;
server_name example.com;

location / {
proxy_pass http://backend;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
}
}
}

在这个配置中:

  • **least_conn**:使用最少连接数算法进行负载均衡。
  • **server backend1.example.com weight=3;**:设置 backend1.example.com 的权重为 3,比其他后端服务器更具优先级。
  • **proxy_set_header**:设置代理请求的头部信息,如真实 IP 地址、转发者 IP 地址和主机地址。

总结

本文介绍了一些常用的 Nginx 基础配置,包括虚拟主机配置、HTTP 重定向、反向代理、SSL/TLS 配置等内容,并提供了详细的示例说明。正确配置 Nginx 不仅可以提高网站的性能和安全性,还能提升用户体验和搜索引擎排名。希望本文能够帮助您更好地理解和应用 Nginx,在实际项目中发挥其作用。

注意:

配置完成后,保存并关闭文件,然后重新加载Nginx以使配置生效:

1
sudo systemctl reload nginx

Web安全之静态内容防爬虫

Web安全之静态内容防爬虫

随着互联网的快速发展,网络安全问题日益突出。对于静态内容网站来说,防止恶意爬取网站内容成为了一项重要的任务。恶意爬取不仅会导致网站内容被盗用,还可能引发一系列安全问题,如数据泄露、恶意攻击等。因此,本文将探讨静态内容网站如何防范恶意爬取,提高网络安全。

静态内容网站的特点

静态内容网站主要是指那些网页内容固定不变或变化较少的网站。这类网站通常以 HTMLCSSJavaScript 等静态文件形式存在,不依赖后端数据库或复杂的应用程序。由于其结构简单、加载速度快,静态网站在新闻发布、产品展示、个人博客等领域得到广泛应用。

恶意爬取的威胁

恶意爬取是指未经授权,通过自动化手段(如爬虫程序)大规模抓取网站内容的行为。这种行为通常具有以下特点:

  1. 大规模:恶意爬取往往涉及大量的请求,给服务器带来沉重负担。
  2. 未经授权:爬取行为未经网站所有者许可,违反了版权和隐私政策。
  3. 恶意目的:恶意爬取可能导致内容被盗用、数据泄露、恶意攻击等后果。

防恶意爬取策略

为了防范恶意爬取,静态内容网站可以采取以下措施:

设置robots.txt文件

robots.txt 文件是一个用于告知搜索引擎爬虫哪些页面可以爬取、哪些页面不能爬取的文本文件。通过设置 robots.txt 文件,可以限制恶意爬虫的访问。

下面是一个 robots.txt 文件的例子,展示了如何设置一些基本的规则。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
User-agent: *
Disallow: /

# 禁止所有爬虫访问网站的所有页面

User-agent: Googlebot
Disallow:

# 允许 Googlebot 访问网站的所有页面

User-agent: Bingbot
Disallow: /private/

# 禁止 Bingbot 访问 /private/ 目录下的所有页面

User-agent: Yahoo! Slurp
Disallow: /admin/

# 禁止 Yahoo! Slurp 访问 /admin/ 目录下的所有页面

User-agent: *
Disallow: /cgi-bin/

# 禁止所有其他爬虫访问 /cgi-bin/ 目录下的所有页面

# 允许特定爬虫访问特定页面
User-agent: SpecificBot
Allow: /special-page/
Disallow: /

# 对于名为 SpecificBot 的爬虫,只允许访问 /special-page/ 页面,其他页面都不允许访问

在上面的例子中:

  • User-agent: * 表示该规则适用于所有爬虫。
  • Disallow: / 表示禁止爬虫访问网站的根目录及其所有子目录和文件。
  • Allow: /special-page/ 表示允许特定爬虫访问 /special-page/ 页面。Allow 指令必须在 Disallow 指令之前,否则将无效。
  • User-agent: SpecificBot 表示该规则仅适用于名为 SpecificBot 的爬虫。

请注意, robots.txt 文件必须放置在网站的根目录下(通常是 http://www.example.com/robots.txt),以便爬虫能够找到它。同时,虽然大多数负责任的爬虫都会遵守 robots.txt 的规则,但并非所有爬虫都会遵守,因此它不能作为一种安全机制来防止数据被爬取。

使用验证码技术

对于关键页面或敏感内容,可以引入验证码技术。用户在访问这些页面时需要输入正确的验证码才能继续浏览,从而有效阻止恶意爬虫的访问。

验证码技术的案例有很多,以下列举几个常见的案例:

  1. 网站注册和登录验证码:这是最常见的验证码技术案例。用户在注册或登录网站时,系统会显示一组随机生成的字符或图片,并要求用户输入或选择正确的字符或图片来完成验证。这种技术可以有效防止自动化程序恶意攻击网站,如进行暴力破解密码、刷票等行为。
  2. 图片验证码:图片验证码是一种将随机生成的字符或数字嵌入到图片中,并要求用户识别并输入正确字符或数字的验证码技术。这种技术可以有效防止自动化程序识别并输入验证码,提高网站的安全性。
  3. 滑动验证码:滑动验证码是一种要求用户通过滑动解锁来完成验证的技术。用户需要按照指定的方向或轨迹滑动滑块,才能完成验证。这种技术可以有效防止自动化程序模拟用户操作,提高网站的安全性。
  4. 音频验证码:音频验证码是一种将随机生成的字符或数字转换为语音,并要求用户听取并输入正确字符或数字的验证码技术。这种技术适用于视觉障碍用户或无法通过图片验证码验证的情况。
  5. 逻辑验证码:逻辑验证码是一种要求用户解决一个简单数学问题或逻辑问题来完成验证的技术。例如,系统可能会显示一个加法或减法问题,并要求用户输入正确答案。这种技术可以有效防止自动化程序识别并输入验证码,提高网站的安全性。

限制访问频率

通过设置合理的访问频率限制,可以防止恶意爬虫大量请求服务器资源。例如,可以设置每个IP地址在单位时间内的最大请求次数。

限制访问频率的 Node.js 例子可以使用 Redis 来实现。下面是一个简单的示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const Redis = require('ioredis');
const redis = new Redis({
port: 6379, // Redis 端口
host: 'localhost', // Redis 主机地址
password: 'your_redis_password', // Redis 密码
});

const ACCESS_FREQUENCY = 5; // 设定访问频率限制,例如每分钟最多访问 5 次
const EXPIRE_TIME = 60; // 设定过期时间,例如每分钟过期

app.get('/protected-route', async (req, res) => {
const ip = req.ip; // 获取请求 IP
const key = `access-frequency:${ip}`; // 构建 Redis 键名

try {
const count = await redis.get(key); // 获取当前 IP 的访问次数
if (count && parseInt(count) >= ACCESS_FREQUENCY) {
return res.status(429).send('Too Many Requests'); // 超过访问频率限制,返回 429 状态码
}

const newCount = count ? parseInt(count) + 1 : 1; // 更新访问次数
await redis.set(key, newCount, 'EX', EXPIRE_TIME); // 设置新的访问次数,并设置过期时间

// 处理正常请求逻辑...
res.send('Success');
} catch (error) {
console.error('Error:', error);
res.status(500).send('Internal Server Error'); // 发生错误时返回 500 状态码
}
});

在这个例子中,我们使用 Redis 来存储每个 IP 的访问次数,并设定了一个访问频率限制(例如每分钟最多访问 5 次)。当一个请求到达时,我们首先获取当前 IP 的访问次数,如果超过了限制,则返回 429 状态码表示请求过多。否则,我们更新访问次数,并设置过期时间,以便在下一分钟内重置访问次数。最后,我们处理正常的请求逻辑并返回成功响应。

请注意,这只是一个简单的示例代码,实际应用中可能需要更多的逻辑和安全性考虑,例如使用分布式锁来防止并发访问问题,以及使用更复杂的算法来计算访问频率等。

数据混淆

数据混淆是指通过改变数据的表示方式或结构,使得爬虫无法直接解析出真实数据的方法。

假设你有一个包含敏感信息的API接口,返回的数据是JSON格式的。为了防止爬虫直接获取到这些数据,你可以对返回的数据进行混淆处理。

原始数据:

1
2
3
4
5
{
"name": "张三",
"age": 30,
"email": "zhangsan@example.com"
}

混淆后的数据:

1
2
3
4
5
{
"n1": "Z3Nj",
"a2": "MzAi",
"e3": "emFuc2FuQGV4YW1wbGUuY29t"
}

在这个例子中,你可以使用一种简单的混淆算法(如Base64编码)对原始数据进行编码,然后在前端使用相应的解码算法进行解码,以显示真实的数据。这样,爬虫获取到的只是混淆后的数据,无法直接解析出真实的信息。

加密传输

使用HTTPS协议对网站内容进行加密传输,可以防止恶意爬虫在传输过程中窃取数据。此外,HTTPS协议还能提高网站的安全性,保护用户隐私。

使用反爬虫技术

反爬虫技术是一种主动防御手段,通过识别并阻止恶意爬虫的访问。例如,可以通过分析请求头、请求频率、用户代理等信息来判断是否为恶意爬虫,并采取相应的防御措施。

下面是一个简单的 express 示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
const express = require('express');
const app = express();
const rateLimit = require('express-rate-limit');

// 设置访问频率限制
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15分钟
max: 100, // 在时间窗口内的最大请求数
message: 'Too many requests from this IP, please try again later.'
});

app.use('/protected-route', limiter);

app.get('/protected-route', (req, res) => {
const userAgent = req.headers['user-agent'];

// 检查User-Agent是否像是浏览器的
if (!userAgent || !userAgent.includes('Mozilla')) {
return res.status(403).send('Forbidden');
}

// 假设这是从数据库或API获取敏感数据的函数
fetchSensitiveData().then(data => {
res.send(data);
}).catch(err => {
console.error(err);
res.status(500).send('Internal Server Error');
});
});

app.listen(3000, () => {
console.log('Server is running on port 3000');
});

function fetchSensitiveData() {
// 这里模拟从数据库或API获取数据的过程
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ sensitiveInformation: 'This is sensitive data' });
}, 1000);
});
}

在这个例子中,我们使用了 express-rate-limit 中间件来限制来自同一 IP 的请求频率,并检查了 User-Agent 头来识别非浏览器请求。

定期更新内容

定期更新网站内容可以降低恶意爬虫的兴趣。同时,通过不断更新网站结构和内容,可以增加恶意爬虫爬取的难度。

建立安全监测机制

建立安全监测机制,及时发现并应对恶意爬取行为。通过监控网站访问日志、流量异常等信息,可以及时发现恶意爬虫并采取相应的措施。

总结

网络安全是互联网发展的基石,而静态内容网站的防爬虫工作是其中的重要一环。防范恶意爬取对于静态内容网站来说至关重要。通过了解恶意爬取的特点和采取相应的防范措施,可以有效提高网站的安全性,维护网站所有者的权益,同时提升用户体验。同时,网站所有者还应持续关注网络安全动态,不断更新和完善防范措施,确保网站内容的安全与稳定。