WebAssembly應用到前端工程(下)—— webpack和webassembly

在上一篇文章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模塊的編寫

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章