Android 路由設計最佳實踐
引子
這篇文章會告訴你
- 什麼是路由,是為了解決什麼問題才產生的
- 業界現狀是怎麼樣的,我們可以做什麼來優化當前的問題
- 路由設計思路是怎麼樣的,該怎麼設計比較好
- 如何用註解實現路由表
- URL的引數如何依賴注入到Activity、Fragement
- 如何Hook
OnActivityResult
,不需要再進行requstCode判斷 - 如何非同步攔截路由,實現執行緒切換,不阻塞頁面跳轉
- 如何用Apt實現
Retrofit
介面式呼叫 - 如何找到
Activity
的呼叫方 - 如何實現路由的
安全
呼叫 - 如何避開
Apt
不能彙總所有Module路由的問題
前言
當前Android的路由庫實在太多了,剛開始的時候想為什麼要用路由表的庫,用Android原生的Scheme碼不就好了,又不像iOS只能類依賴,後面越深入就越發現當時想的太簡單了,後面看到Retrofit和OKHttp,才想到頁面請求本質和網路請求不是一樣嗎,終於業界最簡單高效的路由方案1.0出來了
背景
什麼是路由
根據路由表
將頁面請求
分發到指定頁面
使用場景
- App接收到一個通知,點選通知開啟App的某個頁面
- 瀏覽器App中點選某個連結開啟App的某個頁面
- 運營活動需求,動態把原生的頁面替換成H5頁面
- 開啟頁面需要某些條件,先驗證完條件,再去開啟那個頁面
- 不合法的開啟App的頁面被遮蔽掉
- H5開啟連結在所有平臺都一樣,方便統一跳轉
- App存在就開啟頁面,不存在就去下載頁面下載,只有Google的App Link支援
為什麼要有路由
Android原生已經支援AndroidManifest
去管理App跳轉,為什麼要有路由庫,這可能是大部分人接觸到Android各種Router庫不太明白的地方,這裡我講一下我的理解
- 顯示Intent:專案龐大以後,類依賴耦合太大,不適合元件化拆分
- 隱式Intent:協作困難,呼叫時候不知道調什麼引數
- 每個註冊了Scheme的Activity都可以直接開啟,有安全風險
- AndroidMainfest集中式管理比較臃腫
- 無法動態修改路由,如果頁面出錯,無法動態降級
- 無法動態攔截跳轉,譬如未登入的情況下,開啟登入頁面,登入成功後接著開啟剛才想開啟的頁面
- H5、Android、iOS地址不一樣,不利於統一跳轉
怎麼樣的路由才算好路由
路由說到底還是為了解決開發者遇到的各種奇葩需求,使用簡單、侵入性低、維護方便是首要條件,不影響你原來的程式碼,寫入程式碼也很少,這裡就要說說我的OkDeepLink
的五大功能了,五大功能瞬間擊中你的各種痛點,早點下班不是夢。
- 編譯時註解,實現靜態路由表,不再需要在臃腫的
AndroidManifest
中找到那個Actvity寫Scheme和Intent Filter - 非同步攔截器,實現動態路由,安全攔截、動態降級難不倒你
- 模仿
Retrofit
介面式呼叫,實現方式用apt
,不耗效能,引數呼叫不再是問題 - Hook
OnActivityResult
,支援RxJava響應式呼叫,不再需要進行requestCode判斷 - 引數依賴注入,自動儲存,不再需要手動寫
onSaveInstance
、onCreate(SaveInstace)
、onNewIntent(Intent)
、getQueryParamer
具體使用見OkDeepLink
詳細比較
大部分路由庫都用Apt(編譯時註解)生成路由表,然後用路由錶轉發到指定頁面
方案對比 | OkDeepLink | Airbnb DeepLinkDispatch | 阿里 ARouter | 天貓 統跳協議 | ActivityRouter |
---|---|---|---|---|---|
路由註冊 | 註解式介面註冊 | 每個module都要手動註冊 | 每個module的路由表都要類查詢 | AndroidManiFest配置 | 每個module都要手動註冊 |
路由查詢 | 路由表 | 路由表 | 路由表 | 系統Intent | 路由表 |
路由分發 | Activity轉發 | Activity轉發 | Activity轉發 | Activity轉發 | Activity轉發 |
動態替換 | Rxjava實現非同步攔截器 | 不支援 | 執行緒等待 | 不支援 | 不支援 |
動態攔截 | Rxjava實現非同步攔截器 | 不支援 | 執行緒等待 | 不支援 | 主執行緒 |
安全攔截 | Rxjava實現非同步攔截器 | 不支援 | 執行緒等待 | 不支援 | 主執行緒 |
方法呼叫 | 介面 | 手動拼裝 | 手動拼裝 | 手動拼裝 | 手動拼裝 |
引數獲取 | Apt依賴注入,支援所有型別,不需要在Activity的onCreate 中手動呼叫get方法 |
引數定義在path,不利於多人協作 | Apt依賴注入,但是要手動呼叫get方法 | 手動呼叫 | 手動呼叫 |
結果返回 | Rxjava回撥 | onActivityResult | onActivityResult | onActivityResult | onActivityResult |
Module接入不同App | 支援 | 不支援 | 支援 | 不支援 | 支援 |
其實說到底,路由的本質就是註冊再轉發,圍繞著轉發可以進行各種操作,攔截,替換,引數獲取等等,其他Apt、Rxjava說到底都只是為了方便使用出現的,這裡你會發現各種路由庫反而為了修復各種工具帶來的問題,出現了原來沒有的問題,譬如DeepLinkDispatch為了解決Apt沒法彙總所有Module路由,每個module都要手動註冊,ARouter為了解決Apt沒法彙總所有Module路由,通過類操作耗時,才出現分組的概念。
原理分析
定義路由
我這邊是完全按照URL規範了,這裡要說一下,現在好多方法是把引數定義在path裡面的,雖然這樣做,有不需要額外傳引數的好處,但是這樣路由就沒有那麼靈活,除錯起來就沒有那麼方便了。
建議有好幾款app的公司,host都一樣,只有scheme不一樣,這樣只要替換Scheme就能實現降級,維護也簡單。
路由註冊
AndroidManifest
裡面的acitivity
宣告scheme碼是不安全的,所有App都可以開啟這個頁面,這裡就產生有兩種方式去註冊,
- 註解產生路由表,通過
DispatchActivity
轉發 AndroidManifest
註冊,將其export=fasle
,但是再通過DispatchActivity轉發Intent,天貓就是這麼做的,比上面的方法的好處是路由查詢都是系統呼叫,省掉了維護路由表的過程,但是AndroidManifest配置還是比較不方便的
我現在還是採用了註解,後面我會結合兩種方法,將註解自動修改AndroidManifest,對於接入方是沒有變動的,方法已經找到了,用自定義Lint
掃描出註解相關的Activity,然後用processManifestTask修改Manifest,有個demo了,後面會接入。
生成路由表
譬如通過Apt把這段程式碼
public interface SampleService { @Path("/main") @Activity(MainActivity.class) void startMainActivity(@Query("key") String key); }
生成
@After("execution(* okdeeplink.DeepLinkClient.init(..))") public void init() { DeepLinkClient.addAddress(new Address("/main", MainActivity.class)); }
初始化路由表
這裡就要提一下使用Apt會造成每個module都要手動註冊
DeepLinkDispatch是這麼做的
@DeepLinkModule public class SampleModule { }
@DeepLinkHandler({ SampleModule.class, LibraryDeepLinkModule.class }) public class DeepLinkActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); DeepLinkDelegate deepLinkDelegate = new DeepLinkDelegate( new SampleModuleLoader(), new LibraryDeepLinkModuleLoader()); deepLinkDelegate.dispatchFrom(this); finish(); } }
ARouter是通過類查詢,就比較耗時了,所以他又加入了分組的概念,按需載入
/** * 通過指定包名,掃描包下面包含的所有的ClassName * * @param context U know * @param packageName 包名 * @return 所有class的集合 */ public static List<String> getFileNameByPackageName(Context context, String packageName) throws PackageManager.NameNotFoundException, IOException { List<String> classNames = new ArrayList<>(); for (String path : getSourcePaths(context)) { DexFile dexfile = null; try { if (path.endsWith(EXTRACTED_SUFFIX)) { //NOT use new DexFile(path), because it will throw "permission error in /data/dalvik-cache" dexfile = DexFile.loadDex(path, path + ".tmp", 0); } else { dexfile = new DexFile(path); } Enumeration<String> dexEntries = dexfile.entries(); while (dexEntries.hasMoreElements()) { String className = dexEntries.nextElement(); if (className.contains(packageName)) { classNames.add(className); } } } catch (Throwable ignore) { Log.e("ARouter", "Scan map file in dex files made error.", ignore); } finally { if (null != dexfile) { try { dexfile.close(); } catch (Throwable ignore) { } } } } Log.d("ARouter", "Filter " + classNames.size() + " classes by packageName <" + packageName + ">"); return classNames; }
ActivityRouter就比較巧妙了,通過Stub專案,其他地方都是provide的,只有主工程裡面用Apt生成RouterInit類,雖然還是要寫module
的註解
// RouterInit if (hasModules) { debug("generate modules RouterInit"); generateModulesRouterInit(moduleNames); } else if (!hasModule) { debug("generate default RouterInit"); generateDefaultRouterInit(); }
天貓 統跳協議 是最簡單的,轉發一下Intent就可以,但是這樣就沒法享受註解的好處了。
而我用aspectj
解決了這個問題,會自動彙總所有module的路由省略了這些多餘的程式碼,或者有誰知道用Apt自生怎麼解決,請聯絡我一下。
@After("execution(* okdeeplink.DeepLinkClient.init(..))") public void init() { DeepLinkClient.addAddress(new Address("/main", MainActivity.class)); }
路由查詢
路由查詢就是查詢路由表對應的頁面,值得提起的就是因為要適應Module接入不同App,Scheme要自動適應,路由表其實是Path—》Activity,這樣的話內部跳轉的時候ARouterUri是沒有的。而我這邊是有的,我組裝了一個內部的Uri,這樣攔截器不會有影響。
public Request buildRequest(Intent sourceIntent) { if (sourceIntent == null) { return null; } Intent newIntent = new Intent(sourceIntent); Uri uri = newIntent.getData(); addNewTaskFlag(newIntent); if (uri != null) { addBundleQuery(newIntent, uri); Address entry = new DeepLinkClient(context).matchUrl(uri.toString()); if (entry == null || entry.getActivityClass() == null) { return new Request(newIntent, this).setDeepLink(false); } newIntent.setComponent(new ComponentName(context, entry.getActivityClass())); return new Request(newIntent, this); } return new Request(newIntent, this).setDeepLink(false); }
路由分發
現在所有路由方案分發都是用Activity
做分發的,這樣做會有這幾個缺點
- 每次都要啟動一個Activity,而Activity就算不寫任何程式碼啟動都要0.1秒
- 如果是非同步等待的話,Activiy要在合適時間
finish
,不然會有一層透明的頁面阻擋操作
對於第一個問題,有兩個方法
- QQ音樂是把
DispatchActivity
設為SingleInstacne
,但是這樣的話,動畫會奇怪,堆疊也會亂掉,後退會有一層透明的頁面阻擋操作 DispatchActivity
只在外部開啟的時候呼叫
我選擇了第二種
對於第二個問題,有兩個方法
DispatchActivity
再把Intent轉發到Service
,再finish,這種方法唯一的缺陷是攔截器裡面的context是Servcie的activity,就沒發再攔截器裡面彈出對話方塊了。DispatchActivity
在開啟和錯誤的時候finish
,如果activity
已經finish了,就用application的context去轉發路由
我選擇了第二種
public void dispatchFrom(Intent intent) { new DeepLinkClient(this) .buildRequest(intent) .dispatch() .subscribe(new Subscriber<Request>() { @Override public void onCompleted() { finish(); } @Override public void onError(Throwable e) { finish(); } @Override public void onNext(Request request) { Intent dispatchIntent = request.getIntent(); startActivity(dispatchIntent); } }); }
其實處理透明Activity阻擋操作可以採用取消所有事件變成無感頁面的方法,但是還是覺得會影響activity堆疊
沒有采用這種方案
getwindow().addflags( windowmanager.layoutparams.flag_not_focusable | windowmanager.layoutparams.flag_not_touch_modal | windowmanager.layoutparams.flag_not_touchable);
結果返回
這裡我封裝了一個庫RxActivityResult
去捕獲onActivityResult
,這樣能保正流式呼叫
譬如拍照可以這樣寫,先定義一個介面
public interface ImageCaptureService { @Action(MediaStore.ACTION_IMAGE_CAPTURE) Observable<Response> startImageCapture(); }
然後這樣呼叫
public class MainActivity extends AppCompatActivity { @Service ImageCaptureService imageCaptureService; public void captureImage(){ imageCaptureService .startImageCapture() .subscribe(new Action1<Response>() { @Override public void call(Response response) { Intent data = response.getData(); int resultCode = response.getResultCode(); if (resultCode == RESULT_OK) { Bitmap imageBitmap = (Bitmap) data.getExtras().get("data"); } } }); } } }
是不是很簡單,原理是這樣的,通過封裝一個RxResultHoldFragment去處理onActivityResult
private IActivityObservable buildActivityObservable() { T target = targetWeak.get(); if (target instanceof FragmentActivity) { FragmentActivity activity = (FragmentActivity) target; android.support.v4.app.FragmentManager fragmentManager = activity.getSupportFragmentManager(); IActivityObservable activityObservable = RxResultHoldFragmentV4.getHoldFragment(fragmentManager); return activityObservable; } if (target instanceof Activity) { Activity activity = (Activity) target; FragmentManager fragmentManager = activity.getFragmentManager(); IActivityObservable activityObservable = RxResultHoldFragment.getHoldFragment(fragmentManager); return activityObservable; } if (target instanceof Context) { final Context context = (Context) target; IActivityObservable activityObservable = new RxResultHoldContext(context); return activityObservable; } if (target instanceof Fragment) { Fragment fragment = (Fragment) target; FragmentManager fragmentManager = fragment.getFragmentManager(); if (fragmentManager != null) { IActivityObservable activityObservable = RxResultHoldFragment.getHoldFragment(fragmentManager); return activityObservable; } } if (target instanceof android.support.v4.app.Fragment) { android.support.v4.app.Fragment fragment = (android.support.v4.app.Fragment) target; android.support.v4.app.FragmentManager fragmentManager = fragment.getFragmentManager(); if (fragmentManager != null) { IActivityObservable activityObservable = RxResultHoldFragmentV4.getHoldFragment(fragmentManager); return activityObservable; } } return new RxResultHoldEmpty(); }
動態攔截
攔截器是重中之重,有了攔截器可以做好多事情,可以說之所以要做頁面路由,就是為了要實現攔截器。ARouter是用執行緒等待實現的,但是現在有Rxjava了,可以實現更優美的方式。
先來看一下我做的攔截器的效果.
@Intercept(path = "/second") public class SecondInterceptor extends Interceptor { @Override public void intercept(final Call call) { Request request = call.getRequest(); final Intent intent = request.getIntent(); Context context = request.getContext(); StringBuffer stringBuffer = new StringBuffer(); stringBuffer.append("Intercept\n"); stringBuffer.append("URL: " + request.getUrl() + "\n"); AlertDialog.Builder builder = new AlertDialog.Builder(context,R.style.Theme_AppCompat_Dialog_Alert); builder.setTitle("Notice"); builder.setMessage(stringBuffer); builder.setNegativeButton("cancel", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { call.cancel(); } }); builder.setPositiveButton("ok", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { intent.putExtra("key1", "value3"); call.proceed(); } }); builder.setOnDismissListener(new DialogInterface.OnDismissListener() { @Override public void onDismiss(DialogInterface dialog) { call.cancel(); } }); builder.show(); } }
是不是很簡單,參考了部分OkHttp
的實現思路,加入Rxjava,實現非同步攔截。
首先將請求轉換成責任鏈模式RealCallChain
,RealCallChain的call方法實際不會執行路由跳轉,只有Interceptor
裡面呼叫了call.proceed或者call.cancel才會執行.
private Observable<Request> buildRequest() { RealCallChain chain = new RealCallChain(interceptors, 0, request); chain.setTimeout(interceptTimeOut); chain.call(); return chain .getRequestObservable() .map(new Func1<Request, Request>() { @Override public Request call(Request request) { if (interceptors != null) { for (Interceptor interceptor : interceptors) { interceptor.onCall(request); } } return request; } }); }
接著處理非同步的問題,這裡用到了Rxjava的AsyncSubject和BehaviorSubject,
- AsyncSubject具有僅釋放Observable釋放的最後一個資料的特性,作為路由請求的傳送器
- BehaviorSubject具有一開始就會釋放最近釋放的資料的特性,作為路由攔截器的傳送器
具體實現看核心程式碼
@Override public void proceed() { if (index >= interceptors.size()) { realCall(); return; } final Interceptor interceptor = interceptors.get(index); Observable .just(1) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Action1<Integer>() { @Override public void call(Integer integer) { interceptor.intercept(RealCallChain.this); } }); interceptorSubject.onNext(interceptor); index = index + 1; }
方法呼叫
大部分路由庫都是手動拼引數呼叫路由的,這裡模仿了Retrofit
介面式呼叫,受了LiteRouter的啟發,不過Retrofit
使用了動態代理,我使用的Apt
沒有效能損耗。
通過Apt生成每個介面的實際方法
譬如把SecondService
介面
public interface SecondService { @Path("/second") @Activity(SecondActivity.class) void startSecondActivity(); }
生成
@Aspect public final class SecondService$$Provider implements SecondService { public DeepLinkClient deepLinkClient; public SecondService$$Provider(DeepLinkClient deepLinkClient) { this.deepLinkClient= deepLinkClient; } @Override public void startSecondActivity() { Intent intent = new Intent(); intent.setData(Uri.parse("app://deeplink/second")); Request request = deepLinkClient.buildRequest(intent); if (request != null) { request.start(); } } @Around("execution(* okdeeplink.DeepLinkClient.build(..))") public Object aroundBuildMethod(ProceedingJoinPoint joinPoint) throws Throwable { DeepLinkClient target = (DeepLinkClient)joinPoint.getTarget(); if (joinPoint.getArgs() == null || joinPoint.getArgs().length != 1) { return joinPoint.proceed(); } Object arg = joinPoint.getArgs()[0]; if (arg instanceof Class) { Class buildClass = (Class) arg; if (buildClass.isAssignableFrom(getClass())) { return new SecondService$$Provider(target); } } return joinPoint.proceed(); } }
然後呼叫
SecondService secondServicenew = DeepLinkClient(target).build(SecondService.class);
SecondService就生成了。
為了呼叫方便,直接在Activity
或者fragement
寫這段程式碼,sampleServive就自動生成了
@Service SampleService sampleService;
但是如果用到MVP
模式,不是在Activity
裡面呼叫路由,後面會支援在這些類裡面自動注入SampleService,現在先用java程式碼build
引數獲取
大部分路由庫都是手動獲取引數的,這樣還要傳入引數key比較麻煩,這裡模仿了ARouter,不過我支援型別更全一些,支援Bundle支援的所有型別,而且不需要在Acitivty的onCreate
呼叫獲取程式碼。
通過Apt把這段程式碼
public class MainActivity extends AppCompatActivity { @Query("key") String key; }
生成
@Aspect public class MainActivity$$Injector { @Around("execution(* okdeeplink.sample.MainActivity.onCreate(..))") public void onCreate(ProceedingJoinPoint joinPoint) throws Throwable { MainActivity target = (MainActivity)joinPoint.getTarget(); Bundle dataBundle = new Bundle(); Bundle saveBundle = (Bundle)joinPoint.getArgs()[0]; Bundle targetBundle = BundleCompact.getSupportBundle(target); if(targetBundle != null) { dataBundle.putAll(targetBundle); } if(saveBundle != null) { dataBundle.putAll(saveBundle); } try { target.key= BundleCompact.getValue(dataBundle,"key",String.class); } catch (Exception e) { e.printStackTrace(); } joinPoint.proceed(); } @After("execution(* okdeeplink.sample.MainActivity.onSaveInstanceState(..))") public void onSaveInstanceState(JoinPoint joinPoint) throws Throwable { MainActivity target = (MainActivity)joinPoint.getTarget(); Bundle saveBundle = (Bundle)joinPoint.getArgs()[0]; Intent intent = new Intent(); intent.putExtra("key",target.key); saveBundle.putAll(intent.getExtras()); } @Around("execution(* okdeeplink.sample.MainActivity.onNewIntent(..))") public void onNewIntent(ProceedingJoinPoint joinPoint) throws Throwable { MainActivity target = (MainActivity)joinPoint.getTarget(); Intent targetIntent = (Intent)joinPoint.getArgs()[0]; Bundle dataBundle = targetIntent.getExtras(); try { target.key= BundleCompact.getValue(dataBundle,"key",String.class); } catch (Exception e) { e.printStackTrace(); } joinPoint.proceed(); } }
Module接入不同App
這裡是參考ARouter把path作為key對應activity
,這樣接入到其他app中,就自動替換了scheme碼
了
DeepLinkClient.addAddress(new Address("/main", MainActivity.class));
安全
現在有好多人用指令碼來開啟App,然後幹壞事,其實時可以用路由來遮蔽掉.
有三種方法供君選擇,不同方法適合不同場景
簽名遮蔽
就是把所有引數加密成一個資料作為sign引數,然後比對校驗,但是這要求加密方法不變,要不然升級了以前的app就打不開了
adb開啟遮蔽
在android5.1手機上,用adb開啟的app它的mReferrer為空
public boolean isStartByAdb(android.app.Activity activity){ if (Build.VERSION.SDK_INT >= 22) { android.net.Uri uri = ActivityCompat.getReferrer(activity); return uri == null | TextUtils.isEmpty(uri.toString()) ; } return false; }
包名過濾
在Android 4.4手機上, 寫了android:ssp的元件,只有特定應用可以開啟
<activity android:name="okdeeplink.DeepLinkActivity" android:noHistory="true" android:theme="@android:style/Theme.Translucent.NoTitleBar"> <intent-filter> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <data android:ssp="com.app.test" android:host="app" android:scheme="odl" /> </intent-filter> </activity>
這三種方法,比較適合的還是簽名校驗為主,adb過濾為副
如何解決路由造成的Activity堆疊錯亂的問題
activity的launchMode使用不當會照成閃屏頁面開啟多次的問題,可以參考我這篇文章。
未來展望
路由是一個基礎模組,技術難度雖然不是很大,但是如果每個開發都重新踩一遍,價效比就比較低,我希望能把路由相關的所有鏈路都替你弄好,你可以留著時間去幹其他更重要的事情,譬如陪陪家人,逗逗狗什麼的。
接下來我會在這幾個方面努力,把整條鏈路補全。
- 做一個像
Swagger
的平臺,支援一鍵匯出所有路由、二維碼開啟路由 - 註解修改AndroidManifest,不再需要路由表
- 支援路由方法接收器,Url直接開啟某個方法,不再侷限
Activity
如果大家有意見,歡迎聯絡我kingofzqj@gmail.com
相關文章
- 最佳實踐:路徑路由匹配規則的設計與實現路由
- Android MVP 最佳實踐AndroidMVP
- Android SharedPreferences最佳實踐Android
- Android Emoji 最佳實踐Android
- MaxCompute表設計最佳實踐
- react 設計模式與最佳實踐React設計模式
- 設計微服務的最佳實踐微服務
- Android 元件化最佳實踐Android元件化
- Python程式設計規範+最佳實踐Python程式設計
- vSAN 設計、部署、運維最佳實踐運維
- Android元件化最佳實踐-ARetrofitAndroid元件化
- Laravel最佳實踐–事件驅動程式設計Laravel事件程式設計
- 13 個設計 REST API 的最佳實踐RESTAPI
- 函數語言程式設計最佳實踐函數程式設計
- Laravel 最佳實踐 -- 事件驅動程式設計Laravel事件程式設計
- Laravel最佳實踐 -- 事件驅動程式設計Laravel事件程式設計
- Android 元件化最佳實踐 ARetrofit 原理Android元件化
- 編寫 Android Library 的最佳實踐Android
- Spring Boot中五個設計模式最佳實踐Spring Boot設計模式
- Java程式設計師的八個最佳實踐Java程式設計師
- 敏捷最佳實踐:設計衝刺完整指南 -Useberry敏捷
- 資料庫設計的十個最佳實踐資料庫
- iOS VIPER架構實踐(三):面向介面的路由設計iOS架構路由
- 設計出色API的最佳實踐與原則 - JamesAPI
- [01] C#網路程式設計的最佳實踐C#程式設計
- Node之道:設計、架構和最佳實踐 | Alex Kondov架構
- 領域驅動設計最佳實踐--程式碼篇
- TypeScript 資料模型層程式設計的最佳實踐TypeScript模型程式設計
- Android學習之活動的最佳實踐Android
- Android開發中API層的最佳實踐AndroidAPI
- 阿里研究員谷樸:API 設計最佳實踐的思考阿里API
- Ftj aRTTy因子計算最佳實踐
- 邊緣計算的最佳實踐
- 面向切面程式設計AspectJ在Android埋點的實踐程式設計Android
- 鴻蒙Next應用介面設計:國際化最佳實踐鴻蒙
- 資料庫設計中的6個最佳實踐步驟資料庫
- 來自Google資深工程師的API設計最佳實踐Go工程師API
- SQL Server安全設定最佳實踐SQSQLServer
- [實踐系列] 前端路由前端路由