首页 文章

将React Native集成到Xamarin项目中

提问于
浏览
6

我的任务是查看是否可以将React Native集成到Xamarin.Forms项目中 .
我认为我已经非常接近实现这一目标了,但是我能够意识到这是一个奇怪的/向后的解决方案,但我还是想了解它,看看我是否可以击败它......

Intro
我的雇主想要看看是否可以使用React Native for UI并使用C#作为业务逻辑 . 它正在作为一种解决方案进行探索,以便UI / UX团队可以与RN合作,我们(开发团队)可以将逻辑链接到它 .

What I've tried so far
我拿出了React Native输出的Xcode项目,并通过cd'ing终端删除本地Node服务的依赖性到项目目录并运行 react-native bundle --entry-file index.ios.js --platform ios --dev false --bundle-output ios/main.jsbundle --assets-dest ios (取自this博客文章) . 然后我更改了 AppDelegate 行,它正在寻找main.jsbundle文件 .
然后我添加了一个静态库作为项目的目标 . 与应用程序的构建阶段相比,我添加了所有相同的链接库
enter image description here

在此之后,我创建了一个Xamarin.Forms解决方案 . 由于我只创建了iOS库,因此我创建了一个iOS.Binding项目 . 我添加了Xcode .a lib作为本机引用 . 在 ApiDefinition.cs 文件中,我使用以下代码创建了接口

BaseType(typeof(NSObject))]
    interface TheViewController
    {
        [Export("setMainViewController:")]
        void SetTheMainViewController(UIViewController viewController);
    }

在Xcode项目中,创建了一个 TheViewController 类 . setMainViewController: 以下列方式实施:

-(void)setMainViewController:(UIViewController *)viewController{

  AppDelegate * ad = (AppDelegate*)[UIApplication sharedApplication].delegate;

  NSURL * jsCodeLocation = [NSURL fileURLWithPath:[[NSBundle mainBundle]pathForResource:@"main" ofType:@"jsbundle"]];

  RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
                                                      moduleName:@"prototyper"
                                               initialProperties:nil
                                                   launchOptions:ad.savedLaunchOptions];
  rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1];

  ad.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
  viewController.view = rootView;
  ad.window.rootViewController = viewController;
  [ad.window makeKeyAndVisible];
}

我有效地尝试从Xamarin传入一个 UIViewController 来为React Native添加自己的东西 .
我用以下方式从Xamarin.iOS中调用它:

private Binding.TheViewController _theViewController;

        public override void ViewDidLoad()
        {
            base.ViewDidLoad();
            _theViewController = new TheViewController();
            _theViewController.SetTheMainViewController(this);
        }

这个类正在实现 PageRenderer ,覆盖Xamarin.Forms' ContentPage 使用

[assembly:ExportRenderer(typeof(RNTest.MainViewController), typeof(RNTest.iOS.MainViewController))]

好了,毕竟,我去了部署到设备,并且,预计会遇到一些错误 . AOT编译器进入我的lib并尝试做它的魔法并在React Native项目中抛出许多链接器错误,如下所示 .

enter image description here

Pastebin dump of full Build Output

我打算在绑定中设置更多方法来设置回调等,以开始构建一些关于使用Objective-C来回传递信息的功能,我将通过一些本机代码链接传递给React Native .

Summary
我知道's pretty long breathed, but if we can get this off the ground, then we can basically do all of our (fairly comlex) business logic in C# and leave all the UI changes to the dedicated UI team, who have a strong preference for React Native (fair enough, with their prototype being in pretty good condition). Really, it'只是我为我们的应用程序的下一个主要版本组装的另一个POC .
如果有人能想到更好的方法,我全都听见了 . 当然,我已经对一些细节进行了釉面处理,所以如果有任何需要澄清的话,请问我,我会推荐 .
非常感谢 .
卢克

