国产亚洲精品福利在线无卡一,国产精久久一区二区三区,亚洲精品无码国模,精品久久久久久无码专区不卡

當(dāng)前位置: 首頁(yè) > news >正文

網(wǎng)站更新中打開免費(fèi)百度啊

網(wǎng)站更新中,打開免費(fèi)百度啊,現(xiàn)在湖南疫情嚴(yán)重嗎,門窗營(yíng)銷型網(wǎng)站前言 幾個(gè)月前 Compose Multiplatform 的 iOS 支持就宣布進(jìn)入了 Alpha 階段,這意味著它已經(jīng)具備了一定的可用性。 在它發(fā)布 Alpha 的時(shí)候,我就第一時(shí)間嘗鮮,但是只是淺嘗輒止,沒有做過多的探索,最近恰好有點(diǎn)時(shí)間&…

前言

幾個(gè)月前 Compose Multiplatform 的 iOS 支持就宣布進(jìn)入了 Alpha 階段,這意味著它已經(jīng)具備了一定的可用性。

在它發(fā)布 Alpha 的時(shí)候,我就第一時(shí)間嘗鮮,但是只是淺嘗輒止,沒有做過多的探索,最近恰好有點(diǎn)時(shí)間,于是我又重新開始學(xué)習(xí) Compose Multiplatform ,并且嘗試移植我已有的項(xiàng)目使其支持 iOS,并且將移植過程整理記錄了下來,即為本文。

這次移植我選擇的依舊是這個(gè)使用 Compose 寫的計(jì)算器項(xiàng)目 calculator-Compose-MultiPlatform 。本來這次我想著移植一個(gè)涉及技術(shù)稍微多一點(diǎn)的項(xiàng)目的比如這個(gè) githubAppByCompose,但是我仔細(xì)研究了一下,畢竟現(xiàn)在 Compose Multiplatform 還處于實(shí)驗(yàn)階段,好多對(duì)應(yīng)的功能和庫(kù)都還沒有,所以只能選擇移植前者。

對(duì)于這個(gè)計(jì)算器項(xiàng)目,最開始只是一個(gè)使用 Compose 實(shí)現(xiàn)的純 Android 項(xiàng)目,后來移植到了支持 Android 和 桌面 端,所以其實(shí)現(xiàn)在再給它添加上 iOS 支持,也算是補(bǔ)齊了最后一個(gè)平臺(tái)了,哈哈。

在開始閱讀本文之前,我會(huì)假設(shè)你已經(jīng)了解并且知道 Compsoe 的基本使用方法。

為了更好的理解本文,可能需要首先閱讀這兩篇前置文章:

  1. 【譯】快速開始 Compose 跨平臺(tái)項(xiàng)目
  2. Kotlin & Compose Multiplatform 跨平臺(tái)(Android端、桌面端)開發(fā)實(shí)踐之使用 SQLDelight 將數(shù)據(jù)儲(chǔ)存至數(shù)據(jù)庫(kù)

前言的最后看一下運(yùn)行效果:

Android 端:

2.png

ios 端:

4.png

桌面端:

3.png

開始移植

準(zhǔn)備工作

首當(dāng)其沖,我們需要為 iOS 的支持更改編譯配置文件和添加對(duì)應(yīng)的平臺(tái)特定代碼。

在我的這個(gè)項(xiàng)目中,我通過以下幾個(gè)步驟為其添加了對(duì) iOS 的支持:

更改共享代碼模塊名稱

把公用代碼模塊由 common 改為 shared ,其實(shí)這里不用改也行,只是模板配置文件中寫的 iOS 使用的公用代碼路徑是 shared ,但是直接改模塊名比改配置文件簡(jiǎn)單多了,所以我們直接把模塊名改了就好了。

改完之后切記要檢查一下其他模塊引用的名字是否改了,以及注意檢查一下包名是否正確。

添加 native.cocoapods 插件

shared 模塊的 build.gradle.kts 文件的 plugins 增加 native.cocoapods 插件:

plugins {kotlin("native.cocoapods")// ……
}
添加 cocoapods 配置

