最近在对项目中支付模块的重构,经过三个月的努力,让项目的支付焕然一新。过程是艰辛的,结果是完美的,哈哈。接下来分享一下在重构支付整个流程设计和实现。

为什么要独立模块?随时公司业务不断的发展,项目需要对接不同的支付方式和支付渠道,随着时间的推移,对接的支付方式不断增加,同时支付模块的代码量不断的增加, 很多相同的代码。更严重的是每新增一种支付方式或者支付渠道,工作量也随之在增加。同时业务需求也在变化,老的支付模块就不能适应需求, 即使通过改造去适应都需要花费很多时间。最后经过小组开会讨论,准备对支付中心进行重构,要求不同的支付方式必须独立成模块,能独立运行,不同的支付方式可以随机组合。讨论了一周,也确定了重构的方案。接下来就是开始动手干。

设计思路:创建一个支付中心, 所有终端发起支付,必须先经过支付中心, 支付中心接收到支付请求后, 对发起支付前的基本参数进行检查, 参数检查通过后, 根据不同的支付方式,使用arouter进行路由分发到对应的支付sdk, 具体的支付sdk负责执行支付操作。支付的结果通过回调的方式返还给发起支付的终端。 支付中心提供一个默认的(包含所有的支付方式)支付路由分发器。当然接入支付中心的终端,可以通过配置文件实现自己的支付路由分发。

具体实现

通之前集成的支付方式不难发现,不同的支付方式之间,支付参数和支付成功返回的数据差别不大或者说基本相同。所以首先我们需要对支付请求参数和支付返回的数据进行封装

/*** 支付必须的参数*/
public class PayCenterPayParams implements Serializable {/*** 支付来源类型*/@PaySourceType.IntDefprivate int paySourceType = PaySourceType.DEFAULT;/*** 支付方式*/@PaySupportType.IntDefprivate int payType;/*** 支付、退款、刷新* 默认是支付*/@PayIntention.IntDefprivate int payIntention = PayIntention.PAY;/*** 业务模块与支付模块协商【实际支付信息都存在这个json数据中】*/private String parameterJson;/*** 是否是重试*/private boolean isRetry;
}//-----------------------------------------------------------------------------
/*** 支付结果*/
public class PayResults {@PaySourceType.IntDefprivate int paySourceType;/*** 业务模块与支付模块协商【实际返回的支付数据】*/private String resultJson;private boolean needPrint;/*** 记录服务器返回的失败原因*/private String reasonStr;/*** 是否是重试*/private boolean isRetry;/*** 分步支付时,各模块已经支付的金额,用于回调时来判断是否支付完成,* 若为null,则认为支付完成*/private BigDecimal payedAmount;
}

从代码中可以看出, 支付请求和支付结果中的实际参数我们使用String类型的json字符串代表,通过json字符串在不同模块之间进行传输。至于json字符串包含什么数据, 这个只需要和服务器端约定好即可。

接下来需要为支付结果定义一个回调接口,将支付的结果返回给调用者

/*** 支付通用回调*/
public interface PayCallBack {/*** 支付结果回调* @param code {@link PayCenterConstants}* @param results {@link PayResults}*/void onPayCallBack(@PayCenterConstants.PayIntDef int code, PayResults results);
}

约定好支付请求和支付结果数据,接下来我们需要定义支付的公共接口。所有的支付方式都需要实现该公共接口中的方法。接口定义了支付所需要的行为,当然我们肯定也会为该公共接口提供默认的实现或者抽象类, 进步进行封装。