2 回答

  • 1

    我能够使用以下步骤实现这一目标 . 这里有很多,所以如果我错过了一个细节,请原谅我 .

    构建静态库

    • 在Xcode中创建 Cocoa Touch Static Library 项目 .

    • 在同一目录中安装React Native .

    npm install react-native
    
    • 将所有React Xcode项目添加到项目中 . (Screenshot)您可以查看现有React Native应用程序的.pbxproj文件,以获取有关如何查找所有这些内容的线索 .

    • 将React添加到 Target Dependencies 构建阶段 . (Screenshot

    • Link Binary With Libraries 构建阶段包含所有React目标 . (Screenshot

    • 请务必在 Other Linker Flags 构建设置中包含 -lc++ .

    • 使用lipo创建通用库(胖文件) . 请参阅Xamarin documentation中的 Building Universal Native Libraries 部分 .

    创建一个Xamarin应用程序

    • 在Visual Studio中创建一个新的iOS Single View App 项目/解决方案 . (Screenshot

    • 将iOS Bindings Library 项目添加到解决方案中 . (Screenshot

    • 将通用静态库添加为绑定库项目的本机引用 .

    • 在本机引用的属性中将 Frameworks 设置为 JavaScriptCore 并将 Linker Flags 设置为 -lstdc++ . 这修复了原始问题中提到的链接器错误 . 同时启用 Force Load . (Screenshot

    • 将以下代码添加到ApiDefinition.cs . 请务必为 SystemFoundationUIKit 添加 using 语句 .

    // @interface RCTBundleURLProvider : NSObject
    [BaseType(typeof(NSObject))]
    interface RCTBundleURLProvider
    {
        // +(instancetype)sharedSettings;
        [Static]
        [Export("sharedSettings")]
        RCTBundleURLProvider SharedSettings();
    
        // -(NSURL *)jsBundleURLForBundleRoot:(NSString *)bundleRoot fallbackResource:(NSString *)resourceName;
        [Export("jsBundleURLForBundleRoot:fallbackResource:")]
        NSUrl JsBundleURLForBundleRoot(string bundleRoot, [NullAllowed] string resourceName);
    }
    
    // @interface RCTRootView : UIView
    [BaseType(typeof(UIView))]
    interface RCTRootView
    {
        // -(instancetype)initWithBundleURL:(NSURL *)bundleURL moduleName:(NSString *)moduleName initialProperties:(NSDictionary *)initialProperties launchOptions:(NSDictionary *)launchOptions;
        [Export("initWithBundleURL:moduleName:initialProperties:launchOptions:")]
        IntPtr Constructor(NSUrl bundleURL, string moduleName, [NullAllowed] NSDictionary initialProperties, [NullAllowed] NSDictionary launchOptions);
    }
    
    // @protocol RCTBridgeModule <NSObject>
    [Protocol, Model]
    [BaseType(typeof(NSObject))]
    interface RCTBridgeModule
    {
    
    }
    
    • 将以下代码添加到Structs.cs . 请务必为 SystemSystem.Runtime.InteropServicesFoundation 添加 using 语句 .
    [StructLayout(LayoutKind.Sequential)]
    public struct RCTMethodInfo
    {
        public string jsName;
        public string objcName;
        public bool isSync;
    }
    
    public static class CFunctions
    {
        [DllImport ("__Internal")]
        public static extern void RCTRegisterModule(IntPtr module);
    }
    
    • 在应用程序项目中添加对bindings库项目的引用 .

    • 将以下代码添加到AppDelegate.cs中的 FinishedLaunching 方法 . 不要忘记为绑定库的命名空间添加 using 语句,并指定React Native应用程序的名称 .

    var jsCodeLocation = RCTBundleURLProvider.SharedSettings().JsBundleURLForBundleRoot("index", null);
    var rootView = new RCTRootView(jsCodeLocation, "<Name of your React app>", null, launchOptions);
    
    Window = new UIWindow(UIScreen.MainScreen.Bounds);
    Window.RootViewController = new UIViewController() { View = rootView };
    Window.MakeKeyAndVisible();
    
    • 将以下内容添加到Info.plist .
    <key>UIViewControllerBasedStatusBarAppearance</key>
    <false/>
    <key>NSAppTransportSecurity</key>
    <dict>
        <key>NSExceptionDomains</key>
        <dict>
            <key>localhost</key>
            <dict>
                <key>NSTemporaryExceptionAllowsInsecureHTTPLoads</key>
                <true/>
            </dict>
        </dict>
    </dict>
    

    此时,您应该能够通过在相应目录中启动React Packager( react-native start )来运行任何React Native应用程序 . 以下部分将向您展示如何从React Native调用C# .

    创建本机模块

    • 在iOS应用项目中添加课程 .

    • 让类继承 RCTBridgeModule (来自绑定库) .

    public class TestClass : RCTBridgeModule
    
    • ModuleName 方法添加到您的 class . 将返回的值更改为您希望在JavaScript中调用该类的任何值 . 您可以指定空字符串来使用原版的 .
    [Export("moduleName")]
    public static string ModuleName() => "TestClass";
    
    • RequiresMainQueueSetup 方法添加到您的 class . 我认为如果您实现本机(UI)组件,则需要返回 true .
    [Export("requiresMainQueueSetup")]
    public static bool RequiresMainQueueSetup() => false;
    
    • 编写要导出的方法(从JavaScript调用) . 这是一个例子 .
    [Export("test:")]
    public void Test(string msg) => Debug.WriteLine(msg);
    
    • 对于导出的每个方法,请编写一个返回有关它的信息的其他方法 . 每个方法的名称都需要以 __rct_export__ 开头 . 只要它是唯一的,名称的其余部分无关紧要 . 我能找到让它工作的唯一方法是返回一个 IntPtr 而不是 RCTMethodInfo . 以下是一个例子 .
    [Export("__rct_export__test")]
    public static IntPtr TestExport()
    {
        var temp = new RCTMethodInfo()
        {
            jsName = string.Empty,
            objcName = "test: (NSString*) msg",
            isSync = false
        };
        var ptr = Marshal.AllocHGlobal(Marshal.SizeOf(temp));
    
        Marshal.StructureToPtr(temp, ptr, false);
    
        return ptr;
    }
    
    • jsName 是您要从JavaScript调用方法的名称 . 您可以指定空字符串以使用原始字符串 .

    • objcName 是该方法的等效Objective-C签名 .

    • 我不确定 isSync 是什么 .

    • 在AppDelegate.cs中启动视图之前注册您的类 . 类的名称将是带有下划线而不是点的完全限定名称 . 这是一个例子 .

    CFunctions.RCTRegisterModule(ObjCRuntime.Class.GetHandle("ReactTest_TestClass"));
    

    从JavaScript调用您的本机模块

    • NativeModules 导入JavaScript文件 .
    import { NativeModules } from 'react-native';
    
    • 调用您导出的方法之一 .
    NativeModules.TestClass.test('C# called successfully.');
    
  • 8

    以下是创建胖静态库和Xamarin Binding项目的完全自动化脚本,以将其包装到Xamarin Binding _745002中:

    https://github.com/voydz/xamarin-react-native

相关问题