如何在小程序下優(yōu)雅的使用THREE.JS

成果

three-platfromize目前已實(shí)現(xiàn) 微信小程序 和 淘寶小程序 的適配,均可加載GLB文件

微信小程序

點(diǎn)擊查看微信小程序DEMO

起因

估計(jì)嘗試過在小程序使用 THREE 的朋友估計(jì)都體會到微信官方推出的適配版的難用,直接讓開發(fā)體驗(yàn)回到解放前。

  1. 無類型提示,對于three新手不友好,對于習(xí)慣了代碼即文檔的也不友好。
  2. 無法順暢使用 three.js 生態(tài)的 npm 包,需要手動 scope 注入 three 的依賴。
  3. 無 tree shaking,在只有 2mb 容量的包大小限制下,three 是個(gè)龐然大物。
  4. 沒有平臺相關(guān)資源釋放接口(即VSCode里面的Disposable模式)。
  5. 每個(gè)小程序平臺需要單獨(dú)適配一個(gè),定制 three 的維護(hù)成本高。

所以有了 THREE 平臺化的想法。既然微信官方的適配版這么多不能,所以平臺化的目標(biāo)就是把“”去掉。

目標(biāo)

  1. 支持TS類型提示,能方便查閱API文檔(d.ts)
  2. 可以通過構(gòu)建修改方便使用 three 生態(tài) npm 包,無需手動 scope,比如 GLTFLoader
  3. 支持 tree shaking 能減少多點(diǎn)就少點(diǎn),加個(gè) tfjs 就更加頭大。
  4. 有資源釋放 dispose 接口。
  5. 支持方便動態(tài)注入多個(gè)小程序平臺的平臺接口實(shí)現(xiàn)適配器,多 backends 。

參考

既然需要多平臺支持,所以需要向平臺化很好的項(xiàng)目學(xué)習(xí)比如 tfjs ,實(shí)現(xiàn)了多個(gè)backend 有 CPU,WebGL,WASM。其運(yùn)行的平臺的適配有PlatformBrowser,PlatformNode,PlatformWechat(tfjs的微信小程序插件里有)

image

然后在一個(gè)Environment全局單例實(shí)現(xiàn)平臺的切換邏輯。

image

實(shí)現(xiàn)

所以需要一個(gè)全局的單例實(shí)現(xiàn)平臺依賴的轉(zhuǎn)發(fā)切換(模仿 THREE 的源碼風(fēng)格)

實(shí)現(xiàn)平臺的全局單例

// 為了避免重新聲明的報(bào)錯(cuò)
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 是提前就做了平臺化的計(jì)劃,所以從源碼上就平臺化了,但是 THREE 并沒有,所以需要從構(gòu)建入手。實(shí)現(xiàn)平臺依賴轉(zhuǎn)發(fā),比如源碼的window對象需要指向平臺的window。

實(shí)現(xiàn)平臺依賴轉(zhuǎn)發(fā)

經(jīng)過多查閱,發(fā)現(xiàn)@rollup/plugin-inject能十分輕松實(shí)現(xiàn)依賴轉(zhuǎn)發(fā),這里是把平臺有關(guān)的變量轉(zhuǎn)發(fā)到Platform的導(dǎo)出

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/, // 平臺自定義代碼無需轉(zhuǎn)發(fā)

    'self.URL': [platformPath, '$URL'],
    ...list.reduce((acc, curr) => {
      acc[curr] = [platformPath, `$${curr}`];
      return acc;
    }, {}),
  });
}

編寫平臺比如WechatPlatform

里面可以參考微信官方適配器,同理適配淘寶小程序時(shí)候,只需編寫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;

  }

}

實(shí)現(xiàn)支持類型提示

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'

沒錯(cuò),就是如此的簡單

支持tree shaking

package.json 設(shè)置 sideEffectsfalse

{
    ...
    "sideEffects": false,
    ...
}

支持THREE的生態(tài)

目前是指 three 包下面的examples/jsm/\*\*/\*.js,依然是通過構(gòu)建支持

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 包如果是平臺無關(guān)的話,只需要通過 alias 指向平臺化后的 three 即可。若平臺相關(guān)的,則仍需編寫插件支持,可類比上面rollup插件platformize。

成果

所以three-platfromize的項(xiàng)目誕生了。目前已實(shí)現(xiàn)微信小程序和淘寶小程序平臺的適配。

微信小程序

點(diǎn)擊查看微信小程序DEMO

淘寶小程序

點(diǎn)擊查看淘寶小程序DEMO

后續(xù)會適配更多小程序平臺,讓3D開發(fā)變得更加優(yōu)雅。

demo的動圖實(shí)現(xiàn)是通過three-sprite-player實(shí)現(xiàn),能避免微信小程序紋理大小限制,也歡迎大家品嘗。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容