长期以来,Photoshop 在设计领域占据着重要的地位。Adobe 工程师经过多年来的努力,并与 Chrome 等浏览器供应商密切合作,通过 WebAssembly
+ Emscripten
、Web Components
+ Lit
等技术以及新的 Web API 的支持,终于在近期推出了 Web 版 Photoshop(photoshop.adobe.com)。本文就来看看这些核心技术。
WebAssembly + Emscripten
WebAssembly
是重新在 JavaScript 中实现 Photoshop 计算密集型图形处理的关键因素之一。为了将现有的 C/C++ 代码库移植到 JavaScript 中,Adobe使用了 Emscripten
编译器生成 WebAssembly
模块代码。接下来就让我们以 WebAssembly
的基础知识开始我们今天的学习之旅。
WebAssembly
WebAssembly
是一种低级的汇编类语言,具有紧凑的二进制格式,以接近本机的性能运行,并为C/C++,C#和Rust等语言提供编译目标,以便它们可以在Web上运行,并被设计与 JavaScript 等其他语言进行交互。
WebAssembly
诞生的背景是为了解决 JavaScript
在浏览器中运行的性能问题。JS 引擎运行程序通常需要经过解析、编译、优化、执行等多个步骤,这些步骤会消耗大量的时间,导致 JS 程序的运行效率低下。
WebAssembly
通过引入一种新的二进制格式,可以直接编译成机器码,从而提高程序的运行效率。WebAssembly
与 JavaScript
一起工作,可以在浏览器中运行,也可以在 Node.js 等其他环境中运行。
WebAssembly 优点
高性能:WebAssembly 二进制格式的代码加载和执行速度更快,因为它是一种低级语言,不需要解释器,而是直接编译成机器码,因此性能更好。 安全:WebAssembly 在沙箱环境中运行,保护系统免受恶意代码的侵害 可移植:WebAssembly 可以在任何支持它的浏览器和平台上运行,无需修改代码 与 JavaScript 互操作:可以与 JavaScript 代码无缝协作,JavaScript 代码可以调用 WebAssembly 模块中的函数,反之亦然 语言支持:支持多种编程语言,如 C/C++、Rust、C#、TypeScript 等
WebAssembly 兼容性
WebAssembly
已经在 Chrome、Firefox、Safari、Edge、Opera 等主流浏览器中得到支持,可以在 caniuse 查看 WebAssembly
的兼容性。
Emscripten
Emscripten
是 WebAssembly
的一个完整的开源编译器工具链。使用 Emscripten
,您可以将C和C++代码或任何其他使用LLVM的语言编译到 WebAssembly
中,并在Web、Node.js或其他 Wasm 运行时上运行它。实际上,任何可移植的C 或 C++ 代码库都可以使用 Emscripten
编译成 WebAssembly
,从需要渲染图形、播放声音以及加载和处理文件的高性能游戏,到 Qt 等应用程序框架。
使用 Emscripten 编写第一个 WebAssembly 程序
emsdk 安装
略。请参考 MDN 文档 emsdk 安装(https://developer.mozilla.org/en-US/docs/WebAssembly/C_to_wasm#installing_the_emscripten_toolchain)。代码编写 -- JS 调用 C 函数
为了更直观地了解如何使用 WebAssembly,我们接下来编写一个 math.c 程序,并将其编译成 math.wasm 在 javascript 代码中调用。
#ifndef EM_PORT_API
# if defined(__EMSCRIPTEN__)
# include <emscripten.h>
# if defined(__cplusplus)
# define EM_PORT_API(rettype) extern "C" rettype EMSCRIPTEN_KEEPALIVE
# else
# define EM_PORT_API(rettype) rettype EMSCRIPTEN_KEEPALIVE
# endif
# else
# if defined(__cplusplus)
# define EM_PORT_API(rettype) extern "C" rettype
# else
# define EM_PORT_API(rettype) rettype
# endif
# endif
#endif
EM_PORT_API(int) add(int a, int b) {
return a + b;
}
EM_PORT_API(int) multiply(int a, int b) {
return a * b;
}
为了方便函数导出,我们在上面的代码里定义了一个函数导出宏 EM_PORT_API
,将除了 main() 函数之外的全局函数导出至 JS 语言环境。在这个例子中,我们导出了 add() 和 multiply() 函数。
在 Emscripten
编译器环境的终端窗口中,执行以下命令:
emcc math.c -o math.js
此时我们的项目根目录中会生成 math.wasm 以及 math.js 胶水代码文件,其中 math.wasm 是 WebAssembly
模块。math.js 是一个 javascript 模块,它包含了一个 javascript 函数,该函数将 WebAssembly
模块加载到内存中,并将其导出为一个对象,以便我们在 JavaScript 中使用它。下面我们编写 index.html 代码,引入文件并调用 C 程序函数。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
Module = {};
Module.onRuntimeInitialized = function() {
// 在 JS 中调用 C 导出的函数,需要在原有导出函数签名的签名加上 _ 前缀,如 Module._add。
const add = Module._add;
const multiply = Module._multiply;
console.log(`1 + 2 = ${add(1, 2)}`);
console.log(`3 * 4 = ${multiply(3, 4)}`);
}
</script>
<script src="math.js"></script>
</body>
</html>
代码执行结果如下所示:
当然以上的代码,我们也可以不生成胶水代码,执行 emcc math.c -o math.wasm
生成 WebAssembly 模块,然后直接通过 WebAssembly JS API 来加载 WebAssembly 模块,如下所示:
function loadWebAssembly(filename) {
return fetch(filename)
.then(res => res.arrayBuffer())
.then(buffer => WebAssembly.instantiate(buffer))
.then(module => module.instance)
}
loadWebAssembly("math.wasm")
.then(instance => {
const add = instance.exports.add;
const multiply = instance.exports.multiply;
console.log(`1 + 2 = ${add(1,2)}`)
console.log(`3 * 4 = ${multiply(3, 4)}`)
})
代码编写 -- JS 函数注入 C 环境 Emscripten
提供了多种在 C 环境调用 JS 函数的方法。
EM_JS / EM_ASM
宏内联 JS 代码emscripten_run_scripten()
函数JS 函数注入 C 环境
我们这里使用最后一种方法,JS 函数注入 C 环境。我们在 math.c 中添加如下代码:
EM_PORT_API(int) js_add(int a, int b);
EM_PORT_API(void) js_console_log(int param);
EM_PORT_API(void) log_the_answer() {
float i = js_add(1, 2);
js_console_log(i);
}
我们在 log_the_answer
中调用了函数 js_add 和 js_console_log,这两个函数在 C 环境中仅给出了声明,具体实现是在 JS 环境中完成的,我们需要新建 math_js.js 文件,在文件中定义这两个函数,如下所示:
mergeInto(LibraryManager.library, {
js_add: function(a, b) {
console.log(`js_add 被调用了`);
return a + b;
},
js_console_log: function(param) {
console.log(`js_console_log 被调用了,参数为${param}`)
}
})
随后我们在 Emscripten 编译器终端窗口中执行如下命令:
emcc math.c --js-library math_js.js -o math.js
--js-library
表示将 math_js.js
作为附加库参与链接。然后还是熟悉的配方,我们在 index.html 中引用 math.js 文件。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
Module = {};
Module.onRuntimeInitialized = function() {
Module._log_the_answer();
}
</script>
<script src="math.js"></script>
</body>
</html>
代码执行结果如下所示:
在 Webpack 中使用 WebAssembly模块
自Webpack
4起,Webpack
已经内置了对WebAssembly
的支持,这意味着我们可以导入.wasm
文件就像导入 javascript 模块一样。
// 1. 安装 wasm-loader
npm i wasm-loader --save-dev
// 2. 配置 webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.wasm$/,
type: 'webassembly/experimental'
}
]
},
experiments: {
asyncWebAssembly: true,
syncWebAssembly: true
}
}
// 3. 导入 WebAssembly 模块
import wasmModuleUrl from './my-wasm-module.wasm';
WebAssembly.instantiateStreaming(fetch(wasmModuleUrl), { /* imports */ })
.then(result => {
// 你现在可以使用WebAssembly模块了
const exports = result.instance.exports;
// ...
});
以上的示例,旨在帮助大家对 WebAssembly
整体有个认知,现在,你已经了解了 WebAssembly
的基本使用方法,从编译源代码到在Web页面中加载和运行WASM模块。无论是游戏开发、图像处理,还是复杂的科学计算,WebAssembly
都在打开一扇扇新窗口,让我们的应用能够以前所未有的速度和效率运行。而更多的技术细节以及高级功能需要读者后续自行探索。
Web Components + Lit
作为开发者,我们总是想尽可能多的重用代码,但是这对于自定义标记结构来说并不是那么容易,Web Components
旨在解决这些问题。
Web Components
Web Components
不是单一的规范,而是一系列的技术组成,可以一起使用来创建封装功能的定制元素,可以在你喜欢的任何地方重用而不必担心代码冲突。具体的技术组成如下所示:
Custom element(自定义元素):定义 custom elements 及其行为。 Shadow DOM(影子DOM):用于将封装的“影子”DOM树附加到元素(与主文档DOM分开呈现)并控制其关联的功能。通过这种方式,可以保持元素的功能私有,避免被外部样式和行为所影响。 HTML templates(HTML 模板): <template>
和<slot>
元素使你可以编写不在呈现页面中显示的标记模板,然后它们可以作为自定义元素结构的基础被多次重用。
实现 Web Components 的基本方法包括以下几个步骤:
创建一个类或函数来指定 web 组件的功能 使用 customElements.define()
方法注册你的新自定义元素,并向其传递要定义的元素名称、指定元素功能的类、以及可选的其所继承自的元素如果需要的话,使用 Element.attachShadow()
方法将一个 shadow DOM 附加到自定义元素上。使用通常的DOM方法向 shadow DOM 中添加子元素、事件监听器等。如果需要的话,使用 <template>
和<slot>
定义一个HTML模板。再次使用常规 DOM 方法克隆模板并将其附加到你的 shadow DOM 中。在页面任何位置使用你的自定义元素。
Web Components 生命周期
在 custom element 的构造函数中,可以指定多个不同的回调函数,它们会在元素的不同生命时期被调用:
connectedCallback:当 custom element 首次被插入文档 DOM 时,被调用 disconnectedCallback:当 custom element 从文档 DOM 中删除时,被调用 adoptedCallback:当 custom element 被移动到新的文档时,被调用 attributeChangedCallback:当 custom element 增加、删除、修改自身属性时,被调用
Web Components 兼容性
WebAssembly 已经在 Chrome、Firefox、Safari、Edge、Opera 等主流浏览器中得到支持,可以在 caniuse 查看 Web Components 的兼容性。
编写第一个 Web Component
我们先来编写一个简单的 Web Component
它在页面中作为用户卡片,展示用户的相关信息。首先我们需要创建一个类来指定 web 组件的功能,如下所示:
class UserCard extends HTMLElement {
constructor() {
super();
// 创建一个 shadow root
const shadow = this.attachShadow({ mode: 'open' });
const div = document.createElement('div');
div.innerHTML = `<p>姓名:小唐</p>
<p>职业:退堂鼓一级表演艺术家</p>
`
div.addEventListener('click', function() {
alert('你好,我是小唐,很高兴认识你!')
})
const style = document.createElement('style');
style.textContent = this.defineStyle();
shadow.appendChild(style);
shadow.appendChild(div);
}
defineStyle() {
return `
div {
border: 1px solid #ccc;
border-radius: 4px;
padding: 20px;
cursor: pointer;
}
div:hover {
border-color: #888;
}
`;
}
connectedCallback() {
console.log('Component added to the DOM');
}
disconnectedCallback() {
console.log('Component added to the DOM');
}
}
然后我们使用 customElements.define(name, constructor)
方法注册我们的新自定义元素,并向其传递要定义的元素名称、指定元素功能的类,如下所示:
customElements.define('user-card', UserCard);
最后我们在页面中使用我们的自定义元素,如下所示:
<user-card></user-card>
以上只是一个最简单的用例,如果组件内部很复杂,那么这种写法就会很困难,此时我们可以借助 template
和 slot
的能力来简化代码。
使用 template
和slot
修改 UserCard 类的构造函数,如下所示:
class UserCard extends HTMLElement {
constructor() {
super();
// 创建一个 shadow root
const shadow = this.attachShadow({ mode: 'open' });
const template = document.getElementById('user-card-template');
const div = template.content.cloneNode(true);
shadow.appendChild(div);
this.$div = shadow.querySelector('div');
this.$div.addEventListener('click', () => {
alert('clicked');
});
}
}
修改 index.html 文件,增加 template
模板代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<template id="user-card-template">
<style>
div {
border: 1px solid #ccc;
border-radius: 4px;
padding: 20px;
cursor: pointer;
}
div:hover {
border-color: #888;
}
</style>
<div>
<p>姓名:<slot name="username">xxx</slot></p>
<p>职业:<slot name="career">xxxx</slot></p>
</div>
</template>
<script src="main.js"></script>
<user-card>
<span slot="username">小唐</span>
<span slot="career">退堂鼓一级表演艺术家</span>
</user-card>
</body>
</html>
以上的示例旨在帮助大家了解如何构建一个 Web Component 组件,以及如何使用 template
和 slot
来简化代码。更多的用法可以参考 Web Components MDN(https://developer.mozilla.org/en-US/docs/Web/Web_Components)。
Lit
Lit 是 Google 开发的,基于 Web Components
构建的前端框架。Lit 提供了如下具有竞争力的特性:
基于 Web Components
的更高层封装,提供了现代前端开发习惯的响应式数据,声明式的模板,减少了 Web Components 的一部分样板代码。小。运行时仅有5K 性能强悍。规避了 VDOM 的一些弊端,更新时仅处理 UI 中的异步部分 兼容性好。因为 Web Components 是 HTML 的原生能力,与框架无关,因此 Lit 也是与框架无关的,可以与任何框架一起使用
有了 Lit 组件库,我们就可以更方便的编写 Web Components 了。我们来试试写个组件来看看 Lit 的基本用法。
import { LitElement, html, css } from 'lit';
class MyUserCard extends LitElement {
static styles = css`
:host([hidden]) { display: none; }
.user-card {
display: block;
border: 2px solid black;
padding: 16px;
width: 200px;
}
`;
static get properties() {
return {
name: { type: String },
occupation: { type: String },
favoriteColor: { type: String, state: true },
};
}
constructor() {
super();
this.name = 'Guest';
this.occupation = 'None';
this.favoriteColor = 'black';
}
// 使用生命周期函数
connectedCallback() {
super.connectedCallback();
console.log('Component added to the DOM');
}
disconnectedCallback() {
console.log('Component removed from the DOM');
super.disconnectedCallback();
}
// 更新颜色状态
handleColorChange(e) {
this.favoriteColor = e.target.value;
}
render() {
return html`
<div class="user-card" style="border-color: ${this.favoriteColor};">
<h2>${this.name}</h2>
<h3>${this.occupation}</h3>
<slot name="extra-info"></slot>
<label for="colorInput">Favorite color:</label>
<input id="colorInput" type="color" @input="${this.handleColorChange}" value="${this.favoriteColor}">
</div>
`;
}
}
customElements.define('my-user-card', MyUserCard);
在 index.html 使用 my-user-card
组件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Lit App</title>
</head>
<body>
<!-- 使用你的 Lit 组件 -->
<my-user-card name="Jane Doe" occupation="Developer">
<div slot="extra-info">Extra information can go here.</div>
</my-user-card>
<!-- 引入你的 JavaScript 模块 -->
<script type="module" src="/src/user-card.js"></script>
</body>
</html>
在以上的示例中,我们实现了组件状态管理,生命周期函数,模板以及插槽等功能。更多的用法可以参考 Lit 文档(https://lit.dev/docs/)。
跟随本篇文章,你已经掌握了 Web Components
的基础知识并且对 Google Lit
组件库的使用方法有了了解,我们可以看到,现代的web开发正变得更加模块化和可重用。这不仅仅是技术上的进步,更是对我们如何思考和构建Web应用的一种革新。
后记
随着技术的快速发展,Web环境已不再局限于简单的文本和图像展示。今天,我们探讨了两项革命性的技术——WebAssembly
和 Web Components
,它们正在重新定义我们与网络互动的方式。
每一次技术的飞跃都伴随着新的挑战。安全性、兼容性以及性能优化将是我们不断学习和探索的课题。今天的分享,仅仅是一个开始,我相信每位热爱技术的你,都能在这波澜壮阔的变革中找到自己的位置。
感谢你的阅读与陪伴,让我们继续在技术的大海中探索更多的未知。