WebAssembly应用到前端工程下-webpack和webassembly

51次阅读

共计 3807 个字符,预计需要花费 10 分钟才能阅读完成。

在上一篇文章 WebAssembly 应用到前端工程(上)—— webassembly 模块的编写中,完成了 @ne_fe/gis 模块的编写与发布。然而 webassembly 在当前以 webpack4 为主要编译工具的实际工程应用中依然存在问题。

尽管 webpack4 新增了对 wasm 文件的编译支持,在 wasm 模块编写完成之后将其与 webpack 结合的过程中发现,wasm 模块无法被正确加载。在对 @ne_fe/gis 编译输出文件的检查之后,有了新的解决方案 wasm-module-webpack-plugin。

出现的问题

直接在前端工程中引入 @ne_fe/gis 并使用的话,控制台会输出错误信息

index.js?558c:1 GET http://localhost:8080/gps.wasm 404 (Not Found)
Uncaught (in promise) TypeError: Incorrect response MIME type. Expected ‘application/wasm’.

查看所引用的 @ne_fe/gis/dist/index.js 后发现这样一句话

var Pn="undefined"!=typeof location?location.pathname.split("/"):[];Pn.pop(),(Cn?WebAssembly.instantiateStreaming(fetch(Pn.join("/")+"/gps.wasm"),o):fetch(Pn.join("/")+"/gps.wasm").then(e=>e.arrayBuffer()).then(e=>WebAssembly.instantiate(e,o)))

出错原因时由于 fetch 直接从根路径下直接获取 wasm 文件,但文件并没有生成或移动,webpack 并不能处理 fetch 所加载的文件,最终导致 wasm 加载失败。

babel

webpack 不能处理 js 的 fetch 语句,导致了上面问题的出现,那么只有一种方法能够处理 fetch 语句,那就是 babel。
下面来编写一个 babel 插件处理 fetch 加载 wasm 的情况

// babel-plugin.js
module.exports = function() {
  return {
    visitor: {CallExpression(path, state) {if(path.node.callee.name === 'fetch'){const argument = JSON.parse(JSON.stringify(path.node.arguments[0]));
          for (const i in argument.right) {if (i === 'value' && argument.right[i].endsWith('.wasm')) {console.log('argument.right[ i]', argument.right[i], 'state.file.opts.filename', state.file.opts.filename);
            }
          }
        }
      },
    }
  }
};

在 webpack 中使用

// webpack.config.js
const path = require('path');
const BabelPlugin = require('./babel-plugin');
module.exports = {
  module: {
    rules: [
      ...
      {
        test: /\.js$/,
        loader: 'babel-loader',
        include: [path.join(process.cwd(), './node_modules/@ne_fe/gis') ],
        options: {plugins: [ BabelPlugin],
        },
      },
      ...
    ],
  },
  plugins: [...],
};

启动 webpack 进行编译,控制台输出

argument.right[i] /gps.wasm 
state.file.opts.filename C:\dir\test\node_modules\@ne_fe\gis\dist\index.js

最终获得了 fetch 所加载的 wasm 文件名与 fetch 语句所在文件。

webpack

在上一步中获取到了 fetch 语句加载的 wasm 文件名与 fetch 语句所在文件。
为了将 wasm 文件输出到 webpack 编译结果中,需要添加 webpack 插件。经修改之后,整个结合 wasm 与 webpack 的插件如下

// event-listener.js
const EventEmitter = require('events').EventEmitter;
class Events extends EventEmitter {constructor(prop) {super(prop);
    this.data = {};}
}
const events = new Events();
events.on('wasm', data => {if (!events.data[data.wasmRefFileName]) {events.data[data.wasmRefFileName] = {};
    events.data[data.wasmRefFileName][data.wasmRefPath] = data.wasmDir;
  } else {if (!events.data[data.wasmRefFileName][data.wasmRefPath]) {events.data[data.wasmRefFileName][data.wasmRefPath] = data.wasmDir;
    }
  }
});
module.exports = events;
// babel-plugin.js
const eventEmitter = require('./event-listener');
const pathInternal = require('path');
module.exports = function() {
  return {
    visitor: {CallExpression(path, state) {if(path.node.callee.name === 'fetch'){const argument = JSON.parse(JSON.stringify(path.node.arguments[0]));
          for (const i in argument.right) {if (i === 'value' && argument.right[i].endsWith('.wasm')) {
              eventEmitter.emit('wasm', {wasmRefPath: argument.right[i],
                wasmRefFileName: state.file.opts.filename,
                wasmDir: pathInternal.parse(state.file.opts.filename).dir,
              });
            }
          }
        }
      },
    }
  }
};
// webpack-plugin
const eventEmitter = require('./event-listener');
const path = require('path');

class WasmPlugin {apply(compiler) {compiler.plugin('emit', function(compilation, callback) {for (const i in eventEmitter.data) {for (const j in eventEmitter.data[i]) {const filePath = path.join(eventEmitter.data[ i][j], '.' + j);
          const content = compiler.inputFileSystem._readFileSync(filePath);
          const stat = compiler.inputFileSystem._statSync(filePath);
          const wasmRefPath = j;
          const wasmName = wasmRefPath.substring(1, wasmRefPath.length);
          compilation.assets[wasmName] = {size() {return stat.size;},
            source() {return content;},
          };
        }
      }
      callback();});
  }
}

module.exports = WasmPlugin;

event-listener 的作用是为了保存 babel-plugin 中获取到的 wasm 相关信息然后在 webpack 插件执行的时候使用,webpack-plugin 将获取到的 wasm 文件输出到正确路径。

涉及到的技术主要为 commonjs 模块机制、babel 插件编写与 webpack 插件编写。

使用

可以参考 wasm-module-webpack-plugin 或 @ne_fe/gis,欢迎 start。

尾语

尽管 webassembly 的出现对前端开发高性能浏览器应用有了重大的作用,webpack4 也新增了对 webassembly 的支持,但是目前以 webpack 编译为主的前端工程对 webassembly 的支持依然不有好,开发难度不小,希望以后有更好的解决方案。
 
 
上一篇:WebAssembly 应用到前端工程(上)—— webassembly 模块的编写

正文完
 0