很久很久之前学过PureMVC,这里是同事前公司和现公司项目中用的改版PureMVC框架

使用到目前为止,个人觉得PureMVC框架的巨大优点是模块之间基本没有互相引用的关系,一个模块的Mediator,UI,proxy形成闭环,与外界沟通是用命令来进行,命令已经清晰的指出了该命令具有的功能。这个超低的耦合性使得,PureMVC的一个功能模块能够很方便地抽取出来,然后用于任何一个Unity的项目,不需要任何更改,基本就是脚本和预制体放进去,可能有些tag之类的要设置一下。

MVC的方式 将一个功能划分为显示逻辑 ,功能逻辑和数据逻辑,并且PureMVC方式的Mediator控制UI和Proxy的写法使得单纯修改UI或者Proxy的时候,对其他模块的影响较小基本没有。

框架需要Unity的机制绑定起来才能运行,通过这个脚本进行绑定。

using System;
using System.Collections;
using UnityEngine;
using UnityEngine.SceneManagement;public class Launcher : MonoBehaviour
{private static Launcher instance;private Launcher() { }public static Launcher GetInstance(){return instance;}// Start is called before the first frame updatevoid Start(){DontDestroyOnLoad(gameObject);Application.targetFrameRate = -1;instance = this;ApplicationFacade.GetInstance().Startup();}private AsyncOperation asyncOperation = null;private string notificationName;private object data;private IEnumerator LoadSceneCoroutine(LoadSceneValueObject loadSceneValueObject){yield return new WaitForSeconds(0.5f);if (loadSceneValueObject != null){notificationName = loadSceneValueObject.NotificationName;data = loadSceneValueObject.Data;if (!string.IsNullOrEmpty(loadSceneValueObject.SceneName)){yield return asyncOperation = SceneManager.LoadSceneAsync(loadSceneValueObject.SceneName);}}}public void LoadScene(LoadSceneValueObject loadSceneValueObject){StartCoroutine(LoadSceneCoroutine(loadSceneValueObject));}// Update is called once per framevoid Update(){if (asyncOperation != null && asyncOperation.isDone && !string.IsNullOrEmpty(notificationName)){asyncOperation = null;ApplicationFacade.GetInstance().SendNotification(notificationName, data);}}private void OnApplicationQuit(){}
}

PureMVC用到的设计模式

  • 命令模式:Command类的设计符合命令模式
  • 中介者模式:各个模块的Mediator通过 Notification注册的信息途径 ApplicationFacade 来通知到其他的模块
  • 观察者模式: ApplicationFacade里面的注册Notification消息属于观察者模式
  • 单例模式:每个模块都是一个单例

下面记录一些使用它的注意点

初始化次序

初始化次序是越基础的模块,越在之前初始化好

StartUpCommand 里面的初始化次序很重要
举个例子,


public class StartupCommand : SimpleCommand
{public override void Execute(INotification notification){base.Execute(notification);ApplicationFacade.GetInstance().SendNotification(NotificationNames.LOAD_EXHIBITION_HALL_DATA);ApplicationFacade.GetInstance().SendNotification(NotificationNames.INIT_MINIMAP);ApplicationFacade.GetInstance().SendNotification(NotificationNames.DOUBLE_SCREEN_SHOW);ApplicationFacade.GetInstance().SendNotification(NotificationNames.INIT_ROAMING_CAMERA);}
}

这里的INIT_MINIMAP初始化的Mediator模块在初始化后几乎立刻会调用到INIT_ROAMING_CAMERA的mediator的模块的一些指令,

case NotificationNames.UPDATE_ROAMING_CAMERA_LOACL_POS:UpdateCameraLocalPosition updateCameraLocalPosition = (UpdateCameraLocalPosition)body;GetView().UpdateCameraLocalPosition(updateCameraLocalPosition);break;  

这个指令会导致需要用到GetView()
但是如果像一开始那样的顺序进行注册,那么调用GetView()的时候,因为INIT_ROAMING_CAMERA对应的模块还没有初始化,所以返回的是空,就会报这个错,这个错是不太容易被发现的。所以正确的

public class StartupCommand : SimpleCommand
{public override void Execute(INotification notification){base.Execute(notification);ApplicationFacade.GetInstance().SendNotification(NotificationNames.INIT_ROAMING_CAMERA);ApplicationFacade.GetInstance().SendNotification(NotificationNames.LOAD_EXHIBITION_HALL_DATA);ApplicationFacade.GetInstance().SendNotification(NotificationNames.INIT_MINIMAP);ApplicationFacade.GetInstance().SendNotification(NotificationNames.DOUBLE_SCREEN_SHOW);}
}

