【WebGL系列】Typescript+WebGL+Webpack開發環境搭建

目前Web實現矢量渲染的主流技術包括SVG、VML和WebGL。相對而言,VML是一種較古老的技術,雖然未成爲W3C標準,但被早期的IE瀏覽器(IE9如下)和微軟Office普遍使用,目前已經遠離了瀏覽器戰場。因此可供選擇的僅剩SVG和WebGL。SVG是XML的一個子集,秉承了一個標籤對應一條數據的原則,目前常常被使用於數據量較小的web項目,好比圖表和地鐵圖。Web矢量地圖的數據量很是龐大,舉個例子,以下圖所示的一個512px*512px的瓦片,其數據量是一個接近5位數的二維數組。而這個瓦片僅僅是最簡單的大陸和海洋輪廓,同尺寸街道圖的數據量更加龐大。
javascript

處理龐大的數據量必然對性能的要求很是苛刻,何況因爲中間隔着一層瀏覽器,Web地圖並不能徹底發揮CPU的計算能力。在有限的CPU資源下若是可以藉助其餘計算資源則必事半功倍,可以調用GPU資源的WebGL便成爲了惟一的選擇。html

SVG不適合開發Web矢量地圖的緣由主要有兩點:前端

  • 沒法藉助GPU提升性能;
  • Web地圖交互很是頻繁,好比移動、縮放、旋轉等等,若是使用SVG則須要藉助頻繁操做DOM實現,而DOM操做是瀏覽器最消耗性能的行爲。

技術選型

肯定了底層技術-WebGL以後,接下來須要選擇合適的輔助技術,針對目標有兩點:java

  • JavaScript
  • 構建工具

WebGL渲染與CSS無關,因此CSS開發框架的選型對總體的影響微乎其微,在此略過。node

WebGL能夠理解爲OpenGL在瀏覽器環境下的變種,保留了OpenGL ES的語義和規範,提供相對簡潔的JavaScript API。絕大部分的shader能夠實現WebGL和OpenGL的共用。開發WebGL shader的語言GLSL是一種語法接近C的強類型編程語言。這一點對於習慣了JavaScript的前端開發者們須要必定的調整。既然是調整,那麼不妨調整的完全一些:將總體開發都引入強類型的概念。目前支持在JavaScript中引入強類型的主流框架有兩種:TypeScript和Flow.js。TypeScript是JavaScript的強類型超集,Flow則更接近於一種類型註解或者註釋工具。相對而言,引入Flow的成本更低,你能夠自由決定哪些文件開啓或者關閉類型檢查,僅僅須要在文件頂部添加一行註釋:webpack

// @flow

因此Flow很是適合現有的項目進行遷移,而若是使用TypeScript則更須要將所有源代碼進行改寫。好在目前要作的項目並無歷史包袱,因此Flow的這點優點並不能做爲技術選型的決定性因素。git

最終選擇TypeScript的緣由有如下幾點:es6

  • 語法更嚴謹甚至有些繁瑣,但習慣以後很是順手;
  • 生態更豐富,目前大部分主流第三方庫均提供TypeScript支持。

ES6正式推出了Typed Array標準,但其實早在ES6以前,支持WebGL的瀏覽器就已經提供了強類型數組的API,目的是爲了提升計算性能。github

構建工具的選擇相對比較多,Webpack、Rollup、gulp都是很是優秀的工具。最終選擇Webpack的緣由很是簡單:比較熟。web

構建配置

Webpack的配置與常規的web項目大致相同,須要注意的兩點是:

  • TypeScript與Babel的配合
  • shader的構建

TypeScript&Babel

TypeScript自己支持編譯爲ES5或ES3,即將tsconfig.json的編譯選型target修改成"es5"/"es3":

{
  "compilerOptions": {
    "target": "es5"
  }
}

TypeScript編譯器對於語法規範的轉譯功能能夠知足絕大多數ES6新功能,可是其功能的全面性相比較Babel仍然有些不足,因此爲了對編譯進行更精準的控制,項目中採用的方案是將TypeScript首先轉譯爲ES6語法,再借助Babel將其轉譯爲ES5,即將tsconfig.json中的compilerOptions.target設置爲"es6",webpack配置以下:

