Flutter 官方提供的混合工程搭建方法: Add Flutter to existing apps文章中介紹瞭如何在現有 App 里加入Flutter,下面進行逐步介紹一下ios
請自行 百度/Google Flutter 安裝教程,安裝Flutter。而後到任意目錄下執行flutter create -t module my_flutter
,"my_flutter" 是要建立的 Flutter 工程的名稱。git
在Podfile添加如下下代碼github
flutter_application_path = "xxx/xxx/my_flutter"
eval(File.read(File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')), binding)
複製代碼
而後執行pod install面試
###3. 修改 Native 工程 打開Xcode工程,選擇要加入 Flutter App 的 target,選擇 Build Phases,點擊頂部的 + 號,選擇 New Run Script Phase,而後輸入如下腳本xcode
"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" build
"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" embed
複製代碼
按照上面三個步驟進行逐一分析每一步的問題,並提供優化方案。ruby
這一步首先在本身電腦上安裝 Flutter,而後使用 flutter create
。這裏就存在問題,在團隊開發中每一個人安裝的 Flutter 版本可能並不一樣,這樣會出現Dart層Api兼容性或Flutter虛擬機不一致等問題。 在團隊協做中必定要保證 Flutter 工程依賴相同的 Flutter SDK,全部須要一個工具在執行 flutter
指令時能夠根據當前 Flutter 工程使用對應版本的 Flutter SDK,目前有一個名叫flutter_wrapper的工具,使用 flutterw
代替 flutter
指令,工具會自動將 Flutter SDK 放在當前 Flutter 工程目錄下,並執行當前工程中的 flutter
命令,這樣就再也不依賴本地電腦安裝的 Flutter SDK。 flutter_wrapper使用: 一、flutter create 建立 Flutter 工程,這裏使用的是本地的 Flutter SDK 二、進入 Flutter 工程目錄安裝 'flutter_wrapper',執行 sh -c "$(curl -fsSL raw.githubusercontent.com/passsy/flut…)" 三、此後在當前 Flutter 工程須要使用 flutter 命令的地方都使用 ./flutterw來代替bash
###2. 經過 Cocoapods 將 Flutter 引入 現有 Native 工程 這一步在 Podfile 裏添加里一個 'podhelper.rb' ruby 腳本,腳本會在 pod install/update 時執行,腳本主要作4點事情: 一、解析 'Generated.xcconfig' 文件,獲取 Flutter 工程配置信息,文件在'my_flutter/.ios/Flutter/'目錄下,文件中包含了 Flutter SDK 路徑、Flutter 工程路徑、Flutter 工程入口、編譯目錄等。 二、將 Flutter SDK 中的 Flutter.framework 經過 pod 添加到 Native 工程。 三、將 Flutter 工程依賴的插件經過 pod 添加到 Native 工程,由於有些插件有 Native 部分代碼。 四、使用 post_install 這個 pod hooks 來關閉 Native 工程的 bitcode,並將 'Generated.xcconfig' 文件加入 Native 工程。 這一步存在問題是,'podhelper.rb'腳本是經過一個本地 Flutter 工程路徑'flutter_application_path'來讀取,在團隊協做中咱們很難保證每一個人的本地 Flutter 工程路徑都同樣,在同步代碼時你們可能都要頻繁修改'flutter_application_path'變量,這樣很不友好。 解決這個問題的思路就是將 Flutter 工程放在當前 Native 工程的目錄下,咱們能夠再加入一個 ruby 腳本,在每次執行 pod install/update 時,將 Flutter 工程從 git 上拉取一份放在當前目錄下,這樣 Flutter 工程的路徑就統一了。大體代碼以下:服務器
flutter_application_path = __dir__ + "/.flutter/app"
`git clone git://xxxx/my_flutter.git #{flutter_application_path}`
# 若是想要調試本地的 Flutter 工程,就把下面這行註釋放開
# flutter_application_path = "xxx/xxx/my_flutter"
eval(File.read(File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')), binding)
複製代碼
上述代碼只是臨時代碼,爲了演示將 Flutter 工程放在當前目錄下這個思路,後面會有完整的實現代碼。網絡
###3. 修改 Native 工程 這裏執行了一個'xcode_backend.sh'腳本的兩個命令build、embed,兩個命令分別的做業是: build: 根據當前 Xcode 工程的 'configuration' 和其餘編譯配置編譯 Flutter 工程,'configuration'一般爲'debug'或者'release' embed: 將 build 出來的 framework、資源包放入 Xcode 編譯目錄,並簽名 framework 這裏存在的問題是:Flutter 工程依賴 Native工程來執行編譯,並影響Native工程的開發流程與打包流程。 一般 'configuration' 裏不止有 'debug' 或者 'release',可能會有自定義的名稱,若是咱們使用自定義的 'configuration' 編譯,那麼 xcode_backend.sh build 就會執行失敗。由於Flutter 編譯模式是經過 'configuration' 獲取的,Flutter 支持 Debug、Profil、Release 三種編譯模式,而咱們自定義的名稱不在這三種之中,Flutter 就不知道該怎麼編譯。app
每次 Native 編譯時 Flutter 就須要編譯,實際上是產生了相互依賴:Flutter 編譯依賴 Native 編譯環境,Native 依賴 Flutter 編譯經過。
咱們但願作到:Native 依賴 Flutter 編譯出來的產物,而且保留依賴 Flutter 源碼進行調試的能力。 實現這個目標咱們須要兩部分: 第一部分:Flutter 工程裏建立一個打包腳本,能夠一鍵產生 Flutter 工程產物; 第二部分:在 Native 工程獲取 FLutter 工程的編譯產物,並經過 pod 添加到工程;而且保留依賴 Flutter 工程源碼的功能。 ##3、實現 Native Flutter 混合工程 下面咱們來實現上文提到的兩個部分 第一部分實現「打包腳本」 這一部分咱們須要實現腳本自動打包 Flutter 工程,拆分一下這個腳本流程,大體分爲一下幾個步驟: 一、檢查 Flutter 環境,拉取 Flutter plugin 二、編譯 Flutter 工程產物 三、複製 Flutter 插件中的 Native 代碼 四、將產物同步到產物發佈的服務器
下面來一步一步的分析並實現每一步: (1) 檢查 Flutter 環境,拉取 Flutter plugin 這一步作的工做是檢查是否安裝了 'flutter_wrapper',若是安裝則進行安裝,而後執行 ./flutterw packages get,Shell代碼以下:
flutter_get_packages() {
echo "================================="
echo "Start get flutter app plugin"
local flutter_wrapper="./flutterw"
if [ -e $flutter_wrapper ]; then
echo 'flutterw installed' >/dev/null
else
bash -c "$(curl -fsSL https://raw.githubusercontent.com/passsy/flutter_wrapper/master/install.sh)"
if [[ $? -ne 0 ]]; then
# 上一步腳本執行失敗
echo "Failed to installed flutter_wrapper."
exit -1
fi
fi
${flutter_wrapper} packages get --verbose
if [[ $? -ne 0 ]]; then
# 上一步腳本執行失敗
echo "Failed to install flutter plugins."
exit -1
fi
echo "Finish get flutter app plugin"
複製代碼
###(2) 編譯 Flutter 工程產物 這一步是腳本的核心,主要邏輯和上文中'xcode_backend.sh build'相似,大體代碼以下:
# 默認debug編譯模式
BUILD_MODE="debug"
# 編譯的cpu平臺
ARCHS_ARM="arm64,armv7"
# Flutter SDK 路徑
FLUTTER_ROOT=".flutter"
# 編譯目錄
BUILD_PATH=".build_ios/${BUILD_MODE}"
# 存放產物的目錄
PRODUCT_PATH="${BUILD_PATH}/product"
# 編譯出的flutter framework 存放的目錄
PRODUCT_APP_PATH="${PRODUCT_PATH}/Flutter"
build_flutter_app() {
echo "================================="
echo "Start Build flutter app"
# 建立目錄
mkdir -p -- "${PRODUCT_APP_PATH}"
# flutter 工程入口 dart文件
local target_path="lib/main.dart"
# flutter sdk 目錄解析
local artifact_variant="unknown"
case "$BUILD_MODE" in
release*)
artifact_variant="ios-release"
;;
profile*)
artifact_variant="ios-profile"
;;
debug*)
artifact_variant="ios"
;;
*)
echo "ERROR: Unknown FLUTTER_BUILD_MODE: ${BUILD_MODE}."
exit -1
;;
esac
if [[ "${BUILD_MODE}" != "debug" ]]; then
# 非debug編譯模式
# build fLutter app,output App.framework
${FLUTTER_ROOT}/bin/flutter --suppress-analytics \
--verbose \
build aot \
--output-dir="${BUILD_PATH}" \
--target-platform=ios \
--target="${target_path}" \
--${BUILD_MODE} \
--ios-arch="${ARCHS_ARM}"
if [[ $? -ne 0 ]]; then
echo "Failed to build flutter app"
exit -1
fi
else
# debug編譯模式直接使用編譯好的App.framework,
# 由於在 debug 模式下 flutter 代碼並無編譯成二進制機器碼,而是在後續build bundle時被打包進資源包,
# 在'xcode_backend.sh'腳本里,這一步這裏只是編譯成一個App.framework空殼。
# 提早編譯好的緣由是'xcode_backend.sh'腳本執行和Xcode一塊兒執行,因此執行時能獲取到Xcode設置的編譯配置,能正確的編譯出'App.framework',
# 而本腳本不依賴Xcode執行,即使把'xcode_backend.sh'對應的代碼拷貝出來也不能正確的編譯出'App.framework',除非咱們能正確的配置編譯環境。
#
# 而我又不想那麼麻煩,選擇另闢蹊徑:
# 隨便建立了一個 Flutter 工程,
# 在debug模式下,先在模擬器編譯運行一下,獲得x86_64的App.framework,
# 再到真機運行一下,獲得arm64/armv7的App.framework,
# 最後使用lipo命令將兩個App.framework合併,獲得x86_64/arm64/armv7的App.framework,
# 這樣最後獲得的App.framework在模擬器和真機均可以用
# 由於debug模式下App.framework就是佔位的空殼,因此其餘flutter工程同樣用
local app_framework_debug="iOSApp/Debug/App.framework"
cp -r -- "${app_framework_debug}" "${BUILD_PATH}"
fi
# copy info.plist to App.framework
app_plist_path=".ios/Flutter/AppFrameworkInfo.plist"
cp -- "${app_plist_path}" "${BUILD_PATH}/App.framework/Info.plist"
local framework_path="${FLUTTER_ROOT}/bin/cache/artifacts/engine/${artifact_variant}"
local flutter_framework="${framework_path}/Flutter.framework"
local flutter_podspec="${framework_path}/Flutter.podspec"
# copy framework to PRODUCT_APP_PATH
cp -r -- "${BUILD_PATH}/App.framework" "${PRODUCT_APP_PATH}"
cp -r -- "${flutter_framework}" "${PRODUCT_APP_PATH}"
cp -r -- "${flutter_podspec}" "${PRODUCT_APP_PATH}"
local precompilation_flag=""
if [[ "$BUILD_MODE" != "debug" ]]; then
precompilation_flag="--precompiled"
fi
# build bundle
${FLUTTER_ROOT}/bin/flutter --suppress-analytics \
--verbose \
build bundle \
--target-platform=ios \
--target="${target_path}" \
--${BUILD_MODE} \
--depfile="${BUILD_PATH}/snapshot_blob.bin.d" \
--asset-dir="${BUILD_PATH}/flutter_assets" \
${precompilation_flag}
if [[ $? -ne 0 ]]; then
echo "Failed to build flutter assets"
exit -1
fi
# copy Assets
local product_app_assets_path="${PRODUCT_APP_PATH}/Assets"
mkdir -p -- "${product_app_assets_path}"
cp -rf -- "${BUILD_PATH}/flutter_assets" "${PRODUCT_APP_PATH}/Assets"
# setting podspec
# replace:
# 'Flutter.framework'
# to:
# 'Flutter.framework', 'App.framework'
# s.resource='Assets/*'
sed -i '' -e $'s/\'Flutter.framework\'/\'Flutter.framework\', \'App.framework\'\\\n s.resource=\'Assets\/*\'/g' ${PRODUCT_APP_PATH}/Flutter.podspec
echo "Finish build flutter app"
}
複製代碼
###(3) 複製 Flutter 插件中的 Native 代碼 Flutter 使用的各類插件可能會包含 Native 代碼,而且這些代碼已經提供了podspec,可使用 pod 直接引入。咱們要作的就是把插件的 Native 代碼拷貝到產物目錄。 Flutter 建立了一個給 Native 註冊插件的 pod 庫 'FlutterPluginRegistrant',這個也須要拷貝出來, 在 Flutter 工程根目錄下有一個 .flutter-plugins 文件,文件內部記錄了插件的名字和插件的路徑,格式爲 pugin_name=/xx/xx/xx,解析這個文件就能夠獲得插件信息,代碼以下:
flutter_copy_packages() {
echo "================================="
echo "Start copy flutter app plugin"
local flutter_plugin_registrant="FlutterPluginRegistrant"
local flutter_plugin_registrant_path=".ios/Flutter/${flutter_plugin_registrant}"
echo "copy 'flutter_plugin_registrant' from '${flutter_plugin_registrant_path}' to '${PRODUCT_PATH}/${flutter_plugin_registrant}'"
cp -rf -- "${flutter_plugin_registrant_path}" "${PRODUCT_PATH}/${flutter_plugin_registrant}"
local flutter_plugin=".flutter-plugins"
if [ -e $flutter_plugin ]; then
OLD_IFS="$IFS"
IFS="="
cat ${flutter_plugin} | while read plugin; do
local plugin_info=($plugin)
local plugin_name=${plugin_info[0]}
local plugin_path=${plugin_info[1]}
if [ -e ${plugin_path} ]; then
local plugin_path_ios="${plugin_path}ios"
if [ -e ${plugin_path_ios} ]; then
if [ -s ${plugin_path_ios} ]; then
echo "copy plugin 'plugin_name' from '${plugin_path_ios}' to '${PRODUCT_PATH}/${plugin_name}'"
cp -rf ${plugin_path_ios} "${PRODUCT_PATH}/${plugin_name}"
fi
fi
fi
done
IFS="$OLD_IFS"
fi
echo "Finish copy flutter app plugin"
}
複製代碼
###(4) 將產物同步到保留產物的服務器
通過上面的幾個步驟後會生成一個產物目錄,這個目錄下會有幾個二級目錄,每一個二級目錄裏都包含一個 podspec 文件。 也就是說這個產物目錄裏存放的就是 cocoapods 庫,將目錄拷貝到 Native 工程,而後用 pod 'pod_name', :path=>'xx/xxx' 的形式引用就能夠了。 有了產物後咱們須要一個存放產物的地方, 你們能夠去這個地方下載,這一步比較靈活,能夠選擇將產物放在git倉庫、http服務器、ftp服務器等。我最終選擇將產物壓縮成 zip 上傳到 Maven 上,緣由是爲了和 Android Flutter 產物放在一個地方,而且 Maven 已成作好的產物版本管理。 Maven上傳代碼比較簡單,這裏再也不贅述,有興趣能夠到文末的github倉庫查看代碼。 Flutter 工程版本設置是在工程目錄下的 'pubspec.yaml' 文件,打包腳本讀取這個文件來肯定產物的版本。 最後這個腳本使用方式爲 ./build_ios.h -m debug ./build_ios.h -m release,上文中沒有提到的一點是隻有 release 模式編譯的包纔會上傳的服務器,debug 只是編譯到產物目錄。 ##第二步 Native 依賴 Flutter 產物
這部分咱們須要實現獲取指定版本 Flutter 工程 release 產物,並集成到 Native 項目,並保留能夠調試 Flutter 工程的能力。 也是來拆分一下腳本流程: 獲取 Flutter 工程產物 獲取 release 產物 獲取 debug 產物 經過 pod 引入 Flutter 工程產物 ###(1) 獲取 Flutter 工程產物 上文說到只有 release 產物放在了產物服務器上,debug 只是編譯到產物目錄。不上傳 debug 的緣由是,debug 階段就是開發階段,舉個不太恰當的例子:哪有開發階段就把包上傳 app store 的? 也就表明這 release 的產物和 debug 的產物獲取邏輯不同,而且咱們的腳本支持兩種方式的切換,因此在 Podfile 添加以下代碼:
# 設置要引入的 flutter app 的版本
FLUTTER_APP_VERSION="1.1.1"
# 是否進行調試 flutter app,
# 爲true時FLUTTER_APP_VERSION配置失效,下面的三項配置生效
# 爲false時FLUTTER_APP_VERSION配置生效,下面的三項配置失效
FLUTTER_DEBUG_APP=false
# Flutter App git地址,從git拉取的內容放在當前工程目錄下的.flutter/app目錄
# 若是指定了FLUTTER_APP_PATH,則此配置失效
FLUTTER_APP_URL="git:/xxxx.git"
# flutter git 分支,默認爲master
# 若是指定了FLUTTER_APP_PATH,則此配置失效
FLUTTER_APP_BRANCH="master"
# flutter本地工程目錄,絕對路徑或者相對路徑,若是有值則git相關的配置無效
FLUTTER_APP_PATH="../my_flutter"
eval(File.read(File.join(__dir__, 'flutterhelper.rb')), binding)
複製代碼
Podfile 其實就是 Ruby 代碼,上面幾個由大寫字母組成的變量是全局變量,最後一句代碼的做用爲讀取'flutterhelper.rb'裏的代碼並執行,在'flutterhelper.rb'裏能夠獲取到上面定義的全局變量,根據這幾個變量作不一樣的操做,其中選擇使用 release 仍是 debug 的代碼以下:
if FLUTTER_DEBUG_APP.nil? || FLUTTER_DEBUG_APP == false
# 使用 flutter release 模式
puts "開始安裝 release mode flutter app"
install_release_flutter_app()
else
# 存在debug配置,使用 flutter debug 模式
puts "開始安裝 debug mode flutter app"
install_debug_flutter_app()
end
複製代碼
install_release_flutter_app爲操做 release 產物的函數,install_debug_flutter_app爲操做 debug 產物的函數。 處理 release 模式主要就是獲取 release 產物,代碼以下:
# 安裝正式環境環境app
def install_release_flutter_app
if FLUTTER_APP_VERSION.nil?
raise "Error: 請在 Podfile 裏設置要安裝的 Flutter app 版本 ,例如:FLUTTER_APP_VERSION='1.0.0'"
else
puts "當前安裝的 flutter app 版本爲 #{FLUTTER_APP_VERSION}"
end
# 存放產物的目錄
flutter_release_path = File.expand_path('.flutter_release')
# 是否已經存在當前版本的產物
has_version_file = true
if !File.exist? flutter_release_path
FileUtils.mkdir_p(flutter_release_path)
has_version_file = false
end
# 存放當前版本產物的目錄
flutter_release_version_path = File.join(flutter_release_path, FLUTTER_APP_VERSION)
if !File.exist? flutter_release_version_path
FileUtils.mkdir_p(flutter_release_version_path)
has_version_file = false
end
# 產物包
flutter_package = "flutter.zip"
flutter_release_zip_file = File.join(flutter_release_version_path, flutter_package)
if !File.exist? flutter_release_zip_file
has_version_file = false
end
# 產物包下載完成標誌
flutter_package_downloaded = File.join(flutter_release_version_path, "download.ok")
if !File.exist? flutter_package_downloaded
has_version_file = false
end
if has_version_file == true
# 解壓
flutter_package_path = unzip_release_flutter_app(flutter_release_version_path, flutter_release_zip_file)
# 開始安裝
install_release_flutter_app_pod(flutter_package_path)
else
# 刪除老文件
FileUtils.rm_rf(flutter_release_zip_file)
# 刪除標誌物
FileUtils.rm_rf(flutter_package_downloaded)
# 下載
download_release_flutter_app(FLUTTER_APP_VERSION, flutter_release_zip_file, flutter_package_downloaded)
# 解壓
flutter_package_path = unzip_release_flutter_app(flutter_release_version_path, flutter_release_zip_file)
# 開始安裝
install_release_flutter_app_pod(flutter_package_path)
end
end
複製代碼
unzip_release_flutter_app爲解壓zip格式產物的函數,download_release_flutter_app爲從 Maven 下載產物的函數,這兩個比較簡單,詳細代碼請看文末 github 倉庫。install_release_flutter_app_pod爲經過 pod 將產物添加到 Native 的函數,後面會詳細介紹。
處理 debug 模式的操做爲,獲取 Flutter 工程源代碼,執行 build_ios.sh -m debug 進行打包,而後獲得 debug 產物目錄,詳細代碼以下:
# 安裝開發環境app
def install_debug_flutter_app
puts "若是是第一次運行開發環境Flutter項目,此過程可能會較慢"
puts "請耐心等️待☕️️️️️☕️☕️\n"
# 默認Flutter App 目錄
flutter_application_path = __dir__ + "/.flutter/app"
flutter_application_url = ""
flutter_application_branch = 'master'
# 指定了FLUTTER_APP_PATH就用本地代碼,複製從git拉取
if FLUTTER_APP_PATH != nil
File.expand_path(FLUTTER_APP_PATH)
if File.exist?(FLUTTER_APP_PATH)
flutter_application_path = FLUTTER_APP_PATH
else
flutter_application_path = File.expand_path(FLUTTER_APP_PATH)
if !File.exist?(flutter_application_path)
raise "Error: #{FLUTTER_APP_PATH} 地址不存在!"
end
end
puts "\nFlutter App路徑: "+flutter_application_path
else
if FLUTTER_APP_URL != nil
flutter_application_url = FLUTTER_APP_URL
if FLUTTER_APP_BRANCH != nil
flutter_application_branch = FLUTTER_APP_BRANCH
end
else
raise "Error: 請在'Podfile'裏增長Flutter App git地址配置,配置格式請查看'flutterhelper.rb'文件"
end
puts "\n拉取 Flutter App 代碼"
puts "Flutter App路徑: "+flutter_application_path
update_flutter_app(flutter_application_path, flutter_application_url, flutter_application_branch)
end
puts "\n編譯 Flutter App"
# PUB_HOSTED_URL FLUTTER_STORAGE_BASE_URL 爲了加快速度,使用國內鏡像地址
`export PUB_HOSTED_URL=https://pub.flutter-io.cn && \
export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn && \
cd #{flutter_application_path} && \
#{flutter_application_path}/build_ios.sh -m debug`
if $?.to_i == 0
flutter_package_path = "#{flutter_application_path}/.build_ios/debug/product"
# 開始安裝
install_release_flutter_app_pod(flutter_package_path)
else
raise "Error: 編譯 Flutter App失敗"
end
end
複製代碼
update_flutter_app爲從 git 拉取代碼的函數也不贅述,詳情見文末 github 倉庫。
###(2) 經過 pod 引入 Flutter 工程產物
上文兩個函數執行完成後,就獲得了產物的存放目錄,下面只須要引入到 Native 倉庫就能夠了,也就是install_release_flutter_app_pod函數,從代碼以下:
# 將 Flutter app 經過 pod 安裝
def install_release_flutter_app_pod(product_path)
if product_path.nil?
raise "Error: 無效的 flutter app 目錄"
end
puts "將 flutter app 經過 pod 導入到 工程"
Dir.foreach product_path do |sub|
if sub.eql?('.') || sub.eql?('..')
next
end
sub_abs_path = File.join(product_path, sub)
pod sub, :path=>sub_abs_path
end
post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['ENABLE_BITCODE'] = 'NO'
end
end
end
end
複製代碼
若是要修改 release 產物版本,則設置FLUTTER_APP_VERSION。 若是想要 debug flutter,則設置 FLUTTER_DEBUG_APP=true,若是調試本地代碼則設置 FLUTTER_APP_PATH="../my_flutter",負責將 FLUTTER_APP_PATH 註釋掉,配置 FLUTTER_APP_URL FLUTTER_APP_BRANCH。
##4、總結
對照上文中提到的對混合工程的要求,總結一下:
Flutter 工程徹底不依賴 Native 工程,而是經過 'build_ios.sh' 腳本進行編譯打包;
經過 pod 引入 Flutter 工程對 Native 也沒有浸入,不要在 Native 工程裏增長 Flutter 打包腳本;
Native 開發工程師只須要執行 pod install 全部的 Flutter 依賴就都加入進工程,不須要工程師配置 Flutter 開發環境;也不影響 Native 打包;
也保留了本地調試 Flutter 工程的功能;
文章來源於網絡 若有侵權請及時聯繫本人刪除