/*** 支持中心提供的方法,包括发起读取支付范本,调取支付面板,具体支付实现由各模块自己实现* 流程为:* 1.各业务模块向支付中心发起读取支付范本的请求,支付中心回传结果;
</>* 2.业务模块可向支付中心请求调起支付面板,也可自己实现
</>* 3.业务模块向支付中心指定支付方式并发起支付* 4.对开发票及抹零操作由各模块自己实现*/
public interface IPayCenter extends IProvider {/*** 发起支付** @param activity    调起的页面* @param payParams   支付参数* @param payCallBack 支付回调*/void pay(Activity activity, PayCenterPayParams payParams, PayCallBack payCallBack);/*** 添加支付代理操作,用于在{@link IPayCenter#pay(Activity, PayCenterPayParams, PayCallBack)}执行前和执行后的操作** @param proxy 代理器{@link IProxy}*/void setPayProxy(IPayProxy proxy);/*** 添加的除支付外的其他操作,用于业务与底层的交互** @param context         调起者* @param operateParams   {@link PayCenterPayParams}参数封装* @param operateCallback {@link OperateCallBack}*/void operate(Context context, PayCenterPayParams operateParams, OperateCallBack operateCallback);/*** 做一些销毁操作*/void destroy();interface IPayProxy extends IProxy {}
}

定义一个抽象的类, 对公共接口进行实现,将接口定义的行为隐藏起来。不同的支付方式只需要实现该抽象类中的抽象方法即可

