资源加载

2017年6月29日,作者:Eric Traut


我们收到了关于如何处理资产(图像、视频、声音)的问题,以使其适用于 React Native 和 React JS(Web)。

指定资产位置

在 Web 上,资产仅通过 URL 引用,并由浏览器异步加载。

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

React Native 应用程序通常将资产打包到应用程序包中,因此它们是从本地设备存储加载的。在这种情况下,路径以相对文件系统路径的形式指定。但是,您需要通过调用“require”来调用 React Native 打包器,而不是直接传递路径。

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

打包器要求资产路径被指定为字符串字面量。换句话说,它不能在运行时构建或由辅助方法返回。有关此限制的更多详细信息,请参阅 React Native 文档

这使得编写在 Web 和原生平台上运行的跨平台代码变得困难。以下是我们在 Skype 应用程序中解决此问题的方法。

AppAssets 模块

我们创建了一个“AppAssets”接口,其中包含应用程序中每个资产的访问器方法。

// File: AppAssets.d.ts

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

然后,我们针对 Web 和原生平台实现了此接口。

// 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();

上面的代码有几点值得注意。首先,我们使用了一个接口来确保 Web 和原生实现保持同步。如果您忘记将资产添加到这两个文件,TypeScript 编译器将在构建时检测到错误。

其次,Web 实现使用辅助方法 getImagePath 来构建完整的 URL。它使用动态可配置的域名构建此 URL,允许我们将应用程序部署到测试 Web 服务器或发布到生产服务器。

第三,原生实现使用了访问器。这会将资产的加载延迟到首次访问时。如果没有这个技巧,所有资产将在 AppAssetsNative 模块初始化时加载,从而增加了应用程序启动时间。

现在我们可以以跨平台的方式引用资产。

import AppAssets = require('AppAssets');

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

别名

现在我们有了两种实现(一种用于 Web,另一种用于原生),我们如何根据我们正在构建的平台“链接”正确的版本?我们通过构建过程中的轻量级“别名”步骤来完成此操作。此步骤根据正在构建的平台将 require('AppAssets') 替换为 require('./ts/AppAssetsWeb')require('./ts/AppAssetsNative')

我将提供 gulp 语法示例,但相同的技术可用于 grunt 或其他任务脚本运行时。

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);
});