Lynx

原生库与 Autolink

Lynx 原生库是一个 npm 包,可以打包元素、原生模块和 Service 中的任意组合,供宿主 Lynx 应用消费。Autolink 是一种集成机制,让宿主应用自动发现 node_modules 中安装的库,并接入它们的原生能力,避免为每个元素、原生模块或 Service 手动写注册代码。

Autolink 覆盖 Android 和 iOS 原生应用接入,也覆盖 Lynxtron 原生库使用的包元数据。它不生成 Web 或 HarmonyOS 的接入代码。

开始前的准备
工具可用性

请使用与应用 Lynx SDK 同一发布渠道的 Autolink 工具。相关 package 和 plugin 名称如下:

  • npm:create-lynx-library@lynx-js/autolink-codegenlynx-autolink-codegen binary)
  • Android:Gradle plugin org.lynxsdk.library-settingsorg.lynxsdk.library-build
  • iOS:Ruby gem cocoapods-lynx-library
  • Lynxtron:create-lynx-library 可以生成 shared native library 包,@lynx-js/lynxtron-dev-plugins 通过 pluginLynxtron() 提供宿主侧加载能力。

如果当前配置的 registry 还无法解析其中某个包,说明你使用的 Lynx SDK 发布版本尚未在该 registry 中包含 Native Autolink。此时请继续使用既有手动原生注册方式,等待匹配版本发布后再接入。

什么是 Lynx 原生库

一个库就是一个 npm 包,可以包含以下能力的任意组合:

  • 元素:Lynx 渲染的自定义原生元素,例如 <x-button>
  • 原生模块:可以在 Lynx 应用代码中调用的、带类型声明的 JavaScript-to-native API。
  • Service:应用级的原生单例,可以被库内的其他能力依赖。

每个库都通过包根目录下的 lynx.lib.json 清单文件声明原生入口。这三类能力相互独立:一个库可以只暴露元素、只暴露原生模块、只暴露 Service,也可以暴露它们的任意组合。

应用侧像安装普通 npm 依赖一样安装库。Android Gradle 插件和 iOS CocoaPods 插件会读取 lynx.lib.json,把原生代码链接进宿主应用;宿主应用本身无需了解每个库具体暴露了哪些能力。Lynxtron 会从已安装包中读取同一份清单,把匹配到的包文件收集到宿主输出目录,并在宿主 bundle 前置生成代码,加载每个已收集包的 ./lynxtron 入口。

使用库

本节面向把一个或多个库集成进宿主 Lynx 应用的应用团队。

宿主应用项目结构

接入 Autolink 前,需要先确保宿主应用有一个可以安装 npm 包的项目根目录,并暴露原生应用的构建入口。典型结构如下:

lynx-app/
├── package.json
├── android/
│   ├── settings.gradle
│   └── app/
│       └── build.gradle
├── ios/
│   └── Podfile
├── shared/
│   ├── CMakeLists.txt
│   └── ...
├── dist/
└── src/
  • package.json 是必需的,用来声明 Autolink 库依赖。
  • Android 接入需要 Gradle settings 文件,例如 settings.gradlesettings.gradle.kts,以及 Android application 的构建文件,例如 app/build.gradleapp/build.gradle.kts
  • iOS 接入需要 CocoaPods 入口,通常是 Podfile。如果团队通过 Bundler 管理 Ruby 依赖,可以在 Gemfile 中维护 cocoapods-lynx-library gem。
  • shared/ 用于宿主应用自有的跨端原生源码和 CMake 构建入口,需要自行编译。
  • Lynxtron 接入需要应用具备宿主侧 Rspack 构建,并通过 pluginLynxtron() 安装和加载原生库;一般消费编译后的 dist/ 产物。

安装依赖后,Autolink 会从已安装 npm 包的包根目录扫描 lynx.lib.jsonpackage-lock.jsonpnpm-lock.yamlyarn.lock 等 lockfile 有助于可复现安装,但不是 Autolink 的必需项。