shared 模塊的 build.gradle.kts 文件的 kotlin 下增加 cocoapods 相應(yīng)的配置:

kotlin {// ……iosX64()iosArm64()iosSimulatorArm64()cocoapods {version = "1.0.0"summary = "Some description for the Shared Module"homepage = "Link to the Shared Module homepage"ios.deploymentTarget = "14.1"podfile = project.file("../iosApp/Podfile")framework {baseName = "shared"isStatic = true}extraSpecAttributes["resources"] = "['src/commonMain/resources/**', 'src/iosMain/resources/**']"}// ……
}
配置 iOS 源集

shared 模塊的 build.gradle.kts 文件的 kotlin 中的 sourceSets 下增加 iOS 的源集配置:

kotlin {// ……sourceSets {// ……val iosX64Main by gettingval iosArm64Main by gettingval iosSimulatorArm64Main by gettingval iosMain by creating {dependsOn(commonMain)iosX64Main.dependsOn(this)iosArm64Main.dependsOn(this)iosSimulatorArm64Main.dependsOn(this)}}
}
添加其他插件

在項(xiàng)目根目錄下的 settings.gradle.kts 文件的 pluginManagement 中的 plugins 增加插件配置:

pluginManagement {//……plugins {kotlin("jvm").version(extra["kotlin.version"] as String)// ……}
}
添加 iOS 項(xiàng)目文件

直接把官方模板中的 iosAPP 模塊整個(gè)目錄復(fù)制到項(xiàng)目根目錄來。

需要注意的是,其實(shí)這個(gè) iosAPP 目錄并不是一個(gè) idea 模塊,而是一個(gè) Xcode 項(xiàng)目。但是目前暫時(shí)不需要知道這是什么,只需要把相應(yīng)的文件整個(gè)復(fù)制到自己項(xiàng)目中就行了。

然后把官方模版中的 sahred -> iosMain 文件夾整個(gè)復(fù)制到 我們項(xiàng)目的 sahred 模塊根目錄中。

適配代碼

在這一節(jié)中,主要需要適配的有兩種類型的代碼:

一是之前就已經(jīng)在項(xiàng)目中聲明了的 expect 函數(shù),需要為 iOS 也加上對(duì)應(yīng)的 actual 函數(shù)。

二是需要將原本使用到的 jvm 相關(guān)或者說所有使用 java 實(shí)現(xiàn)的庫(kù)和相關(guān)代碼都需要重新編寫或適配。

因?yàn)椴煌?Android 和 桌面端,kotlin 最終會(huì)被編譯成 jvm 代碼,在 iOS 端,kotlin 會(huì)編譯成 native 代碼,所以所有使用 java 寫的代碼將無法再使用。

這也就是我前言中說的為啥不選擇移植更復(fù)雜的項(xiàng)目的原因,就是因?yàn)槲以谄渲幸昧舜罅康氖褂?java 編寫的第三方庫(kù),而這些第三方庫(kù)又暫時(shí)沒有使用純 kotlin 實(shí)現(xiàn)的可用替代品。

下面,我們就開始適配代碼。

更改入口

為了保證三端界面一致,我們將原本的UI界面再額外的抽出一個(gè)統(tǒng)一的入口函數(shù) APP(),將其放到 shared 模塊的 common 包下:

@Composable
fun APP(standardChannelTop: Channel<StandardAction>? = null,programmerChannelTop: Channel<ProgrammerAction>? = null,
) {val homeChannel = remember { Channel<HomeAction>() }val homeFlow = remember(homeChannel) { homeChannel.consumeAsFlow() }val homeState = homePresenter(homeFlow)val standardChannel = standardChannelTop ?: remember { Channel() }val standardFlow = remember(standardChannel) { standardChannel.consumeAsFlow() }val standardState = standardPresenter(standardFlow)val programmerChannel = programmerChannelTop ?: remember { Channel() }val programmerFlow = remember(programmerChannel) { programmerChannel.consumeAsFlow() }val programmerState = programmerPresenter(programmerFlow)CalculatorComposeTheme {val backgroundColor = MaterialTheme.colors.backgroundSurface(modifier = Modifier.fillMaxSize(),color = backgroundColor) {HomeScreen(homeChannel,homeState,standardChannel,standardState,programmerChannel,programmerState)}}
}

