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

處理龐大的資料量必然對性能的要求非常苛刻,況且由于中間隔着一層浏覽器,Web地圖并不能完全發揮CPU的計算能力。在有限的CPU資源下如果能夠借助其他計算資源則必事半功倍,能夠調用GPU資源的WebGL便成為了唯一的選擇。
SVG不适合開發Web矢量地圖的原因主要有兩點:
- 無法借助GPU提高性能;
- Web地圖互動非常頻繁,比如移動、縮放、旋轉等等,如果使用SVG則需要借助頻繁操作DOM實作,而DOM操作是浏覽器最消耗性能的行為。
技術選型
确定了底層技術-WebGL之後,接下來需要選擇合适的輔助技術,針對目标有兩點:
- JavaScript
- 建構工具
WebGL渲染與CSS無關,是以CSS開發架構的選型對整體的影響微乎其微,在此略過。
WebGL可以了解為OpenGL在浏覽器環境下的變種,保留了OpenGL ES的語義和規範,提供相對簡潔的JavaScript API。絕大部分的shader可以實作WebGL和OpenGL的共用。開發WebGL shader的語言GLSL是一種文法接近C的強類型程式設計語言。這一點對于習慣了JavaScript的前端開發者們需要一定的調整。既然是調整,那麼不妨調整的徹底一些:将整體開發都引入強類型的概念。目前支援在JavaScript中引入強類型的主流架構有兩種:TypeScript和Flow.js。TypeScript是JavaScript的強類型超集,Flow則更接近于一種類型注解或者注釋工具。相對而言,引入Flow的成本更低,你可以自由決定哪些檔案開啟或者關閉類型檢查,僅僅需要在檔案頂部添加一行注釋:
是以Flow非常适合現有的項目進行遷移,而如果使用TypeScript則更需要将全部源代碼進行改寫。好在目前要做的項目并沒有曆史包袱,是以Flow的這點優勢并不能作為技術選型的決定性因素。
最終選擇TypeScript的原因有以下幾點:
- 文法更嚴謹甚至有些繁瑣,但習慣之後非常順手;
- 生态更豐富,目前大部分主流第三方庫均提供TypeScript支援。
ES6正式推出了Typed Array标準,但其實早在ES6之前,支援WebGL的浏覽器就已經提供了強類型數組的API,目的是為了提高計算性能。
建構工具的選擇相對比較多,Webpack、Rollup、gulp都是非常優秀的工具。最終選擇Webpack的原因非常簡單:比較熟。
建構配置
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-loader
和
awesome-typescript-loader
。最終選擇後者的原因當然不是因為它的名字中有個
awesome
,而是相對于前者,
awesome-typescript-loader
能夠提供一些更加便利的功能,比如alias-别名。
如果源碼的目錄結構比較複雜,引用一個子產品時可能需要寫很長的路徑名稱,比如:
為了令代碼具有更好的易讀性,我們通常借助一些工具将子產品的引用設定較短的别名。Webpack也有此功能,通過
resolve
配置子產品的别名:
resolve: {
alias: {
'utils': path.resolve(__dirname,'src/utils')
}
}
但遺憾的是
ts-loader
和
awesome-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的流程為:
- 首先建立指定類型的shader執行個體;
- 将shader源碼與執行個體綁定;
- 編譯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
錯誤:
這時候需要用到TypeScript的聲明檔案。聲明檔案的作用簡單來說就是告知TypeScript編譯器一些必要的資訊以便被正确識别。比如聲明一些全局的類型(type)、接口(interface)、子產品(module)等。
預設情況下,TypeScript編譯器會自動識别源碼和
node_modules
目錄中
@types
檔案夾内的聲明檔案,你也可以通過配置
tsconfig.json
中
compilerOptions.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檔案:
而在TypeScript環境下需要使用以下文法:
示例代碼
具體代碼可以參考demo:https://github.com/ihardcoder/demo_ts-webgl-webpack