宿主应用只需要接入一次 Autolink。接入完成后,已安装的库会从 node_modules 中被发现,并在 Lynx 初始化时通过生成的 registry 自动注册。

settings.gradle 中启用 settings plugin,让库的 Android 工程可以通过 lynx.lib.json 被发现并 include 进来:

plugins {
  id 'org.lynxsdk.library-settings'
}

在 Android application 工程中启用 build plugin,让生成的 registry 加入应用源码,并自动把库工程接入为依赖:

plugins {
  id 'com.android.application'
  id 'org.lynxsdk.library-build'
}

Gradle sync/build 后,Autolink 会生成固定的 Android registry 入口并加入应用源码。应用初始化 LynxEnv 时会自动加载该入口,库提供的元素、原生模块和 Service 会按应用全局注册生效;业务侧无需额外编写原生初始化代码。

在 iOS 构建环境中安装 cocoapods-lynx-library gem。然后在应用的 Podfile 中加入 CocoaPods plugin,并调用 use_lynx_library!。执行 pod install 时,插件会加入库的 podspec 和生成的 registry pod:

plugin 'cocoapods-lynx-library'

target 'LynxApp' do
  use_lynx_library!
end

执行 pod install 后,Autolink 会生成 registry pod,并把它接入 Lynx 的初始化流程。应用创建 LynxConfig 或初始化 LynxEnv 时,库提供的元素、原生模块和 Service 会自动注册生效;业务侧无需导入生成文件或编写额外初始化代码。

Lynxtron 为桌面端原生库提供了 Autolink 支持。并不是所有嵌入 Lynx 的桌面宿主都开箱支持 Autolink,这是 Lynxtron 自带的能力。

在 Lynxtron 宿主侧 Rspack 配置中使用 pluginLynxtron()。这个插件默认包含 Lynxtron Autolink:它会扫描已安装依赖中的 lynx.lib.json,把匹配到的原生库包收集到宿主输出目录,并注入前置代码来加载每个已收集包的 ./lynxtron 入口。对于声明了 platforms.lynxtron.path 的包,业务代码直接导入 <package>/lynxtron 时,也会被别名到生成的代理模块,并在运行时解析已收集的包。./lynxtron 入口负责加载当前宿主对应的 .node 产物。

import { pluginLynxtron } from '@lynx-js/lynxtron-dev-plugins/rspack';

export default {
  target: 'electron-main',
  plugins: [
    pluginLynxtron({
      isDev: process.env.NODE_ENV === 'development',
      entry: './dist/desktop',
    }),
  ],
};

entry 是 Lynxtron 宿主自身的 bundle 入口,pluginLynxtron() 会以它为基准放置已收集的原生库包。完整的参数含义请参考 @lynx-js/lynxtron-dev-plugins 包。

Lynxtron 加载原生库后,其中的 Lynx 静态注册会在 Lynx runtime 中生效。库作者不需要 Lynxtron 专用注册宏。

安装库

应用接入 Autolink 后,在 Lynx 应用中安装库:

npm install @example/lynx-button

每个库包都会在包根目录暴露 lynx.lib.json 清单文件。Autolink 会扫描已安装 npm 包中的这个文件。

{
  "platforms": {
    "android": {
      "packageName": "com.example.button",
      "sourceDir": "android"
    },
    "ios": {
      "sourceDir": "ios",
      "podspecPath": "ios/build.podspec"
    },
    "lynxtron": {
      "path": "dist"
    },
    "macos": {
      "sourceDir": "shared"
    },
    "windows": {
      "sourceDir": "shared"
    }
  }
}

Android 侧必须声明 platforms.android.packageNamesourceDir 默认是 android。iOS 侧 sourceDir 默认是 iospodspecPath 默认使用 iOS 源码目录下找到的第一个 .podspec 文件。

platforms.macos.sourceDirplatforms.windows.sourceDir 指向以源码形式暴露的 shared/ 目录。它是跨端原生源码结构,后续也可以被更多平台复用。

