简体   繁体   中英

ES6 Import inside of Android Webview - not a constructor error

I am rendering an html webpage inside of a WebView which uses three.js to render a 3D model. Thus far all of that is working fine. I am now attempting to add touch/drag controls to move the camera around. I found some example code to make this work and I've implemented it. Here are the relevant portions code:

import * as tc from './TrackballControls.js';
//....
controls = new tc.TrackballControls( camera , renderer.domElement);
controls.rotateSpeed = 1.0;
controls.zoomSpeed = 1.2;
controls.panSpeed = 0.8;

On PC in Crome and Firefox everything works perfectly, my model renders and dragging does change the camera view. In Chrome and Firefox on my Android device loading the page via local network everything also works fine.

However in the WebView within my application I am getting this error:

Uncaught TypeError: tc.TrackballControls is not a constructor

It seems that creating the TrackballControls object is failing. Why does it fail in this way inside of the WebView and not in Chrome on the same phone?

This is my html app directory structure:

在此处输入图片说明

I have edited three.module.js and TrackballControls.js to account for them being in the same directory like this:

// in index.html js:
import * as THREE from './three.module.js';
import * as tc from './TrackballControls.js';    

// in TrackballControls.js:
import {
  EventDispatcher,
  MOUSE,
  Quaternion,
  Vector2,
  Vector3
} from "./three.module.js";

Is there anything I can alter in the html/javascript or Android java code to make it work correctly inside the WebView?

I have tried on 2 different devices, their webview userAgent strings are:

"Mozilla/5.0 (Linux; Android 6.0.1; S60 Build/MMB29M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/79.0.3945.93 Mobile Safari/537.36", source: https://appassets.androidplatform.net/assets/www/cpb_3d_model_wgt/index.html (27)

and

"Mozilla/5.0 (Linux; Android 8.1.0; LM-V405 Build/OPM1.171019.026; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/79.0.3945.93 Mobile Safari/537.36", source: https://appassets.androidplatform.net/assets/www/cpb_3d_model_wgt/index.html (27)

EDIT a bit more info: I was trying to serve my page out of the Assets directory using this:

final WebViewAssetLoader assetLoader = new WebViewAssetLoader.Builder()
            .addPathHandler("/assets/", new WebViewAssetLoader.AssetsPathHandler(this))
            .addPathHandler("/res/", new WebViewAssetLoader.ResourcesPathHandler(this))
            .build();

    wv.setWebViewClient(new WebViewClient() {
        @Override
        public WebResourceResponse shouldInterceptRequest(WebView view,
                                                          WebResourceRequest request) {
           if (!request.isForMainFrame() && request.getUrl().getPath().endsWith(".js")) {
                Log.d(TAG, " js file request need to set mime/type " + request.getUrl().getPath());
               try {
                   return new WebResourceResponse("application/javascript", null, new BufferedInputStream(view.getContext().getAssets().open("www/cpb_3d_model_wgt/three.module.js")));
               } catch (IOException e) {
                   e.printStackTrace();
               }
            }
            return assetLoader.shouldInterceptRequest(request.getUrl());
        }

        @RequiresApi(api = Build.VERSION_CODES.M)
        @Override
        public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {
            super.onReceivedError(view, request, error);
            //Log.d(TAG, "error: " + error.getDescription());
            Log.d(TAG, "error: " + request.getUrl());
        }
    });

    wv.loadUrl("https://appassets.androidplatform.net/assets/www/cpb_3d_model_wgt/index.html");

With my page served this way I get the not a constructor error. I've used chrome remote device inspector to look at the network request for the TrackballControl.js file and I see this:

响应头截图

and when the same page is served from my PC using python web server everything works fine (no errors and all functionality works) these are the response headers:

成功响应头截图

As far as I can tell something about the response or headers is causing some issue with importing which is making the code fail because the imported constructor function isn't getting to exist properly.

