插件化技术在React Native中的应用

插件化技术允许将复杂业务模块封装为插件,动态组合和更新,减少发版频率。在Android中,插件化用于动态修复和更新,而ReactNative借助JS引擎实现动态化,更适用于功能扩展。RN通过加载器和自执行函数执行插件代码,实现模块化和代码分离,提高业务灵活性。动态import在RN中可通过自定义实现来支持按需加载。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

什么是插件化?

何为「 插件化 」,顾名思义,就是把一些核心复杂依赖度高的业务模块封装成独立的插件,然后根据不同业务需求进行不同组合,动态进行替换,可对插件进行管理、更新,后期对插件也可进行版本管理等操作。在插件化中有两个主要概念:

  • 宿主

就是需要能提供运行环境,给资源调用提供上下文环境,一般也就是我们主程序 ,要运行的应用,它作为应用的主工程所在,实现了一套插件的加载和管理的框架,插件都是依托于宿主程序而存在的。

  • 插件

插件可以想象成每个独立的功能模块封装 ,可以通过在线配置和更新实现插件在宿主程序中的上线和下线,以及动态更新等功能。

插件化的应用场景

  1. 让用户不用重新安装就能升级应用功能,减少发版本频率,增加用户体验。

  2. 提供一种快速修复线上 BUG 和更新的能力。

  3. 按需加载不同的模块,实现灵活的功能配置。

  4. 模块化、解耦合、并行开发。

Android插件化

插件化技术可以说是Android高级工程师所必须具备的技能之一,从2012年插件化概念的提出(Android版本),到如今插件化的百花争艳,可以说,插件化技术引领着Android技术的进步。

在 Android 中应用插件化技术,其实也就是动态加载的过程,分以下几步:

  • 把可执行文件( .so/dex/jar/apk 等)拷贝到应用 APP 内部。

  • 加载可执行文件,更换静态资源

  • 调用具体的方法执行业务逻辑

经过以上几步, 从而可以实现线上动态更新修复的功能。经过近10年的发展,插件化技术已经沦为修 bug 的工具。这跟插件化的初衷不一样,插件化是实现新功能,而不是修复 bug。

其次,插件化现在有一个更好的替代品——RN。RN依赖js引擎,成为了在原生应用领域中真正实现动态化的最佳方式。借助插件化的技术思想,如何在RN层实现此类功能呢?

React Native 端如何实现

RN已经完美支持了热更新,为什么还需要插件化呢?  热更新和插件化都是为了更好的解决线上app修复更新的问题。在RN中实现插件化, 更多的是以业务需求模块为主,将一些通用代码抽离,借助模块化开发思想,做到多源聚合、并行开发且可扩展的需求场景。例如App换肤、不同音乐平台的音源聚合等等,都是将资源模块作为插件,通过线上下载的方式来实现。优势也非常明显,既做到了代码分离, 也实现了业务高度可扩展。

加载器

RN端依赖js core 引擎实现代码加载,那么我们可以实现一个自执行函数, 将外部代码模块作为插件, 在自执行函数中执行插件代码就可以实现插件化啦。

Function(`
    return function() {
      ${pluginCode}
    }
`)()();

pluginCode就是从外部读入的插件代码。同时还可以让插件通过参数传递的方式使用RN宿主程序中的一些API或功能包(例如RN端的npm三方包)。

Function(`
  return function(console, axios) {
    ${funcCode}
  }
`)()(console, axios);

插件模块

  • 通过require或者类似“全局变量”的方式获取运行时环境
  • 通过module.exports导出插件实例
module.exports = {
  pluginVersion: '1.0.0',
  pluginName: '容器加载器',
  // 省略其他代码...
};

上面曾说到在插件模块代码中可以使用RN宿主程序中的一些功能包,例如axios,代码像这样

const axios = require('axios');

module.exports = {
  pluginVersion: '1.0.0',
  pluginName: '容器加载器',
  // 省略其他代码...
};

将其copy到浏览器控制台执行,会发现这段代码不可以独立运行,提示会缺少三个全局变量:require、module和exports。为了让插件内部可以获取到三个特定的全局变量,可以借助宿主通过函数参数传递给插件模块

Function(`
  return function(require, module, console, axios) {
    ${funcCode}
  }
`)()(_require, _module, console, axios);

pluginCode内获取到的require、module和exports是RN内运行时环境下的,当这个函数执行完毕后,我们可以从module.exports中获取到我们需要的插件返回值,同时也可以用require加载APP内部的npm包或者RN内置的API。

变量定义

const _module = {
  exports: {
  }
};
const packages: Record<string, any> = {
    axios,
    dayjs,
    ...
};

const _require = (packageName: string) => {
    let pkg = packages[packageName];
    pkg.default = pkg;
    return pkg;
};

以上就是在RN端实现插件化开发的基本思路。在围绕插件加载器上,我们还有周边功能没有实现,比如插件的卸载、更新等等。理解了上面的实现方式,相信这些功能也都会迎刃而解。

Dynamic import

RN官方对于dynamic import 特性暂无支持,借助上面的插件化实现思想,我们可以自己来扩展实现

