如何用小程序開發(fā)app

背景

都知道小程序的體驗要比app里面直接嵌入h5的體驗要好,都知道小程序其實也是運行在app上的。那么我們?yōu)槭裁床荒苡眯〕绦騺黹_發(fā)app呢?這樣不僅可以小程序和app只要開發(fā)一次,小程序和app都有了。還可以實現(xiàn)app動態(tài)更新不需要提交應用市場審核,我們只要做個小程序載體的app殼(類微信端小程序 sdk),而且體驗效果也接近原生。做一個類似小程序平臺把我們現(xiàn)在的app項目框架從組件化改成為小程序平臺構(gòu)架。

每個業(yè)務(wù)程序都是一個個小程序,原生提供原生能力。


小程序app框架

什么是小程序

首先我們要知道小程序是啥?現(xiàn)在市場上的小程序有很多,微信小程序、百度小程序、支付寶小程序、字節(jié)跳動小程序等但都差不多。與傳統(tǒng)app相比,小程序無需安裝、卸載,運行在微信、百度、支付寶等這樣大型app載體上。雖然每種小程序都差不太多,但都定義了自己的開發(fā)語言和規(guī)范,這對開發(fā)者來說也是不少的麻煩。

小程序是介于web網(wǎng)頁應用和原生應用的一種產(chǎn)物;


小程序是什么

小程序和Hybrid APP的關(guān)系

原以為Hybrid APP就是用app的webview去加載一個h5文件,然后webview通過js橋梁和原生通信,實現(xiàn)js調(diào)用h5方法,h5方法調(diào)原生方法。來彌補h5無法拍照、打電話等不足。但是做出來的效果h5都會有短暫白屏,體驗也無法達到原生效果(提供一個簡單的demo)。后來接觸了小程序,覺得小程序的體驗和原生差不了多少了,可以說不是專業(yè)人員基本是看不出區(qū)別的,原先以為小程序是類似RN、weex這樣的原生渲染,后面才知道它也是webview渲染。竟然也是hybird app,那它的體驗是什么上去的?都是webview渲染,為什么小程序的體驗會比普通的h5好?這讓我非常感興趣,于是就開始了小程序的深入了解。

初步了解用h5做Hybrid APP和小程序的區(qū)別:

相同點:

1.都是webview渲染

2.都是js通過橋接和原生通信

3.都可以調(diào)用原生組件

4.都可以把資源文件下載本地加載渲染

不同點:

1.Hybrid是html 有dom操作,小程序是虛擬dom 屏蔽了直接對dom操作

2.小程序有服務(wù)層,負責處理業(yè)務(wù)邏輯和數(shù)據(jù)處理

3.小程序頁面有原生頁面的生命周期管理

4.小程序tab和bar是原生控件

5.小程序類web不是h5

6.小程序基于微信跨平臺

小程序原理

下面以微信小程序為例,進一步展開小程序原理

都知道微信小程序有自己的開發(fā)語言,wx開頭的方法也不少,那它是什么轉(zhuǎn)化為微信app能識別的語言呢?微信開發(fā)工具開發(fā)完提交審核,審核通過下發(fā)到微信端的是什么樣的文件呢?帶著這些問題我查閱了很多資料,小程序在技術(shù)架構(gòu)上非常清晰易懂。JS負責業(yè)務(wù)邏輯的實現(xiàn),而表現(xiàn)層則WXML和WXSS來共同實現(xiàn),前者其實就是一種微信定義的模板語言,而后者類似CSS。但是語法畢竟是自定義的,所以要么在下發(fā)的之前進行編譯,要么就是在渲染的時候進行轉(zhuǎn)化成webview能夠識別的語法。我們發(fā)現(xiàn)這2個節(jié)點微信都做了處理,拿到下發(fā)到微信端的wxapkg格式的小程序包,解開后都是js和html已經(jīng)不是我們開發(fā)的WXML和WXSS格式了。但是這一個個html直接用瀏覽器打開卻是空白的。沒錯<body></body>里面是空的,渲染的時候動態(tài)加進入內(nèi)容的。

1.小程序是如何編譯的

我們先來看看打包編譯這層,微信都做了些啥呢?微信的打包和編譯都在服務(wù)端進行,我這邊找了個類似的來描述下,不一定準確,只能參考下。

檢測app.json文件是否存在

清空并創(chuàng)建指定的輸出目錄

