Flutter混合工程CI/CD最佳实践

背景

项目处于混合开发状态,native开发的同学没有装flutter环境,无法编译flutter的代码,工程无法跑起来。

官方推荐方案

将 Flutter module 集成到 iOS 项目

源码依赖

flutter_application_path = './my_flutter'
load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')

target 'MyApp' do
  install_all_flutter_pods(flutter_application_path)
end

Flutter工程以submodule方式引入native工程

优点

源码依赖,方便调试,分支管理方便。

缺点

没有flutter环境的同学无法编译运行工程,对native侵入性比较强。

产物依赖

流程图

iOS工程持续集成产物依赖

优点

侵入性低,产物依赖提升打包速度

缺点

开发迭代比较麻烦,打包产物步骤麻烦,生成产物需要托管管理,产物体积比较大

我的方案

APP.xcframework和Flutter.xcframework是以产物依赖,其他的插件是以源码形式依赖

产物和源码混合依赖

流程图

iOS持续集成依赖

优点

只把dart代码编译成产物,其他使用源码方式依赖,产物体积很小,侵入性低,打包速度快。

缺点

开发调试比较麻烦,需要托管产物,要花时间实现一套全网没有参考的新方案

比较

方案实现

制作

打包产物脚本

打包app.xcframework产物,然后把flutter.podspec、FlutterPluginRegistrant、plugins复制到binary目录,压缩binary.zip目录

#环境变量
function exportFlutterEnv() {
        export PUB_HOSTED_URL=https://pub.flutter-io.cn
        export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn
}

#打包app.xcframework
function buildiOSFramework() {
        if [ $BUILD_MODE == "Debug" ]
        then
                $FLUTTER_PATH/bin/flutter build ios-framework --no-release --no-profile --output=./ios-framework --verbose --cocoapods --no-tree-shake-icons
        else
          $FLUTTER_PATH/bin/flutter build ios-framework --no-debug --no-profile --output=./ios-framework --verbose --cocoapods --no-tree-shake-icons
        fi

}

#创建目录binary
function createBinaryDir() {
        mkdir -v binary
}

#移动plugins
function movePluginsDir() {
        cp -r -v .ios/.symlinks/plugins/. binary/plugins
}

#移动 app.xcframework、flutter.podspec、FlutterPluginRegistrant
function moveFlutterDir() {
        mkdir -p binary/flutter/FlutterPluginRegistrant
        cp -r -v .ios/Flutter/FlutterPluginRegistrant binary/flutter
        cp -v ios-framework/$BUILD_MODE/Flutter.podspec binary/flutter/Flutter.podspec
        cp -r -v ios-framework/$BUILD_MODE/App.xcframework/. binary/flutter/App.xcframework
}

#创建App.podspec
function createAppPodspec() {
        touch binary/flutter/App.podspec
        echo """Pod::Spec.new do |s|
  s.name                  = 'App'
  s.version               = '1.0.0'
  s.summary               = 'fast apps.'
  s.description           = <<-DESC
Business Code
DESC
  s.homepage              = 'https://flutter.cn'
  s.license               = { :type => 'BSD' }
  s.author                = { 'Jacky' => '[email protected]' }
  s.source                = { :path => '.' }
  s.documentation_url     = 'https://flutter.cn/docs'
  s.platform              = :ios, '9.0'
  s.vendored_frameworks   = 'App.xcframework'
end
        """>binary/flutter/App.podspec
}

#打zip包
function zipBinaryDir() {
        zip -r binary-$BUILD_MODE.zip binary
}

#执行
exportFlutterEnv
buildiOSFramework
createBinaryDir
movePluginsDir
moveFlutterDir
createAppPodspec
zipBinaryDir

jenkins任务

