前言

  • 之前老是听别人提到WebAssembly这个词,一直对其比较模糊,不能理解是个啥东西,后来自己实践了一下,发现其实就是一种提高代码性能的手段。

简介

  • WebAssembly 是一种运行在现代网络浏览器中的新型代码,并且提供新的性能特性和效果。它设计的目的不是为了手写代码而是为诸如 C、C++和 Rust 等低级源语言提供一个高效的编译目标。(解释来自MDN)
  • 通俗一点来讲,就是利用一些C、C++、Rust等偏底层的一些语言去实现部分功能并编译成二进制文件然后暴露给第三方平台(可能是浏览器端,也可能是服务端,还有可能是客户端),可以丰富和优化相关的应用。
  • 当然了,其实也有类似asm.js这种非底层语言去实现,但是它编译后的二进制文件减少了编译的过程,其实也是可以提高代码的性能。

开始尝试

准备工作

  • 我准备使用rust来演示一下,所以需要用到的第三方环境如下
    1. 安装Rustcurl https://sh.rustup.rs -sSf | sh -s -- --help。windows上可以下载https://static.rust-lang.org/rustup/dist/i686-pc-windows-gnu/rustup-init.exe。安装成功后可以在命令行输入rustc -V以及cargo -V来看看是否安装成功,cargoRust一个包管理器,类似npm之于nodejs
    2. 安装wasm-pack,参照官网https://rustwasm.github.io/wasm-pack/installer/。
    3. Rust基础语法,可以参考https://course.rs/about-book.html这位大佬编写的书,不枯燥又有趣,比官方的中文文档可读性更强。

开始

  • 使用上面安装好的wasm-pack新建一个空项目,wasm-pack new test-webassembly
  • 查看目录结构

    test-webassembly

    src

    lib.rc // 主入口
    utils.rs // 依赖的工具方法

    tests

    web.rs // 测试文件

    .appveyor.yml // 项目的前置依赖配置

    .gitignore // git忽略的文件配置

    .travis.yml // 该项目的CI环境配置文件

    cargo.toml // 该项目的配置文件,类似package.json

    README.md // 解释文件

重点文件分析

  • lib.rs,默认自带的这个文件只实现了一个greet方法,并暴露给了浏览器端,里面调用了alert方法。
    use wasm_bindgen::prelude::*; // 引入wasm_bindgen::prelude下所有的类和方法
    #[cfg(feature = "wee_alloc")]
    #[global_allocator]
    static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; // 一般不需要用,可以选择注释
    #[wasm_bindgen]
    extern {
        fn alert(s: &str); // 表示第三方环境可以直接用的方法定义,编译的时候会直接去调用第三方环境中的同名方法
    }
    #[wasm_bindgen] // 类似ts中的装饰器,打上这个标签,表示是需要与第三方环境去交互的,通俗一点,就是这个地方会被打包到第三方环境中。
    pub fn greet() {
        alert("Hello, test-webassembly!");
    }
    
  • Cargo.toml,这个文件主要就是一些项目配置信息了。
    [package]
    name = "test-webassembly" // 包名
    version = "0.1.0" // 包版本
    authors = ["xxx"] // 作者名
    edition = "2018" // rust版本
    [lib] // target设置,类似webpack种的target,将其打包成什么样的包
    crate-type = ["cdylib", "rlib"] // 分别代表动态系统库和rust静态库
    [features] // 用来条件编译
    default = ["console_error_panic_hook"] // 这个可以删掉不需要
    [dependencies]
    wasm-bindgen = "0.2.63" // 这个就是主要的处理rust代码转化成webassembly的包
    # The `console_error_panic_hook` crate provides better debugging of panics by
    # logging them with `console.error`. This is great for development, but requires
    # all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
    # code size when deploying.
    # console_error_panic_hook = { version = "0.1.6", optional = true } 这个其实如果不是特别需要也可以不用
    # `wee_alloc` is a tiny allocator for wasm that is only ~1K in code size
    # compared to the default allocator's ~10K. It is slower than the default
    # allocator, however.
    # wee_alloc = { version = "0.4.5", optional = true } 这个一般用不上,注释掉就可以
    [dev-dependencies]
    wasm-bindgen-test = "0.3.13" // 用来测试的包,自己写一般可以不用
    [profile.release]
    # Tell `rustc` to optimize for small code size.
    opt-level = "s"
    

改造

接下来实现一个简单的图片处理工具,帮助web端更快的处理图片内容。

  1. 会用到image这个rust包。所以先改造它的Cargo.toml
[dependencies]
wasm-bindgen = "0.2.63"
// 在之前的dependencies下面添加image包
image = "0.24.6"
  1. 就以处理图片的其中内置的一个灰度方法来看一下。接下来改造lib.rs文件。