对于 Lynxtron,platforms.lynxtron.path 声明原生产物根目录,通常是 distpluginLynxtron() 会收集匹配到的包文件并校验产物;包的 ./lynxtron export 负责加载当前宿主的 .node,例如 dist/macos/arm64/lynx-button.nodedist/windows/x64/lynx-button.node

安装或更新库后,Android 侧重新 sync/build 应用,iOS 侧重新执行 pod install,让生成的 registry 和原生依赖刷新。应用中不需要为每个库再写手动注册代码。

开发库

本节面向开发可复用原生库供其他 Lynx 应用安装的库作者。

库包结构

一个典型的库结构如下;shared/ 用于跨端原生实现,如果同一个包也发布 Lynxtron 原生产物,同一个结构中也可以包含 Lynxtron 加载入口和按宿主区分的输出:

lynx-button/
├── package.json
├── lynx.lib.json
├── types/
│   ├── index.d.ts
│   ├── platform-native-module.d.ts
│   └── napi-native-module.d.ts
├── src/
│   └── index.ts
├── generated/
│   ├── ButtonModule.ts
│   └── ButtonModuleNapi.ts
├── android/
│   └── src/main/java/com/example/button/
│       ├── ButtonElement.java
│       ├── ButtonModule.java
│       ├── ButtonService.java
│       └── generated/ButtonModuleSpec.java
├── ios/
│   ├── build.podspec
│   └── src/
│       ├── ButtonElement.m
│       ├── ButtonModule.m
│       ├── ButtonService.m
│       └── generated/
│           ├── ButtonModuleSpec.h
│           └── ButtonModuleSpec.m
├── shared/
│   ├── CMakeLists.txt
│   ├── nativeModule/
│   └── elements/
├── lynxtron/
│   ├── CMakeLists.txt
│   ├── index.cjs
│   └── library_entry.cc
├── dist/
│   ├── macos/
│   │   └── arm64/
│   │       └── lynx-button.node
│   └── windows/
│       └── x64/
│           └── lynx-button.node
└── example/
  • package.json 让库可以通过 npm 安装,并通常提供 codegen 脚本。
  • lynx.lib.json 是 Autolink 清单文件,用来告诉宿主应用 Android 和 iOS 源码在哪里,以及 Lynxtron 原生产物根目录在哪里。
  • types/index.d.ts 汇总导出原生模块类型声明;选中对应能力时,平台 Native Module 声明位于 types/platform-native-module.d.ts,N-API Native Module 声明位于 types/napi-native-module.d.ts
  • src/index.ts 导出应用代码需要 import 的 JavaScript API。
  • android/ios/ 放置原生实现以及生成的原生 spec。
  • shared/ 放置跨端原生代码,例如跨端自渲染元素实现,以及 shared/nativeModule/ 下的 C++ N-API 回调桩代码。
  • lynxtron/dist/ 放置 Lynxtron 加载入口和按宿主区分的输出。
  • example/ 是库作者用于本地验证的示例应用。

创建库

使用交互式命令创建库:

npm create lynx-library

在脚本或测试中,也可以用 flags 直接生成:

npm create lynx-library -- \
  --dir ./lynx-button \
  --features native-module,napi-native-module,element,service \
  --platforms android,ios,lynxtron \
  --package-name @example/lynx-button \
  --android-package com.example.button \
  --module-name ButtonModule \
  --element-name x-button \
  --service-name ButtonService

生成的库会包含:

  • 带有 "codegen": "lynx-autolink-codegen"package.json
  • 用于 Android、iOS 和 Lynxtron Autolink 元数据的 lynx.lib.json
  • types/index.d.tstypes/platform-native-module.d.tstypes/napi-native-module.d.ts,根据选择的模块能力生成对应类型声明
  • JavaScript facade 入口 src/index.ts
  • 原生源码目录 android/ios/
  • 生成跨端实现时使用的 shared/ native source
  • 生成 Lynxtron 产物时使用的 lynxtron/ 加载入口
  • example/tsconfig.jsonREADME.md

