不要为天然iOS构建的痛苦缓慢的反应付费。
#reactnative #cicd #ios #codepush

本文讨论了反应本机项目的CI/CD生态系统状态(重点是iOS)。感谢Apple,需要MACOS的Xcode对于创建iOS应用至关重要。

方法

每个人都知道捆绑对象C代码时Xcode非常慢。目前,我们有很多策略来加快iOS构建。我将它们分为两组:

1)仅构建JavaScript

  • ðBuilding and injecting a JS bundle一个基本概念:如果本机代码没有更改,则只需创建JS捆绑包并将其注入预编译应用程序即可。 (如果您使用Hermes js engine,则必须采取额外的步骤将JS文件转换为字节码。)
  • 如果您没有时间将上述策略付诸实践,则可以支付Nitro Build Service(x5更快)。我尝试了一下,对他们的工作感到非常高兴:

Image description

2)技巧和缓存

但是,上述所有方法都必须使用Apple Build工具或第三方服务ðµ。但是我确实有一些特别的特别之处。 ðð


是的,它是CodePush!

我本人更喜欢专注于一种称为CodePushð的免费开源替代品,这使我们能够真正停止为MacOS跑步者付款。安装新的本机依赖性或不经常触摸本地代码非常适合您。这怎么可能?

我使用下一个策略:

  • 如果更改了本机代码(通常是./android/**./ios/**),我们将应用经典流程:为Android和iOS构建整个应用程序。
  • 但是,如果仅修改了JavaScript代码,那么创建新的本机代码的重点是什么?相反,创建一个JavaScript捆绑包并将其部署到CodePush。当然,您不需要使用Mac OS跑步者来编译JS。最便宜的Linux应该足够ð°。

成功构建后,您的团队成员可以使用应用程序DEV菜单快速应用部署的CodePush版本。

演示回购:https://github.com/retyui/CodePushCiCd(CI/CD GitHub Action,Dev-Menu,客户部件)


配置项目

首先是在AppCenter中创建一个新的iOS和Android应用:https://appcenter.ms/orgs/MyOrganizationTest/applications/create

Image description

之后,您需要创建标准部署:

Image description

默认情况下,将创建两个分期和生产部署:

Image description

管理面板的最后一步是收集iOS和Android应用程序的生产部署密钥。 (稍后将在JS侧使用)。打开CodePush页面,然后点击部署Select select

旁边的扳手图标

Image description

Image description

// constants.ts
import {Platform} from 'react-native';

const CODE_PUSH_IOS_PROD_KEY = 'Gbsg8cTjdcSWOwgJEOEHqk8VE1x6ITThqvNe0';
const CODE_PUSH_ANDROID_PROD_KEY = 'Ob7LrQg_w-l4w1SOLDYT5XBw76_6Pz-NVCed1';

export const CODE_PUSH_PROD_KEY = Platform.select({
  ios: CODE_PUSH_IOS_PROD_KEY,
  default: CODE_PUSH_ANDROID_PROD_KEY,
});

客户端(核心逻辑)

首先添加koude2。然后继续使用本地部分:

现在让我们编写一些代码。我创建了一个简单的挂钩koude3,它将封装您需要的所有逻辑,并应在应用程序的根组件中使用:

// App.tsx
import {useSyncOnAppStart} from './codepush/useSyncOnAppStart'

export detault function App(){
  useSyncOnAppStart(); // Init CodePush sync on App mount

  return <View/>;
}
// codepush/useSyncOnAppStart.ts
import {useEffect} from 'react';
import {AppState, AppStateStatus} from 'react-native';
import CodePush from 'react-native-code-push';

import {CODE_PUSH_PROD_KEY} from '../constants';

const noop = () => {};

const isDeploymentNotFoundError = (error: Error) =>
  error?.message?.includes?.('No deployment found');

function syncOnAppStart() {
  async function start() {
    try {
      // see comment#1
      const runningPackage = await CodePush.getUpdateMetadata(
        CodePush.UpdateState.RUNNING,
      );

      const isNonProduction =
        runningPackage?.deploymentKey &&
        runningPackage.deploymentKey !== CODE_PUSH_PROD_KEY;

      // see comment#2
      const nonProdConfig = {
        // Non-Prod sync (To enable this variant use "Dev Menu" to change deployment)
        deploymentKey: runningPackage?.deploymentKey,
        installMode: CodePush.InstallMode.IMMEDIATE,
        updateDialog: {
          // Ask mobile team member before install new version for custom deployment
        },
      };

      // see comment#3
      const prodConfig = {
        // Prod sync (↓↓↓ Default values, see: https://github.com/microsoft/react-native-code-push/blob/master/docs/api-js.md#codepushoptions)
        installMode: CodePush.InstallMode.ON_NEXT_RESTART,
        deploymentKey: CODE_PUSH_PROD_KEY,
        rollbackRetryOptions: {
          maxRetryAttempts: 1,
          delayInHours: 24,
        },
      };

      // see comment#4
      const status = await CodePush.sync(
        isNonProduction ? nonProdConfig : prodConfig,
      );

      return status;
    } catch (error) {
      // see comment#5
      if (isDeploymentNotFoundError(error)) {
        return CodePush.clearUpdates();
      }
      // see comment#6
      trackError(error);
    }
  }

  // see comment#7
  const onAppStateChange = async (newState: AppStateStatus) => {
    if (newState === 'active') {
      await start();
    }
  };

  let unsubscribe = noop;

  start()
    .catch(noop)
    .finally(() => {
      const subscription = AppState.addEventListener(
        'change',
        onAppStateChange,
      );

      unsubscribe = () => subscription.remove();
    });

  return () => {
    unsubscribe();
  };
}

export const useSyncOnAppStart = (): void => {
  useEffect(() => {
    const unsubscribe = syncOnAppStart();

    return unsubscribe;
  }, []);
};

注释#1 :首先获取有关已安装软件包的信息。 注释#2 :处理非生产软件包时,我们采用koude4,它向用户显示更新警报。 Comment#3:对于生产构建,应用程序将使用默认选项。 Comment#4:运行代码推送同步(读取文档:koude7)。 评论#5 :当开发人员仍然拥有已删除的开发部署中的安装程序包时,没有发现任何部署问题。 注释#6 :向您发送错误以进行错误跟踪(如Sentry)。 评论#7 :如果使用该应用程序在使用该应用程序时已发布新的CodePush,请添加应用程序侦听器以使所有内容保持同步。

客户端(Dev-Menu)

本节将容易得多。为了使应用自定义构建简单,我构建了react-native-code-push-dev-menu模块。

# Install
yarn add react-native-code-push-dev-menu
# or npm install react-native-code-push-dev-menu

用法示例:

// DevMenuScreen.tsx
import {
  CodePushDeMenuButton,
  configurateProject,
} from 'react-native-code-push-dev-menu';

configurateProject({
  readonlyAccessToken: Platform.select({
    // Read-only access tokens 
    // https://docs.microsoft.com/en-us/appcenter/api-docs/#creating-an-app-center-app-api-token
    ios: '128009dc42ded5e71ef21e007a24eb67b5c3279f',
    default: '42f471742864bd9c1917f322918b163a90d13904',
  }),
  appCenterAppName: Platform.select({
    ios: 'MyApp-iOS',
    default: 'MyApp-Android',
  }),
  appCenterOrgName: 'MyOrganizationTest',
});

function DevMenuScreen() {
  return (
    <SafeAreaView>
      <CodePushDeMenuButton />
      // Other dev things
    </SafeAreaView>
  );
}

CI/CD

自动化的CodePush构建是最后建立的。在AppCener的管理员中,您需要使用填充访问

创建APPCENTER_ACCESS_TOKEN https://appcenter.ms/settings/apitokens

Image description

接下来,让我们构建配置文件koude9

#!/bin/bash
export APP_CENTER_ORG_NAME=MyOrganizationTest

export APP_CENTER_APP_NAME_IOS=MyApp-iOS
export APP_CENTER_APP_NAME_ANDROID=MyApp-Android

以下步骤是编写一个名为koude10的构建脚本,该脚本将创建新的部署(使用分支名称作为部署名称),并将将在该环境中部署的JS捆绑包。

#!/bin/bash

set -x # all executed commands are printed to the terminal
set -e # immediately exit if any command has a non-zero exit status

source "./scripts/envs.sh"


if [ -z "$DEPLOYMENT_NAME" ]; then
    echo "Please sure that DEPLOYMENT_NAME exists"
    exit 1
fi

if [ "$DEPLOYMENT_NAME" == "Production" ]; then
    echo "You can't use reserved name 'Production' for deployment"
    exit 1
fi

if [ -z "$APPCENTER_ACCESS_TOKEN" ]; then
    echo "Please sure that APPCENTER_ACCESS_TOKEN exists"
    exit 1
fi

if [ "$PLATFORM" != "ios" ] && [ "$PLATFORM" != "android"  ]
then
    echo "Please sure that you set PLATFORM env variable (ios | android)"
    exit 1
fi

APP_CENTER_APP_NAME="$APP_CENTER_APP_NAME_ANDROID"

if [ "$PLATFORM" == "ios" ]; then
    APP_CENTER_APP_NAME="$APP_CENTER_APP_NAME_IOS"
fi

# Create new Codepush Deployment
appcenter codepush deployment add -a "$APP_CENTER_ORG_NAME/$APP_CENTER_APP_NAME" "$DEPLOYMENT_NAME" || true # Ignore "deployment named test-build already exists" error
# Create JS bundle
appcenter codepush release-react -a "$APP_CENTER_ORG_NAME/$APP_CENTER_APP_NAME" -d "$DEPLOYMENT_NAME" --target-binary-version "*" --description "$DESCRIPTION"

您现在可以进行本地测试:

# Required
export APPCENTER_ACCESS_TOKEN=xxxx
export DEPLOYMENT_NAME=my-branch-name
# Optional
export DESCRIPTION="My desc..."

PLATFORM=ios ./scripts/codepush-non-prod.sh     # Codepush release for iOS
PLATFORM=android ./scripts/codepush-non-prod.sh # Codepush release for Android

,如果您使用Github Actions a创建一个koude11文件:

name: Mobile CodePush

on:
  pull_request:
    paths-ignore:
      - 'android/**'
      - 'ios/**'
  workflow_dispatch:


concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  android_beta_mobile_build:
    name: Build Non-Prod
    runs-on: ubuntu-20.04
    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 1

      - name: Yarn cache
        uses: actions/cache@v3
        id: node_cache
        with:
          path: node_modules
          key: ${{ runner.os }}-yarn-${{ hashFiles('./yarn.lock') }}

      - name: Install node_modules
        run: yarn install --frozen-lockfile
        if: steps.node_cache.outputs.cache-hit != 'true'


      - name: Create Beta CodePush Release
        env:
          APPCENTER_ACCESS_TOKEN: ${{ secrets.APPCENTER_ACCESS_TOKEN }}
          DEPLOYMENT_NAME: ${{ github.head_ref || github.ref_name }} # `head_ref` pull_request event, `ref_name` workflow_dispatch event
          DESCRIPTION: Made by - ${{ github.actor }}
          NODE_ENV: production
        run: |
          npm install -g appcenter-cli@2.12.0
          PLATFORM=android ./scripts/codepush-non-prod.sh
          PLATFORM=ios ./scripts/codepush-non-prod.sh

此外,可以使用手动运行来在CI上进行测试:

Image description


让我们回顾创建的内容ð¥³ðÖððð

  • 所有刚刚修改JS的PR将释放到CodePush(分支名称将用作部署名称);
  • 您可以使用Dev Menu快速应用这些更改; ð¶©
  • 您无需使用MacOS来创建CodePush版本;ð

如果您有任何疑问,请随时在评论部分中询问。

Muramur©