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' => 'shanhaoqiang@lizhi.fm' }
  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 ' + 'git@gitlab.xx.fm: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               # =>#<Digest::MD5>
        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
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,063评论 6 510
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,805评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,403评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,110评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,130评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,877评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,533评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,429评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,947评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,078评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,204评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,894评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,546评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,086评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,195评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,519评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,198评论 2 357

推荐阅读更多精彩内容