Asset-Laden

29. Juni 2017 von Eric Traut


Wir haben Fragen dazu erhalten, wie wir Assets (Bilder, Videos, Sounds) so handhaben, dass sie sowohl für React Native als auch für React JS (Web) funktionieren.

Angabe von Asset-Speicherorten

Im Web werden Assets einfach per URL referenziert und vom Browser asynchron geladen.

<RX.Image source={ 'https://mydomain.com/images/appLogoSmall.jpg' }/>

React Native-Apps bündeln typischerweise Assets im App-Bundle, sodass sie aus dem lokalen Gerätespeicher geladen werden. In diesem Fall wird der Pfad in Form eines relativen Dateisystempfads angegeben. Anstatt den Pfad direkt zu übergeben, müssen Sie jedoch den React Native Packager aufrufen, indem Sie "require" aufrufen.

<RX.Image source={ require('./images/appLogoSmall.jpg') }/>

Der Packager verlangt, dass der Asset-Pfad als String-Literal angegeben wird. Mit anderen Worten, er kann nicht zur Laufzeit konstruiert oder von einer Hilfsmethode zurückgegeben werden. Weitere Details zu dieser Einschränkung finden Sie in der React Native-Dokumentation.

Dies erschwert das Schreiben plattformübergreifenden Codes, der sowohl auf Web- als auch auf nativen Plattformen läuft. Hier ist, wie wir dieses Problem in der Skype-App gelöst haben.

AppAssets-Modul

Wir haben eine "AppAssets"-Schnittstelle erstellt, die eine Accessor-Methode für jedes Asset in unserer App enthält.

// File: AppAssets.d.ts

declare module 'AppAssets' {
    interface IAppAssets {
        appLogoSmall: string;
        appLogoLarge: string;
        notificationIcon: string;
        // ... etc.
    }
    const Assets: IAppAssets;
}

Anschließend haben wir diese Schnittstelle für Web- und native Plattformen implementiert.

// File: AppAssetsWeb.ts

import AppAssets = require('AppAssets');
import AppConfig = require('./AppConfig');

class AppAssetsImpl implements AppAssets.IAppAssets {
    appLogoSmall = AppConfig.getImagePath('skypeLogoSmall.png');
    appLogoLarge = AppConfig.getImagePath('skypeLogoLarge.png');
    notificationIcon = AppConfig.getImagePath('notificationIcon.gif');
    // ... etc.
}

export const Assets: AppAssets.IAppAssets = new AppAssetsImpl();
// File: AppAssetsNative.ts

import AppAssets = require('AppAssets');

class AppAssetsImpl implements IAppAssets.Collection {
    get appLogoSmall() { return require('..images/skypeLogoSmall.png'); }
    get appLogoLarge() { return require('..images/skypeLogoLarge.png'); }
    get notificationIcon() { return require('../images/notificationIcon.gif'); }
    // ... etc.
}

export const Assets: AppAssets.IAppAssets = new AppAssetsImpl();

Es gibt ein paar bemerkenswerte Punkte im obigen Code. Erstens verwenden wir eine Schnittstelle, um sicherzustellen, dass die Web- und nativen Implementierungen synchron bleiben. Wenn Sie vergessen, ein Asset in beiden Dateien hinzuzufügen, erkennt der TypeScript-Compiler den Fehler zur Build-Zeit.

Zweitens verwendet die Web-Implementierung eine Hilfsmethode getImagePath, um die vollständige URL zu erstellen. Diese wird mithilfe eines dynamisch konfigurierbaren Domainnamens erstellt, wodurch wir die App auf einem Test-Webserver bereitstellen oder auf dem Produktionsserver veröffentlichen können.

Drittens verwendet die native Implementierung Accessors. Dies verzögert das Laden des Assets bis zum ersten Zugriff. Ohne diesen Trick würden alle Assets beim Initialisieren des AppAssetsNative-Moduls geladen, was die Startzeit der App erhöht.

Jetzt können wir die Assets plattformübergreifend referenzieren.

import AppAssets = require('AppAssets');

<RX.Image source={ AppAssets.Assets.appLogoSmall }/>

Aliasing

Da wir nun zwei Implementierungen haben (eine für das Web und eine zweite für native), wie "verknüpfen" wir die richtige Version basierend auf der zu erstellenden Plattform? Dies geschieht durch einen leichten "Aliasing"-Schritt in unserem Build-Prozess. Dieser Schritt ersetzt require('AppAssets') durch entweder require('./ts/AppAssetsWeb') oder require('./ts/AppAssetsNative'), je nach der zu erstellenden Plattform.

Ich werde Beispiele in Gulp-Syntax bereitstellen, aber dieselbe Technik kann in Grunt oder anderen Task-Scripting-Runtimes verwendet werden.

var config = {
    aliasify: {
        src: './.temp/' + argv.platform,
        dest: getBuildPath() + 'js/',
        aliases: (argv.platform === 'web') ?
        // Web Aliases
        {
            'AppAssets': './ts/AppAssetsWeb'
        } :
        // Native Aliases
        {
            'AppAssets': './ts/AppAssetsNative'
        }
    }
}

function aliasify(aliases) {
    var reqPattern = new RegExp(/require\(['"]([^'"]+)['"]\)/g);

    // For all files in the stream, apply the replacement.
    return eventStream.map(function(file, done) {
        if (!file.isNull()) {
            var fileContent = file.contents.toString();
            if (reqPattern.test(fileContent)) {
                file.contents = new Buffer(fileContent.replace(reqPattern, function(req, oldPath) {
                    if (!aliases[oldPath]) {
                        return req;
                    }

                    return "require('" + aliases[oldPath] + "')";
                }));
            }
        }

        done(null, file);
    });
}

gulp.task('apply-aliases', function() {
    return gulp.src(path.join(config.aliasify.src, '**/*.js'))
        .pipe(aliasify(config.aliasify.aliases))
        .pipe(gulp.dest(config.aliasify.dest))
        .on('error', handleError);
});

// Here's our full build task pipeline. I haven't provided the task
// definitions for all of these stages, but you can see where the
// 'apply-aliases' task fits into the pipeline.
gulp.task('run', function(callback) {
    runSequence('clean', 'build', 'apply-aliases', 'watch', 'lint', callback);
});