其他注意点

  1. 模块内UI向Mediator的通信使用BroadCast,Mediator对UI的通信是持有引用的方式, 如下
    UI
public void OnDemoModeToggleChanged(bool isOn)
{Broadcast(ON_PLAYMODE_TOGGLE_CHANGED, isOn);
}

Mediator

GetView().AddListener<bool>(PlayerSettingsUI.ON_PLAYMODE_TOGGLE_CHANGED, OnDemoModeToggleChanged);private void OnDemoModeToggleChanged(bool switchValue){playerSettingsProxy.SetDemoModeEnable(switchValue);}
  1. 模块之间的通信使用ApplicationFacade.GetInstance().SendNotification(NotificationNames.DOUBLE_SCREEN_SHOW);的方式

  2. UI 负责持有场景里面的引用,对场景里面的只属于自己功能的物体用拖拽引用的方式持有,或者持有静态的数据

  3. Mediator是可以持有其相关的Proxy的,也可以持有多个Proxy

  4. proxy的Init指令在StartUpCommand中发送,由发送指令执行对应的初始化proxy的Command的方式进行。即每创建一个proxy都会有对应的一个proxy初始化command创建。proxy存的时运行时的动态改变的数据,而不是一些模块的静态类型的常量数据

  5. Mediator寻找UI物体通过Tag的方式,一个模块有一个Mediator一个UI,tag。也可以通过FindGameObjectOfType的方式

  6. proxy里面只发送相关的数据准备好的消息,需要用到对应的proxy里面的数据的Mediator自己对相关的消息进行注册监听。如果proxy里面进行相关的数据的初始化后进行相关的Mediator的发送消息的操作,会导致调用关系乱。

  7. 一般需要有数据才能正常使用的Mediator,做法是在所有需要的数据加载完成后才进行初始化,因为考虑到情况是:复杂的Mediator可能需要多个数据,在有时这些数据可能加载会比较慢,在加载期间用户可能做了比较多的相关操作,正常的逻辑是相关操作临时缓存了,等数据加载回来之后才进行反应,但是仔细想想,这样的动作多了以后,模块的逻辑会变的很复杂,所以干脆就是在所有数据都返回前就不进行初始化,点击的时候就飘字提示稍等。

  8. 虽然tag的个数没有限制,但通过tag寻找要求物体一定要是active的,但是编辑UI的时候为了方便会隐藏一些UI,所以当时想了三个方案
    1. 要一个在最开始将所有UI先设置active的command执行,每个mediator找到以后再根据需要关闭
    缺点是 一下子遍历全部打开,每个功能要用额外的代码做关闭其下的相关子UI的操作,有些比较复杂的UI写的代码就有点多,并且改结构了之后,这份代码也可能要重新改重新调试
    优点是 代码部分做好了的话 平时的时候可以随意开启关闭UI 都能正常工作
    2. 隐藏只隐藏这部分,

    打tag的地方在
    缺点是,多人开发中,这种做法无法保证完全无错误。因为不是每一次都能准确地只关闭到content

  9. 框架流程是先加载所有数据,再加载所有模块的初始化,但是这只是调用的先后,执行完成的先后框架本身没有保证,比如网络异步数据或者磁盘读取数据都需要一点点时间延迟, 个人觉得需要Proxy基类有个自个初始化数据加载好的标志位,并且每个Proxy都有个自个数据初始化好的消息通知,并且有个总的proxy数据初始化完成的事件通知。需要在总的数据初始化完成以后,再来进行模块的加载。否则逻辑就可能造成每个模块初始化的时候要对需要的数据模块的初始化完成进行监听以及刚开始初始化时候判断对应的数据模块是否初始化完成并获取数据初始化,一些需要数据的初始化操作都需要做这种事情。 这个可能后面会在提供的改版框架中添加。

  10. 项目中,为防止场景中的文件引用关系混乱, 场景中通常一个模块下涉及到的所有物体都在一个父物体下面,这个模块的UI类一般只引用其下的物体。这样做则职责不那么细分,一个模块到后期涉及的功能会比较多。 例如漫游相机模块,如果涉及到了响应设置更改相机FOV,则直接在这个模块下响应修改的通知并做出更改。而不是新建一个Mediator类,一个新的代表这个Mediator 的挂载对应UI的游戏物体然后进行别的模块的相机引用然后进行功能。一个模块到后期涉及的功能会比较多时,可以使用分布类的方式进行开发,一个分布类负责一个单一分功能

  11. 改proxy遵循的原则是对应模块的mediator可以更改对应模块的proxy,别的模块要用到对应的proxy一般只是用数值而不直接更改。

  12. 程序开始和程序结束都有对应的Command,这个Command由Launcher进行关联Unity机制然后适时发出消息。在Command里面进行一系列的初始化方法以及注销方法。

  13. Notification的名字要和对应唤起的Mediator或者Command名字对应起来

  14. 当遇到功能同时执行的时候,例如某个模块要显示出来,另一个模块要隐藏下去的时候,不应该让隐藏的模块去监听要显示出来的模块的命令,而是在这个地方发模块隐藏的指令,然后发另一个模块显示的指令。这种复合的功能同时执行的次数多了之后,就可以将其抽取到Command里面。如果做了错误的做法,可能会出现某个模块与其他模块耦合度比较大,如下面这个示例