根據(jù)service.html模板,帶上版本信息輸出到指定的目錄中

讀取配置文件app.json,將其注入到app-config.js中,輸出到指定的目錄中

讀取所有小程序代碼中所有的JS文件,同時判斷其是否在app.json中定義,如果其沒被定義也不是app.js,說明其為引入的module, 將這些JS路徑名存入一個數(shù)組中,并確保app.js和頁面文件放置在數(shù)組尾部

遍歷JS文件數(shù)組并讀取它們,根據(jù)用戶設(shè)置項判斷是否使用Babel將其轉(zhuǎn)換為es5的代碼

把js模塊封裝成CommonJS模塊,并合并成app-service.js這個文件輸出

根據(jù)app.json里的pages配置,遍歷每個頁面根據(jù)頁面wxml,wxss生成相應的頁面文件并合成page-frame.html

其他步驟應該都不難理解,我認為最難的應該是wxml,wxss生成相應的頁面文件,這個頁面不是普通的html文件,前面也說過它的body是空的。如果你安裝了微信的開發(fā)工具的化你可以找下是否有wcc和wcsc這2個小工具。wxss 轉(zhuǎn)換成了css,wxml轉(zhuǎn)換成了inject_js,實際上就是virtual_dom。openVendor命令可以在小程序中獲取到構(gòu)建腳本wcc和wcsc,以及各個版本小程序的執(zhí)行SDK***.wxvpkg,這個SDK也可以用Wechat-app-unpack解開,解開后里面就有WAService.js和WAWebview.js等代碼。

根據(jù) /Users/***/Library/Application Support/微信web開發(fā)者工具/WeappVendor 路徑來找到微信開發(fā)者工具目錄,以及查看工具集成的核心類??梢钥吹轿覀兒褪煜さ囊埠苤匾腤AService.js和WAWebview.js2個文件也在里面。不過代碼都是加密混淆的,沒有可讀性。

2.編譯好的小程序包如何下發(fā)解析

再看下下面這張圖,微信下發(fā)的wxapkg格式的文件(每個小程序都是這樣的一個包),這個文件可以通過從越獄的iPhone或者root的安卓手機上拿到。有部分人用charles通過https抓包拿到了下載鏈接,也拿到了包。解壓出來就是這樣的目錄格式。簡單解釋下這每個文件的作用以及是什么來的:

小程序包目錄

app-config.json:小程序的整體配置文件,里面是一個json 主要包括page、entryPagePath、pages、global、tabBar、ext、extAppid等

{

? ? "page": {

? ? ? ? "pages/shop/index.html": {

? ? ? ? ? ? "window": {

? ? ? ? ? ? ? ? "enablePullDownRefresh": false

? ? ? ? ? ? }

? ? ? ? },

? ? ? ? "pages/goods/detail.html": {

? ? ? ? ? ? "window": {

? ? ? ? ? ? ? ? "navigationBarTitleText": "商品詳情",

? ? ? ? ? ? ? ? "enablePullDownRefresh": false

? ? ? ? ? ? }

? ? ? ? },

? ? ? ? "pages/order/address/list.html": {

? ? ? ? ? ? "window": {

? ? ? ? ? ? ? ? "navigationBarTitleText": "地址列表",

? ? ? ? ? ? ? ? "enablePullDownRefresh": false,

? ? ? ? ? ? ? ? "backgroundTextStyle": "light"

? ? ? ? ? ? }

? ? ? ? },

? ? ? ? ? .......

? ? },

? ? "entryPagePath": "pages/shop/index.html",

? ? "pages": ["pages/shop/index", "pages/order/detail/logisticsmap",......],

? ? "global": {

? ? ? ? "window": {

? ? ? ? ? ? "backgroundTextStyle": "light",

? ? ? ? ? ? "navigationBarBackgroundColor": "#f1f1f1",

? ? ? ? ? ? "navigationBarTitleText": " ",

? ? ? ? ? ? "navigationBarTextStyle": "black"

? ? ? ? }

? ? },

? ? "ext": {

? ? ? ? "api": {? ?

? ? ? ? },

? ? ? ? "form": {? ?

? ? ? ? },

? ? ? ? "name": "小店"

? ? },

? ? "extAppid": "########"

}