并且,因?yàn)椴煌脚_(tái)需要差異化實(shí)現(xiàn)部分功能,以及目前我還沒找到一個(gè)好使的支持跨平臺(tái)的依賴注入庫(kù),所以我索性將所有 控制(channel) 和 狀態(tài)(state) 都提升到了最頂層,作為參數(shù)傳遞給下面的 Compose 函數(shù)。

然后,更改三端各自的入口函數(shù):

Android (android 模塊下的 MainActivity.kt 文件)
class MainActivity : AppCompatActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContent {APP()}}
}
desktop (dektop 模塊下的 Main.kt 文件)
fun main() = application {val state = if (Config.boardType.value == KeyboardTypeStandard) {rememberWindowState(size = defaultWindowSize, position = defaultWindowPosition)} else {rememberWindowState(size = landWindowSize, position = defaultWindowPosition)}val standardChannel = remember { Channel<StandardAction>() }val programmerChannel = remember { Channel<ProgrammerAction>() }Window(onCloseRequest = ::exitApplication,state = state,title = Text.AppName,icon = painterResource("icon.png"),alwaysOnTop = Config.isFloat.value,onKeyEvent = {if (isKeyTyped(it)) {val btnIndex = asciiCode2BtnIndex(it.utf16CodePoint)if (btnIndex != -1) {if (Config.boardType.value == KeyboardTypeStandard) {standardChannel.trySend(StandardAction.ClickBtn(btnIndex))}else {programmerChannel.trySend(ProgrammerAction.ClickBtn(btnIndex))}}}true}) {APP()}
}
iOS ( shared模塊 下的 main.ios.kt 文件)
fun MainViewController() = ComposeUIViewController {APP()
}

注意,不同于其他平臺(tái),iOS 的入口函數(shù)在 shared模塊 中。

當(dāng)然,你要是想直接改 iosAPP 目錄中的代碼,那也不是不行,只是對(duì)于我們安卓開發(fā)來說,還是直接改 shared 更方便點(diǎn)。

實(shí)現(xiàn) iOS 的 平臺(tái)代碼

之前我們的項(xiàng)目中有幾個(gè)地方的實(shí)現(xiàn)依賴于平臺(tái),所以寫了一些 expect 函數(shù),現(xiàn)在我們需要給 iOS 實(shí)現(xiàn)對(duì)應(yīng)的 actual 函數(shù)。

首先在 shared 模塊的 iosMain 包中創(chuàng)建一個(gè)包路徑,保持和 commonMainexpect 函數(shù)包一致:

1.png

注意: 包路徑一定要一致,不然會(huì)編譯失敗,我就在這里踩了坑,沒注意到包名不一樣, debug 了好久。

這個(gè)項(xiàng)目中的平臺(tái)差異函數(shù)主要有四個(gè):

控制振動(dòng)

因?yàn)槲覍?duì) iOS 一竅不通,所以不知道怎么寫,索性直接留空了:

actual fun vibrateOnClick() {}actual fun vibrateOnError() {}actual fun vibrateOnClear() {}actual fun vibrateOnEqual() {}

控制屏幕旋轉(zhuǎn)和顯示小窗

這里同上,不知道怎么寫,直接留空:

actual fun showFloatWindows() {}actual fun changeKeyBoardType(changeTo: Int) {}

數(shù)據(jù)庫(kù)(sqldelight)

actual fun createDriver(): SqlDriver {return NativeSqliteDriver(HistoryDatabase.Schema, "history.db")
}

關(guān)于使用 sqldelight 的詳細(xì)介紹,可以看前言中的前置文章了解。

其實(shí)這里這樣寫是編譯不通過的,因?yàn)檫€沒加 sqldelight 依賴,下面介紹一下怎么加依賴,這里又是一個(gè)大坑。

給 iOS 添加 sqldelight 支持