通常使用 import * from "xxx" 引入普通业务模块,那么该模块的代码都会直接打到业务包中。但在引入按需加载业务模块时,使用的是 import("xxx") 引入的。在使用import函数之前 ,少不了React 16.6 引入了一个新的 API:React.lazy(),可以帮助我们动态加载组件。

const MyComponent = React.lazy(() => import('./MyComponent'));

在运行时动态import()的默认实现为metro-runtime/src/modules/asyncRequire.Metro 提供了一个名叫 AsyncRequire.js 的文件模版来做动态导入的语法的 polyfill,具体实现如下

/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @format
 * @flow strict
 */

'use strict';

// $FlowExpectedError Flow does not know about Metro's require extensions.
const dynamicRequire = (require: {importAll: mixed => mixed});
module.exports = function (moduleID: mixed): Promise<mixed> {
  return Promise.resolve().then(() => dynamicRequire.importAll(moduleID));
};

总结一下 Metro 默认会如何处理动态导入:在 Transformation 通过 Babel 插件处理动态导入语法,并在中间产物上增加标识 Async,在 Serialization 阶段用 Asyncrequire.js 作为模板替换动态导入的语法,即

const A = import(A);

变为

const A = function(moduleID) {
    return Promise.resolve().then(() => dynamicRequire.importAll(moduleID));
};

所以要实现上面的逻辑, 就需要修改AsyncRequire的默认逻辑,在最新的metro官方文档中已经支持AsyncRequire的自定义配置,通过asyncRequireModulePath即可指定自定义实现

大致思路是: 在 runtime 运行时,业务执行 import("./xxx") 之后,框架层会去判断 ./xxx 路径对应的 module 是否已经 install。如果没有 install,就会通过 ./xxx 路径找到对应 chunk 包的 URL 地址,接着下载和执行 chunk,最后渲染 xxx Component。

const DIAsyncRequire = async moduleId => {
  // chunkModuleIdToHashMap 是一个映射白名单
  const hashMap = chunkModuleIdToHashMap[moduleId]
  if (!hashMap) { 
    // 一个模块被分解到主包中,但也有异步的地方 
    await Promise.resolve()
  } else if (Array.isArray(hashMap)) {
    // TODO 同时执行多个文件
    await Promise.all(hashMap.map(v => requireEnsure(v)))
  } else {
    // 单个执行
    await requireEnsure(moduleId)
  }
  // 执行默认的 asyncRequire 逻辑
  return asyncRequire(moduleId)
}
module.exports = DIAsyncRequire

核心在requireEnsure函数模块

const requireEnsure = (chunkId) => {
 
  const installedChunks = global.installedChunks = global.installedChunks || {
    // 'buz.ios.bundle': 0 // [chunkId]: [state]
  }
  const promises = []
  let installedChunkData = installedChunks[chunkId]
  if (installedChunkData !== 0) {
    if (installedChunkData) {
      promises.push(installedChunkData[2])
    } else {

      const promise = new Promise(function(resolve, reject) {
        installedChunkData = installedChunks[chunkId] = [resolve, reject]
      })
      promises.push(installedChunkData[2] = promise)
      const error = new Error()
      let timeoutId

      const onComplete = ({ code, data, msg }) => {
        clearTimeout(timeoutId)
        const state = installedChunks[chunkId]
        if (state === undefined) return
        const [resolve, reject] = state
        if (code !== 200) {
          error.message = msg
          error.type = error.name = 'ChunkLoadError'
          error.request = resourceUri
          installedChunks[chunkId] = undefined
          return reject(error)
        }
        // 自执行函数
        Function(`"use strict"; ${data}`)()
        installedChunks[chunkId] = 0
        resolve()
      }

      // 超时处理
      const timeoutTime = xxx
      timeoutId = setTimeout(
        onComplete.bind(null, {
          code: 0,
          msg: `load chunk:${chunkId} failure(timeout:${timeoutTime})`
        }),
        timeoutTime
      )
      // 下载资源包
      fetch(resourceUri).then(res => {
        if (res.status === 200) {
          res.text().then(res => {
            onComplete({ code: 200, data: res })
          }).catch(err => {
            onComplete({ code: 0, msg: err.message })
          })
        } else {
          onComplete({ code: res.status, msg: res.statusText })
        }
      }).catch(err => {
        onComplete({ code: 0, msg: err.message })
      })

    }
  }
  return Promise.all(promises)
}

核心逻辑就是利用fetch去下载对应文件, 下载完成后通过自制行Function函数去加载资源代码块。

业务初始化时,可以只执行主业务包,在跳转到dynamic页面时,才会动态的下载对应的动态包。退出已进入的 dynamic 页面再次进入,不会再下载,会利用原有的缓存直接渲染 dynamic 页面。

const A = React.lazy(() => import('./A'))
const B = React.lazy(() => import('./B'))

const App = () => (
  <NavigationContainer>
    <Stack.Navigator>
       <Stack.Screen name="Main" component={Main} />
       <Stack.Screen name="A" component={A} />
       <Stack.Screen name="B" component={B} />
    </Stack.Navigator>
  </NavigationContainer>
)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Songlcy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值