/*** 支付基础操作*/
public abstract class BasePayProvider implements IPayCenter {protected PayCallBack payCallBack;protected PayCenterPayParams payParams;@Overridepublic void pay(Activity activity, PayCenterPayParams payParams, PayCallBack payCallBack) {this.payParams = payParams;this.payCallBack = payCallBack;checkPayParams();pay(activity, payParams);}@Overridepublic void init(Context context) {//TODO:若需要初始化操作,子类重写即可}@Overridepublic void operate(Context context, PayCenterPayParams operateParams, OperateCallBack operateCallback) {// TODO: 08/03/2018 若有需要,子类重写即可}public PayCallBack getPayCallBack() {return payCallBack;}public PayCenterPayParams getPayParams() {return payParams;}/*** 检查参数后发起支付*/public abstract void pay(Activity activity, PayCenterPayParams payParams);private void checkPayParams() {AssertUtils.assertNotNullParams(payParams, "payParameters is null");if (payParams.getPaySourceType() == PaySourceType.DEFAULT) {throw new NullPointerException("pay source is not define !!!");}if (payParams.getPayType() == PaySupportType.INVALID)throw new IllegalArgumentException("pay type not define!!!");AssertUtils.assertNotNullParams(payParams.getUmengKeys(), "umengKeys is null");}
}

除了上面定义的支付中心接口,我们需要定义一个支付控制器,管理整个支付过程中的支付和回调。将支付的实现类注入到支付控制器中。统一由支付控制器进行管理。这里我们先抽象一个管理控制器的基类,不同的支付方式实现这个管理控制器基类即可

/*** 支付的控制器,子类可用于单例,管理全局回调*/public abstract class BasePayController {private static final String TAG = "BasePayController";private IPayCenter payCenter;private IProxy proxy;public BasePayController() {}public void init(IPayCenter payCenter) {this.payCenter = payCenter;}public void setProxy(IProxy proxy) {this.proxy = proxy;}public IProxy getProxy() {return proxy;}/*** 需要返回全局回调** @return {@link PayCallBack}*/public abstract PayCallBack getPayCallBack();/*** 最初传入的参数** @return {@link PayCenterPayParams}*/public abstract PayCenterPayParams getPayParams();/*** 在支付完成时做一些清理操作*/public void destroy() {proxy = null;}public IPayCenter getPayCenter() {return payCenter;}/*** 代理执行支付操作*/public abstract void payProxy(Activity activity, PayCenterPayParams payParams, PayCallBack payCallBack);public void pay(Activity activity, PayCenterPayParams payParams, PayCallBack payCallBack) {AssertUtils.assertNotNullParams(payCenter, "please init payCenter before use!!!");if (proxy != null) {OperateParams params = new OperateParams();params.setPayCenter(payCenter);params.setActivity(activity);params.setPayParams(payParams);params.setPayCallBack(payCallBack);params.setPayController(this);// FIXME: 24/01/2018 注意此处需要正确处理before的返回值,否则会进入循环调用boolean before = proxy.onBefore(params);MLog.e(TAG, "pay::before is true? " + before);if (!before) {//代理器没有向下执行,需要自己向下执行payProxy(activity, payParams, payCallBack);//FIXME: 在执行payProxy()方法时,可能会调用destroy()方法,对proxy及payCenter置Null;须注意if (proxy != null) {proxy.onAfter(payCenter, activity, payParams, payCallBack);}}} else {payProxy(activity, payParams, payCallBack);}}
}

以上就是整个支付中心的设计思路。从代码中也看到使用了代理,这里的代理主要是对支付前和支付后做一些检查工作, 比如支付前需要检查订单是否已经下单,如果没下单需要先下单。代理这里就不做更多的阐述。 整个支付中心,代码不多, 主要是一些行为的抽象,具体的实现都交给具体的支付sdk完成。接下来以现金支付为列说一下具体的实现

现金支付SDK

先来一张现金支付sdk的架构图

sdk最重要的就是CashPayProvider和CashPayController这两类,CashPayProvider类的作用是现金支付提供者,该类需要继承支付中心的BasePayProvider抽象类, 并实现该类中的所有抽象方法。CashPayController类主要作用是控制现金支付流程,该类需要继承支付中的BasePayController抽象类。

CashPayProvider 类的源码

//path的值就是支付中分发器中的payuri
@Route(path = PayARouterUri.Provider.PAY_CENTER_PAY_CASH)
public class CashPayProvider extends BasePayProvider {@Overridepublic void pay(Activity activity, PayCenterPayParams payCenterPayParams) {CashPayController.getInstance().pay(activity, payCenterPayParams, getPayCallBack());}@Overridepublic void init(Context context) {super.init(context);CashPayController.getInstance().init(this);}@Overridepublic void setPayProxy(IPayProxy iPayProxy) {CashPayController.getInstance().setProxy(iPayProxy);}@Overridepublic void destroy() {CashPayController.getInstance().destroy();}
}

CashPayController类源码

/*** 现金支付控制器*/
public class CashPayController extends BasePayController {private static CashPayController sInstance;private PayCenterPayParams payParams;private PayCallBack payCallBack;private CashPayController() {}public static CashPayController getInstance() {if (sInstance == null) {synchronized (CashPayController.class) {if (sInstance == null) {sInstance = new CashPayController();}}}return sInstance;}@Overridepublic void payProxy(Activity activity, PayCenterPayParams payParams, PayCallBack payCallBack) {this.payParams = payParams;this.payCallBack = payCallBack;dispatchPaySource(activity, payParams, payCallBack);}/*** 根据不同的业务类型支付来源判断是支付还是充值*/private void dispatchPaySource(Activity activity, PayCenterPayParams payParams, PayCallBack payCallBack) {switch (payParams.getPaySourceType()) {case PaySourceType.DINNER:case PaySourceType.SNACK:payByCash(activity, payParams, payCallBack);break;case PaySourceType.RECHARGE:case PaySourceType.SALE_RECHARGE:/rechargeByCash(activity, payParams, payCallBack);break;default:break;}}private void payByCash(Activity activity, PayCenterPayParams payParams, PayCallBack payCallBack) {CashPayJumpbean cashPayJumpbean = JSON.parseObject(payParams.getParameterJson(), CashPayJumpbean.class);//此处在传入处理,此处只校验AssertUtils.assertNotNullParams(cashPayJumpbean.getReceivableAmount(), cashPayJumpbean.getActualAmount());/*** 每次尝试支付的时候,生成新的 paymentItemUUID UUID,这次支付过程中,不需要改变 paymentItemUUID*/cashPayJumpbean.setPaymentItemUUID(AndroidUtil.randomUUID());Intent callingIntent = new Intent(activity, UnityPayCashActivity.class);callingIntent.putExtra(UnityPayCashActivity.INTENT_EXTRA_PARAM_JUMPBEAN, cashPayJumpbean);callingIntent.putExtra(UnityPayCashActivity.INTENT_EXTRA_PARAM_UMENGKEYS, payParams.getUmengKeys());activity.startActivity(callingIntent);}private void rechargeByCash(Activity activity, PayCenterPayParams payParams, PayCallBack payCallBack) {/*** 如果是售卡中得充值, 需要进行特殊处理*/PayCenterBaseRechargeReq payRechargeReq = JSON.parseObject(payParams.getParameterJson(), PayCenterBaseRechargeReq.class);if (payRechargeReq == null || payRechargeReq.getPaidAmount() == null || BigDecimal.ZERO.compareTo(payRechargeReq.getPaidAmount()) == 0) {ToastUtil.showToast(activity, activity.getString(R.string.cash_recharge_zero_tip));return;}if (payParams.getPayType() == PayMode.CASH.getPayType()&& payParams.getPaySourceType() == PaySourceType.SALE_RECHARGE) {//现金支付Intent intent = new Intent(activity, CashPayActivity.class);intent.putExtra(CashPayActivity.CASH_PAY_PARAMS, payRechargeReq);activity.startActivity(intent);} else {CashPayManager.getInstance().startCashRecharge(activity, payRechargeReq);}}@Overridepublic PayCenterPayParams getPayParams() {return payParams;}@Overridepublic void destroy() {super.destroy();payParams = null;payCallBack = null;}@Overridepublic PayCallBack getPayCallBack() {return payCallBack;}}

以上就现金支付SDK和支付中心模块关联的地方。我们通过ARouter将两个模块的耦合度大大降低。至于真正的现金支付逻辑,就需要现金支付SDK自己实现。比如支付密码输入界面, 支付金额抹零等逻辑等,根据不同的支付方式和业务需要,自行在各自的sdk内部完成。

支付路由

接下来简单说一下支付中心提供默认的路由分发。如果业务模块有支付需求,没有提供自定义支付路由,则会按照支付中心默认的路由进行支付分发。

public class PayCenterDispatcher {/*** 使用路由分发支付服务** @param payType {@link PaySupportType}* @return {@link IPayCenter}*/public static IPayCenter dispatchPayCenter(@PaySupportType.IntDef int payType) {//注意:这里就是获取业务模块是否提供类自定义的支付路由IPayUriDispatcher dispatcher = (IPayUriDispatcher) ARouter.getInstance().build(PayARouterUri.Provider.PAY_CENTER_DISPATCHER).navigation();String payUri;if (dispatcher != null) {//调用业务模块提供的自定义支付路由payUri = dispatcher.payUri(payType);} else {//调用支付中心默认的支付路由payUri = defaultPayUri(payType);}if (TextUtils.isEmpty(payUri))throw new IllegalArgumentException("no support pay type!!!");return (IPayCenter) ARouter.getInstance().build(payUri).navigation();}/*** 默认的路由分发器** @param payType payType {@link PaySupportType}* @return uri*/public static String defaultPayUri(@PaySupportType.IntDef int payType) {switch (payType) {case PaySupportType.CASH:return PayARouterUri.Provider.PAY_CENTER_PAY_CASH;case PaySupportType.WECHAT:return PayARouterUri.Provider.PAY_CENTER_PAY_WECHAT;case PaySupportType.ALIPAY:return PayARouterUri.Provider.PAY_CENTER_PAY_ALIPAY;case PaySupportType.BANK_FLASH_PAY://银联云闪付return PayARouterUri.Provider.PAY_CENTER_BANK_FLASH;case PaySupportType.VIP:case PaySupportType.JINCHENG_CARD://金诚刷卡与会员走一样的逻辑return PayARouterUri.Provider.PAY_CENTER_PAY_VIP;case PaySupportType.BANK:return PayARouterUri.Provider.PAY_CENTER_PAY_BANK;case PaySupportType.CUSTOMIZE:return PayARouterUri.Provider.PAY_CENTER_PAY_CUSTOMIZE;case PaySupportType.SHANHUI:return PayARouterUri.Provider.PAY_CENTER_PAY_SHANHUI;case PaySupportType.BAIDUQB:return PayARouterUri.Provider.PAY_CENTER_PAY_BAIDUQB;case PaySupportType.MEITUAN:case PaySupportType.NUOMI:return PayARouterUri.Provider.PAY_CENTER_PAY_TUANGOU;case PaySupportType.INVALID:default:return null;}}/*** 直接发起支付** @param payType     选择支付方式{@link PaySupportType}* @param activity    context* @param payParams   {@link PayCenterPayParams}* @param payCallBack {@link PayCallBack}*/public void pay(@PaySupportType.IntDef int payType, Activity activity, PayCenterPayParams payParams, PayCallBack payCallBack) {IPayCenter payCenter = dispatchPayCenter(payType);if (payCenter == null)throw new NullPointerException(String.format(Locale.CHINESE, "not load pay service for %d !!!", payType));payCenter.pay(activity, payParams, payCallBack);}
}/*** 支付服务分发路由地址*/public class PayARouterUri {public static final String VERSION = "/v1";public static final String TAG = "/payCenter" + VERSION;public final class Provider {public static final String PAY_CENTER_DISPATCHER = "/dispatcher" + PayARouterUri.TAG + "/uri";public static final String PAY_CENTER_PAY_CASH = "/cash" + PayARouterUri.TAG + "/pay";public static final String PAY_CENTER_PAY_WECHAT = "/weChat" + PayARouterUri.TAG + "/pay";public static final String PAY_CENTER_PAY_ALIPAY = "/alipay" + PayARouterUri.TAG + "/pay";public static final String PAY_CENTER_PAY_VIP = "/vip" + PayARouterUri.TAG + "/pay";public static final String PAY_CENTER_PAY_BANK = "/bank" + PayARouterUri.TAG + "/pay";public static final String PAY_CENTER_PAY_CUSTOMIZE = "/customize" + PayARouterUri.TAG + "/pay";public static final String PAY_CENTER_PAY_SHANHUI = "/shanhui" + PayARouterUri.TAG + "/pay";public static final String PAY_CENTER_PAY_BAIDUQB = "/baiduqb" + PayARouterUri.TAG + "/pay";public static final String PAY_CENTER_PAY_TUANGOU = "/tuangou" + PayARouterUri.TAG + "/pay";/*** 银联云闪付*/public static final String PAY_CENTER_BANK_FLASH = "/bankflash" + PayARouterUri.TAG + "/pay";}
}

业务模块应该怎么提供自定义的路由分发?为什么支付中心已经包含了支付路由,还需要让业务模块提供自定义的路由分发?

我们为了让支付中心扩展性更强,也为了满足一些特殊的需求, 比如同样是微信支付和支付宝支付(统称为在线支付), 在接入新的支付渠道商时,对方要求,在线支付需要用他们接入的(就是必须由渠道商与微信平台或者支付宝支付平台对接的支付),这个时候,如果你不提供自定义的支付路由, 当你准备用在线支付方式进行支付时,最后就不能走渠道商实现的在线支付, 而是走我们直接和微信或支付宝支付平台对接的支付流程。这不符合项目的需求。这个时候,我们就需要根据不同的渠道提供自定义的支付路由。

实现自己的支付路由分发逻辑。首先,我们可以在项目的assets目录下创建一个配置文件, 配置不同的支付方式对应的支付routerUri,

{"payTypeUris":[{"payType":"1","routerUri":"/cash/payCenter/v1/pay","payTypeName":"现金"},{"payType":"2","routerUri":"/weChat/payCenter/v1/pay","payTypeName":"微信"},{"payType":"3","routerUri":"/alipay/payCenter/v1/pay","payTypeName":"支付宝"},{"payType":"5","routerUri":"/bank/payCenter/v1/pay","payTypeName":"银行卡"},{"payType":"15","routerUri":"/bankflash/payCenter/v1/pay","payTypeName":"云闪付"}]
}

自定义支付路由解析类

/*** 实际用于分地的pay uri,根据配置文件分渠道动态生成*/
@Route(path = PayARouterUri.Provider.PAY_CENTER_DISPATCHER)//这个地址就是自定义路由的uri
public class PayUriDispatcher implements IPayUriDispatcher {private final String TAG = PayUriDispatcher.class.getSimpleName();private PayTypeUriList payTypeUriList = null;@Overridepublic String payUri(int payType) {if (payTypeUriList != null) {if (payTypeUriList.getPayTypeUris() != null && !payTypeUriList.getPayTypeUris().isEmpty()) {List<PayTypeUri> payTypeUris = payTypeUriList.getPayTypeUris();for (PayTypeUri payTypeUri : payTypeUris) {if (payType == Integer.parseInt(payTypeUri.getPayType())) {String routerUri = payTypeUri.getRouterUri();MLog.d(TAG, String.format("inject pay url [%s] => [%s]", payType, routerUri));return routerUri;}}MLog.e(TAG, "not found payType in pay_uri.json, will use default");} else {MLog.e(TAG, "payTypeUriList.getPayTypeUris() == null || payTypeUriList.getPayTypeUris().isEmpty()");}} else {MLog.e(TAG, "payTypeUriList == null");}return PayCenterDispatcher.defaultPayUri(payType);}@Overridepublic void init(Context context) {payTypeUriList = PayUriManager.getInstance().getPayTypeUriList();if (payTypeUriList == null) {PayUriManager.getInstance().init(BaseApplication.getInstance());payTypeUriList = PayUriManager.getInstance().getPayTypeUriList();}}
}//---------------------------------public boolean init(Context context) {if (context == null) {MLog.e(TAG, "init error context == null");return false;}String payUriJson = JsonUtils.readAssetText(context, "pay_uri.json");try {payTypeUriList = JSON.parseObject(payUriJson, PayTypeUriList.class);} catch (Exception e) {MLog.e(TAG, "init parse error " + e.getMessage());return false;}return true;
}//-------------------------------
/*** 读取assets中的文本文件到string** @param context  context* @param fileName assets下的文件名* @return string ,"" if empty*/
public static String readAssetText(Context context, String fileName) {StringBuilder json = new StringBuilder();InputStreamReader streamReader = null;BufferedReader bufferedReader = null;try {String line = "";streamReader = new InputStreamReader(context.getAssets().open(fileName));bufferedReader = new BufferedReader(streamReader);while ((line = bufferedReader.readLine()) != null) {json.append(line);}} catch (Exception e) {e.printStackTrace();} finally {if (streamReader != null) {try {streamReader.close();} catch (IOException e) {e.printStackTrace();}}if (bufferedReader != null) {try {bufferedReader.close();} catch (IOException e) {e.printStackTrace();}}}return json.toString();
}

总结:以上就是整个支付流程的模块设计方案和实现。实现了通过支付中心进行中转分发到对应的支付模块。不同的支付模块还可以独立的运行,不同的支付模块可以根据配置文件随意组合。如果业务需要对接一个新的支付渠道,此时就方便很多,只需要创建一个独立的支付sdk,sdk只需要做该支付渠道对应的支付逻辑即可,最后通过配置文件,将新的支付渠道配置进去,就能完美实现对新支付渠道的对接。这样业务模块和支付中心都不需要更改任何代码,就能实现新的支付渠道支付。

组件化开发——支付中心相关推荐

  1. Android模块化和组件化开发

    目录 一.模块化介绍 1.1:模块化简介 1.2:模块化和组件化的区别 1.3:模块化的优点 1.4:模块化的层级介绍 二.如何实现组件化 2.1:实现模块化需要解决的问题 2.2:各个问题的解决方法 ...

  2. 我的react组件化开发道路(二) 分页 组件开发

    2019独角兽企业重金招聘Python工程师标准>>> 上一篇文章主要写了关于react组件化开发的一些基本配置,慢慢的深入到每个组件的详细介绍中,今天我们就来分享react的分页组 ...

  3. [Vue.js] 深入 -- 组件化开发

    组件化开发思想 现实中的组件化思想体现 标准 分治 重用 组合 组件注册 全局组件注册语法 Vue.component(组件名称,{data:组件数据,template:组件模板内容 }) 组件用法 ...

  4. android 组件化_你曾遇到的某大厂奇葩问题:Android组件化开发,组件间的Activity页面跳转...

    组件化开发有什么好处? 1.当项目越来越大时,app的业务越来越复杂,会出现业务功能复杂混乱,各功能块.页面相互依赖,相互调用太多导致耦合度高,而采用组件化开发,我们就可以将功能模块合理的划分,降低功 ...

  5. Vue组件化开发 - 非常详细,不要错过哦~

    源码示例链接:https://pan.baidu.com/s/1NEYDmLl2K7nNa-AKWtJqVA 提取码:2c7a 目标 能够知道组件化开发思想 能够知道组件的注册方式 能够说出组件间的数 ...

  6. 前端模块化、组件化开发

    使用过ReactJS进行Web UI的组件化开发,和使用过AngularJS的双向数据绑定和模块化后,感觉到了组件化.模块化.双向数据绑定对Web前端开发的重要性. 1.组件化可以极大提高前端代码的可 ...

  7. Vue第二天学习总结—— Vue全家桶之组件化开发(组件化开发思想、组件注册、Vue调试工具用法、组件间数据交互传递、组件插槽、基于组件的案例——购物车)

    (一) 组件化开发思想 1. 现实中的组件化思想体现 组件化即是对某些可以进行复用的功能进行封装的标准化工作 标准:要想组件能够成功组合在一起,每个组件必须要有标准 分治:将不同的功能封装到不同的组件 ...

  8. 如何理解Unity组件化开发模式

    Unity的开发模式核心:节点和组件,组件可以加载到任何节点上,每个组件都有 gameobject 属性,可以通过这个属性获取到该节点,即游戏物体. 也就是说游戏物体由节点和组件构成,每个组件表示物体 ...

  9. Vue全家桶之组件化开发

    作者 | Jeskson 掘金 | https://juejin.im/user/5a16e1f3f265da43128096cb 学习组件化开发,首先掌握组件化的开发思想,组件的注册方式,组件间的数 ...

最新文章

  1. vlc的应用之十:vlc的远程控制
  2. 详解云计算、大数据和人工智能的区别与联系
  3. 硬盘安装linux_Surface-Laptop3 安装Archlinux折腾小记
  4. C# Window Form解决播放amr格式音乐问题
  5. linux技巧33条
  6. 微信小程序中识别html标签的方法
  7. pdf 分形 张济忠_分形
  8. EJB是什么?有什么优点?
  9. java该选择哪个城市_逃离北上广,java程序员又能选择哪些城市呢?
  10. 基于云开发的查单词小程序设计 报告+PPT+项目源码+演示视频
  11. Web——HTML常见标签及用法
  12. 视频教程-shader 基础之 2D技巧集合-Unity3D
  13. 一切免费的大学视频教程,免费空间和网盘
  14. 《微信小程序》 开源项目
  15. 48小时房价暴涨57%,数据解读站在风口上的这座小城
  16. 年度大戏《“跨界”的诱惑》主演:头部车企、手机巨头
  17. GitLab CI/CD系列教程(一)
  18. java游戏代码潜艇大战_java游戏之潜艇大战
  19. thinkphp使用force
  20. 化学结构式检索obabel

热门文章

  1. 球体动画Android,使用CSS创建一个炫酷的球体动画效果
  2. 高维数据惩罚回归方法:主成分回归PCR、岭回归、lasso、弹性网络elastic net分析基因数据...
  3. 隐私计算--25--联邦学习激励机制
  4. Android 性能优化(一) —— 启动优化提升60%
  5. 一分钟解决QT官网无法下载的问题
  6. openpyxl版本问题
  7. 新开班全栈Linux运维-Linux云计算运维与高级架构班课程 全新自动化运维必学课程
  8. 淘宝白底图有什么要求 淘宝白底图权重及注意事项
  9. ⭐算法入门⭐《堆》中等02 —— LeetCode 703. 数据流中的第 K 大元素
  10. 土豆视频显示服务器走丢了怎么办,江湖风云录玩家常见问题解答 玩家常遇到的四十个问题_3DM手游...