知識(shí)
不管是網(wǎng)站,軟件還是小程序,都要直接或間接能為您產(chǎn)生價(jià)值,我們?cè)谧非笃湟曈X(jué)表現(xiàn)的同時(shí),更側(cè)重于功能的便捷,營(yíng)銷的便利,運(yùn)營(yíng)的高效,讓網(wǎng)站成為營(yíng)銷工具,讓軟件能切實(shí)提升企業(yè)內(nèi)部管理水平和效率。優(yōu)秀的程序?yàn)楹笃谏?jí)提供便捷的支持!
您當(dāng)前位置>首頁(yè) » 新聞資訊 » 小程序相關(guān) >
聊聊如何給一個(gè)小程序歷史老項(xiàng)目“減壓”
發(fā)表時(shí)間:2021-2-28
發(fā)布人:葵宇科技
瀏覽次數(shù):60
前言
在日常的工作中,由于業(yè)務(wù)或者工作安排的需要,有時(shí)候需要我們參與到一些曾經(jīng)沒(méi)有接觸過(guò)卻 歷史悠久 的項(xiàng)目當(dāng)中,如果這個(gè)項(xiàng)目創(chuàng)建初期,創(chuàng)建者有很好的 前瞻性,并且嚴(yán)格遵循 code preview 等項(xiàng)目開(kāi)發(fā)工作流,那代碼看上去就會(huì)像是同一個(gè)人寫(xiě)出來(lái)一樣,十分規(guī)范;否則,將會(huì)逐漸淪為一個(gè) 茅坑代碼集合。
接下來(lái)我想分享一下近期對(duì)公司的一個(gè)小程序項(xiàng)目做的一些優(yōu)化工作,會(huì)分別從以下幾個(gè)方面進(jìn)行闡述:
- 項(xiàng)目現(xiàn)狀
- 項(xiàng)目拆解
- 搭建工具平臺(tái)
- 項(xiàng)目地址
項(xiàng)目現(xiàn)狀
1. 子模塊分包不完全,存在子包內(nèi)文件相互引用的情況
2. 個(gè)別沒(méi)有用到的圖片等靜態(tài)資源文件沒(méi)有及時(shí)刪除,導(dǎo)致包體積過(guò)大,無(wú)法生成預(yù)覽碼
3. 測(cè)試同學(xué)反饋小程序測(cè)試流程過(guò)于繁瑣,復(fù)雜,加大測(cè)試工作量和小程序的出錯(cuò)率
項(xiàng)目拆解
因此,需要針對(duì)以上提到的三個(gè)問(wèn)題對(duì)這個(gè)項(xiàng)目進(jìn)行初步的基于項(xiàng)目目錄結(jié)構(gòu)層面上的優(yōu)化(不涉及到項(xiàng)目里面的業(yè)務(wù)代碼,組件等冗余代碼的優(yōu)化)
其實(shí)也很容易理解,當(dāng)你剛接手一個(gè)項(xiàng)目的時(shí)候,想必是先對(duì)這個(gè)項(xiàng)目的目錄結(jié)構(gòu)有一個(gè)總體初步的認(rèn)識(shí)。
一、 “子模塊分包不完全,存在子包內(nèi)文件相互引用的情況”
1. 分析
如果是微信小程序項(xiàng)目,我們可以通過(guò)以下兩種方法去快速了解一個(gè)項(xiàng)目的模塊分包情況:
- 打開(kāi)根目錄下的
app.json
文件,找到subPackages
字段,這就是當(dāng)前項(xiàng)目的所有子模塊數(shù)組集合;(缺點(diǎn):人工肉眼查找,不智能)
// app.json
{
"pages": [],
"subPackages": [
{
"root": "A",
"pages": [
"pages/A-a",
"pages/A-b",
....
]
},
{
"root": "B",
"pages": [
"pages/B-a",
"pages/B-b",
....
]
}
]
}
復(fù)制代碼
- 通過(guò)微信提供的 cli 命令行工具,查看當(dāng)前的分包情況;(優(yōu)點(diǎn):不僅智能,還能查看每個(gè)子模塊壓縮后的包大?。?/li>
cli preview --project f://workspace/rainbow-mp/xinyu
復(fù)制代碼
效果如下:
通過(guò)cli工具分析出來(lái)的結(jié)果,我們可以很明顯看出當(dāng)前項(xiàng)目總共分了哪幾個(gè)子模塊,以及這些子模塊經(jīng)過(guò)微信壓縮工具(實(shí)則微信開(kāi)發(fā)者工具編譯)之后的大小,由此得出的當(dāng)前項(xiàng)目存在的問(wèn)題有如下幾點(diǎn):
-
主包(
main
)體積已經(jīng)超過(guò)了微信規(guī)定的2MB
最大值,無(wú)法生成預(yù)覽碼用于移動(dòng)端測(cè)試(問(wèn)題很嚴(yán)重); -
子包分包不合理,將子包的子目錄作為分包的入口(如:
/daojia/pages
,/deptstore/pages
,/index/pages/userMaterial
,/shopping/pages
),而不是將子包根目錄本身作為拆包的入口,導(dǎo)致其余目錄下的文件統(tǒng)一打包到了主包中,造成主包體積變大;
2. 細(xì)分拆包
換句話說(shuō),如果我們把子包的分包不合理的問(wèn)題給解決了,主包(main
)的體積過(guò)大的問(wèn)題自然而然也就解決了。
定位到問(wèn)題就相當(dāng)于解決了一半。
接下來(lái)就是想辦法把子包根目錄更改為模塊打包的入口,這時(shí)候有人會(huì)說(shuō)了,把 app.json
文件下的subPackages
模塊數(shù)組字段的每個(gè)模塊的root
的值都改成子模塊的根目錄不就完事了嗎?
沒(méi)毛病,做法就是這樣
但是,在修改之前得保證拆出來(lái)子包根目錄下的其余子目錄下的文件并沒(méi)有被別的模塊引用,否則就會(huì)出現(xiàn)文件引用錯(cuò)誤的bug。
因此,大致有以下兩種做法可以參考一下,我采用的是第一種:
-
對(duì)當(dāng)前拆解子包外的其余模塊(包括主包
main
) 進(jìn)行全文件掃描,通過(guò)正則的方式過(guò)濾出require
引用到的文件路徑,進(jìn)而分析是否有子包下的文件被別的模塊引用; -
復(fù)寫(xiě)
require
方法(因?yàn)槲覀冺?xiàng)目中文件引入的方式是require
方式);
這里簡(jiǎn)單說(shuō)明一下我不采用第二種方法的原因:
-
require
方法沒(méi)有掛載在global
全局下(因?yàn)榻酉聛?lái)我需要寫(xiě)腳本在node環(huán)境下運(yùn)行),因此需要重寫(xiě)一個(gè)如myRequire
的自定義函數(shù),然后掛載到global
對(duì)象下,接著全局匹配所有文件的require
字符替換為myRequire
; -
require
是動(dòng)態(tài)引入,也就是說(shuō),可以在js
文件的任意處進(jìn)行引入,寫(xiě)在了小程序的業(yè)務(wù)代碼中,因?yàn)榻酉聛?lái)的腳本文件是運(yùn)行在微信開(kāi)發(fā)者工具以外的環(huán)境,缺失了微信小程序需要的模塊包,會(huì)導(dǎo)致編寫(xiě)的腳本分析文件報(bào)錯(cuò);
3. 編寫(xiě)腳本
接下來(lái)就正式步入編碼階段了,其實(shí)思路比較簡(jiǎn)單,我大致從以下幾點(diǎn)進(jìn)行這次腳本文件的編寫(xiě):
1. 獲取當(dāng)前項(xiàng)目的所有一級(jí)目錄:除去當(dāng)前需要拆解的子包以外的所有一級(jí)目錄都需要進(jìn)行全局文件掃描
* 獲取當(dāng)前路徑下的第一層目錄
* @param {*} path 項(xiàng)目路徑
* @param {*} targetDir 子包的目錄名
*/
const getOwnDirectorys = async(path, targetDir) => {
const dir = await fs.promises.opendir(path)
const result = []
for await (const dirent of dir) {
const isDir = await isDirectory(`${path}/${dirent.name}`)
// 也就是說(shuō),除去子包以外的目錄都需要進(jìn)行全局文件掃描
if (isDir && dirent.name !== targetDir) {
result.push(`${path}/${dirent.name}`)
}
}
return result
}
復(fù)制代碼
2.過(guò)濾出每個(gè)一級(jí)目錄下所有js和json文件
讀取到目錄了,那接下來(lái)自然就是遍歷這些一級(jí)目錄,然后獲取到這些目錄下的所有資源文件,那為什么只是過(guò)濾其中的js
和json
文件出來(lái)呢?
經(jīng)過(guò)一段時(shí)間的接觸之后,我發(fā)現(xiàn):
-
子包的組件存在被別的模塊包引用的情況,而小程序的組件引入主要是通過(guò)
json
文件的usingComponents
字段; -
子包的
js
文件也存在被別的模塊包引用的情況,多數(shù)發(fā)生在一些工具函數(shù),接口調(diào)用文件上;
因此,為了減少掃描文件的數(shù)量和提高效率,先針對(duì)項(xiàng)目中每個(gè)模塊的js
和json
文件進(jìn)行掃描匹配。
const filterJsAndJsonFiles = async (dirItem, filterDirs) => {
const subDir = await fs.promises.opendir(dirItem)
const jsFiles = []
const jsonFiles = []
for await (const dirent of subDir) {
// 不需要分析的目錄直接跳過(guò)
if (!filterDirs.includes(dirent.name)) continue
const currentFiles = getAllFiles(`${PROJECT_NAME}${dirItem}/${dirent.name}`)
// 過(guò)濾若干不同類型的文件數(shù)組
currentFiles.forEach(fileItem => {
const extname = path.extname(fileItem)
if (extname === '.json') {
jsonFiles.push(fileItem)
}
if (extname === '.js') {
jsFiles.push(fileItem)
}
})
}
return {
jsFiles,
jsonFiles,
}
}
復(fù)制代碼
3. 文件查找 & 匹配
到這里,我們已經(jīng)拿到了每個(gè)模塊對(duì)應(yīng)下的所有js
,json
文件,接下來(lái)就需要針對(duì)這些文件進(jìn)行分析了,大致思路分為以下兩點(diǎn):
json
文件分析:讀取文件內(nèi)容,將json
字符串轉(zhuǎn)為json
對(duì)象格式,過(guò)濾出usingComponents
字段,查找匹配出拆解子包的組件;
{
"usingComponents": {
"a": "./A/a",
"b": "../B/b",
"c": "../C/c"
}
}
復(fù)制代碼
js
文件分析:讀取文件內(nèi)容,通過(guò)正則表達(dá)式
過(guò)濾出require
引入的文件字符數(shù)組,從中查找匹配出拆解子包內(nèi)的文件引用;
const a = require('../../a.js')
const b = require('./b.js')
....
復(fù)制代碼
腳本編寫(xiě):
json文件組件引入分析:
/**
* 統(tǒng)計(jì)json文件引入到的組件數(shù)組
* @param {*} jsonFile
*/
const listComponents = (jsonFile) => {
if (!jsonFile) return
const jsonDataStr = fs.readFileSync(jsonFile)
const jsonData = http://www.wxapp-union.com/JSON.parse(jsonDataStr)
const componentList = []
if (jsonData) {
const { usingComponents } = jsonData
for (let key in usingComponents) {
componentList.push({
name: key,
path: usingComponents[key],
filePath: jsonFile,
})
}
}
return componentList
}
復(fù)制代碼
js文件require
引入分析:
const lineReg = /require\s*\([\'\"][\w\W]*[\'\"]\)/g
// 子模塊初始化
moduleResultMap[dirKey] = {
componentImport: [],
fileImport: {},
}
jsFiles.forEach(filePath => {
const fileContent = fs.readFileSync(filePath, 'utf8')
// 為了避免無(wú)用查找,只針對(duì)前30行文本進(jìn)行內(nèi)容分析
const lines = fileContent.split(/\r?\n/).splice(0, 30)
// 初始化子包目錄文件名
moduleResultMap[dirKey]['fileImport'][filePath] =
lines.reduce((acc, current) => {
const matchArr = current.match(lineReg)
return matchArr && matchArr.length > 0 && matchArr[0].indexOf('/daojia/') > -1 ?
[...acc, matchArr[0]] : acc}
, [])
})
復(fù)制代碼
4. 效果展示
最后,我是將分析出來(lái)的結(jié)果導(dǎo)出到了csv
文件中,以便于為我接下來(lái)的拆包提供一份相對(duì)有保障的可視化的支持:
因?yàn)槲疫@次主要是針對(duì)項(xiàng)目中daojia
這個(gè)子模塊進(jìn)行一個(gè)拆包,因此分析的也是針對(duì)項(xiàng)目中其余子模塊對(duì)該模塊文件的一個(gè)引用情況做一個(gè)分析,表格中的每個(gè)字段所代表的意思我也大概說(shuō)明一下:
interface Table {
module: string //子模塊
type: string // 分析的文件類型
name: string // 分析的文件名
import: string // 引用的組件 || 引用的文件
filePath: string // 分析的文件路徑
}
復(fù)制代碼
5. 終極展示
我們?cè)倩剡^(guò)頭來(lái)看這幅圖:
當(dāng)我們成功地都將以下幾個(gè)子包根目錄從項(xiàng)目中剝離抽身之后,才會(huì)真的有底氣地說(shuō):把app.json
文件下的subPackages
改下就好了
/daojia/pages -> /daojia
/deptstore/pages -> /deptstore
/index/pages/userMaterial -> /index
/shopping/pages -> /shopping
復(fù)制代碼
再來(lái)看看現(xiàn)在的模塊包分析表:
結(jié)論:經(jīng)過(guò)合理化的分包之后,優(yōu)化后的主包體積比優(yōu)化前整整減少了35%
二、“個(gè)別沒(méi)有用到的圖片等靜態(tài)資源文件沒(méi)有及時(shí)刪除,導(dǎo)致包體積過(guò)大,無(wú)法生成預(yù)覽碼”
1. 分析
在上一節(jié)里,我的做法概括起來(lái):拆解子包,合理化模塊打包
由于各種原因,一是在當(dāng)前項(xiàng)目里面存在了過(guò)多的活動(dòng)圖片,重復(fù)的icon等等,但是當(dāng)活動(dòng)下架之后,這些圖片并沒(méi)有得到及時(shí)移除;二是組件引用混亂,相同組件的代碼會(huì)同時(shí)出現(xiàn)在各個(gè)子模塊里面;
這些無(wú)疑都是導(dǎo)致 項(xiàng)目體積過(guò)大 和造成 項(xiàng)目難以維護(hù) 的主要原因;
所以,在這一節(jié)里,也可以概括一句話:剔除無(wú)用資源,減少項(xiàng)目文件
2. 思路
總體來(lái)說(shuō),我也是通過(guò)寫(xiě)腳本來(lái)分析這些資源文件,思路如下:
- 無(wú)用圖片資源查找
① 根據(jù)不同模塊配置信息,依次讀取當(dāng)前模塊圖片目錄下的所有圖片文件,過(guò)濾出圖片文件名,存儲(chǔ)在一個(gè)數(shù)組內(nèi);
② 然后全掃描這個(gè)項(xiàng)目?jī)?nèi)的所有文件,通過(guò)fs
模塊讀取到文件的字符串內(nèi)容,遍歷圖片數(shù)組,根據(jù)字符串匹配indexOf
,如果存在,則標(biāo)記圖片的引用路徑;文件全掃描之后,如果找不到,則在路徑一欄標(biāo)記為“沒(méi)有用到”;
③ 又或者匹配到的圖片,則從數(shù)組內(nèi)剔除出去,當(dāng)掃描完所有的文件之后,剩下的就是沒(méi)有引用到的圖片文件了;(以上方法很蠢,但是勝在簡(jiǎn)單粗暴,希望有更好方法的朋友可以給我留言,不勝感激。)
- 組件引用分析
① 根據(jù)不同模塊的配置信息,依次讀取當(dāng)前模塊內(nèi)pages
和components
目錄下的json
文件(組件引入的入口),實(shí)則一個(gè)JSON字符串的轉(zhuǎn)成JSON對(duì)象;JSON.parse(jsonstring)
{
"usingComponents": {
"a": "./A/a",
"b": "../B/b",
"c": "../C/c"
}
}
復(fù)制代碼
② 然后獲取其相同文件名的js
文件(頁(yè)面或者組件的主體文件),通過(guò)fs
模塊讀取文件內(nèi)容,注意,這時(shí)候是得將這些富文本字符串轉(zhuǎn)為DOM節(jié)點(diǎn)樹(shù)結(jié)構(gòu)對(duì)象,然后遍歷節(jié)點(diǎn)對(duì)象,去匹配解析出對(duì)應(yīng)的json
組件引入入口文件下的json對(duì)象,然后分析出引用到的組件,實(shí)際就是節(jié)點(diǎn)標(biāo)簽名的匹配過(guò)程。
3. 腳本編寫(xiě)
這里就把一些核心代碼貼出來(lái)就好,大家看看就好,不做過(guò)多闡述了
- 無(wú)用圖片資源查找腳本
// 需要分析的圖片目錄地址
const imgDirPath = path.resolve(__dirname + '/../..' + imagesEntry);
const imgFiles = getAllFiles(imgDirPath)
if (imgFiles.length === 0) return
// 只保留圖片的文件名數(shù)組
const allImageFiles = imgFiles.map(imgItem => path.basename(imgItem))
// 查找所有的wxml, js文件
const allWxmlFiles = targetEntrys.reduce((acc, targetEntry) => {
const targetDirPath = path.resolve(__dirname + '/../..' + targetEntry)
const targetAllFiles = getAllFiles(targetDirPath, true)
const allWxmlFiles = targetAllFiles.filter(filePath => {
const extname = path.extname(filePath)
return ['.wxml', '.js'].indexOf(extname) > -1
})
return [...acc, ...allWxmlFiles]
}, [])
// 遍歷圖片集數(shù)組,查找文件是否有引入
const result = allImageFiles.reduce((acc, imgName) => {
const rowItems = allWxmlFiles.reduce((childAcc, filePath) => {
const fileStr = fs.readFileSync(filePath, 'utf8')
return fileStr.indexOf(imgName) === -1 ? childAcc : [...childAcc, {
image: imgName,
existPath: filePath,
}]
}, [])
// 如果查找完畢數(shù)組為空,則說(shuō)明沒(méi)有引入到該圖片
return rowItems.length === 0 ? [...acc, {
image: imgName,
existPath: '沒(méi)有用到'
}] : [...acc, ...rowItems]
}, [])
// 導(dǎo)出csv文件
const csv = new ObjectsToCsv(result)
const exportPath = `${__dirname}${'/../..'}${BASE_EXPORT_IMG}/${imageReportFile}`
await csv.toDisk(exportPath)
復(fù)制代碼
- 組件引用分析腳本
// 解析入口目錄
const entryDir = path.resolve(__dirname + '/../..' + entry)
const allFiles = getAllFiles(entryDir)
if (allFiles.length === 0) return
const filterFiles = getFilterFiles(allFiles, ['wxml', 'json'])
// 組裝導(dǎo)出對(duì)象數(shù)組數(shù)據(jù)
const pageWithComponents = filterFiles.reduce((acc, { jsonFile }) => {
const current = path.basename(jsonFile, '.json')
const currentDir = path.dirname(jsonFile)
const components = listComponents(jsonFile) || []
if (components.length == 0) {
return [...acc, {
page: current,
directory: currentDir,
}]
} else {
// 輸入wxml地址,轉(zhuǎn)化為json標(biāo)簽對(duì)象
const fileJsonData = http://www.wxapp-union.com/getFileJsonData(currentDir + `/${current}.wxml`)
const childs = components.reduce((childAcc, { name, path: compPath }) => {
let used
if (fileJsonData) {
used = isWxmlImportComponent(fileJsonData, name)
used = used ? 'true' : 'false'
} else {
used = '解析出錯(cuò)'
}
return [...childAcc, {
page: current,
directory: path.resolve(currentDir),
component: name,
componentPath: compPath,
used,
}]
}, [])
return [...acc, ...childs]
}
}, [])
// 導(dǎo)出csv文件
const csv = new ObjectsToCsv(pageWithComponents)
const exportPath = `${__dirname}/../..${BASE_EXPORT_COMPONENT}/${exportFileName}`
await csv.toDisk(exportPath)
復(fù)制代碼
結(jié)論:剔除沒(méi)有引入的圖片資源,減少項(xiàng)目體積;分析頁(yè)面的組件引入,為項(xiàng)目的組件庫(kù)的搭建提供數(shù)據(jù)支持。
三、“測(cè)試同學(xué)反饋小程序測(cè)試流程過(guò)于繁瑣,復(fù)雜,加大測(cè)試工作量和小程序的出錯(cuò)率”
1. 分析
小程序測(cè)試步驟如下:
- 開(kāi)發(fā)同學(xué)在功能提測(cè)階段,需提供功能分支名給到測(cè)試同學(xué),比如說(shuō):
feature/monthcard
- 測(cè)試同學(xué)需要切換功能分支,并且拉取最新代碼,執(zhí)行
git checkout feature/montcard
git pull origin feature/monthcard
- 打開(kāi)
小程序開(kāi)發(fā)者工具
,更改配置文件環(huán)境參數(shù),如:config.js
,比如說(shuō)修改成env = test/dev/pre/pro
等等,切換到對(duì)應(yīng)的接口環(huán)境進(jìn)行測(cè)試 - 如只需要本地測(cè)試,直接在工具上面測(cè)試即可,如需要移動(dòng)端測(cè)試,則需要點(diǎn)擊
編譯執(zhí)行
生成小程序預(yù)覽碼,手機(jī)掃碼測(cè)試 - 后期開(kāi)發(fā)同學(xué)推了代碼,需要同步測(cè)試同學(xué)定期去更新代碼,執(zhí)行:
git pull origin 分支名
上面就是我司的關(guān)于小程序提測(cè)時(shí)的做法,相信這也是一部分公司的關(guān)于小程序的測(cè)試流程,又或者一部分公司的做法如下:
-
開(kāi)發(fā)同學(xué)在本地生成測(cè)試預(yù)覽碼,然后將預(yù)覽碼截圖發(fā)給測(cè)試同學(xué)進(jìn)行測(cè)試(測(cè)試預(yù)覽碼有時(shí)效限制,需要開(kāi)發(fā)每隔一段時(shí)間去重新生成一個(gè)新的預(yù)覽碼);
-
開(kāi)發(fā)同學(xué)編寫(xiě)工具,將整個(gè)小程序代碼包壓縮放在內(nèi)網(wǎng)的一個(gè)網(wǎng)頁(yè)下,每次由測(cè)試下載到本地,解壓,然后用開(kāi)發(fā)者工具打開(kāi)測(cè)試(一定程度自動(dòng)化了測(cè)試流程和簡(jiǎn)化了測(cè)試同學(xué)的流程,但是依然很麻煩);
結(jié)論:總得來(lái)說(shuō),開(kāi)發(fā)和測(cè)試同學(xué)都沒(méi)有成功從上面的開(kāi)發(fā)工作流中解耦出來(lái)。
2. 解決方案
基于上述的一些問(wèn)題,我發(fā)現(xiàn)這一系列的測(cè)試步驟可以通過(guò)微信官方提供的ci命令行工具,是完全可以抽象出來(lái),做成一個(gè)可以簡(jiǎn)化測(cè)試工作流的工具平臺(tái),聽(tīng)著是不是很棒?
下面就是我的一些調(diào)研發(fā)現(xiàn):
miniprogram-ci 是從微信開(kāi)發(fā)者工具中抽離的關(guān)于小程序/小游戲項(xiàng)目代碼的編譯模塊。 開(kāi)發(fā)者可不打開(kāi)小程序開(kāi)發(fā)者工具,獨(dú)立使用 miniprogram-ci 進(jìn)行小程序代碼的上傳、預(yù)覽等操作。
搭建工具平臺(tái)
- 前端(js)
- React 搭建前端骨架(借用facebook提供
create-react-app
腳手架即可) - Bootstrap 作為前端界面布局的ui框架庫(kù)
- 后端(nodejs)
- 采用
Express
web應(yīng)用開(kāi)發(fā)框架搭建即可 - 安裝
miniprogram-ci
包(構(gòu)建預(yù)覽碼,提交發(fā)版等) - 安裝
html2json
,objects-to-csv
包(用于項(xiàng)目靜態(tài)資源使用分析等)
ps: 這里就不對(duì)里面的技術(shù)細(xì)節(jié)做過(guò)多闡述了,具體可以查看文末的項(xiàng)目地址,我已經(jīng)開(kāi)源出來(lái)了。
效果展示:
相關(guān)案例查看更多
相關(guān)閱讀
- 小程序商城
- 網(wǎng)站收錄
- 云南網(wǎng)站建設(shè)服務(wù)
- 云南做百度小程序的公司
- 網(wǎng)絡(luò)營(yíng)銷
- 網(wǎng)站建設(shè)優(yōu)化
- 網(wǎng)站上首頁(yè)
- 云南軟件開(kāi)發(fā)
- 云南網(wǎng)站建設(shè)高手
- 微分銷
- 政府網(wǎng)站建設(shè)服務(wù)
- 百度小程序開(kāi)發(fā)
- 快排推廣
- 云南網(wǎng)站建設(shè)優(yōu)化
- 汽車回收管理
- 云南網(wǎng)站建設(shè)公司
- 汽車報(bào)廢管理
- 云南網(wǎng)站建設(shè)價(jià)格
- 昆明網(wǎng)站開(kāi)發(fā)
- 云南網(wǎng)絡(luò)推廣
- 開(kāi)通微信小程序被騙
- 網(wǎng)絡(luò)公司排名
- 小程序分銷商城
- 做小程序被騙
- 網(wǎng)站排名
- 云南網(wǎng)站設(shè)計(jì)
- 網(wǎng)站建設(shè)高手
- 云南網(wǎng)絡(luò)營(yíng)銷顧問(wèn)
- 網(wǎng)站優(yōu)化公司
- 昆明做網(wǎng)站