首先,在 shared 模塊下的 build.gradle.kts 文件中的 kotlin -> sourceSets -> iosMain 添加 sqldelight 的 驅(qū)動(dòng)依賴:

kotlin {// ……sourceSets {// ……val iosMain by creating {// ……dependencies {implementation("app.cash.sqldelight:native-driver:2.0.0")}}}
}

此時(shí)如果你直接 sync gradle 后編譯運(yùn)行,大概率會(huì)報(bào)錯(cuò):

Undefined symbols for architecture arm64:
"_sqlite3_bind_text16", referenced from:
_SQLiter_SQLiteStatement_nativeBindString in app(combined.o)
"_sqlite3_bind_int64", referenced from:
_SQLiter_SQLiteStatement_nativeBindLong in app(combined.o)
"_sqlite3_last_insert_rowid", referenced from:
_SQLiter_SQLiteStatement_nativeExecuteForLastInsertedRowId in app(combined.o)
"_sqlite3_reset", referenced from:
_SQLiter_SQLiteConnection_nativeResetStatement in app(combined.o)
"_sqlite3_changes", referenced from:
_SQLiter_SQLiteStatement_nativeExecuteForChangedRowCount in app(combined.o)
_SQLiter_SQLiteStatement_nativeExecuteForLastInsertedRowId in app(combined.o)
"_sqlite3_open_v2", referenced from:
_SQLiter_SQLiteConnection_nativeOpen in app(combined.o)
"_sqlite3_db_config", referenced from:
_SQLiter_SQLiteConnection_nativeOpen in app(combined.o)
"_sqlite3_busy_timeout", referenced from:
_SQLiter_SQLiteConnection_nativeOpen in app(combined.o)
"_sqlite3_trace", referenced from:
_SQLiter_SQLiteConnection_nativeOpen in app(combined.o)
"_sqlite3_bind_parameter_index", referenced from:
_SQLiter_SQLiteConnection_nativeBindParameterIndex in app(combined.o)
"_sqlite3_column_bytes", referenced from:
_SQLiter_SQLiteConnection_nativeColumnGetString in app(combined.o)
_SQLiter_SQLiteConnection_nativeColumnGetBlob in app(combined.o)
"_sqlite3_finalize", referenced from:
_SQLiter_SQLiteStatement_nativeFinalizeStatement in app(combined.o)
"_sqlite3_column_text", referenced from:
_SQLiter_SQLiteConnection_nativeColumnGetString in app(combined.o)
"_sqlite3_column_name", referenced from:
_SQLiter_SQLiteConnection_nativeColumnName in app(combined.o)
"_sqlite3_bind_double", referenced from:
_SQLiter_SQLiteStatement_nativeBindDouble in app(combined.o)
"_sqlite3_profile", referenced from:
_SQLiter_SQLiteConnection_nativeOpen in app(combined.o)
"_sqlite3_close", referenced from:
_SQLiter_SQLiteConnection_nativeClose in app(combined.o)
_SQLiter_SQLiteConnection_nativeOpen in app(combined.o)
"_sqlite3_prepare16_v2", referenced from:
_SQLiter_SQLiteConnection_nativePrepareStatement in app(combined.o)
"_sqlite3_column_type", referenced from:
_SQLiter_SQLiteConnection_nativeColumnIsNull in app(combined.o)
_SQLiter_SQLiteConnection_nativeColumnType in app(combined.o)
"_sqlite3_column_count", referenced from:
_SQLiter_SQLiteConnection_nativeColumnCount in app(combined.o)
"_sqlite3_bind_blob", referenced from:
_SQLiter_SQLiteStatement_nativeBindBlob in app(combined.o)
"_sqlite3_db_readonly", referenced from:
_SQLiter_SQLiteConnection_nativeOpen in app(combined.o)
"_sqlite3_column_int64", referenced from:
_SQLiter_SQLiteConnection_nativeColumnGetLong in app(combined.o)
"_sqlite3_bind_null", referenced from:
_SQLiter_SQLiteStatement_nativeBindNull in app(combined.o)
"_sqlite3_extended_errcode", referenced from:
android::throw_sqlite3_exception(sqlite3*) in app(combined.o)
android::throw_sqlite3_exception(sqlite3*, char const*) in app(combined.o)
"_sqlite3_column_double", referenced from:
_SQLiter_SQLiteConnection_nativeColumnGetDouble in app(combined.o)
"_sqlite3_column_blob", referenced from:
_SQLiter_SQLiteConnection_nativeColumnGetBlob in app(combined.o)
"_sqlite3_step", referenced from:
_SQLiter_SQLiteConnection_nativeStep in app(combined.o)
_SQLiter_SQLiteStatement_nativeExecute in app(combined.o)
_SQLiter_SQLiteStatement_nativeExecuteForChangedRowCount in app(combined.o)
_SQLiter_SQLiteStatement_nativeExecuteForLastInsertedRowId in app(combined.o)
"_sqlite3_clear_bindings", referenced from:
_SQLiter_SQLiteConnection_nativeClearBindings in app(combined.o)
"_sqlite3_errmsg", referenced from:
android::throw_sqlite3_exception(sqlite3*) in app(combined.o)
android::throw_sqlite3_exception(sqlite3*, char const*) in app(combined.o)
ld: symbol(s) not found for architecture arm64
clang: error: linker command failed with exit code 1 (use -v to see invocation)