// 新增一个灰度方法,接受一个字节类型的集合,返回一个新的字节类型的集合
#[wasm_bindgen]
pub fn gray(_array: &mut [u8]) -> Vec<u8> {
    let mut img = image::load_from_memory(_array).unwrap(); // 将传入的u8集合转化成一个image对象
    img = img.grayscale(); // 调用第三方包的灰度方法得到新的图片
    let mut bytes: Vec<u8> = Vec::new(); // 定义一个新的u8集合
    img.write_to(&mut Cursor::new(&mut bytes), image::ImageOutputFormat::Png).unwrap(); // 将变换后的image转化成新的u8集合
    bytes // 将u8集合作为返回值抛出
}
  1. 运行命令wasm-pack build --release --target web,将rust文件打包成目标为web项目的可用二进制文件以及加载的js文件。可以看到项目多了一个pkg文件夹

    pkg

    .gitignore
    package.json
    README.md
    test_webassembly_bg.wasm // 二进制文件
    test_webassembly_bg.wasm.d.ts
    test_webassembly.d.ts
    test_webassembly.js // 这个就是加载这个二进制的文件

// test_webassembly.js这个文件主要看几个重点地方,其实不看也行,这个地方wasm-pack已经帮你处理好了,基本只需要学会用就行
// 加载二进制后的处理,返回暴露出的模块的实例
async function load(module, imports) {
    if (typeof Response === 'function' && module instanceof Response) {
      // 在支持instantiateStreaming的情况下,优先使用instantiateStreaming加载对应的二进制文件
        if (typeof WebAssembly.instantiateStreaming === 'function') {
            try {
                return await WebAssembly.instantiateStreaming(module, imports); 
            } catch (e) {
                if (module.headers.get('Content-Type') != 'application/wasm') {
                    console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e);
                } else {
                    throw e;
                }
            }
        }
        const bytes = await module.arrayBuffer();
        return await WebAssembly.instantiate(bytes, imports);
    } else {
      // 不支持的时候,就使用instantiate文件来加载
        const instance = await WebAssembly.instantiate(module, imports);
        if (instance instanceof WebAssembly.Instance) {
            return { instance, module };
        } else {
            return instance;
        }
    }
}
// 针对已经解析完的实例,做一些初始化操作,抛出模块实例的exports
function finalizeInit(instance, module) {
    wasm = instance.exports;
    init.__wbindgen_wasm_module = module;
    cachedInt32Memory0 = null;
    cachedUint8Memory0 = null;
    return wasm;
}
// 文件暴露的入口
async function init(input) {
    if (typeof input === 'undefined') {
        input = new URL('test_webassembly_bg.wasm', import.meta.url); // 默认的二进制文件地址
    }
    const imports = getImports();
    if (typeof input === 'string' || (typeof Request === 'function' && input instanceof Request) || (typeof URL === 'function' && input instanceof URL)) {
        input = fetch(input); // 加载对应的二进制文件
    }
    initMemory(imports); // 这个就是之前上面申请空间的,可以注释掉的,不用看
    const { instance, module } = await load(await input, imports); // 调用webassembly的api去加载二进制文件
    return finalizeInit(instance, module); // 得到模块中抛出的exports
}

在html中使用,新建一个index.html

<!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>Document</title>
</head>
<body>
    <input type="file" multiple="false" id="file">
    <div id="box"></div>
    <div>
        <button id="gray-btn">灰度</button>
    </div>
    <div id="target"></div>
    <script type="module">
        import init, { gray } from './pkg/test_webassembly.js';
        await init();
        const file = document.getElementById('file');
        const grayBtn = document.getElementById('gray-btn');
        let current = null;
        let uint8Array = [];
        file.addEventListener('change', (e) => {
            current = e.target.files[0];
            var reader = new FileReader(); 
            reader.readAsDataURL(current);
            reader.onload = function() {
                const img = new Image();
                img.src = this.result;
                document.querySelector('#box').appendChild(img);
            }
        });
        grayBtn.addEventListener('click', () => {
            var reader = new FileReader();
            reader.readAsArrayBuffer(current);
            reader.onload = function () {
                uint8Array = new Uint8Array(this.result);
                let newUnit8Array = gray(uint8Array); // 调用webassembly提供的灰度方法获取到灰度之后的结果
                const img = new Image();
                let blob = new Blob([newUnit8Array], { type: 'image/png' });
                img.src = window.URL.createObjectURL(blob);
                document.querySelector('#target').appendChild(img);
            }
        })
    </script>
</body>
</html>

使用npx http-server --port 3000启动静态服务器,因为webassembly是不支持本地的文件的,下面就是最终效果,先选择一张图片,之后点击灰度就能快速得到一张灰度的图片

WebAssembly初尝试

小结

  • WebAssembly是一个很有意思的技术方向,它为web应用提供了很多可能,图片处理,视频解析、加密等等一些复杂的场景都可以变得更轻便化。
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。