page節(jié)點:管理每個頁面的整體設(shè)置,比如原生navigationBar的標題樣式、是否需要下拉刷新等可以說這些設(shè)置都是對原生的ViewController的一個設(shè)置。沒錯一個小程序頁面都有一個對應的原生頁面,所以它可以封裝原生頁面的生命周期暴漏給小程序頁面使用,包括原生的navigationBar和tabBar、下拉刷新控件等。這也是為什么小程序如果屏蔽調(diào)自帶的navigationBar,自己定義navigationBar 下拉刷新也要自己重新做的原因。自定義navigationBar就不再是原生的了,它默認下拉刷新效果是在原生navigationBar下的整個webview做動畫效果,這顯然滿足不了我們的需求。

entryPagePath:這個節(jié)點就簡單了,小程序的入口頁面的配置

pages節(jié)點:頁面路徑數(shù)組 對應小程序源代碼里面的app.json文件里面的pages節(jié)點

global節(jié)點:全局設(shè)置和全局變量等;window 整個小程序的私有頁面都起作用,也就是說每個頁面沒有自己單獨設(shè)置都直接用這個全局設(shè)置的效果。

ext、extAppid節(jié)點:對應的就是ext.json文件 第三方平臺部署小程序才需要

tabBar節(jié)點:對原生tabBar的樣式設(shè)置和頁面路徑和圖標等配置

app-service.js文件:這個是小程序一個很重要的文件,小程序體驗好一個很重要的環(huán)節(jié)。從下圖截取的代碼片段很容易看出這個文件就是小程序里面全部js文件內(nèi)容的集合。通過__wxRoute來路由,__wxRouteBegin=true來標記啟始頁面。

var__wxAppData = __wxAppData || {};var__wxRoute = __wxRoute ||"";var__wxRouteBegin = __wxRouteBegin ||"";var__wxAppCode__ = __wxAppCode__ || {};varglobal = global || {};var__WXML_GLOBAL__=__WXML_GLOBAL__ || {};var__wxAppCurrentFile__=__wxAppCurrentFile__||"";varComponent = Component ||function(){};vardefinePlugin = definePlugin ||function(){};varrequirePlugin = requirePlugin ||function(){};varBehavior = Behavior ||function(){};? ? define("app.js",function(require, module, exports, window,document,frames,self,location,navigator,localStorage,history,Caches,screen,alert,confirm,prompt,XMLHttpRequest,WebSocket,Reporter,webkit,WeixinJSCore){"use strict";App({onLaunch:function(){vare=this,o=wx.getStorageSync("logs")||[];o.unshift(Date.now()),wx.setStorageSync("logs",o),wx.login({success:function(e){}}),wx.getSetting({success:function(o){o.authSetting["scope.userInfo"]&&wx.getUserInfo({success:function(o){e.globalData.userInfo=o.userInfo,e.userInfoReadyCallback&&e.userInfoReadyCallback(o)}})}})},globalData:{userInfo:"hello world",text:"hello world"}});? ? ? ? ? ? ? });require("app.js");? ? ? ? __wxRoute ='pages/page2/page2';__wxRouteBegin =true;? ? define("pages/page2/page2.js",function(require, module, exports, window,document,frames,self,location,navigator,localStorage,history,Caches,screen,alert,confirm,prompt,XMLHttpRequest,WebSocket,Reporter,webkit,WeixinJSCore){"use strict";Page({data:{},onLoad:function(n){},onReady:function(){},onShow:function(){},onHide:function(){},onUnload:function(){},onPullDownRefresh:function(){},onReachBottom:function(){},onShareAppMessage:function(){}});? ? ? ? ? ? ? });require("pages/page2/page2.js");

3.小程序如何渲染

除了小程序每個頁面的js還包含了app.js 這個包含小程序的整個生命周期管理邏輯的js文件內(nèi)容也都在這個文件里面。在微信端里面小程序的sdk會有一個單獨的webview來加載app-service.js文件當作這個小程序的服務(wù)層,負責每個頁面邏輯處理,而且這個服務(wù)在這個小程序的整個生命周期它是一直在的,每個頁面的js文件都已經(jīng)壓縮在這個文件里面,并在小程序服務(wù)啟動的時候已經(jīng)加載到內(nèi)存中,所以在點擊按鈕需要做邏輯交互的時候體驗會那么快。


