如何在小程序下优雅地使用 THREE.JS ?

3,603

成果

three-platfromize目前已实现 微信小程序 和 淘宝小程序 的适配,均可加载GLB文件

微信小程序

点击查看微信小程序DEMO

起因

估计尝试过在小程序使用 THREE 的朋友估计都体会到微信官方推出的适配版的难用,直接让开发体验回到解放前。

  1. 无类型提示,对于three新手不友好,对于习惯了代码即文档的也不友好。
  2. 无法顺畅使用 three.js 生态的 npm 包,需要手动 scope 注入 three 的依赖。
  3. 无 tree shaking,在只有 2mb 容量的包大小限制下,three 是个庞然大物。
  4. 没有平台相关资源释放接口(即VSCode里面的Disposable模式)。
  5. 每个小程序平台需要单独适配一个,定制 three 的维护成本高。

所以有了 THREE 平台化的想法。既然微信官方的适配版这么多不能,所以平台化的目标就是把“”去掉。

目标

  1. 支持TS类型提示,能方便查阅API文档(d.ts)
  2. 可以通过构建修改方便使用 three 生态 npm 包,无需手动 scope,比如 GLTFLoader
  3. 支持 tree shaking 能减少多点就少点,加个 tfjs 就更加头大。
  4. 有资源释放 dispose 接口。
  5. 支持方便动态注入多个小程序平台的平台接口实现适配器,多 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 设置 sideEffectsfalse

{
    ...
    "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实现,能避免微信小程序纹理大小限制,也欢迎大家品尝。