Web版PhotoShop已登场,核心技术你学会了吗

科技   2023-11-09 09:18   江苏  

长期以来,Photoshop 在设计领域占据着重要的地位。Adobe 工程师经过多年来的努力,并与 Chrome 等浏览器供应商密切合作,通过 WebAssembly + EmscriptenWeb 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 通过引入一种新的二进制格式,可以直接编译成机器码,从而提高程序的运行效率。WebAssemblyJavaScript 一起工作,可以在浏览器中运行,也可以在 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

EmscriptenWebAssembly 的一个完整的开源编译器工具链。使用 Emscripten ,您可以将C和C++代码或任何其他使用LLVM的语言编译到 WebAssembly 中,并在Web、Node.js或其他 Wasm 运行时上运行它。实际上,任何可移植的C 或 C++ 代码库都可以使用 Emscripten 编译成 WebAssembly,从需要渲染图形、播放声音以及加载和处理文件的高性能游戏,到 Qt 等应用程序框架。

使用 Emscripten 编写第一个 WebAssembly 程序

  1. emsdk 安装
    略。请参考 MDN 文档 emsdk 安装(https://developer.mozilla.org/en-US/docs/WebAssembly/C_to_wasm#installing_the_emscripten_toolchain)。

  2. 代码编写 -- 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)}`)
})
  1. 代码编写 -- 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>

代码执行结果如下所示:

  1. 在 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 的基本方法包括以下几个步骤:

  1. 创建一个类或函数来指定 web 组件的功能
  2. 使用 customElements.define() 方法注册你的新自定义元素,并向其传递要定义的元素名称、指定元素功能的类、以及可选的其所继承自的元素
  3. 如果需要的话,使用 Element.attachShadow() 方法将一个 shadow DOM  附加到自定义元素上。使用通常的DOM方法向 shadow DOM 中添加子元素、事件监听器等。
  4. 如果需要的话,使用 <template><slot> 定义一个HTML模板。再次使用常规 DOM 方法克隆模板并将其附加到你的 shadow DOM 中。
  5. 在页面任何位置使用你的自定义元素。

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

  1. 我们先来编写一个简单的 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>

以上只是一个最简单的用例,如果组件内部很复杂,那么这种写法就会很困难,此时我们可以借助 templateslot 的能力来简化代码。

  1. 使用 templateslot修改 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 组件,以及如何使用 templateslot 来简化代码。更多的用法可以参考 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环境已不再局限于简单的文本和图像展示。今天,我们探讨了两项革命性的技术——WebAssemblyWeb Components,它们正在重新定义我们与网络互动的方式。

每一次技术的飞跃都伴随着新的挑战。安全性、兼容性以及性能优化将是我们不断学习和探索的课题。今天的分享,仅仅是一个开始,我相信每位热爱技术的你,都能在这波澜壮阔的变革中找到自己的位置。

感谢你的阅读与陪伴,让我们继续在技术的大海中探索更多的未知。

点个赞再走吧
↓↓↓

中国制造网UED
这里是中国制造网UED公众号,我们是一个专注于用户体验研究领域和前瞻技术的团队,我们致力于为大家提供最新、最实用的设计资讯、前端技术和案例分享。通过优秀的用户体验来传递美好。