public override void HandleNotification(INotification notification){string name = notification.Name;object body = notification.Body;switch (name){case NotificationNames.INIT_PREVIEW_CAMERA:exhibitionHallProxy = ApplicationFacade.GetInstance().RetrieveProxy(ExhibitionHallProxy.NAME) as ExhibitionHallProxy;playerSettingsProxy = ApplicationFacade.Instance.RetrieveProxy(PlayerSettingsProxy.NAME) as PlayerSettingsProxy;InitUI();SetFOV();break;case NotificationNames.SETTINGS_PROXY_DATA_INIT_COMPLETED:case NotificationNames.SETTINGS_PROXY_CAMERA_FOV_CHANGED:SetFOV();break;case NotificationNames.EXHIBITION_HALL_DETECTED_MARKER_CHANGED:case NotificationNames.EXHIBITION_HALL_PAGE_STATE_CHANGED:RefreshOpenState();break;case NotificationNames.CLOSE_PREVIEW_CAMERA:if (IsViewExist()){GetView().SetOpen(false);}break;case NotificationNames.REFRESH_PREVIEW_CAMERA_OPEN_STATE:RefreshOpenState();break;}}
  1. 流程要明确,举个例子,当Mediator控制U部分开启或者关闭一群UI组件的时候,错误的做法是UI组件里面写一个方法,不接受参数,自行判断当前是否是开启状态,是就关闭,否就开启。这样做的一个隐患是当这个状态判断与Mediator里面的状态出现不同时,就会变成相关变量的取反。造成一些逻辑错误。例如当Mediator想开启的时候UI部分关闭了,当Mediator想开启的时候UI部分开启了,而这时因为控制变量在Mediator这边,即使开启了,这些UI调节的时候也没有什么反应。所以正确的做法是UI组件写两个方法,单独只进行UI的开启和关闭,由Mediator进行开启或者关闭。

  2. 模块之间的关系操作一般是定义在command里面,例如有三个模块ABC,只能任意开启其中一个,这样的操作不应该定义在ABC三个模块里面,而是应该定义在一个command里面,写三个方法分别定义ABC的任意一个开启,然后调用这个command的时候传入的参数决定了调用哪个方法。

  3. Mediator之间的通信通过发Notification消息的方式进行

  4. 当一个本身一个模块的划分就因为不得已而比较大的类,太长的时候用分布类的方式来进行,每个脚本文件内只负责对某个小的分部功能。这样能使得维护更加容易

  5. 代码需要进行各类的安全性措施,例如判空,判断数组越界,判断分母是否为0等,然后注意BoradCast和Notification的字符串的内容不要有几个模块的重名。重名容易唤起错误。当Proxy或者Mediator的NAME的定义的名字和模块本身的名字一样,有重名时,会导致取出错的,然后转换类型的时候转出的为空。进行各类型的安全性措施是为了防止实机运行时的一个功能崩溃后,接下来的时间段都不能使用这个软件。对于排错来说因为缺少了报错信息,是不方便维护的。

