每个系列一本前端好书,帮你轻松学重点。
本系列来自曾供职于Google的知名前端技术专家马特·弗里斯比 编写的 《JavaScript高级程序设计》(第5版)
了解一门语言能做什么很重要,既是学习的起点 ,也是应用的落点。
JavaScript曾被认为是"玩具"语言,谁都想不到,它后来把触角伸到了服务端、工具链、App、桌面端、甚至是硬件和深度学习。
本文是此系列的最终篇,我们来探究一下,除了按照正确的语法和良好的组织去写代码,前端到底能做什么?
网络请求
动态数据是给网页注入活力的重要因素,可以使得每天的内容,每个人看到的内容,都不同。
靠手写是不行的,需要发起网络请求。
在过去,请求任务由XMLHttpRequest来完成的,但在现代Web开发中,已经被Fetch替代。
Fetch
前面提过,Fetch是基于Promise的,它能以更简洁清晰的方式实现异步,并且它支持流式响应、请求取消和自动请求重发。
fetch()方法只有一个必须的参数,即请求的url,其他参数按需配置,一个基本的POST请求写法如下:
javascript
const payload = JSON.stringify({
foo:"bar"
});
fetch("url", {
method: "POST",
body: payload,
headers: {
"Content-Type": "application/json",
},
})
.then((res) => {
if (res.status == 200) {
console.log(res.text());
}
})
这段代码较为完整地展现了POST请求的大概结构,请求返回后,可以通过返回的状态码,及各种不同数据的处理方法,来接收结果。
数据的处理方法有多种,除了text(),还有json()、blob()等,可根据返回数据的类型和需求进行选用。
Web Socket
Fetch请求是由客户端发起,服务端做响应的单向通信,但有时候我们需要实现双向通信,服务端主动向客户端推送数据,主流方案就是 Web Socket。
简单使用:
javascript
const socket = new WebSocket("ws://localhost:8080");
socket.onopen = () => {
console.log("连接成功");
};
socket.onmessage = (e) => {
console.log("收到消息", e.data);
};
EventSource API
这是一种比较新的API,但这两年应用很广,大家看到的关于AI的信息交互,返回结果像打字机一样输出,起关键作用的就是它。
示例如下:
javascript
const eventSource = new EventSource("http://localhost:8080/stream");
eventSource.onmessage = (e) => {
console.log("收到消息", e.data);
};
应用比较简单,但有一点需要说明,看起来它只支持get请求,无法传参?如果需要传参,怎么办,有一个包可以帮助我们实现------@microsoft/fetch-event-source。
File上传
丰富的网页功能,不仅让用户能输入内容,还要能上传文件。
在早期,处理文件的唯一方式是把<input type="file">
放到一个表单里。
File API 与 Blob API 就是为了让Web开发者更好地与文件交互而设计。它仍然以表单中的文件为基础,但增加了访问文件信息的能力。
File类型
HTML5在DOM上为文件输入元素添加了files集合,它包含一组File对象,表示被选中的文件。
每个File对象都有一些只读属性:name(文件名);size(文件大小);type(文件MIME类型);lastModifiedDate(文件最后修改时间)
FileReader类型
File API还提供了FileReader类型,用于从文件中读取数据。
每个FileReader会发布几个事件,最有用的是progress、error和load。
javascript
const reader = new FileReader();
reader.readAsText(file);
// 还有数据
reader.onprogress = (e) => {
if (e.lengthComputable) {
const percentLoaded = Math.round((e.loaded / e.total) * 100);
console.log(`File loading progress: ${percentLoaded}%`);
}
};
// 读取完成
reader.onload = () => {
console.log('File content:', reader.result);
};
// 发生错误
reader.onerror = () => {
console.error('Error reading file:', reader.error);
};
某些情况下,可能需要读取部分文件而不是整个文件。为此,File对象提供了一个名为slice()的方法。
Blob URL
有时候你会看到一个blob开头的字符串来展示文件,它就是对象URL,也称作Blob URL,是指引用存储在File或Blob中数据的URL。
对象URL的优点是不用把文件内容读取到JavaScript也可以使用文件。
要创建对象URL,可以使用window.URL.createObjectURL()方法并传入File或Blob对象,这个函数返回的值是一个指向内存中地址的字符串,因为这个字符串是URL,所以可以在DOM中直接使用。
拖拽上传
除了点击选择上传,将拖放API与File API结合使用,可实现拖拽上传,在页面上创建放置目标后,把文件拖动到放置目标。会触发drop事件,被放置的文件可通过事件的event.dataTransfer.files属性读到。
图形渲染
图形图像是网页不可缺少的部分,我们见到所有亮眼、炫酷的效果,都由它来完成,会给网页吸引力加分。
最常见的img元素,但它只能用来展示图片,想要实现更多,如图形绘制、图片处理,或者复杂动画,就要用到Canvas。
javascript
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// 设定宽高
canvas.width = 200;
canvas.height = 100;
// 设定填充色
ctx.fillStyle = 'red';
// 绘制矩形
ctx.fillRect(10, 10, 180, 80);
document.body.appendChild(canvas);
上面这段代码,绘制了一个"红色矩形",属于图形界的"Hello World"。
Canvas能做的事情,包括且不限于:画线、各种形状、文本、阴影、渐变、绘制图像、填充图案。
还有一项重要能力是获取和操作图像数据。
javascript
// 获取图像数据
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
// 灰度处理
for (let i = 0; i < data.length; i += 4) {
const avg = (data[i] + data[i + 1] + data[i + 2]) / 3;
data[i] = avg; // Red
data[i + 1] = avg; // Green
data[i + 2] = avg; // Blue
}
// 修改重新绘制到画布
ctx.putImageData(imageData, 0, 0);
当你拿到图像的像素值,就可以为所欲为,当然,需要具备一定的图像知识。
这是2D绘制,还可以进行3D绘制,是现在很热门的领域,同样是用Canvas,但需要额外用到WebGL,WebGL并不属于W3C标准,且涉及OpenGL语言,超出了常规前端范畴,学习曲线比较陡峭,深入掌握的人少之又少,平时大家常用Three.js等工具库做3D效果展示,这里不再赘述。
客户端存储
通常,网页端的数据来自两处:接口获取、代码定义。
代码中的需考虑维护性、灵活性,同时无法跨项目。
接口获取的每次需要都得重新请求,消耗时间,特别是更新频率很低时,会造成资源浪费。
于是,就有客户端存储这样一种机制,将数据暂存在浏览器。
主要方案有:
-
cookie:与特定域绑定的,长度有限,数量有限,通常用于存储较简单的值。
需要注意的是,所有名和值都是URL编码的,须用decodeURIComponent()解码。
-
WebStorage:包括localStorage和sessionStorage。
localStorage是永久存储机制,sessionStorage是跨会话的存储机制。
这两种存储方式都不受页面刷新影响,且容量比cookie大得多。
-
IndexedDB:IndexedDB在浏览器层面创建了一个数据库,但它的形式不是表,是对象。
它的应用通常是大对象或者文件,可省去用户反复上传或者频繁请求的消耗。
富文本编辑
怎样实现一个可编辑的输入框?
大部分人的第一反应是input或者textarea,这两种确实比较常用,但它们是单纯用于"输入",别的干不了。
还有少部分可能会说contenteditable,能想起这个算不错。
还有第三种,就是在页面中嵌入一个iframe,给它的designMode属性设置为"on"。
通常的做法像这样:
javascript
<iframe name="richedit"></iframe>;
window.addEventListener("load", () => {
frames["richedit"].document.designMode = "on";
});
这样以来,被嵌入的那部分整个都可编辑。
但只是可编辑并不满足需求,能交互才行,与富文本编辑器交互的方法有两种:
execCommand()
它的作用是,将字体变成"粗体、斜体",或者改变文本背景,添加下划线,插入"p、hr"等元素。
javascript
document.addEventListener('keydown', (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'b') {
e.preventDefault();
if (document.execCommand) {
document.execCommand('bold', false, null);
}
}
});
注意的点是,它用于修改内嵌窗格(iframe)中富文本区域的外观,如果想更加精细地控制当前窗口的文本,要用第二种。
getSelection()
这个方法可以获得富文本编辑器的选区,其暴露在document和window对象上,返回表示当前选中文本的Selection对象。
拿到对象后,其中有特别多的方法可用。直接看代码:
javascript
const selection = window.getSelection();
if (selection.toString().length > 0) {
const range = selection.getRangeAt(0); // 获取选区的第一个范围
const selectedText = range.extractContents(); // 提取选区范围的内容
const highlightSpan = document.createElement('span'); // 创建一个新的 span 元素
highlightSpan.style.backgroundColor = 'yellow'; // 设置 span 元素的背景颜色为黄色
highlightSpan.appendChild(selectedText); // 将选中文本添加到 span 元素中
range.insertNode(highlightSpan); // 将 span 元素插入到选区范围中
selection.removeAllRanges(); // 移除选区的所有范围
selection.addRange(range); // 将修改后的范围添加回选区
}
上面这段代码实现的是,将鼠标划过选中的文本背景设为黄色。
JavaScript API
网页的载体是浏览器,产品能实现什么,开发者能做什么,取决于浏览器。
如今的浏览器已经成为集各种API于一身的"瑞士军刀",择取一些功能性较强的API给大家。
Clipboard API
肯定有人和我一样,得知这个API的时候,才知道在网页端实现复制如此简单。
其实不是到这个API才具备这个能力,但Clipboard API使得这件事变得简单优雅。
它通过readText() 和 writeText()方法实现读取和写入字符串。
javascript
navigator.clipboard.readText().then((text) => {
console.log("剪贴板内容:", text);
});
navigator.clipboard.writeText("Hello, World!").then(() => {
console.log("剪贴板内容已更新");
});
同时,可以有剪贴板事件cut、copy、paste等进行监听。
跨上下文通信
跨文档消息,也简称XDM(cross-document messaging),是一种在不同工作线程或不同源的页面间传递信息的能力。
它的核心是postMessage()方法,接收到XDM消息后,window对象上会触发message事件。
但最好只通过postMessage()发送字符串。如果需要传递结构化数据,最好先对该数据调用JSON.stringify(),通过postMessage()传过去之后,再在onmessage事件处理程序中调用JSON.parse()。
除此之外,现在还比较常用的有 MessageChannel() 和 BroadcastChannel(),一个典型场景是,同域名下部署了几个相互独立的项目,要么一个内嵌了另一个,要么在一个页面上产生交互,另一个页面需要接收状态变化,这时候,相互通信的感知能力就很有用。
Observer API
开发者所期望的重要能力之一,就是"监听变化",对变化的监测会产生安全感。
DOM的变化在很长时间里是无法感知的,直到Observer API的出现。
现代浏览器支持的观察者有如下几个。
MutationObserver:监听DOM的修改,用于观察整个文档、DOM子树或者一个元素,还能观察元素属性、子节点、文本等变化。
ResizeObserver:用于跟踪DOM元素尺寸的变化,适用于响应式Web设计或动态布局更新。
IntersectionObserver:监听DOM元素相对于指定视口或容器元素的可见性及位置。特别适合实现基于滚动的动画、图片懒加载、无穷滚动等性能优化的实施。
以MutationObserver为例:
javascript
const observer = new MutationObserver((mutationsList) => {
for (let mutation of mutationsList) {
if (mutation.type === "childList") {
console.log("A child node has been added or removed.");
} else if (mutation.type === "attributes") {
console.log("The " + mutation.attributeName + " attribute was modified.");
}
}
});
使用Observer API时,关键要处理好回调的性能,它可能会以惊人的频率执行,从而影响性能。一种方式是防抖,另一种方式是在不需要时把观察者删除。
Device API
现代网页需要我们提供细致的个性化设计,而个性化离不开对网页环境和设备的检测,包括且不限于:设备类型、浏览器类型、操作系统等,这时就需要 Device API 的能力。
主要通过暴露在navigator对象上的一组属性得到,比如:oscpu(系统)、userAgent(浏览器)、orientation(屏幕朝向)等。
同时,还可通过Connection State 和 Networkinformation API 获取到用户的网络连接情况,以便做出断网或弱网处理,它们同样暴露在navigator上,以及有相应的 online、offline 事件可监听。
Page Visiblily API
Web开发中有个常见问题,就是不知道用户当前在使用页面,还是切到别的界面做其他事情去了。
最常见的场景就是视频网站播广告,你停在当前页面看,它才会播,否则就是浪费资源。
Page Visibily API就提供了这个能力。
document.visibilyState有三个值:visible(当前可见)、hidden(不可见)、prerender(页面在预渲染)。
其中不可见包括"标签页切换"和"最小化",同时,还可通过 visibilityChange 事件监听可视状态的变化。
URL API
这个API的存在让人觉得很贴心,因为你总会需要在URL中携带信息,获取URL信息是特别常见的需求。
但在之前,需要通过拼接组件、正则匹配、字符串查找等一系列繁琐操作,而URL API 使这件事变得简单。
你只需要把URL传进去,就能直接获得一系列有价值的信息。
javascript
const url = new URL('https://example.com/8080/page?q1=vall#fragment');
console.log('Protocol:', url.protocol); // https
console.log('Search Parameters:', url.search); // ?q1=vall
其中的参数更是能够直接用过 URL的 searchParams 属性轻易获得和操作。
javascript
let qs = "?q1=vall"
let searchParams = new URLSearchParams(qs);
searchParams.has(q1) // true
searchParams.get(q1) // vall
计时API
关于时间,多数人知道Date,但是Date的本意是日期,它更适用于日期相关处理,在时间精度上只能做到毫秒,对一些精度要求更高的场景无法满足。
于是有了performance,它包含一批API,用在不同场景。
performance.now():从0开始计时,返回微妙精度的浮点值。
performance.mark():记录自定义性能条目。
performance.getEntriesByType:度量当前页面加载速度,或者资源加载速度。
这些能力,可以辅助我们对用户的真实体验数据做收集和分析,以便做针对性的优化。
工作者线程
前端开发者常说:"JavaScript是单线程的"。所以经常会面对一个问题---要做的事情太多,浏览器"忙不过来",产生卡顿。
工作者线程,就是浏览器可以在原始页面环境之外再分配一个完全独立的二级子环境。这个子环境不能与依赖单线程交互的API(如DOM)互操作,但可以与父环境并行执行代码。
较为常见的就是"Web Worker",通常会用来做文件的输入输出,进行密集型计算,处理大数据。
创建Web Woker的常见方式,是建立一个任务执行脚本,然后把文件路径给Worker构造函数。
javascript
const webWorker = new Worker("worker.js");
webWorker.postMessage({
cmd: "init",
});
webWorker.onmessage = (e) => {
console.log("主线程收到消息", e.data);
};
还有一种常见的称作"Service Worker"(服务工作者线程),类似一个代理服务器,用于拦截外出请求和缓存响应。可以让网页在没有网络连接的时候正常使用。
它最大的使用场景就是开发离线应用,是开发PWA(渐进式Web应用)的关键技术。
WebAssembly
书中并未涉及WebAssembly,但这是前端开发在浏览器变得更强大绕不开的话题。
正常来说,图片处理、音视频处理、模型应用等都是JavaScript不能办到的,需要借助C++或者Python,但前端要做的话,有没有办法,答案就是WebAssembly。
比如,可以借助OpenCV.js、FFmpeg.js进行图片和音视频处理,可以借助TensorFlow.js、Transformer.js做模型训练和应用。它们无一例外都用到了WebAssembly。
所以,WebAssembly的设计目标就是为C/C++、Rust等语言提供高效的编译目标,使其在浏览器中以接近原生性能运行。感兴趣的朋友可进一步拓展学习。
前方的路
行文至此,内容很多,每一段内容的取舍都很难,但我相信,通过这些知识,一定会激起你的兴趣,去了解更多知识。
归根结底,前端能做什么,不取决于语言和API的设计,在不同的人手里,它能发挥不同的威力,所以,你的技术深度,你的创造力,才是它真正的能力边界。
结束了这个系列,就要开始下一段征程了,会是什么呢,欢迎留言说出你的答案~
更多好文第一时间接收,可关注公众号:"前端说书匠"