App Service(邏輯層)主要就是由app-service.js文件和集成在微信app里面的WAService.js組成,如一個頁面加載需要網(wǎng)絡(luò)請求就是由邏輯層處理請求參數(shù)并交給原生來進行請求,原生把請求到的數(shù)據(jù)返回給App Service(邏輯層)進行數(shù)據(jù)處理,最后把處理好的數(shù)據(jù)通過原生JSBridge傳給view(試圖層)進行渲染。對 邏輯層和視圖層沒有直接的交互,邏輯服務(wù)層和視圖層也不在一個線程里面,2個webview 只能通過原生來進行通信。

幾個文件夾沒啥特別的 就是和你微信小程序開發(fā)的目錄是一樣,放的都是你的頁面和組件的html文件。但是值得一提的是這里面的html文件內(nèi)容很少,它不是一個完整的頁面,可以說這個頁面的樣式,靜態(tài)內(nèi)容都不在這個html里面。里面放的是這個頁面css、路由路徑和加載入口方法的調(diào)用generateFunc: $gwx('./pages/order/rights/index.wxml')。可以看下我的hello world頁面代碼,就更加清晰了。

<!--pages/page2/page2.wxml-->

<text class="pageText">hello world page2</text>

/* pages/page2/page2.wxss */

.pageText {

? ? background-color: red;

? ? width: 100%;

? ? height: 100rpx;

? ? display: flex;

? ? align-items: center;

? ? justify-content: center;

}

這是一個很簡單的小程序頁面代碼,那它編譯后的html頁面代碼是什么樣的呢?下面我展示出來給大家看。

var__setCssStartTime__ =Date.now();? ? setCssToHead([".",[1],"pageText { background-color: red; width: 100%; height: ",[0,100],"; display: -webkit-flex; display: flex; -webkit-align-items: center; align-items: center; -webkit-justify-content: center; justify-content: center; }\n",],undefined,{path:"./pages/page2/page2.wxss"})()var__setCssEndTime__ =Date.now();document.dispatchEvent(newCustomEvent("generateFuncReady", {? ? ? ? detail: {? ? ? ? ? ? generateFunc: $gwx('./pages/page2/page2.wxml')? ? }}))? ?

發(fā)現(xiàn)了啥?“hello world page2“ 這樣頁面關(guān)鍵內(nèi)容不見了,那它是如何渲染的?懂js的人估計很容易就可以看到這個頁面的入口方法generateFunc: $gwx 是的 這個方法是一個很重要的方法。那么這個方法在哪里?“hello world page2“ 這樣頁面關(guān)鍵元素又在哪里?肯定是跟著這個資源包一起下發(fā)的。對的所以的頁面標簽、元素 內(nèi)容都在page-frame.html文件里面。也就是wxml文件的代碼都編譯壓縮到page-frame.html文件里面,而對應頁面的html文件只放對應的wxss文件代碼和入口js代碼。而入口方法的觸發(fā)是由原生app調(diào)用js"generateFuncReady"事件觸發(fā)調(diào)用。原生sdk這塊后面可以以O(shè)C為例貼出對應代碼,再展開說明下。

page-frame.html文件:這個文件應該是一個小程序包里面最大一個文件,所以里面的代碼也不好都貼出來,簡單介紹下里面的組成和作用。第一大塊:模版 View(視圖)層如何繪制每個頁面都是公共的,如何把vdom渲染到webview上。$gwx方法就在這個模版里面,一個頁面的入口。

再找找我們剛才html頁面消失的關(guān)鍵元素“hello world page2“在哪里?把每個元素都轉(zhuǎn)化為Z數(shù)組了,每個頁面都是一個類似這樣的代碼塊。由于混淆和壓縮加大了我們閱讀的難度,但是如果你以為看完這個文件就完了,那就錯了。View(視圖)層除了這幾個html文件外 還需要一個很重要的文件,那就是放在微信app包里面的WAWebview.js文件。和服務(wù)層里面也有一個WAService.js文件配合使用達到和原生交互的效果。


和原生交互這塊是屬于小程序框架,所以肯定不會在下發(fā)的小程序包里面,除了原生sdk代碼之外,還有2個很重要的js文件就是上面提到的WAService.js文件和文件。接下來我們就開始簡單了解下:

為了方便理解我整理了js和原生的一些API,有app(小程序)級別的、有頁面級別的、有原生組件級別的??梢妀s和原生交互是非常頻繁的,可以說每個操作都是需要提供View(視圖)層的WAWebview.js調(diào)用原生的橋梁需要原生處理就原生處理后再調(diào)用app Service(邏輯)層的WAService.js 由WAService.js通知 邏輯層處理對應邏輯,再把處理結(jié)果返回到原生。


