WebAssembly (abbreviated Wasm) is a binary instruction format for a stack-based virtual machine. Wasm is designed as a portable target for compilation of high-level languages like C/C++/Rust, enabling deployment on the web for client and server applications. WebAssembly(縮寫 Wasm)是基於堆棧虛擬機的二進制指令格式。Wasm爲了一個可移植的目標而設計的,可用於編譯C/C+/RUST等高級語言,使客戶端和服務器應用程序可以在Web上部署。javascript
webassembly的介紹能夠參考圖說 WebAssembly。html
本文以@ne_fe/gis這個模塊的開發過程梳理webassembly如何應用到前端工程中。 注:使用emscripten完成weassembly開發至少須要基礎的c/c++編碼能力。前端
該模塊主邏輯由c++編寫,webpack配合emscripten附帶的emcc編譯器將其編譯到wasm。提供大批量座標的經緯度轉換功能,在十幾萬座標點轉換的狀況下,依然有優秀的性能表現。 具體其餘信息能夠參考npm上該模塊的Readme。vue
emscripten是webassembly官方推出的將c/c++代碼編譯成wasm文件的工具。 具體安裝能夠參照官網文檔。java
主要針對c++源碼文件,須要添加正確的loader進行處理。使用的loader爲cpp-wasm-loader,下面是個人webpack.config.js所寫大概配置,其餘配置跟普通的webpack配置大體相同。webpack
module.exports = {
...
resolve: {
extensions: [ '.js', '.vue', '.c', '.cc', '.cpp', '.wasm' ],
alias: {
vue$: 'vue/dist/vue.esm.js',
},
},
...
module: {
...
{
test: /\.(c|cc|cpp)$/,
use: {
loader: 'cpp-wasm-loader',
options: {
// 這裏的兩個參數,第一個是讓emcc可以識別c++11的語法與特性
// 第二個是讓emcc可以將EMSCRIPTEN_BINDINGS宏裏面所指定的類與方法可以在綁定到模塊導出的js對象上,讓js可以直接調用
// 還能夠傳入其餘clang編譯器可接受的參數
emccFlags: existingFlags => existingFlags.concat([ '-std=c++11', '--bind' ]), // add or modify compiler flags
// emccPath: "path/to/emcc", // only needed if emcc is not in PATH,
memoryClass: false, // disable javascript memory management class,
fetchFiles: true,
asmJs: false, // 不生成wasm.js
wasm: true, // 生成wasm文件
fullEnv: true,
},
},
},
...
},
};
複製代碼
emscripten的主要api能夠參考官方文檔上的說明,不過建議參考本地頭文件(emsdk安裝路徑/emsdk/emscripten/1.38.22/system/include/),相比文檔,本地頭文件更能看得明白。c++
以高德地圖座標轉gps座標代碼爲例git
// em.cc
#include <math.h>
#include <vector>
#include <string>
#include <emscripten.h>
#include <emscripten/bind.h>
#include <emscripten/val.h>
#define PI 3.14159265
#define ee 0.00669342162296594323
#define a 6378245.0
using namespace emscripten;
extern "C"
{
std::vector<float> gcj02towgs84(float lat, float lng);
bool out_of_china(float lat, float lng);
float transformlat(float lat, float lng);
float transformlng(float lat, float lng);
val translateFromGPSInCPP(val data, std::string target, int type);
// 相應地圖座標轉gps座標
// data爲座標點數組,target爲轉換目標
// type 是否轉換座標對象 0 只會對數值作計算轉換 1 不只會對數值作計算轉換,還會轉爲騰訊地圖經緯度對象
// val 爲c++中表明js對象的數據類型,頭文件爲<emscripten/val.h>
val translateFromGPSInCPP(val data, std::string target, int type) {
unsigned l = data["length"].as<unsigned>();
val res = val::array();
val _mid = val::object();
val amap = val::global("AMap");
val bmap = val::global("BMap");
val _Object = val::global("Object");
val qq = val::global("qq");
for(unsigned i = 0; i < l; ++i) {
val midObj = data[i];
float lat = midObj["latitude"].as<float>();
float lng = midObj["longitude"].as<float>();
std::vector<float> translateOneResult;
if (target == "a") {
translateOneResult = wgs84togcj02(lat, lng); // 轉高德座標
} else if (target == "b") {
translateOneResult = wgs84tobd(lat, lng); // 轉百度座標,忽略
} else {
translateOneResult = wgs84togcj02(lat, lng); // 轉騰訊座標,忽略
}
if (type == 0) { // just translate number
_mid.set<std::string, float>("latitude", translateOneResult[0]);
_mid.set<std::string, float>("longitude", translateOneResult[1]);
} else {
if (target == "a") {
if (!amap.isUndefined()) {
_mid = amap["LngLat"].new_(translateOneResult[1], translateOneResult[0]);
}
}
if (target == "b") {
if (!bmap.isUndefined()) {
_mid = bmap["Point"].new_(translateOneResult[0], translateOneResult[1]);
}
}
if (target == "t") {
val tmap = qq["maps"];
if (!qq.isUndefined() && !tmap.isUndefined()) {
_mid = tmap["LatLng"].new_(translateOneResult[0], translateOneResult[1]);
}
}
_Object.call<val>("assign", _mid, midObj);
}
res.set<int, val>(i, _mid);
}
return res;
}
std::vector<float> gcj02towgs84(float lat, float lng) {
std::vector<float> res;
bool out_of_china_res = out_of_china(lat, lng);
if (out_of_china_res) {
res.push_back(lat);
res.push_back(lng);
} else {
float lng1 = lng - 105.0;
float lat1 = lat - 35.0;
float dlat = transformlat(lng1, lat1);
float dlng = transformlng(lng1, lat1);
float radlat = lat / 180.0 * PI;
float magic = sin(lat / 180.0 * PI);
magic = 1 - ee * magic * magic;
float sqrtmagic = sqrt(magic);
dlat = (dlat * 180.0) / ((a * (1 - ee)) / (magic * sqrtmagic) * PI);
dlng = (dlng * 180.0) / (a / sqrtmagic * cos(radlat) * PI);
const float mglat = lat - dlat;
const float mglng = lng - dlng;
res.push_back(mglat);
res.push_back(mglng);
}
return res;
}
bool out_of_china(float lat, float lng) {
return (lng < 72.004 || lng > 137.8347) || ((lat < 0.8293 || lat > 55.8271) || false);
}
float transformlat(float lat, float lng) {
float ret = -100.0 + 2.0 * lat + 3.0 * lng + 0.2 * lng * lng + 0.1 * lat * lng + 0.2 * sqrt(abs(lat));
ret += (20.0 * sin(6.0 * lat * PI) + 20.0 * sin(2.0 * lat * PI)) * 2.0 / 3.0;
ret += (20.0 * sin(lng * PI) + 40.0 * sin(lng / 3.0 * PI)) * 2.0 / 3.0;
ret += (160.0 * sin(lng / 12.0 * PI) + 320 * sin(lng * PI / 30.0)) * 2.0 / 3.0;
return ret;
}
float transformlng(float lat, float lng) {
float ret = 300.0 + lat + 2.0 * lng + 0.1 * lat * lat + 0.1 * lat * lng + 0.1 * sqrt(abs(lat));
ret += (20.0 * sin(6.0 * lat * PI) + 20.0 * sin(2.0 * lat * PI)) * 2.0 / 3.0;
ret += (20.0 * sin(lat * PI) + 40.0 * sin(lat / 3.0 * PI)) * 2.0 / 3.0;
ret += (150.0 * sin(lat / 12.0 * PI) + 300.0 * sin(lat / 30.0 * PI)) * 2.0 / 3.0;
return ret;
}
EMSCRIPTEN_BINDINGS(module) {
function("translateToGPSInCPP", &translateToGPSInCPP);
}
}
複製代碼
測試代碼運行的瀏覽器爲chrome63 translateFromGPSInJS方法是js實現的,爲了兼容不能使用webassembly技術的瀏覽器 同時因爲新版瀏覽器如chrome70及以上、firefox60及以上、safari12及以上優化了數組的性能,js實現與webassembly實現效果差距不大,只使用js進行經緯度轉換github
import wasm from './em.cc';
async function test() {
const innerModule = (await wasm.init()).emModule;
const gpsarr1 = [];
gpsarr1.push({ longitude: lngX, latitude: latY });
for (let i = 1; i < 50000; i++) {
let lngX = 116.3;
let latY = 39.9;
lngX = lngX + Math.random() * 0.0005;
if (i % 2) {
latY = latY + Math.random() * 0.0001;
} else {
latY = latY + Math.random() * 0.0006;
}
gpsarr1.push({ longitude: lngX, latitude: latY });
}
// performance Webassembly vs Js
console.time('translateFromGPSInCPP');
const res1 = await innerModule.translateFromGPSInCPP(gpsarr1, 't', 0);
console.timeEnd('translateFromGPSInCPP');
console.log('res1', res1);
const gpsarr2 = JSON.parse(JSON.stringify(gpsarr1));
console.time('translateFromGPSInJS');
const res2 = await gpsjs.translateFromGPSInJS(gpsarr2, 't', 0);
console.timeEnd('translateFromGPSInJS');
console.log('res2', res2);
}
test();
複製代碼
如下是7次測試50000條經緯度轉換的執行耗時(ms)web
1 | 2 | 3 | 4 | 5 | 6 | 7 | |
---|---|---|---|---|---|---|---|
webassembly | 317.8198 | 260.3901 | 270.0729 | 283.7041 | 351.6569 | 287.3720 | 312.5078 |
js | 2709.5219 | 2642.2451 | 2694.9921 | 2891.1311 | 3816.5019 | 2648.9201 | 3287.1430 |
最後通過測試5000、500條座標的經緯度轉換 萬條數量級座標的經緯度轉換,webassembly的執行效率是js的8-10倍。 千條數量級座標的經緯度轉換,webassembly的執行效率是js的4-6倍。 百條數量級座標的經緯度轉換,webassembly的執行效率是js的1.5-2.5倍。
公司的編譯環境缺乏emscripten,因此在容器中編譯,最後發佈到npm公共倉庫。