拉取flutter工程代码,执行上面的shell脚本,生成binary.zip,归档zip到jenkins的artifacts目录中,获取zip链接env.BUILD_URL + "artifact/binary-${BUILD_MODE}.zip"

    stage('\u261D 使用分支名称作为任务名称') {
        currentBuild.displayName = "#${BUILD_NUMBER}_${BRANCH_NAME}"
    }

    stage('\u262D 拉取代码') {
        checkout([$class: 'GitSCM', branches: [[name: '*/' + BRANCH_NAME]], doGenerateSubmoduleConfigurations: false, extensions: [[$class: 'CleanBeforeCheckout']], submoduleCfg: [], userRemoteConfigs: [[credentialsId: '25b61d91-a240-4eaa-8390-9ae2655fb969', refspec: '+refs/heads/' + BRANCH_NAME + ':refs/remotes/origin/' + BRANCH_NAME, url: GIT_URL]]])
        withCredentials([sshUserPrivateKey(credentialsId: '25b61d91-a240-4eaa-8390-9ae2655fb969', keyFileVariable: 'SSH_KEY')]) {
            sh """
                git checkout ${BRANCH_NAME}
                git pull origin ${BRANCH_NAME}
            """
        }

    }
    stage('\u261D 拉取脚本') {
        sh 'rm -rf ./' + 'flutterbinary'
        sh 'git clone ' + '[email protected]:ocean/xx.git' + ' -b master --depth=1 ./' + 'flutterbinary'
    }

    stage('\u2615 编译产物') {
        sh """
        cp flutterbinary/build.sh build.sh
        sh build.sh ${BUILD_MODE} ${BUILD_NUMBER} ${BUILD_MODEL} ${FLUTTER_PATH} ${JDK_PATH}
        """
    }

    stage('\u26B0 保存成品') {
            archiveArtifacts artifacts: "binary-${BUILD_MODE}.zip", fingerprint: true
    }

    stage('\u2709 发送通知') {
        //iOS产物地址
        url = env.BUILD_URL + "artifact/binary-${BUILD_MODE}.zip"

        wrap([$class: 'BuildUser']) {
            USER_ID = BUILD_USER_ID
            USER_NAME = BUILD_USER
        }

        def updateLog = "${env.UPDATELOG}".trim()

        String content = "请相关同事知悉。本次Flutter产物发布信息如下:\\n 操作人:${USER_NAME}" + "\\n 打包类型:${BUILD_MODE}" + "\\n 任务名:${env.JOB_NAME}" + "\\nFlutter iOS产物地址:${url}"+ "\\nFlutter Android aar包地址:${aarUrl}"+"\\n对应分支:${BRANCH_NAME}\\nFlutter地址:${GIT_URL}\\n更新内容:${updateLog.replace("\n", "\\n")}\\n"

        def contentall = """
    {"content":{"text": "${content}"},"msg_type":"text"}
"""
        println("contentall:" + contentall)
        def command = """
    curl -X POST -H "Content-Type: application/json"\
      -d '${contentall}' \
      "https://open.feishu.cn/open-apis/bot/v2/hook/${env.NOTIFY_KEY}"
  """
        sh(script: command)
    }

集成

一行代码集成flutter,Podfile中填写jenkins打包的flutter项目的zip链接

def pod_flutter
  puts "=== 集成flutter sdk ==="
  install_remote_flutter_binary('https://jksclient.xx.fm/job/%E8%8D%94%E6%9E%9D-flutter/106/artifact/binary-Release.zip')
end

编写Podfile插件

  • 收到传进来的url,对url做md5,创建Flutter缓存目录,下载zip包,解压
  • 安装flutter引擎,flutter指向podspec,podspec的source zip是官方地址
  • 安装plugins,各个plugin包含FlutterPluginRegistrant,指向解压后端path地址
  • 安装App.xcframework,指向App所在的path路径
## author:Jacky
## desc:install binary pods in Podfile
#!/usr/bin/env ruby

require 'digest'
require 'fileutils'
require 'uri'
require 'net/http'
require 'net/https'

module Pod
  class Podfile
    module DSL

      #下载
      def install_remote_flutter_binary(url = nil)
        #md5
        md5 = Digest::MD5.new               # =>#
        md5 << url
        md5value = md5.hexdigest                        # => "78e73102..."
        flutter_f_home = Dir.home+'/Library/Caches/CocoaPods/Flutter/'
        flutter_binary_home = flutter_f_home+md5value
        flutter_binary_path = flutter_binary_home+'/binary'
        
        #创建目录
        FileUtils.mkdir_p(flutter_binary_home)

        #清除超过30天的缓存
        xxxx
        
        #判断缓存
        if File::directory?(flutter_binary_path) == false
          #下载
          puts "开始下载 "+url

        #集成
        install_all_lzflutter_pods(flutter_binary_path)
      end

      #安装
      def install_all_lzflutter_pods(flutter_binary_path)
        install_lzflutter_engine_pod(flutter_binary_path)
        install_lzflutter_plugin_pods(flutter_binary_path)
        install_lzflutter_application_pod(flutter_binary_path)
      end

      # 安装flutter引擎
      def install_lzflutter_engine_pod(flutter_binary_path)
          xxx
      end

      # Install Flutter plugin pods.
      def install_lzflutter_plugin_pods(flutter_binary_path)
        # Keep pod path relative so it can be checked into Podfile.lock.
        # Process will be run from project directory.

        #FlutterPluginRegistrant
        xxx
        
        #插件目录
        xxx

        #plugins遍历
        xxx
      end

      # Install Flutter application pod.
      def install_lzflutter_application_pod(flutter_binary_path)
          xxx
      end
    end
  end
end

你可能感兴趣的:(Flutter混合工程CI/CD最佳实践)