module: {
    rules: [{
      test: /\.ts$/,
      exclude: /node_modules/,
      use: ['babel-loader','awesome-typescript-loader']
    }
}

Webpack編譯TypeScript的loader有兩個:ts-loaderawesome-typescript-loader。最終選擇後者的緣由固然不是由於它的名字中有個awesome,而是相對於前者,awesome-typescript-loader可以提供一些更加便利的功能,好比alias-別名。

若是源碼的目錄結構比較複雜,引用一個模塊時可能須要寫很長的路徑名稱,好比:

import Utils from '../../../utils';

爲了令代碼具備更好的易讀性,咱們一般藉助一些工具將模塊的引用設置較短的別名。Webpack也有此功能,經過resolve配置模塊的別名:

resolve: {
  alias: {
    'utils': path.resolve(__dirname,'src/utils')
  }
}

但遺憾的是ts-loaderawesome-typescript-loader並不能直接使用Webpack的alias配置,源碼中直接使用模塊別名將會拋出not found錯誤,請注意這個錯誤是TypeScript編譯器拋出而非Webpack。解決方案很簡單:在tsconfig.json中配置模塊別名。以下:

{
  "paths": {
    "utils/*": ["src/utils/*"]
  }
}

但這並非最終的解決方案,由於若是使用ts-loader做爲Webpack集成的話,Webpack並不能獲取tsconfig.json的別名配置,也就是說,Webpack將會拋出not found錯誤。awesome-typescript-loader很好地解決了這個問題,它能夠將tsconfig.json的別名配置映射至Webpack的resolve.alias。固然,若是你仍然堅持使用ts-loader也能夠解決,若是你不怕麻煩的話:在Webpack中手動配置一樣的resolve.alias

另外須要注意的是,使用awesome-typescript-loader須要在Webpack的resolve中建立對應的插件:

const TsConfigPathsPlugin = require('awesome-typescript-loader').TsConfigPathsPlugin;

module.exports = {
  module: {
    rules: [{
      test: /\.ts$/,
      exclude: /node_modules/,
      use: ['babel-loader','awesome-typescript-loader']
    },
    // other rules
    ]
  },
  resolve: {
    plugins: [
      new TsConfigPathsPlugin({
        configFileName: Path.resolve(__dirname,'../tsconfig.json')
      })
    ]
  }
}

shader

WebGL建立shader的流程爲:

  1. 首先建立指定類型的shader實例;
  2. 將shader源碼與實例綁定;
  3. 編譯shader。

示例代碼以下:

const source = `
precision mediump float;

attribute vec2 a_pos;

uniform vec4 u_color;
uniform vec2 u_resolution;
uniform vec2 u_translate;

varying vec4 v_color;

void main() {
    vec2 real_poistion = (a_pos+u_translate) / u_resolution * 2.0 - 1.0;
    gl_Position = vec4(real_poistion * vec2(1, 1), 0, 1);
    v_color = u_color;
}`;
// 建立shader實例
const Shader = gl.createShader(gl.VERTEX_SHADER);
// 綁定shader源碼
gl.shaderSource(Shader,source);
// 編譯
gl.compileShader(Shader);

shader的源碼以字符串的形式綁定至shader實例,也就是說,不論shader的源碼是用什麼編程語言編寫(好比能夠按照上述代碼中用JavaScript字符串編寫,也能夠直接用glsl語言編寫),必定要保證以字符串的形式引入shader源碼模塊。秉承這項原則,最簡單的shader構建方案即是上述代碼中的字符串形式。好比將上述示例代碼中的shader源碼單獨抽離爲vertex.js以下:

export default `
precision mediump float;

attribute vec2 a_pos;

uniform vec4 u_color;
uniform vec2 u_resolution;
uniform vec2 u_translate;

varying vec4 v_color;

void main() {
    vec2 real_poistion = (a_pos+u_translate) / u_resolution * 2.0 - 1.0;
    gl_Position = vec4(real_poistion * vec2(1, 1), 0, 1);
    v_color = u_color;
}`;

而後在主文件中引入:

import VertexShaderSource from './vertex.js';
// 建立shader實例
const Shader = gl.createShader(gl.VERTEX_SHADER);
// 綁定shader源碼
gl.shaderSource(Shader,VertexShaderSource);
// 編譯
gl.compileShader(Shader);

這種書寫方式優勢是不須要對Webpack進行任何配置,可是卻等於放棄了IDE對glsl語法的高亮、糾錯等輔助功能。若是shader源碼只有幾行倒也沒什麼大問題,可是對於幾十上百行的代碼若是沒有高亮輔助的話可能會嚴重影響開發效率。

解決這個問題的辦法要從兩方面入手:

  • 令Webpack可以正確編譯glsl代碼;
  • 令TypeScript可以將glsl模塊與ts模塊融合。

第一個問題很好解決,由於咱們的目的是把glsl模塊引入到js模塊中而且做爲字符串使用,因此Webpack要作的就是將glsl源碼構建爲字符串便可:

{
  test: /\.glsl$/,
  loader: 'raw-loader'
}

raw-loader的功能是將被引入的文件內容轉換爲字符串。

除了強類型帶來的開發模式轉變之外,TypeScript最大的問題是不能自動識別ts之外的任何其餘類型模塊,即便最廣泛的JSON也不行。好比下述代碼在TypeScript環境下會報not found錯誤:

import Data from '../data.json';

這時候須要用到TypeScript的聲明文件。聲明文件的做用簡單來講就是告知TypeScript編譯器一些必要的信息以便被正確識別。好比聲明一些全局的類型(type)、接口(interface)、模塊(module)等。

默認狀況下,TypeScript編譯器會自動識別源碼和node_modules目錄中@types文件夾內的聲明文件,你也能夠經過配置tsconfig.jsoncompilerOptions.typeRoots指定聲明文件目錄。針對上文提到的TypeScript不識別glsl和json模塊問題,咱們在源碼目錄的@types文件夾中建立聲明文件global.d.ts,內容以下:

declare module '*.glsl';
declare module '*.json';
declare type WidthAndHeight = {
  width: number;
  height: number;
};

上述代碼中聲明瞭三個信息:

  • 聲明glsl後綴類型的文件爲可識別模塊;
  • 聲明json後綴類型的文件爲可識別模塊;
  • 聲明全局類型WidthAndHeight,此類型將在任何源碼文件中直接使用。

在以上配置的基礎上還有一個注意事項:與ES6 modules不一樣的是,TypeScript引入declare聲明的非ts模塊並不能將其內容自動轉化爲默認導出,即export default。好比在ES6環境下引入一個json文件:

import JsonData from './data.json';

而在TypeScript環境下須要使用如下語法:

import * as JsonData from './data.json';

示例代碼

具體代碼能夠參考demo:https://github.com/ihardcoder/demo_ts-webgl-webpack

相關文章
相關標籤/搜索