Once I noticed this difference in the response headers I ditched the WebViewAssetLoader and starting serving my page with NanoHTTPD and when it's served that way everything works as expected.

Another EDIT: Looking back at my Asset Loader / intercept request code now I can see that I have hard coded the "wrong" js file to get returned for every request that ends with .js which is very likely the reason for the "not a constructor error" My mistake, and I should have included that code when I originally posted the question but I overlooked it.

TrackballControls.js wasn't written as a module that you can import. Instead, it assumes the THREE variable is globally available, and it appends it to THREE . To fix this, you might need to copy-and paste its source code into your own file (let's call it TC.js ) and make some alterations at the beginning and at the end:

TC.js:

// 1. Import THREE instead of assuming it's globally available
import * as THREE from './three.module.js';

// 2a. Remove "THREE." and make it a stand-alone variable
var TrackballControls = function ( object, domElement ) {

    // ... All 600+ lines of internal content stays the same

};

// 2a. Remove "THREE." at the bottom of the file as well
TrackballControls.prototype = Object.create( THREE.EventDispatcher.prototype );
TrackballControls.prototype.constructor = TrackballControls;

// 3. Export the object to be used in other files
export default TrackballControls;

Now you should be able to use your own TC.js file in your project:

main.js:

import * as THREE from './three.module.js';
import TrackballControls from 'TC.js';

const controls = new TrackballControls(camera , renderer.domElement);