在之前的项目经历中,判空出现的条件包括:
每次用到的proxy,使用时判空
每次用到的View,使用时判空
每次循环遍历的变量,使用时判空
每个[SerializedFileld]类型的变量,使用时判空
每个方法传入的参数,使用时候能判空的要进行判空
在一个方法中得到其他方法返回的变量,这个变量要进行判空
除法的除数是否为0的判断
数值类型赋值给Unity组件的属性的时候需要进行NaN或者Infinity的判断,但是这种情况的错误出现几率很小,所以一般不严格要求

  1. Mediator对UI的事件的注册统一写到一个叫做RegisterViewEvents的函数里面。方便以后其他人要注册的时候找到地方添加注册或更改注册

  2. 程序中重新赋值的变量,使用前记得判空

  3. 一个模块对于其UI的事件的监听的消息与其他模块重名也是没有关系的,即Broadcast和AddListener两个使用的消息与其他模块相同也没关系。因为每个UI类里面的Messenger都是一个新的独属于自己的Messenger

  4. 多人合作开发的时候,对于场景中的模块,一个模块下的所有物体做好预制体,各个变量要赋值好, 预制体要根据模块名字归类好,然后到时场景冲突的时候,重新摆放比手动去除场景冲突方便的话,就能方便重新摆放。

  5. Mediator里面使用别的模块的Proxy一般只是需要用到变量来进行判断,不进行赋值操作,因为这样的赋值操作多了之后,代码会变得很乱,赋值操作一般只由相同功能模块的Mediator使用

  6. 一个Mediator里面对于其他模块引起的操作,由增加一个command来进行处理,然后对相应模块发送相应的消息,例如房间强制播放模块对于自动演示模块的开启时如果正在进行强制播放则需要关闭,做法不应该由房间强制播放模块对自动演示模块的开启消息直接进行监听,而是应该新加一个command对开启消息监听然后发送房间强制播放模块的关闭消息。这样当对于一些特定情况的操作多了之后,可以集中在一个command中进行处理,在出问题的时候只需要在这个command脚本中查找,方便维护,如果采用了第一种类型的做法,则属于职责扩散,这样的情况多了之后,如果没有特定将这些额外的模块间处理的代码抽取出来的化, 该模块的mediator就变得看起来比较混乱,即使是抽取,也可能会出错,并且将很多情况的抽取出来到同一个MEdiator种,而不是像command那样专门针对某个特定情况,但是这种做法做多了之后,可能会引起command类大增,然后要将属于某个模块的command分文件夹处理好。

  7. 关于用NotificationNames传递的参数的传递的对象如果比较多的都是新定义一个类或者结构体来封装,这些类或者结构体都用ValueObject来做后缀

  8. Command一般用于在一些情况下,调用起不同的Mediator,达到该情况下想要达到的功能,例如在某个模块的某个功能开启的时候,需要协调其他功能,这时可以在该模块开启该功能的语句中,在所有语句开始前即开启前或者所有语句写完后即开启后的发出命令的语句,这个命令由一个专门的命令进行监听,做一些对应的操作,之所以不和该模块的该功能的开启命令一致是因为,这可能会有顺序的问题,方便维护的写法还是在该功能的代码中的某个段添加,如下 。