WAService.js

小程序在App中執(zhí)行時的時候分為三個不同的模塊,View/Service/Native,各司其職。View和Service都在WKWebView中執(zhí)行,互相無法調(diào)用,不直接操作DOM。他們之間通過Native層通信。Native和WebView之間通過webkit.messagehandler和evaluateJavascript互相調(diào)用。小程序借助的是JSBridge實現(xiàn)了對底層API接口的調(diào)用,所以在小程序里面開發(fā),開發(fā)者不用太多去考慮IOS,安卓的實現(xiàn)差異的問題,安心在上層的視圖層和邏輯層進行開發(fā)即可。

WeixinJSBridge.publish: view和service之間的透傳,在WKWebView之間傳遞消息。

WeixinJSBridge.subscribe: 注冊監(jiān)聽,監(jiān)聽view和service之間的消息調(diào)用。

WeixinJSBridge.invoke: View或者Service傳遞消息到Native,然后Native使用邏輯調(diào)用js callback。

WeixinJSBridge.on:監(jiān)聽Native的事件。


sdk構(gòu)架圖

啟動小程序服務(wù)startAppWithAppInfo根據(jù)appid等基本信息判斷小程序是否已經(jīng)下載到本地,沒有的話下載解壓加載配置信息等。然后進入manager,manager其實也是分為幾部分一個是小程序級別的管理,一個是單個小程序的管理。具體的可以通過下面的類圖更加直觀。


SDK核心類圖

下圖顯示了小程序啟動時會從CDN和服務(wù)器校驗和下載資源。也就是是小程序啟動的時候會有點慢的主要原因,還有一些時間就是需要初值化小程序本地服務(wù)。


小程序啟動流程

4.小程序的生命周期

關(guān)于小程序的生命周期,可以兩個部分來理解:應用生命周期(左側(cè)藍色部分)和頁面生命周期(右側(cè)綠色部分)。

其中應用的生命周期是這樣一個流程:1、用戶首次打開小程序,觸發(fā) onLaunch(全局只觸發(fā)一次)。2、小程序初始化完成后,觸發(fā)onShow方法,監(jiān)聽小程序顯示。3、小程序從前臺進入后臺,觸發(fā) onHide方法。4、小程序從后臺進入前臺顯示,觸發(fā) onShow方法。5、小程序后臺運行一定時間,或系統(tǒng)資源占用過高,會被銷毀。

頁面生命周期是這樣的一個流程:1、小程序注冊完成后,加載頁面,觸發(fā)onLoad方法。2、頁面載入后觸發(fā)onShow方法,顯示頁面。3、首次顯示頁面,會觸發(fā)onReady方法,渲染頁面元素和樣式,一個頁面只會調(diào)用一次。4、當小程序后臺運行或跳轉(zhuǎn)到其他頁面時,觸發(fā)onHide方法。5、當小程序有后臺進入到前臺運行或重新進入頁面時,觸發(fā)onShow方法。6、當使用重定向方法wx.redirectTo(OBJECT)或關(guān)閉當前頁返回上一頁wx.navigateBack(),觸發(fā)onUnload。同時,應用生命周期會影響到頁面生命周期。


小程序生命周期

用Page 實例說明的頁面的生命周期


小程序頁面生命周期

由上圖可知,小程序由兩大線程組成:負責界面的視圖線程(view thread)和負責數(shù)據(jù)、服務(wù)處理的服務(wù)線程(appservice thread),兩者協(xié)同工作,完成小程序頁面生命周期的調(diào)用。

視圖線程有四大狀態(tài):

初始化狀態(tài):初始化視圖線程所需要的工作,初始化完成后向 “服務(wù)線程”發(fā)送初始化完成信號,然后進入等待狀態(tài),等待服務(wù)線程提供初始化數(shù)據(jù)。

首次渲染狀態(tài):當收到服務(wù)線程提供的初始化數(shù)據(jù)后(json和js中的data數(shù)據(jù)),渲染小程序界面,渲染完畢后,發(fā)送“首次渲染完成信號”給服務(wù)線程,并將頁面展示給用戶。

持續(xù)渲染狀態(tài):此時界面線程繼續(xù)一直等待“服務(wù)線程”通過this.setdata()函數(shù)發(fā)送來的界面數(shù)據(jù),只要收到就重新局部渲染,也因此只要更新數(shù)據(jù)并發(fā)送信號,界面就自動更新。