這是因?yàn)?ios 的 Xcode 項(xiàng)目沒有添加 sqlite 依賴,我們還需要為 ios 單獨(dú)添加 sqlite 依賴。

ios 使用的是 cocoapods 進(jìn)行依賴管理,我們需要使用 pod 添加依賴。

我們有兩種選擇:

一是在 shared 模塊的 build.gradle.kts 中相應(yīng)的位置添加 pod 依賴配置。

二是直接在 pod 配置文件中添加。

這里我們就選擇直接改 pod 的配置文件。

打開項(xiàng)目根目錄下的 iosAPP 目錄中的 Podfile 文件,在其中添加 sqlite3 依賴:

target 'iosApp' do# ……pod 'sqlite3', '~> 3.42.0'# ……end

添加完記得需要 sync 一下 gradle。

此時(shí)再編譯運(yùn)行,大概率還是會(huì)報(bào)錯(cuò):

ld: file not found: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/arc/libarclite_iphonesimulator.a

不用擔(dān)心,再在剛才的配置文件中加上這么一段:

# iosApp's podfile
post_install do |installer|installer.pods_project.targets.each do |target|target.build_configurations.each do |config|config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '14.1'endend
end

此時(shí)應(yīng)該就不會(huì)有任何問題了。

適配 jvm 相關(guān)代碼

正如我們?cè)谏弦还?jié)所說,由于 iOS 使用 native 代碼,所以項(xiàng)目中就不能再使用 java 代碼,包括引用的第三方庫(kù)也是。

在我這個(gè)項(xiàng)目中涉及到需要適配的主要有兩個(gè)地方。一個(gè)是進(jìn)制轉(zhuǎn)換時(shí)使用到了 java 的 Long 類的方法;另一個(gè)就是運(yùn)算時(shí)使用的是 BigInteger BigDecimal 。

進(jìn)制轉(zhuǎn)換

之前的代碼使用的是 java 中的 java.lang.Long.toXXXString 。

這里適配起來其實(shí)很簡(jiǎn)單,要么自己使用 kotlin 實(shí)現(xiàn)一個(gè)進(jìn)制轉(zhuǎn)換工具類,要么就像我一樣,直接把 Long.java 中需要的部分 CV 一下,然后使用 Android studio 的 java 轉(zhuǎn) kotlin 一鍵轉(zhuǎn)換就行了。

下面就是我轉(zhuǎn)好的工具類:

