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

成果

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的微信小程序插件里有)

image

然后在一个Environment全局单例实现平台的切换逻辑。

image

实现

所以需要一个全局的单例实现平台依赖的转发切换(模仿 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实现,能避免微信小程序纹理大小限制,也欢迎大家品尝。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,670评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,928评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,926评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,238评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,112评论 4 356
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,138评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,545评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,232评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,496评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,596评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,369评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,226评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,600评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,906评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,185评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,516评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,721评论 2 335

推荐阅读更多精彩内容