I made an example below to demonstrate how copy-pasting TrackballControls can still work by making it a stand-alone variable The top 600+ lines are literally copy-pasted, with the modifications I mentioned above. The rest of the scene setup is at the end of the JS:

 // In your code, you'd import THREE here, instead of via <script> tag // import * as THREE from 'three'; const TrackballControls = function ( object, domElement ) { if ( domElement === undefined ) console.warn( 'THREE.TrackballControls: The second parameter "domElement" is now mandatory.' ); if ( domElement === document ) console.error( 'THREE.TrackballControls: "document" should not be used as the target "domElement". Please use "renderer.domElement" instead.' ); var _this = this; var STATE = { NONE: - 1, ROTATE: 0, ZOOM: 1, PAN: 2, TOUCH_ROTATE: 3, TOUCH_ZOOM_PAN: 4 }; this.object = object; this.domElement = domElement; // API this.enabled = true; this.screen = { left: 0, top: 0, width: 0, height: 0 }; this.rotateSpeed = 1.0; this.zoomSpeed = 1.2; this.panSpeed = 0.3; this.noRotate = false; this.noZoom = false; this.noPan = false; this.staticMoving = false; this.dynamicDampingFactor = 0.2; this.minDistance = 0; this.maxDistance = Infinity; this.keys = [ 65 /*A*/, 83 /*S*/, 68 /*D*/ ]; this.mouseButtons = { LEFT: THREE.MOUSE.ROTATE, MIDDLE: THREE.MOUSE.ZOOM, RIGHT: THREE.MOUSE.PAN }; // internals this.target = new THREE.Vector3(); var EPS = 0.000001; var lastPosition = new THREE.Vector3(); var lastZoom = 1; var _state = STATE.NONE, _keyState = STATE.NONE, _eye = new THREE.Vector3(), _movePrev = new THREE.Vector2(), _moveCurr = new THREE.Vector2(), _lastAxis = new THREE.Vector3(), _lastAngle = 0, _zoomStart = new THREE.Vector2(), _zoomEnd = new THREE.Vector2(), _touchZoomDistanceStart = 0, _touchZoomDistanceEnd = 0, _panStart = new THREE.Vector2(), _panEnd = new THREE.Vector2(); // for reset this.target0 = this.target.clone(); this.position0 = this.object.position.clone(); this.up0 = this.object.up.clone(); this.zoom0 = this.object.zoom; // events var changeEvent = { type: 'change' }; var startEvent = { type: 'start' }; var endEvent = { type: 'end' }; // methods this.handleResize = function () { var box = this.domElement.getBoundingClientRect(); // adjustments come from similar code in the jquery offset() function var d = this.domElement.ownerDocument.documentElement; this.screen.left = box.left + window.pageXOffset - d.clientLeft; this.screen.top = box.top + window.pageYOffset - d.clientTop; this.screen.width = box.width; this.screen.height = box.height; }; var getMouseOnScreen = ( function () { var vector = new THREE.Vector2(); return function getMouseOnScreen( pageX, pageY ) { vector.set( ( pageX - _this.screen.left ) / _this.screen.width, ( pageY - _this.screen.top ) / _this.screen.height ); return vector; }; }() ); var getMouseOnCircle = ( function () { var vector = new THREE.Vector2(); return function getMouseOnCircle( pageX, pageY ) { vector.set( ( ( pageX - _this.screen.width * 0.5 - _this.screen.left ) / ( _this.screen.width * 0.5 ) ), ( ( _this.screen.height + 2 * ( _this.screen.top - pageY ) ) / _this.screen.width ) // screen.width intentional ); return vector; }; }() ); this.rotateCamera = ( function () { var axis = new THREE.Vector3(), quaternion = new THREE.Quaternion(), eyeDirection = new THREE.Vector3(), objectUpDirection = new THREE.Vector3(), objectSidewaysDirection = new THREE.Vector3(), moveDirection = new THREE.Vector3(), angle; return function rotateCamera() { moveDirection.set( _moveCurr.x - _movePrev.x, _moveCurr.y - _movePrev.y, 0 ); angle = moveDirection.length(); if ( angle ) { _eye.copy( _this.object.position ).sub( _this.target ); eyeDirection.copy( _eye ).normalize(); objectUpDirection.copy( _this.object.up ).normalize(); objectSidewaysDirection.crossVectors( objectUpDirection, eyeDirection ).normalize(); objectUpDirection.setLength( _moveCurr.y - _movePrev.y ); objectSidewaysDirection.setLength( _moveCurr.x - _movePrev.x ); moveDirection.copy( objectUpDirection.add( objectSidewaysDirection ) ); axis.crossVectors( moveDirection, _eye ).normalize(); angle *= _this.rotateSpeed; quaternion.setFromAxisAngle( axis, angle ); _eye.applyQuaternion( quaternion ); _this.object.up.applyQuaternion( quaternion ); _lastAxis.copy( axis ); _lastAngle = angle; } else if ( ! _this.staticMoving && _lastAngle ) { _lastAngle *= Math.sqrt( 1.0 - _this.dynamicDampingFactor ); _eye.copy( _this.object.position ).sub( _this.target ); quaternion.setFromAxisAngle( _lastAxis, _lastAngle ); _eye.applyQuaternion( quaternion ); _this.object.up.applyQuaternion( quaternion ); } _movePrev.copy( _moveCurr ); }; }() ); this.zoomCamera = function () { var factor; if ( _state === STATE.TOUCH_ZOOM_PAN ) { factor = _touchZoomDistanceStart / _touchZoomDistanceEnd; _touchZoomDistanceStart = _touchZoomDistanceEnd; if ( _this.object.isPerspectiveCamera ) { _eye.multiplyScalar( factor ); } else if ( _this.object.isOrthographicCamera ) { _this.object.zoom *= factor; _this.object.updateProjectionMatrix(); } else { console.warn( 'THREE.TrackballControls: Unsupported camera type' ); } } else { factor = 1.0 + ( _zoomEnd.y - _zoomStart.y ) * _this.zoomSpeed; if ( factor !== 1.0 && factor > 0.0 ) { if ( _this.object.isPerspectiveCamera ) { _eye.multiplyScalar( factor ); } else if ( _this.object.isOrthographicCamera ) { _this.object.zoom /= factor; _this.object.updateProjectionMatrix(); } else { console.warn( 'THREE.TrackballControls: Unsupported camera type' ); } } if ( _this.staticMoving ) { _zoomStart.copy( _zoomEnd ); } else { _zoomStart.y += ( _zoomEnd.y - _zoomStart.y ) * this.dynamicDampingFactor; } } }; this.panCamera = ( function () { var mouseChange = new THREE.Vector2(), objectUp = new THREE.Vector3(), pan = new THREE.Vector3(); return function panCamera() { mouseChange.copy( _panEnd ).sub( _panStart ); if ( mouseChange.lengthSq() ) { if ( _this.object.isOrthographicCamera ) { var scale_x = ( _this.object.right - _this.object.left ) / _this.object.zoom / _this.domElement.clientWidth; var scale_y = ( _this.object.top - _this.object.bottom ) / _this.object.zoom / _this.domElement.clientWidth; mouseChange.x *= scale_x; mouseChange.y *= scale_y; } mouseChange.multiplyScalar( _eye.length() * _this.panSpeed ); pan.copy( _eye ).cross( _this.object.up ).setLength( mouseChange.x ); pan.add( objectUp.copy( _this.object.up ).setLength( mouseChange.y ) ); _this.object.position.add( pan ); _this.target.add( pan ); if ( _this.staticMoving ) { _panStart.copy( _panEnd ); } else { _panStart.add( mouseChange.subVectors( _panEnd, _panStart ).multiplyScalar( _this.dynamicDampingFactor ) ); } } }; }() ); this.checkDistances = function () { if ( ! _this.noZoom || ! _this.noPan ) { if ( _eye.lengthSq() > _this.maxDistance * _this.maxDistance ) { _this.object.position.addVectors( _this.target, _eye.setLength( _this.maxDistance ) ); _zoomStart.copy( _zoomEnd ); } if ( _eye.lengthSq() < _this.minDistance * _this.minDistance ) { _this.object.position.addVectors( _this.target, _eye.setLength( _this.minDistance ) ); _zoomStart.copy( _zoomEnd ); } } }; this.update = function () { _eye.subVectors( _this.object.position, _this.target ); if ( ! _this.noRotate ) { _this.rotateCamera(); } if ( ! _this.noZoom ) { _this.zoomCamera(); } if ( ! _this.noPan ) { _this.panCamera(); } _this.object.position.addVectors( _this.target, _eye ); if ( _this.object.isPerspectiveCamera ) { _this.checkDistances(); _this.object.lookAt( _this.target ); if ( lastPosition.distanceToSquared( _this.object.position ) > EPS ) { _this.dispatchEvent( changeEvent ); lastPosition.copy( _this.object.position ); } } else if ( _this.object.isOrthographicCamera ) { _this.object.lookAt( _this.target ); if ( lastPosition.distanceToSquared( _this.object.position ) > EPS || lastZoom !== _this.object.zoom ) { _this.dispatchEvent( changeEvent ); lastPosition.copy( _this.object.position ); lastZoom = _this.object.zoom; } } else { console.warn( 'THREE.TrackballControls: Unsupported camera type' ); } }; this.reset = function () { _state = STATE.NONE; _keyState = STATE.NONE; _this.target.copy( _this.target0 ); _this.object.position.copy( _this.position0 ); _this.object.up.copy( _this.up0 ); _this.object.zoom = _this.zoom0; _this.object.updateProjectionMatrix(); _eye.subVectors( _this.object.position, _this.target ); _this.object.lookAt( _this.target ); _this.dispatchEvent( changeEvent ); lastPosition.copy( _this.object.position ); lastZoom = _this.object.zoom; }; // listeners function keydown( event ) { if ( _this.enabled === false ) return; window.removeEventListener( 'keydown', keydown ); if ( _keyState !== STATE.NONE ) { return; } else if ( event.keyCode === _this.keys[ STATE.ROTATE ] && ! _this.noRotate ) { _keyState = STATE.ROTATE; } else if ( event.keyCode === _this.keys[ STATE.ZOOM ] && ! _this.noZoom ) { _keyState = STATE.ZOOM; } else if ( event.keyCode === _this.keys[ STATE.PAN ] && ! _this.noPan ) { _keyState = STATE.PAN; } } function keyup() { if ( _this.enabled === false ) return; _keyState = STATE.NONE; window.addEventListener( 'keydown', keydown, false ); } function mousedown( event ) { if ( _this.enabled === false ) return; event.preventDefault(); event.stopPropagation(); if ( _state === STATE.NONE ) { switch ( event.button ) { case _this.mouseButtons.LEFT: _state = STATE.ROTATE; break; case _this.mouseButtons.MIDDLE: _state = STATE.ZOOM; break; case _this.mouseButtons.RIGHT: _state = STATE.PAN; break; default: _state = STATE.NONE; } } var state = ( _keyState !== STATE.NONE ) ? _keyState : _state; if ( state === STATE.ROTATE && ! _this.noRotate ) { _moveCurr.copy( getMouseOnCircle( event.pageX, event.pageY ) ); _movePrev.copy( _moveCurr ); } else if ( state === STATE.ZOOM && ! _this.noZoom ) { _zoomStart.copy( getMouseOnScreen( event.pageX, event.pageY ) ); _zoomEnd.copy( _zoomStart ); } else if ( state === STATE.PAN && ! _this.noPan ) { _panStart.copy( getMouseOnScreen( event.pageX, event.pageY ) ); _panEnd.copy( _panStart ); } document.addEventListener( 'mousemove', mousemove, false ); document.addEventListener( 'mouseup', mouseup, false ); _this.dispatchEvent( startEvent ); } function mousemove( event ) { if ( _this.enabled === false ) return; event.preventDefault(); event.stopPropagation(); var state = ( _keyState !== STATE.NONE ) ? _keyState : _state; if ( state === STATE.ROTATE && ! _this.noRotate ) { _movePrev.copy( _moveCurr ); _moveCurr.copy( getMouseOnCircle( event.pageX, event.pageY ) ); } else if ( state === STATE.ZOOM && ! _this.noZoom ) { _zoomEnd.copy( getMouseOnScreen( event.pageX, event.pageY ) ); } else if ( state === STATE.PAN && ! _this.noPan ) { _panEnd.copy( getMouseOnScreen( event.pageX, event.pageY ) ); } } function mouseup( event ) { if ( _this.enabled === false ) return; event.preventDefault(); event.stopPropagation(); _state = STATE.NONE; document.removeEventListener( 'mousemove', mousemove ); document.removeEventListener( 'mouseup', mouseup ); _this.dispatchEvent( endEvent ); } function mousewheel( event ) { if ( _this.enabled === false ) return; if ( _this.noZoom === true ) return; event.preventDefault(); event.stopPropagation(); switch ( event.deltaMode ) { case 2: // Zoom in pages _zoomStart.y -= event.deltaY * 0.025; break; case 1: // Zoom in lines _zoomStart.y -= event.deltaY * 0.01; break; default: // undefined, 0, assume pixels _zoomStart.y -= event.deltaY * 0.00025; break; } _this.dispatchEvent( startEvent ); _this.dispatchEvent( endEvent ); } function touchstart( event ) { if ( _this.enabled === false ) return; event.preventDefault(); switch ( event.touches.length ) { case 1: _state = STATE.TOUCH_ROTATE; _moveCurr.copy( getMouseOnCircle( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ) ); _movePrev.copy( _moveCurr ); break; default: // 2 or more _state = STATE.TOUCH_ZOOM_PAN; var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; _touchZoomDistanceEnd = _touchZoomDistanceStart = Math.sqrt( dx * dx + dy * dy ); var x = ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ) / 2; var y = ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ) / 2; _panStart.copy( getMouseOnScreen( x, y ) ); _panEnd.copy( _panStart ); break; } _this.dispatchEvent( startEvent ); } function touchmove( event ) { if ( _this.enabled === false ) return; event.preventDefault(); event.stopPropagation(); switch ( event.touches.length ) { case 1: _movePrev.copy( _moveCurr ); _moveCurr.copy( getMouseOnCircle( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ) ); break; default: // 2 or more var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; _touchZoomDistanceEnd = Math.sqrt( dx * dx + dy * dy ); var x = ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ) / 2; var y = ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ) / 2; _panEnd.copy( getMouseOnScreen( x, y ) ); break; } } function touchend( event ) { if ( _this.enabled === false ) return; switch ( event.touches.length ) { case 0: _state = STATE.NONE; break; case 1: _state = STATE.TOUCH_ROTATE; _moveCurr.copy( getMouseOnCircle( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ) ); _movePrev.copy( _moveCurr ); break; } _this.dispatchEvent( endEvent ); } function contextmenu( event ) { if ( _this.enabled === false ) return; event.preventDefault(); } this.dispose = function () { this.domElement.removeEventListener( 'contextmenu', contextmenu, false ); this.domElement.removeEventListener( 'mousedown', mousedown, false ); this.domElement.removeEventListener( 'wheel', mousewheel, false ); this.domElement.removeEventListener( 'touchstart', touchstart, false ); this.domElement.removeEventListener( 'touchend', touchend, false ); this.domElement.removeEventListener( 'touchmove', touchmove, false ); document.removeEventListener( 'mousemove', mousemove, false ); document.removeEventListener( 'mouseup', mouseup, false ); window.removeEventListener( 'keydown', keydown, false ); window.removeEventListener( 'keyup', keyup, false ); }; this.domElement.addEventListener( 'contextmenu', contextmenu, false ); this.domElement.addEventListener( 'mousedown', mousedown, false ); this.domElement.addEventListener( 'wheel', mousewheel, false ); this.domElement.addEventListener( 'touchstart', touchstart, false ); this.domElement.addEventListener( 'touchend', touchend, false ); this.domElement.addEventListener( 'touchmove', touchmove, false ); window.addEventListener( 'keydown', keydown, false ); window.addEventListener( 'keyup', keyup, false ); this.handleResize(); // force an update at start this.update(); }; TrackballControls.prototype = Object.create( THREE.EventDispatcher.prototype ); TrackballControls.prototype.constructor = TrackballControls; //////////////////////////// END OF TRACKBALL SOURCE CODE //////////////////////////// // Boilerplate Three setup const renderer = new THREE.WebGLRenderer({canvas: document.querySelector("canvas")}); const camera = new THREE.PerspectiveCamera(70, 1, 1, 1000); camera.position.z = 20; const scene = new THREE.Scene(); const geometry = new THREE.TorusBufferGeometry(8, 3, 16, 32); const material = new THREE.MeshNormalMaterial(); const mesh = new THREE.Mesh(geometry, material); scene.add(mesh); // Now we can use the controls as expected const controls = new TrackballControls(camera , renderer.domElement ); controls.rotateSpeed = 1.0; controls.zoomSpeed = 1.2; controls.panSpeed = 0.8; function resize() { var width = renderer.domElement.clientWidth; var height = renderer.domElement.clientHeight; if (renderer.domElement.width !== width || renderer.domElement.height !== height) { renderer.setSize(width, height, false); camera.aspect = width / height; camera.updateProjectionMatrix(); } } function animate(time) { controls.update(); renderer.render(scene, camera); requestAnimationFrame(animate); } resize(); requestAnimationFrame(animate);
 body { margin: 0; } canvas { width: 100vw; height: 100vh; display: block; }
 <canvas></canvas> <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/110/three.min.js"></script>

TrackballControls.js is not an ES6 module, and does not export the TrackballControls constructor function. TrackballControls is attached to THREE . ts is likely just an empty object, and ts.TrackballControls is undefined. Define controls like the following instead.

controls = new THREE.TrackballControls( camera , renderer.domElement);

My answer is based on this TrackballControls.js on Github.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM