随着后端js代码复杂度的增加,js模块化是必然趋势。 除了易于维护之外,同时依赖关系非常清晰,不会产生全局污染。 明天我们来整理一下几个模块化规格吧~
首先梳理一下模块化的发展~
无模块化 --> CommonJS 规范 --> AMD 规范 --> CMD 规范 --> ES6 模块化 1. 无模块化
script标签引入js文件,互相列出,把依赖的放在上面,不然使用的时候会报错。 如下:
<script src="jquery.js">
<script src="jquery_scroller.js">
<script src="main.js">
<script src="other1.js">
<script src="other2.js">
<script src="other3.js">
也就是把所有js文件放在一起就可以了。 而且此类文件的顺序不能错。 比如需要先引入jquery,可以引入jquery插件,其他文件中也可以使用jquery。 缺点也很明显:
2.CommonJS规范
该规范最初用于服务器端节点,它有四个重要的环境变量为模块化实现提供支持:module、exports、require 和 global。 实际使用时,使用module.exports定义当前模块的外部输出socket(不建议直接使用exports),使用require加载模块(同步)。
// a-commonJs.js (导出)
var a = 5;var add = function(param){//在这里写上需要向外暴露的函数、变量 return a + param}module.exports.a = a;module.exports.add = add===========================
// b-commonJs.js引用自定义模块,参数包含路径,可省略.js(导入)
var addFn = require('./a-commonJs')console.log(addFn.add(3)) //8console.log(addFn.a) //5
一点解释:
exports 是对 module.exports 的引用。比如我们可以认为在一个模块的顶部有这句代码: exports = module.exports所以,我们不能直接给exports赋值,比如number、function等。
注意:由于 module.exports 本身是一个对象,所以我们可以在导入时使用它
module.exports={foo:'bar'}//true
module.exports.foo='bar'//true。
不过exports是对module.exports的引用,或者可以理解为exports是一个指针javascript模块化,exports指向module.exports。 这样我们就只能使用exports.foo='bar'的形式来代替
Exports={foo:'bar'}//error 这几个方法都是错误的,相当于重新定义了exports
优势一:解决依赖和全局变量污染问题
缺点之一:CommonJS 同步加载模块。 在服务器端,模块文件都存放在本地c盘,读取速度很快,所以这样做不会有问题。 而在浏览器端,由于网络原因,CommonJS并不适合浏览器端模块加载。 更合理的解决方案是使用异步加载,例如下面的AMD规范。
3.AMD规格
继续上面的内容,AMD规范是一个异步加载模块,它允许指定一个反弹函数。 AMD是RequireJS推广过程中模块定义的标准化输出。 。
AMD标准中定义了以下三个API:
require([模块],回调)define(id,[取决于],回调)require.config()
即通过define定义一个模块,然后使用require加载模块,并使用require.config()指定引用路径。
首先去require.js官网下载最新版本,然后引入到页面中,如下:
data-main 属性不能省略。
以上是定义模块、引用模块以及在浏览器中运行时弹出的提示信息。
引用模块时,我们将模块名称放在[]中作为require()的第一个参数; 如果我们定义的模块本身还依赖于其他模块,那么我们需要将它们放在[]中作为define()第一个参数的第一个参数。
使用require.js时,我们必须提前加载所有依赖才可以使用,而不是需要使用时才加载。
优点一:适合异步加载模块,在浏览器环境下并行加载多个模块
缺点一:无法按需加载javascript模块化,开发成本高
4.命令提示符
AMD主张靠后定位、提前执行,而CMD主张靠就近、延迟执行。 CMD是SeaJS的模块定义在推广过程中的标准化输出。
显然,CMD是按需加载的,基于就近原则。
5.ES6模块化
在ES6中,我们可以使用import关键字导入模块,通过export关键字导入模块。 相比之前的解决方案,功能更加强大,也是我们所欣赏的,而且由于ES6目前还没有在浏览器中实现,因此我们只能通过babel将不支持的import编译成目前广泛支持的require。
es6导入时有一个默认的import,exportdefault,用它导入后,导入时不需要加{},模块名可以任意。 该名称实际上是一个包含导入模块上的函数或变量的对象。
并且一个模块只能有一个exportdefault。
6.CommonJs和ES6的区别
以下引自阮一峰先生:
(1) CommonJS 模块输出值的副本,而 ES6 模块输出对值的引用。 (2) CommonJS模块在运行时加载,ES6模块在编译时是输出套接字。
CommonJS加载的是一个对象(即module.exports属性),该对象只能在脚本运行后生成。 ES6模块不是一个对象,它的外部socket只是一个静态定义,会在代码的静态分析阶段生成。
小记:这篇文章只是我自己的一个小记,方便我记忆和理解。 (^o^)/~
前言
有一些后端的技术点,即使以前用过,但是自己没有总结过,需要很长时间才能回去重新整理。 那么,本文将对Web Worker进行梳理。
为什么我们需要网络工作者
由于 JavaScript 语言使用单线程,因此一次只能完成一件事。 如果有多个同步估算任务执行,则其下面的代码要等到同步估算逻辑执行完毕后才会执行,从而导致阻塞,用户交互也可能变得无响应。
但是,如果这段同步估算逻辑是在Web Worker上执行的,那么在这段逻辑估算执行过程中,它下面的代码仍然可以被执行,并且也可以响应用户的操作。
什么是网络工作者?
HTML5提供并标准化了Web Worker等一组API,它允许JavaScript程序在主线程之外的另一个线程(Worker线程)中运行。
Web Worker的作用是为JavaScript创建一个多线程环境,让主线程创建Worker线程,并分配一些任务给前者运行。 这样做的用处在于,一些计算密集型或高延迟的任务由Worker线程承担,主线程会很流畅,不会被阻塞或变慢。
Web Worker 的分类
根据工作环境的不同,Web Worker可以分为专用线程Dedicated Worker和共享线程Shared Worker。
Dedicated Worker的Worker只能从创建Worker的脚本中访问,而SharedWorker则可以由多个脚本访问。
如果开发中使用Web Worker,目前大部分场景主要使用Dedicated Worker,只能被一个页面使用,而本文也是讲这一类的; 而Shared Worker可以被多个页面共享,为跨浏览器选项卡共享数据提供了解决方案。
Web Worker 使用限制同源限制
分配给Worker线程运行的脚本文件必须与主线程的脚本文件同源。
文件限制
Worker线程很难读取本地文件(file://),并且会拒绝使用文件协议创建Worker实例。 它加载的脚本必须来自网络。
DOM 操作限制
Worker线程所在的全局对象与主线程不同,区别在于:
通讯限制
Worker线程和主线程不在同一个上下文中。 它们不能直接沟通,必须通过消息来完成。 交互方法为postMessage和onMessage,在传输数据时,Worker采用复制的形式。
脚本限制
工作线程无法执行alert()方法和confirm()方法,但可以使用XMLHttpRequest对象发送AJAX请求,或者使用setTimeout/setInterval等API
基本API
const worker = new Worker(aURL, options);
worker.addEventListener('error', function (e) {
console.log(e.message) // 可读性良好的错误消息
console.log(e.filename) // 发生错误的脚本文件名
console.log(e.lineno) // 发生错误时所在脚本文件的行号
})
常用使用方法1.直接指定脚本文件
const myWorker = new Worker(aURL, options);
aURL表示Worker要执行的脚本的URL(脚本文件),即Web Worker要执行的任务。
案例如下:
// 主线程下创建worker线程
const worker = new Worker('./worker.js')
// 监听接收worker线程发的消息
worker.onmessage = function (e) {
console.log('主线程收到worker线程消息:', e.data)
}
// 向worker线程发送消息
worker.postMessage('主线程发送hello world')
工人.js:
// self 代表子线程自身,即子线程的全局对象
self.addEventListener("message", function (e) {
// e.data表示主线程发送过来的数据
self.postMessage("worker线程收到的:" + e.data); // 向主线程发送消息
});
Web Worker的执行上下文名称是self,不能调用主线程的window对象。 上面的表示法等价于下面的表示法:
this.addEventListener("message", function (e) {
// e.data表示主线程发送过来的数据
this.postMessage("worker线程收到的:" + e.data); // 向主线程发送消息
});
将JS文件导入HTML并在本地开发环境中运行。 结果如下:
主线程收到worker线程消息: worker线程收到的:主线程发送hello world
2. 使用 blob URL 创建
除了这些引入js文件的形式之外javascript实战,还可以通过URL.createObjectURL()创建URL对象来创建嵌入式worker
/**
* const blob = new Blob(array, options);
* Blob() 构造函数返回一个新的 Blob 对象。blob 的内容由参数数组中给出的值的串联组成。
* @params array 是一个由ArrayBuffer, ArrayBufferView, Blob, DOMString 等对象构成的 Array
* @options type,默认值为 "",它代表了将会被放入到 blob 中的数组内容的 MIME 类型。还有两个这里忽略不列举了
*/
/**
* URL.createObjectURL():静态方法会创建一个 DOMString,其中包含一个表示参数中给出的对象的 URL。这个 URL 的生命周期和创建它的窗口中的 document 绑定。这个新的 URL 对象表示指定的 File 对象或 Blob 对象
*/
const worker = new Worker(URL.createObjectURL(blob));
function func() {
console.log('hello')
}
function createWorker(fn) {
// const blob = new Blob([fn.toString() + ' fn()'], { type: 'text/javascript' })
const blob = new Blob([`(${fn.toString()})()`], { type: 'text/javascript' })
return URL.createObjectURL(blob)
}
createWorker(func)
将脚本导入工作线程
要在工作线程内加载其他脚本,可以使用 importScripts()
// worker.js
importScripts("constants.js");
// self 代表子线程自身,即子线程的全局对象
self.addEventListener("message", function (e) {
self.postMessage(foo); // 可拿到 `foo`、`getAge()`、`getName`的结果值
});
// constants.js
const foo = "变量";
function getAge() {
return 25;
}
const getName = () => {
return "jacky";
};
还可以同时加载多个脚本
importScripts('script1.js', 'script2.js');
实际应用场景会处理大量的CPU时长估算操作
大家最关心的就是Web Worker的实战场景。 我们一开始就说过,当有大量复杂的估算场景时,可以使用Web Worker
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>worker计算</title>
</head>
<body>
<div>计算从 1 到给定数值的总和</div>
<input type="text" placeholder="请输入数字" id="num" />
<button onclick="calc()">开始计算</button>
<span>计算结果为:<span id="result">-</span></span>
<div>在计算期间你可以填XX表单</div>
<input type="text" placeholder="请输入姓名" />
<input type="text" placeholder="请输入年龄" />
<script>
function calc() {
const num = parseInt(document.getElementById('num').value)
let result = 0
let startTime = performance.now()
// 计算求和(模拟复杂计算)
for (let i = 0; i <= num; i++) {
result += i
}
// 由于是同步计算,在没计算完成之前下面的代码都无法执行
const time = performance.now() - startTime
console.log('总计算花费时间:', time)
document.getElementById('result').innerHTML = result
}
</script>
</body>
</html>
如上,第一个输入框和按钮负责模拟复杂的估算,比如输入10000000000,点击开始估算,此时主线程处理的依然是处理同步估算逻辑,在估算完成之前,你会发现页面卡住了,下面的两个输入框也很难点击交互。 在我的笔记本上,估计需要大约 14 秒。 这个冻结时间给用户带来了非常差的体验。
打开控制台调用也可以看到CPU使用率为100%
如果这部分的估算交给Web Worker的话,修改代码:
<script>
const worker = new Worker('./worker.js')
function calc() {
const num = parseInt(document.getElementById('num').value)
worker.postMessage(num)
}
worker.onmessage = function (e) {
document.getElementById('result').innerHTML = e.data
}
</script>
./worker.js
function calc(num) {
let result = 0
let startTime = performance.now()
// 计算求和(模拟复杂计算)
for (let i = 0; i <= num; i++) {
result += i
}
// 由于是同步计算,在没计算完成之前下面的代码都无法执行
const time = performance.now() - startTime
console.log('总计算花费时间:', time)
self.postMessage(result)
}
self.onmessage = function (e) {
calc(e.data)
}
然后重复上面同样的操作,输入10000000000进行计算,你会发现下面的两个输入框都可以正常流畅的输入,整个页面不会卡顿。
Worker运行在独立于主线程的后台线程中,共享并执行大量CPU密集型操作(但运行时间不会缩短),解放了主线程,主线程可以在一个线程中响应用户操作。及时处理javascript实战,不会造成死机现象。 使用Web Worker后,控制台工具可以看到CPU使用率处于较低的正常水平,计算过程与估计之前相同。
音视频画布绘图屏幕录制
这是我在工作中遇到的场景,通过绘制WebRTC视频流来录制视频,最后生成视频。
之前写过一篇文章,介绍如何实现前端录屏。 这篇文章基本上没有认真写,纯粹是一个记录,而且现在的代码和本文的示例代码差别很大(也就是说示例代码还有很大的改进空间),就是建议大体看思路,而不是去研究具体的实现细节,毕竟思路是相通的。
以上面文章的代码为例(现在最新的代码与公司代码业务结合在一起,不会发布),有一个CPU密集型的操作
// 16ms一次的定时器
refreshTimer.current = window.setInterval(onRefreshTimer, 16)
// onRefreshTimer 函数里面做的实际就是高频执行 recorderDrawFrame() 方法
// 录屏绘制操作
const recorderDrawFrame = () => {
const $recorderCanvas = recorderCanvas.current!
const $player = videoRef.current!
const ctx = recorderContext.current!
const { width, height } = getResolution()
$recorderCanvas.width = width
$recorderCanvas.height = height
// 其中这个绘制函数对CPU占用率会比较高(在低配置的电脑浏览器上)
ctx.drawImage(
$player,
0,
0,
$player.videoWidth,
$player.videoHeight,
0,
0,
$recorderCanvas.width,
$recorderCanvas.height,
)
drawWatermark(ctx, width)
}
那么如何优化呢? 就是将整个onRefreshTimer定时器函数交给Web Worker来执行。
上面提到,Web Worker 虽然对 DOM 操作有限制,但是可以使用 setTimeout/setInterval 等 API,所以具体实现就是将 Worker 封装为一个类,处理类内部的逻辑,然后将 setInterval 等方法暴露给调用外部实例。
if (worker) {
refreshTimer.current = worker.setInterval(onRefreshTimer, 16)
}
其他场景
我看过网上的文章,其他场景包括:
可以参考阅读:我不让大家学worker但是没有应用场景
结语
恐怕Web Worker整体在日常业务开发中使用的不多,但如果遇到上述类似的场景,我们在优化的方向上可以有另一种选择。
参考
本文为参加“金石计划.瓜分6万现金奖”