在库根目录运行 codegen:

npm run codegen

lynx-autolink-codegen 会读取 lynx.lib.json。对于原生模块,它会扫描 types/ 下的 Native Module 声明文件,查找带有 @lynxmodule 的声明:

/** @lynxmodule */
export declare class ButtonModule {
  getLabel(id: string): string;
  setEnabled(id: string, enabled: boolean): void;
}

它会生成:

  • JavaScript facade:generated/<ModuleName>.ts
  • Android platform native module:<ModuleName>Spec.java
  • iOS platform native module:<ModuleName>Spec.h<ModuleName>Spec.m
  • N-API 原生模块:shared/nativeModule/ 下的 C++ N-API 回调桩代码

Codegen 支持基础类型、带 null 的 nullable union、数组、对象/maps/sets、promise,以及 ArrayBuffer、typed arrays、BigIntDateFunctionSymbolBufferValue 等 N-API wrapped value types。

编写 Native API

在原生源码中使用平台对应的 Lynx 注解或注册方式,Autolink 就能发现库提供的能力。Android 和 iOS 原生模块通常继承 lynx-autolink-codegen 生成的 spec;元素和 Service 会通过原生标记被发现。

原生模块示例:

package com.example.button;

import com.example.button.generated.ButtonModuleSpec;
import com.lynx.jsbridge.LynxNativeModule;
import com.lynx.jsbridge.LynxMethod;
import com.lynx.tasm.behavior.LynxContext;
import java.util.HashMap;
import java.util.Map;

@LynxNativeModule(name = "ButtonModule")
public final class ButtonModule extends ButtonModuleSpec {
  private final Map<String, Boolean> enabledState = new HashMap<>();

  public ButtonModule(LynxContext context) {
    super(context);
  }

  @Override
  @LynxMethod
  public String getLabel(String id) {
    return "Button " + id;
  }

  @Override
  @LynxMethod
  public void setEnabled(String id, boolean enabled) {
    enabledState.put(id, enabled);
  }
}

元素示例:

package com.example.button;

import android.content.Context;
import android.view.Gravity;
import android.widget.TextView;
import com.lynx.tasm.behavior.LynxElement;
import com.lynx.tasm.behavior.LynxContext;
import com.lynx.tasm.behavior.LynxProp;
import com.lynx.tasm.behavior.ui.LynxUI;

@LynxElement(name = "x-button")
public final class ButtonElement extends LynxUI<TextView> {
  public ButtonElement(LynxContext context) {
    super(context);
  }

  @Override
  protected TextView createView(Context context) {
    TextView view = new TextView(context);
    view.setGravity(Gravity.CENTER);
    view.setText("x-button");
    return view;
  }

  @LynxProp(name = "text")
  public void setText(String text) {
    mView.setText(text == null ? "" : text);
  }
}

Service 示例:

package com.example.button;

import android.content.Context;
import com.lynx.tasm.service.IServiceProvider;
import com.lynx.tasm.service.LynxService;

@LynxService
public final class ButtonService implements IServiceProvider {
  private Context appContext;

  @Override
  public Class<? extends IServiceProvider> getServiceClass() {
    return ButtonService.class;
  }

  @Override
  public void onInitialize(Context context) {
    appContext = context.getApplicationContext();
  }

  public void recordClick(String id) {
    // Send analytics or call platform capabilities here.
  }
}

原生模块示例:

// ButtonModule.h
#import <Foundation/Foundation.h>
#import <Lynx/LynxModule.h>
#import "generated/ButtonModuleSpec.h"

NS_ASSUME_NONNULL_BEGIN

@LynxNativeModule("ButtonModule")
@interface ButtonModule : NSObject <ButtonModuleSpec>

@end

NS_ASSUME_NONNULL_END

// ButtonModule.m
#import "ButtonModule.h"

@implementation ButtonModule {
  NSMutableDictionary<NSString *, NSNumber *> *_enabledState;
}