結(jié)束狀態(tài):頁面被回收或者銷毀、應用被系統(tǒng)回收、銷毀時觸發(fā)。

服務(wù)線程五大狀態(tài):

初始化狀態(tài):此階段僅啟動服務(wù)線程所需的基本功能,比如信號發(fā)送模塊。系統(tǒng)的初始化工作完畢,就調(diào)用自定義的onload和onshow,然后等待視圖線程的“視圖線程初始化完成”號。onload是只會首次渲染的時候執(zhí)行一次,onshow是每次界面切換都會執(zhí)行,簡單理解,這就是唯一差別。

等待激活狀態(tài):接收到“視圖線程初始化完成”信號后,將初始化數(shù)據(jù)發(fā)送給“視圖線程”,等待視圖線程完成初次渲染。

激活狀態(tài):收到視圖線程發(fā)送來的“首次渲染完成”信號后,就進入激活狀態(tài)既程序的正常運行狀態(tài),并調(diào)用自定義的onReady()函數(shù)。此狀態(tài)下就可以通過 this.setData 函數(shù)發(fā)送界面數(shù)據(jù)給界面線程進行局部渲染,更新頁面。

后臺運行狀態(tài):如果界面進入后臺,服務(wù)線程就進入后臺運行狀態(tài),從目前的官方解讀來說,這個狀態(tài)挺奇怪的,和激活狀態(tài)是相同的,也可以通過setdata函數(shù)更新界面的。畢竟小程序的框架剛推出,應該后續(xù)會有很大不同吧。

結(jié)束狀態(tài):頁面被回收或者銷毀、應用被系統(tǒng)回收、銷毀時觸發(fā)。

小程序在App中的應用場景

說了這么多技術(shù)理論,最后說下小程序在項目中如何應用。整個項目都是小程序不現(xiàn)實,畢竟小程序的定義是輕量級的,像IM、消息等用原生肯定比小程序更加適合,所以用小程序和原生混合開發(fā)是不可少的。還有一個讓你不得不混合開發(fā)的一個重要原因,你的app不是一個新項目,是一個現(xiàn)有的原生app,一次性用小程序重新做一次不現(xiàn)實,所以混合會是最好的選擇。我這邊做的混合開發(fā)不是技術(shù)層面的,我們都知道小程序是很原生通信很頻繁的,它需要原生提供各種能力才能到達接近原生的體驗,所以本身就是一個混合。而我這里說的混合是指業(yè)務(wù)層面的混合開發(fā),打破我們以往對小程序的認知。不管是百度小程序還是微信小程序都是運行在他們生態(tài)下的一個獨立應用程序。比如一個商城小程序它不會有部分頁面是原生部分頁面是小程序,也只會有一個入口,一個出口。而我們要用小程序來開發(fā)app,我們app有自己的需求我們需要讓小程序看起來像原生頁面一樣,對用戶來說它還是一個app,不存在哪個頁面是原生哪個頁面是原生的。所以一切都是從技術(shù)層面來說,就是小程序和原生進行混合開發(fā)的業(yè)務(wù)app。你可以理解為你的app里面嵌入h5一樣的開發(fā)模式,只是小程序頁面比一般的h5頁面交互體驗要好一些而已。

我可以從原生頁面跳轉(zhuǎn)到小程序的任何頁面,如果必要的話也可以從小程序頁面跳轉(zhuǎn)到原生頁面。所以小程序服務(wù)不能在進入小程序頁面的時候才啟動,也不能因為回到原生頁面而銷毀。必須根據(jù)你的業(yè)務(wù)場景來調(diào)用控制。

以上是個人對與小程序開發(fā)app的一些淺薄看法,期待和業(yè)界同仁共同探討。你有什么想法呢?歡迎評論交流。

最后感謝業(yè)界各位大佬的貢獻,在這里附上我的參考文獻:

https://blog.csdn.net/ListenToSennTyou/article/details/53258163

http://www.itdecent.cn/p/92c6a75c2323

https://blog.csdn.net/xiangzhihong8/article/details/66521459

https://yq.aliyun.com/articles/72825?t=t1

https://github.com/weidian-inc/hera-cli

https://www.cnblogs.com/viaiu/p/9935602.html

http://www.itdecent.cn/p/51ac882ea9f4

http://lrdcq.com/me/read.php/66.htm

https://github.com/wdfe/weweb

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容