成果
three-platfromize
目前已实现 微信小程序 和 淘宝小程序 的适配,均可加载GLB
文件
点击查看微信小程序DEMO
起因
估计尝试过在小程序使用 THREE 的朋友估计都体会到微信官方推出的适配版的难用,直接让开发体验回到解放前。
- 无类型提示,对于three新手不友好,对于习惯了代码即文档的也不友好。
- 无法顺畅使用 three.js 生态的 npm 包,需要手动 scope 注入 three 的依赖。
- 无 tree shaking,在只有 2mb 容量的包大小限制下,three 是个庞然大物。
- 没有平台相关资源释放接口(即VSCode里面的Disposable模式)。
- 每个小程序平台需要单独适配一个,定制 three 的维护成本高。
所以有了 THREE 平台化的想法。既然微信官方的适配版这么多不能,所以平台化的目标就是把“不”去掉。
目标
- 支持TS类型提示,能方便查阅API文档(d.ts)
- 可以通过构建修改方便使用 three 生态 npm 包,无需手动 scope,比如 GLTFLoader
- 支持 tree shaking 能减少多点就少点,加个 tfjs 就更加头大。
- 有资源释放 dispose 接口。
- 支持方便动态注入多个小程序平台的平台接口实现适配器,多 backends 。
参考
既然需要多平台支持,所以需要向平台化很好的项目学习比如 tfjs ,实现了多个backend 有 CPU,WebGL,WASM。其运行的平台的适配有PlatformBrowser,PlatformNode,PlatformWechat(tfjs的微信小程序插件里有)
然后在一个Environment全局单例实现平台的切换逻辑。
实现
所以需要一个全局的单例实现平台依赖的转发切换(模仿 THREE 的源码风格)
实现平台的全局单例
// 为了避免重新声明的报错
let $URL = null;
let $atob = null;
let $Blob = null;
let $window = null;
let $document = null;
let $XMLHttpRequest = null;
let $OffscreenCanvas = null;
let $HTMLCanvasElement = null;
let $createImageBitmap = null;
let $requestAnimationFrame = null;
class Platform {
set(platform) {
this.platform && this.platform.dispose();
this.platform = platform;
const globals = platform.getGlobals();
$atob = globals.atob;
$Blob = globals.Blob;
$window = globals.window;
$document = globals.document;
$XMLHttpRequest = globals.XMLHttpRequest;
$OffscreenCanvas = globals.OffscreenCanvas;
$HTMLCanvasElement = globals.HTMLCanvasElement;
$createImageBitmap = globals.createImageBitmap;
$requestAnimationFrame = globals.requestAnimationFrame;
$URL = globals.window.URL;
}
dispose() {
this.platform && this.platform.dispose();
$URL = null;
$Blob = null;
$atob = null;
$window = null;
$document = null;
$XMLHttpRequest = null;
$OffscreenCanvas = null;
$HTMLCanvasElement = null;
$createImageBitmap = null;
$requestAnimationFrame = null;
}
}
const PLATFORM = new Platform();
export { PLATFORM, $window, $document, $XMLHttpRequest, $atob, $OffscreenCanvas, $HTMLCanvasElement, $requestAnimationFrame, $Blob, $URL, $createImageBitmap };
由于 tfjs 是提前就做了平台化的计划,所以从源码上就平台化了,但是 THREE 并没有,所以需要从构建入手。实现平台依赖转发,比如源码的window对象需要指向平台的window。
实现平台依赖转发
经过多查阅,发现@rollup/plugin-inject
能十分轻松实现依赖转发,这里是把平台有关的变量转发到Platform
的导出
import path from 'path';
import inject from '@rollup/plugin-inject';
export const platformVariables = [
'URL',
'atob',
'Blob',
'window',
'document',
'XMLHttpRequest',
'OffscreenCanvas',
'HTMLCanvasElement',
'createImageBitmap',
'requestAnimationFrame',
];
export function platformize(
list = platformVariables,
platformPath = path
.resolve(__dirname, '../src/Platform')
.replaceAll('\\', '\\\\'),
) {
return inject({
exclude: /src\/platforms/, // 平台自定义代码无需转发
'self.URL': [platformPath, '$URL'],
...list.reduce((acc, curr) => {
acc[curr] = [platformPath, `$${curr}`];
return acc;
}, {}),
});
}
编写平台比如WechatPlatform
里面可以参考微信官方适配器,同理适配淘宝小程序时候,只需编写TaobaoPlatform
即可
import URL from '../libs/URL'
import Blob from '../libs/Blob'
import atob from '../libs/atob'
import EventTarget from '../libs/EventTarget'
import XMLHttpRequest from './XMLHttpRequest'
import copyProperties from '../libs/copyProperties'
function OffscreenCanvas() {
return wx.createOffscreenCanvas()
}
export class WechatPlatform {
constructor( canvas ) {
const systemInfo = wx.getSystemInfoSync()
this.canvas = canvas;
this.document = {
createElementNS( _, type ) {
if (type === 'canvas') return canvas;
if (type === 'img') return canvas.createImage();
}
};
this.window = {
innerWidth: systemInfo.windowWidth,
innerHeight: systemInfo.windowHeight,
devicePixelRatio: systemInfo.pixelRatio,
AudioContext: function() {},
URL: new URL(),
requestAnimationFrame: this.canvas.requestAnimationFrame,
};
[this.canvas, this.document, this.window].forEach(i => {
copyProperties(i.constructor.prototype, EventTarget.prototype)
});
this.patchCanvas();
}
patchCanvas() {
Object.defineProperty(this.canvas, 'style', {
get() {
return {
width: this.width + 'px',
height: this.height + 'px'
}
}
})
Object.defineProperty(this.canvas, 'clientHeight', {
get() { return this.height }
})
Object.defineProperty(this.canvas, 'clientWidth', {
get() { return this.width }
})
}
getGlobals() {
return {
atob: atob,
Blob: Blob,
window: this.window,
document: this.document,
HTMLCanvasElement: undefined,
XMLHttpRequest: XMLHttpRequest,
requestAnimationFrame: this.canvas.requestAnimationFrame,
OffscreenCanvas: OffscreenCanvas,
createImageBitmap: undefined,
}
}
dispose() {
this.document = null;
this.window = null;
this.canvas = null;
}
}
实现支持类型提示
Platform.d.ts
export class Platform {
set(platform: any): void;
dispose(): void;
}
export const PLATFORM: Platform;
export let $atob: any;
export let $window: any;
export let $document: any;
export let $XMLHttpRequest: any;
export let $OffscreenCanvas: any;
export let $HTMLCanvasElement: any;
export let $createImageBitmap: any;
export let $requestAnimationFrame: any;
ThreePlatformize.d.ts
export * from 'three'
export * from './Platform'
没错,就是如此的简单
支持tree shaking
package.json
设置 sideEffects 为 false
{
...
"sideEffects": false,
...
}
支持THREE的生态
目前是指 three 包下面的examples/jsm/\*\*/\*.js
,依然是通过构建支持
import path from 'path';
import copy from 'rollup-plugin-copy';
import * as fastGlob from 'fast-glob';
import { platformVariables, platformize } from './platfromize';
const ThreeOrigin = path.resolve(__dirname, '../three/build/three.module.js');
export default fastGlob.sync('three/examples/jsm/**/*.js').map(input => {
return {
input,
output: {
format: 'esm',
file: input.replace('three/', ''),
},
external: () => true,
plugins: [
platformize(platformVariables, ThreeOrigin),
copy({
targets: [
{
src: input.replace('.js', '.d.ts'),
dest: path.dirname(input.replace('three/', '')),
},
],
}),
],
};
});
依赖 three 包的npm 包如果是平台无关的话,只需要通过 alias
指向平台化后的 three 即可。若平台相关的,则仍需编写插件支持,可类比上面rollup插件platformize
。
成果
所以three-platfromize
的项目诞生了。目前已实现微信小程序和淘宝小程序平台的适配。
点击查看微信小程序DEMO
点击查看淘宝小程序DEMO
后续会适配更多小程序平台,让3D开发变得更加优雅。
demo的动图实现是通过three-sprite-player
实现,能避免微信小程序纹理大小限制,也欢迎大家品尝。