- (instancetype)init {
  self = [super init];
  if (self) {
    _enabledState = [NSMutableDictionary dictionary];
  }
  return self;
}

- (NSString *)getLabel:(NSString *)buttonId {
  return [NSString stringWithFormat:@"Button %@", buttonId];
}

- (void)setEnabled:(NSString *)buttonId enabled:(BOOL)enabled {
  _enabledState[buttonId] = @(enabled);
}

@end

元素示例:

// ButtonElement.h
#import <UIKit/UIKit.h>
#import <Lynx/LynxUI.h>

NS_ASSUME_NONNULL_BEGIN

@interface ButtonElement : LynxUI<UILabel *>

@end

NS_ASSUME_NONNULL_END

// ButtonElement.m
#import "ButtonElement.h"
#import <Lynx/LynxPropsProcessor.h>

@LynxElement("x-button")
@implementation ButtonElement

LYNX_PROP_SETTER("text", setText, NSString *) {
  self.view.text = value ?: @"";
}

- (UILabel *)createView {
  UILabel *label = [[UILabel alloc] init];
  label.textAlignment = NSTextAlignmentCenter;
  label.text = @"x-button";
  return label;
}

@end

Service 示例:

// ButtonService.h
#import <Foundation/Foundation.h>
#import <LynxServiceAPI/ServiceAPI.h>

NS_ASSUME_NONNULL_BEGIN

@protocol ButtonServiceProtocol <LynxServiceProtocol>

- (void)recordClick:(NSString *)buttonId;

@end

@interface ButtonService : NSObject <ButtonServiceProtocol>

@end

NS_ASSUME_NONNULL_END

// ButtonService.m
#import "ButtonService.h"

@LynxService(ButtonService, ButtonServiceProtocol)
@implementation ButtonService

+ (instancetype)sharedInstance {
  static ButtonService *service;
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    service = [[ButtonService alloc] init];
  });
  return service;
}

- (void)recordClick:(NSString *)buttonId {
  // Send analytics or call platform capabilities here.
}

@end

在 iOS 侧,Autolink 会扫描库源码中的 @LynxNativeModule(...)@LynxElement(...)@LynxService(...) 声明,并把发现的能力加入生成的 registry。旧版 iOS Service API 仍提供 @LynxServiceRegister(...) 以兼容已有代码。新库代码推荐使用 @LynxService(...)

Lynxtron 原生库使用共享的 Lynx C/C++ 扩展 API 和静态注册宏。本页只说明 Autolink 如何发现和加载包;原生模块的实现请看 Desktops (Node-API) 标签页,自定义元素的实现请看 Desktops 标签页:

发布库

像发布普通 npm 包一样发布库:选一个包名,通常是带 scope 的 @example/lynx-button,在 package.json 中设置版本号,然后执行 npm publish。应用团队把发布版本写入自己的 package.json,下次安装依赖时 Autolink 会自动识别。建议在库的 peerDependencies 中固定兼容的 Lynx SDK 版本范围,这样当库和宿主 SDK 出现版本错配时,应用团队能够第一时间收到清晰提示。

Autolink 在构建期运行。它会扫描 node_modules 中每个 npm 包的 lynx.lib.json 清单,然后为当前平台生成或提供宿主接入数据。Android 侧的 registry 生成由 Gradle settings 插件和 build 插件配合完成;iOS 侧由 CocoaPods 插件完成并产出 registry pod。宿主应用在初始化 LynxEnv 时会一次性加载生成的 registry,因此 Android 或 iOS 宿主代码中不需要任何逐库的接入逻辑。Lynxtron Autolink 运行在宿主侧 Rspack 构建中:它会收集匹配到的原生包,把加载代码注入宿主 bundle,并让已加载库里的 Lynx 静态注册提供对应的原生模块和元素。

除非另有说明,本项目采用知识共享署名 4.0 国际许可协议进行许可,代码示例采用 Apache License 2.0 许可协议进行许可。