package com.equationl.common.utilsimport kotlin.math.maxobject LongUtil {val digits = charArrayOf('0', '1', '2', '3', '4', '5','6', '7', '8', '9', 'a', 'b','c', 'd', 'e', 'f', 'g', 'h','i', 'j', 'k', 'l', 'm', 'n','o', 'p', 'q', 'r', 's', 't','u', 'v', 'w', 'x', 'y', 'z')fun toBinaryString(i: Long): String {return toUnsignedString0(i, 1)}fun toHexString(i: Long): String {return toUnsignedString0(i, 4)}fun toOctalString(i: Long): String {return toUnsignedString0(i, 3)}fun toUnsignedString0(`val`: Long, shift: Int): String {// assert shift > 0 && shift <=5 : "Illegal shift value";val mag: Int = Long.SIZE_BITS - numberOfLeadingZeros(`val`)val chars: Int = max((mag + (shift - 1)) / shift, 1)//if (COMPACT_STRINGS) {val buf = ByteArray(chars)formatUnsignedLong0(`val`, shift, buf, 0, chars)return buf.map { it.toInt().toChar() }.toCharArray().concatToString()
//        } else {
//            val buf = ByteArray(chars * 2)
//            java.lang.Long.formatUnsignedLong0UTF16(`val`, shift, buf, 0, chars)
//            return String(buf, UTF16)
//        }}private fun formatUnsignedLong0(`val`: Long,shift: Int,buf: ByteArray,offset: Int,len: Int) {var `val` = `val`var charPos = offset + lenval radix = 1 shl shiftval mask = radix - 1do {buf[--charPos] = digits[`val`.toInt() and mask].code.toByte()`val` = `val` ushr shift} while (charPos > offset)}fun numberOfLeadingZeros(i: Long): Int {val x = (i ushr 32).toInt()return if (x == 0) 32 + numberOfLeadingZeros(i.toInt()) else numberOfLeadingZeros(x)}fun numberOfLeadingZeros(i: Int): Int {// HD, Count leading 0'svar i = iif (i <= 0) return if (i == 0) 32 else 0var n = 31if (i >= 1 shl 16) {n -= 16i = i ushr 16}if (i >= 1 shl 8) {n -= 8i = i ushr 8}if (i >= 1 shl 4) {n -= 4i = i ushr 4}if (i >= 1 shl 2) {n -= 2i = i ushr 2}return n - (i ushr 1)}
}

然后更改我們的代碼中使用到的地方即可,例如:

Long.toBinaryString 改為 LongUtil.toBinaryString(long) 。

記得把導(dǎo)入的包也改了:

import java.lang.Long 改為 import com.equationl.common.utils.LongUtil

當(dāng)然,如果你的工具類直接取名叫 Long 的話,那么調(diào)用代碼就不用改了,改導(dǎo)入包就行了。

BigInteger 和 BigDecimal

接下來就是 BigInteger 和 BigInteger,同樣的思路,我們可以選擇自己使用 kotlin 寫一個(gè)功能相同的工具類,但是顯然,這兩個(gè)類可不同于進(jìn)制轉(zhuǎn)換,它涉及到的代碼量可要大多了。

好在已經(jīng)有大神寫好了純 kotlin 的支持跨平臺(tái)的 BigInteger 和 BigDecimal: kotlin-multiplatform-bignum 。我們只需要簡(jiǎn)單的引用它就可以了。

shared 模塊下的 build.gradle.kts 文件中的 kotlin -> sourceSets -> commonMain -> dependencies 添加依賴

kotlin {sourceSets {val commonMain by getting {dependencies {implementation("com.ionspin.kotlin:bignum:0.3.8")}}}
}

sync gradle 后,依次修改項(xiàng)目中使用到 BigInteger 和 BigDecimal 地方的代碼即可。

需要注意的是,這個(gè)庫(kù)的 API 和 java 的 BigInteger 以及 BigDecimal 并非完全一致,因此需要我們逐個(gè)檢查并修改。

例如,在 java 的 BigDecimal 中,除法的 API 是: divide(BigDecimal divisor, int scale, RoundingMode roundingMode)

而在這個(gè)庫(kù)中則變?yōu)榱?divide(other: BigDecimal, decimalMode: DecimalMode? = null)

除此之外,還有一些小地方的代碼可能引用的是 java 代碼,這里就不再贅述了,按照上述兩種思路逐個(gè)適配即可。

總結(jié)