case NotificationNames.SETTINGS_PROXY_DATA_INIT_COMPLETED://在功能开启前调用命令,但是不要将命令与SETTINGS_PROXY_DATA_INIT_COMPLETED绑定在一起ApplicationFacade.Instance.SendNotification(NotificationNames.STARTUP);SetFOV();//有时在功能开启前调用命令,但是不要将命令与SETTINGS_PROXY_DATA_INIT_COMPLETED绑定在一起ApplicationFacade.Instance.SendNotification(NotificationNames.STARTUP);break;
  1. 当要控制别的模块时,别的模块的临时状态变量要存到控制者模块上,而不是存在被控制模块上。因为存在被控制模块上的话在控制者需要完全控制的期间,可能被控制模块有一些额外的变化,例如我要关闭某个模块,我要记录这个模块在我关闭前是否是开启的,这个记录的变量应该放在控制者模块上,因为如果放在被控制者身上,当这样的情况多了以后,被控制者可能会出现很多类似的代码就比较乱,并且通过发消息来通过别的模块的Mediator来改这个模块的proxy,有个好处是当在改这个 的时候都需要一些额外处理,可以统一写到这个Mediator中,如果有分支情况,也可以通过不同的消息来区分,不建议通过传参数来区分。

  2. RegisterCommand对于同一条字符串,只有最后一个command能够被注册成功。但是同样的字符串注册Command和Mediator的话,command和mediator都会起效,在发出的时候都会起效,只是起效的顺序不敢保证,所以一些做法是在Mediator的一些命令执行响应的开始阶段或者结束去发送指令达到目的。

  3. Command类可以存储变量,因为一般程序中只有一个相同的command类对象,存储的变量都在这个对象里面,每次模块唤起的时候,唤起的是同一个对象。

  4. 发送的消息如果只是对应Command, 习惯上名字要和Command对应上。

  5. 目前觉得一个独立的和外界交互比较少的与业务关系很小的功能模块,可以作为一个由view控制的模块。

  6. 模块与模块之间的交互逻辑操作一般是用Command去做,command获取交互模块的proxy,command的命名偏向于面向过程,是以一些功能来命名的,在一些节骨眼上面可以用Command去做,例如某个时机,例如从游戏开始,这种涉及到多模块启动的一个时机,做成一个Mediator又不是那么合适,因为这个地方定义的代码只是一个时机,不符合Mediator应该管控的范围,Mediator一般管控一些常驻的功能。 我建议是除非是一些功能归类模块特别难的部分可以用command去写,否则还是归为mediator去写,面向过程的逻辑不是那么容易维护的。

  7. Simplecommand可以作为不同模块之间发送信息的桥梁,而不是一个模块直接监听另一个模块的消息。这样也完全解耦了业务上有联系的两个模块,首先便于移植。举个例子,当设置模块的某个选项更改之后,可能很多地方都会做出相应的更改,如果很多个模块都对这个设置模块的某个选项的更改消息进行监听,有点属于职责扩散,针对这个选项的更改引发的改变应该都放在一个command里面,比起查找引用,更容易一目了然,特别是对这个消息的使用多了以后查找引用的时候好处会更明显。然后一个模块可能也会对来自多个模块的command响应,这些响应都触发的逻辑需要这些模块的一些proxy变量综合判断,这个最好也是写在command里面,多个消息可以注册一个command。

  8. PureMVC用了好久了,里面的各个特点和用法也记录得比较明白。他特别适合于软件中全程只有一个的模块搭建,以及这样的模块之间的通讯。写法耦合度低,比较容易移植复用和拓展维护。但是对于个数较多的部分,就不适合了。例如一个主角的二十多种状态,目前最方便的做法还是状态机。

错误

  1. UI部分发送消息给Mediator表示没有注册,
    第一种情况是真的没有注册,第二种情况是注册了但是函数的参数不对
        GetView().AddListener<int>(PlayerSettingsUI.ON_MOVE_SPEED_VALUE_CHANGED, OnMoveSpeedValueChanged);

比如这里注册的时候参数是整数类型,但是实际调用的时候参数是float类型,也会报没有listener监听的错


其他不清楚的地方参考PureMVC官方文档中文版

用 PureMVC 创建健壮、易扩展、易维护的客户端程序
附 ActionScript 3 及 MXML 实例

  1. 初始化时候出错一般会导致后面的模块初始化失败,所以这时的出错应该看控制台最靠上的错误,一般场景的错误有没有在ApplicationFacade里面添加没有在StartupCommand里面添加,模块内部初始化逻辑运行异常等。

  2. 发送Notification的传参的时候,如果传的是0,1,8等这样的整数写法或者是整数变量,则在接收使用时如果转为float,会报类型转换的错误,所以在传的时候,要注意传的值类型变量或者常量的类型和接收的时候要转换的一致

  3. Mediator的名字不要写错了,如果有两个MEdiator的Name重复了的话,会导致注册MEdiator失败,例如有mediatorA和B,B用了A的名字, 当A上面的命令被唤起的时候,就会导致唤起的是后面注册的B,但是B一般没有这个A的命令,所以没响应,所以看起来像是A没响应一样。