自此,我們的項(xiàng)目就完全移植到了完整形態(tài)的 Compose Multiplatform 中了!現(xiàn)在它已經(jīng)完全支持 Android、iOS 和 desktop 了!

不知道你們有沒有發(fā)現(xiàn),在全文中,我?guī)缀醵际窃谡f怎么適配和移植邏輯代碼,并沒有說到有關(guān) UI 的代碼。

哈哈,不是因?yàn)槲彝浾f了,而是因?yàn)?Compose Multiplatform 代碼真的做到了一套代碼,多平臺(tái)通用。新增加 iOS 支持完全不用動(dòng) UI 部分的代碼。

完整項(xiàng)目代碼: calculator-Compose-MultiPlatform

http://m.aloenet.com.cn/news/32472.html

相關(guān)文章:

  • 什么網(wǎng)站做的最好寧德市人民政府
  • 沈陽(yáng)網(wǎng)站建設(shè)小志網(wǎng)站的推廣優(yōu)化
  • 重慶營(yíng)銷網(wǎng)站建設(shè)平臺(tái)app001推廣平臺(tái)官網(wǎng)
  • 網(wǎng)站域名解析錯(cuò)誤怎么辦seo與sem的關(guān)系
  • 網(wǎng)站建設(shè)技術(shù)規(guī)范河南省鄭州市金水區(qū)
  • 福州官網(wǎng)網(wǎng)站建設(shè)谷歌seo網(wǎng)站推廣怎么做優(yōu)化
  • 網(wǎng)頁(yè)公正流程有名的seo外包公司
  • 做網(wǎng)站推廣的方法58網(wǎng)絡(luò)推廣
  • 手機(jī)網(wǎng)站建設(shè)浩森宇特seo建站優(yōu)化
  • 鄭州外貿(mào)建站做推廣
  • 國(guó)家建設(shè)協(xié)會(huì)官方網(wǎng)站百度瀏覽器網(wǎng)頁(yè)版入口
  • 溫州微信網(wǎng)站開發(fā)抖音搜索seo軟件
  • php地方門戶新聞網(wǎng)站源碼卡點(diǎn)視頻軟件下載
  • 不懂編程如何做網(wǎng)站萬能推廣app
  • 怎么把網(wǎng)站做火網(wǎng)絡(luò)營(yíng)銷管理系統(tǒng)
  • 安平縣外貿(mào)網(wǎng)站建設(shè)正規(guī)微商免費(fèi)推廣軟件
  • 可以做網(wǎng)站的渠道廊坊seo關(guān)鍵詞排名
  • 隨州公司做網(wǎng)站營(yíng)銷案例分析報(bào)告模板
  • 網(wǎng)站建設(shè)一對(duì)一培訓(xùn)seo根據(jù)什么具體優(yōu)化
  • 網(wǎng)站風(fēng)格模板營(yíng)銷策劃的六個(gè)步驟
  • 網(wǎng)站建設(shè)流程策劃方案前端培訓(xùn)哪個(gè)機(jī)構(gòu)靠譜
  • 南通市網(wǎng)站建設(shè)我的完凡科網(wǎng)
  • 建湖做網(wǎng)站尋找鄭州網(wǎng)站優(yōu)化公司
  • 男女做爰高清免費(fèi)網(wǎng)站關(guān)鍵詞代發(fā)包收錄
  • 寶安網(wǎng)站建設(shè)公司968seo培訓(xùn)網(wǎng)的優(yōu)點(diǎn)是
  • 做網(wǎng)站網(wǎng)頁(yè)維護(hù) 手機(jī)App 開發(fā)免費(fèi)打廣告網(wǎng)站
  • 網(wǎng)站托管適合中層管理的培訓(xùn)
  • 做網(wǎng)站是通過怎么掙錢一個(gè)新產(chǎn)品怎么推廣
  • 網(wǎng)頁(yè)設(shè)計(jì) 做網(wǎng)站的代碼制作網(wǎng)站大概多少錢
  • 互聯(lián)網(wǎng)行業(yè)新聞的靠譜網(wǎng)站怎么做屬于自己的網(wǎng)站