PureMVC使用体会相关推荐

  1. 记录第一次在egret项目中使用Puremvc

    这几天跟着另一个前端在做一个小游戏,使用的是egret引擎和puremvc框架,这对于我来说还是个比较大的突破吧,特此记录下. 因为在此项目中真是的用到了mvc及面向对象编程,值得学习 记录第一次在e ...

  2. 菜鸟学PureMVC记

    最近工作中需要用到FLASH,开发框架中又是以PureMVC为主.MVC是有了解,但是PureMVC这个之前则是从没接触过.那就学呗~~ 说学就学,可是第一步就让我感觉很费事~~(哎~菜鸟当久了).要 ...

  3. Unity3D架构之PureMVC

    之前了解过UI实现框架大多是用MVC架构的,才听说有这么一个基于MVC的跨平台开源框架叫PureMVC,前几天用到了做了一下,写一写分析总结 官网位置:http://puremvc.org/ Pure ...

  4. 无意间看到Pure-Mvc记录下

    Pure-MVC ###前言### 在学习creator的一些框架的时候看到一些使用了MVC的内容,就在此做个思考和总结.PureMVC-TypeScript版本代码量很少非常容易看懂. 什么是MVC ...

  5. 什么是 PureMVC 框架(提供下载)

    PureMVC是在基础的经典模型.视图和控制器上建立的一个轻量级的应用框架,这种开源框架是免费的,它最初是执行的ActionScript 3语言使用的Adobe Flex.Flash和AIR,现在已经 ...

  6. pureMVC介绍及学习

    1   简介 Pure MVC是在基于模型.视图和控制器MVC模式建立的一个轻量级的应用框架,这种开源框架是免费的,它最初是执行的ActionScript 3语言使用的Adobe Flex.Flash ...

  7. 关于Puremvc的理解

    PureMVC框架的目标很明确,即把程序分为低耦合的三层:Model.View和Controller.它们合称为PureMVC框架的核心,由Facade统一管理.关于它的核心层,我们不需要管太多,只需 ...

  8. PureMVC(AS3)剖析:吐槽

    PureMVC(AS3)剖析:吐槽 写在前面 世上没有银弹--不存在适用于所有情况的框架,只有适合的框架.再者任何一个好的东西(语言.框架等)最终还取决于用的人,语言和框架本身并不能保证用户的代码清晰 ...

  9. PureMVC(AS3)剖析:设计模式(二)

    PureMVC(AS3)剖析:设计模式(二) 模式 上一篇中介绍了PureMVC中使用的3种设计模式:单例模式.观察者模式.外观模式.本篇将继续介绍剩下的3种设计模式: l  使用中介者(Mediat ...

最新文章

  1. Android第三十一期 - 市面上所有引导页的效果
  2. java list按照元素对象的指定多个字段属性进行排序
  3. 机器人 蓝buff 钩_lol:机器人史诗级加强,从河道钩蓝buff,对面打野要骂人
  4. 基础拾遗------泛型详解
  5. 计算机一级考试有三科,全国计算机一级考试是一级WPS Office 一级MS Office 一级Photoshop 三个任选一个考试吗?...
  6. 造轮子-AgileConfig一个基于.NetCore开发的轻量级配置中心
  7. 2017.10.9 DZY Loves Math VI 失败总结
  8. 机器学习-python的工作目录
  9. ACM POJ 2965 The Pilots Brothers' refrigerator
  10. Java正则表达式判断一个字符串是否是ipv4地址
  11. 成为一名嵌入式Linux开发工程师需要学习哪些知识?
  12. java木马源码_用Java编写木马程序【附源代码下载】
  13. web如何加入视频?video
  14. [转]UserData使用总结 - lanyu
  15. 【NOIP2010普及组】三国游戏题解
  16. Hbuilder开发移动新闻客户端(二)
  17. 真实揭秘·程序员个人兼职在威客外包平台究竟赚钱否
  18. 摩尔斯电码(Morse code)
  19. 燃气热水器打不着火水压低的解决方法(zt)
  20. mysql插入数据时中文乱码_MySQL 插入数据时,中文乱码???问题的解决

热门文章

  1. 包头钢铁职业技术学院题库计算机,包头钢铁职业技术学院单独招生题库(计算机).DOC...
  2. ubuntu18.0.4安装网卡驱动记录
  3. IDEA 连接数据库报错
  4. android 教学白板功能,Android集成互动白板
  5. Android Init Language(RC文件)介绍
  6. eos 连接mysql_EOS智能合约中数据库的使用与常见问题
  7. qt制作棋牌游戏之XO棋(井字棋)
  8. 同样是测试员,为什么有的人薪资15K,有的人薪资20-30W,学会谈薪真的很重要
  9. python2.7下载教程_Python 2.7安装和下载教程
  10. 6-FAM 6-羧甲基荧光素标记金纳米团簇|Rhodamine B 罗丹明B标记金纳米团簇|金纳米团簇的复杂定制