写点什么

MonoTouch 中的 MVC 简介

  • 2010-08-09
  • 本文字数:11059 字

    阅读完需:约 36 分钟

在我们的第一篇文章中,用 MonoTouch 在 iPhone 上创建了一个应用程序。我们用到了 outlet 和 action,了解了基本的应用程序结构,并创建了一个简单的用户界面。在这篇文章中,我们将要创建另外一个简单的应用程序,不过这次要学习下如何使用 Views(视图)和 View Controllers(视图控制器)来创建一个具有多个界面的应用程序。特别地,我们将使用UINavigationController来在应用程序里的两个界面间进行导航。

在开始构建应用程序之前,让我们简单熟悉下 iPhone 应用程序所用的这个重要设计模式。

模型 - 视图 - 控制器(MVC)模式

Cocoa Touch 使用了一种修改版本的 MVC 模式来处理 GUI 的显示。MVC 模式(自 1979 年以来)已经出现很长时间了,它皆在分离显示用户界面所需的大量任务,并处理用户交互。

正如名称所蕴含的,MVC 具有三个主要部分,Model(模型)、View(视图)和 Controller(控制器):

  • 模型——模型是特定于领域的数据表现形式。比如说,我们正在创建一个任务列表应用程序。你可能会有一个 Task 对象的集合,书写为 List。 你或许把这些数据保存在数据库、XML 文件,或者甚至从 Web Service 中得到,不过 MVC 不那么关心它们是在何处 / 如何来持久保存的(乃至它们是什么)。相反,它特别专注于如何显示这些数据,并处理与用户交互的。
  • 视图——视图代表了数据如何实际地显示出来。在我们这个假设的任务应用程序中,会在一个网页(以 HTML 的方式)中来显示这些任务,也会在一个 WPF 页面中(以 XAML 的方式)来显示,或者在一个 iPhone 应用程序中显示为UITableView 。如果用户点击某个任务,要删除之,那么视图通常会触发一个事件,或对 Controller(控制器)进行一个回调。
  • 控制器——控制器是模型和视图间的粘合剂。控制器的目的就是获取模型中的数据,告知视图来显示。控制器还侦听着视图的事件,在用户选中一个任务来删除的时候,控制着任务从模型中删除。

通过分离显示数据、持久化数据和处理用户交互的职责,MVC 模式有助于创建易于理解的代码。而且,它促进了视图和模型的解耦,以便模型能被重用。例如,在你的应用程序中,有两个界面,基于 Web 的和 WPF 的,那么你可以在两者中都使用同样的模型定义代码。

因而,在很多 MVC 框架中不管具体的工作方式如何,基本原理都大致如此的。然而,在 Cocoa(及 Cocoa Touch)中,还是或多或少有所不同,苹果用 MVC 来代表 Views(视图)、View Controller(视图控制器)和 Models(模型);但是在不同的控件中,它们却不是完全一致的,实现的方式也不太一样。我们将在构建示例应用程序的时候了解更多细节。

在 MonoTouch 中的视图和视图控制器

我之前简短地提到,在 iPhone 应用程序中,你只能显示一个窗口。不过可以包含很多界面。要做到这点,你需要为每个界面都添加一个视图和视图控制器。

视图实际上包含了所有可视化元素,比如标签、按钮等等,而视图控制器处理在视图上的实际用户交互(通过事件),并让你在这些事件被触发的时候运行相应的代码。做一个粗略的比喻的话,这就是和 ASP.NET 或 WPF 有点类似的模型,在这些模型中,你通过 HTML 或 XAML 来定义用户界面,在后置代码中处理事件。

在你导向另外一个页面的时候,就把视图控制器放到视图控制器堆栈中。在这个要构建的应用程序中,我们将使用 Navigation View Controller(导航视图控制器,UINavigationController)来处理不同的界面,因为它提供了一种方式可以在界面之间非常容易地导航,通过这种基于层级模式的导航栏,让你的用户能够藉由视图控制器往后和往前进行导航。

UINavigationController 在很多内置的 iPhone 应用程序都能看到。例如,在查看短信列表的时候,如果你点击其中一个,顶部导航栏将在顶部显示一个左箭头按钮,让你可以回到显示消息列表的视图。

具有多个界面的 Hello World 应用

现在,在概念上了解了 MVC 的工作原理后,让我们实际地创建一个应用程序来实践下。

首先,在 MonoDevelop 中新建一个 MonoTouch iPhone 解决方案,命名为 Example_HelloWorld_2(如果你忘记如何操作可以参考一下第一篇文章)。

接着,添加两个视图控制器(以及相关的视图)来服务于我们将要执行导航的应用程序中的界面。要完成这个步骤,在项目上点击右键,选择“Add : New File”。

在 Interface Builder 中打开.xib 文件,添加一个标签到HelloWorldScreen上,修改文本为“Hello World”,另外添加一个文本到HelloUniverseScreen上,修改文本为“Hello Universe”,如下图所示:

现在,让我们添加一个 Navigation Controller 到 Main Window 上。方式是,在 Interface Builder 里打开 MainWindow.xib,从 Library Window 中拖一个 Navigation Controller 到 Document Window 上:

Navigation Controller 具有如下几个部分:

  • Navigation Controller(导航控制器)——这是控制器的主要部分,处理导航事件,把所有东西糅合在一起。
  • Navigation Bar(导航栏)——这是显示在顶部的工具条,让用户能够看到它处于导航层级的什么位置,并可以导航回去。
  • 视图控制器——这个部分用来控制着视图的显示。
  • Navigation Item(导航条目)—— 就是显示在导航栏上的部分,实际上就是用于导航的按钮,也显示相应的标题

接下来,我们添加一个 Table View 到 Navigation Controller 上,以便能创建一个用于各个界面的链接列表。要完成这个步骤,从 Library 中拖一个UITableView到 Navigation Controller 里的 View Controller 上:

改变一下导航栏的标题。在 Navigation Controller 上双击顶部栏,键入“Hello World Home!”:

我必须使用 Table View 来包含 Navigation Items 吗?

不用,你可以放任何东西到 View Controller 中。我们将在后面看到,在你导航到一个新界面的时候,你是调用NavigationController.PushViewController方法,并把要去的界面的 View Controller 传递给它。在用户点击按钮的时候,我们能轻易地实现它。

现在,我们获得了所需的 Navigation Controller 以及相关的 Table View,还需要让两者都可被后置代码访问。需要让 Navigation Controller 在代码中可访问,以便我们能把 View Controllers 传给它;也需要让 Table View 在代码中可访问,以便我们能用要导航到的界面的名称来填充它。

要实现这个步骤,要为它们创建 Outlets,正如我们在第一篇文章所做的那样的。我们把 Navigation Controller 取名为mainNavigationController,把 Table View 取名为mainNavTableView。要确保在 AppDelegate 中创建它们。在你完成后,Connection Inspector 应该看上去如下所示:

接着,需要设置在应用程序启动的时候显示 Navigation Controller。还记得之前在 Main.cs 中注释掉的Window.AddSubview代码吗?对,这就是我们现在要使用的代码。我们把那行代码改为如下:

复制代码
// If you have defined a view, add it here:
window.AddSubview (this.mainNavigationController.View);

AddSubView 很像 WPF、ASP.NET 等中的 AddControl 语句。通过把它传递给mainNavigationController对象的 View 属性,我们就可告知窗口去显示这个 Navigation Controller 的界面。

现在让我们来运行一下应用程序,会看到下图所示的样子:

这样 Navigation Controller 就可显示出来了,不过还没有任何链接指向其他界面。为了设置链接,必须用数据来填充 Table View。这就需要创建一个UITableViewDataSource 对象,把它绑定给 Table View 的 DataSource 属性。在传统的.NET 编程中,你可以绑定任何实现了IEnumerable 接口的对象到 DataSource 属性上,并设定一些数据绑定参数(比如需要显示那些字段),这样就实现了巧妙的数据绑定。在 Cocoa 中,工作方式稍微不同,正如我们看到的,在绑定上的对象需要创建新条目的时候,DataSource 本身都会被调用,DataSource 实际负责它们的创建。

之前,我们实现了 DataSource,现在来创建将要真正使用的条目。创建一个名为 NavItem 的类。在项目上点右键,选择“Add : New File”,再选择“General : Empty Class”,命名为“NavItem”,如下图:

现在,把如下代码写到里面:

复制代码
using System;
using MonoTouch.UIKit;
namespace Example_HelloWorld_2
{
//========================================================================
/// <summary>
///
/// </summary>
public class NavItem
{
//=============================================================
#region -= declarations =-
/// <summary>
/// The name of the nav item, shows up as the label
/// </summary>
public string Name
{
get { return this._name; }
set { this._name = value; }
}
protected string _name;
/// <summary>
/// The UIViewController that the nav item opens. Use this property if you
/// wanted to early instantiate the controller when the nav table is built out,
/// otherwise just set the Type property and it will lazy-instantiate when the
/// nav item is clicked on.
/// </summary>
public UIViewController Controller
{
get { return this._controller; }
set { this._controller = value; }
}
protected UIViewController _controller;
/// <summary>
/// The Type of the UIViewController. Set this to the type and leave the Controller
/// property empty to lazy-instantiate the ViewController when the nav item is
/// clicked.
/// </summary>
public Type ControllerType
{
get { return this._controllerType; }
set { this._controllerType = value; }
}
protected Type _controllerType;
/// <summary>
/// a list of the constructor args (if neccesary) for the controller. use this in
/// conjunction with ControllerType if lazy-creating controllers.
/// </summary>
public object[] ControllerConstructorArgs
{
get { return this._controllerConstructorArgs; }
set
{
this._controllerConstructorArgs = value;
this._controllerConstructorTypes = new Type[this._controllerConstructorArgs.Length];
for (int i = 0; i < this._controllerConstructorArgs.Length; i++)
{
this._controllerConstructorTypes[i] = this._controllerConstructorArgs[i].GetType ();
}
}
}
protected object[] _controllerConstructorArgs = new object[] {
};
/// <summary>
/// The types of constructor args.
/// </summary>
public Type[] ControllerConstructorTypes
{
get { return this._controllerConstructorTypes; }
}
protected Type[] _controllerConstructorTypes = Type.EmptyTypes;
#endregion
//========================================================================
//========================================================================
#region -= constructors =-
public NavItem ()
{
}
public NavItem (string name) : this()
{
this._name = name;
}
public NavItem (string name, UIViewController controller) : this(name)
{
this._controller = controller;
}
public NavItem (string name, Type controllerType) : this(name)
{
this._controllerType = controllerType;
}
public NavItem (string name, Type controllerType, object[] controllerConstructorArgs) : this(name, controllerType)
{
this.ControllerConstructorArgs = controllerConstructorArgs;
}
#endregion
//===============================================================
}
}

这个类非常简单。我们首先来看一下其中的属性:

  • Name——打算在 Navigation Table 中显示的界面名称。
  • Controller——界面对应的实际UIViewController
  • ControllerType——界面对应的UIVeiwController的类型,这里只是存储着这个控制器的类型,并在需要的时候才来创建它,从而实现UIViewController的后期实例化目标。
  • ControllerConstructorArgs ——如果你的 UIViewController 具有任何构造参数,并且你希望传递它的话,就在这个属性上设置。在我们的例子中,不需要用到这个属性,所以现在可以忽略它,不过我在这里还是列出,因为它对于需要后期创建的类是很有用的。
  • ControllerConstructorTypes ——这是一个只读属性,读取从ControllerConstructorArgs设置的类型,其用于实例化控件。

类的剩余部分就是一些基本的构造器。

现在,我们编写好了 NavItem,就可以来为 Navigation Table View 创建一个能实际使用的 DataSource。创建一个名为NavTableViewDataSource的新类。做法和已经编好的 NavItem 的类似。

现在,把下面代码写入:

复制代码
using System;
using System.Collections.Generic;
using MonoTouch.UIKit;
using MonoTouch.Foundation;
namespace Example_HelloWorld_2
{
//========================================================================
//
// The data source for our Navigation TableView
//
public class NavTableViewDataSource : UITableViewDataSource
{
/// <summary>
/// The collection of Navigation Items that we bind to our Navigation Table
/// </summary>
public List<NavItem> NavItems
{
get { return this._navItems; }
set { this._navItems = value; }
}
protected List<NavItem> _navItems;
/// <summary>
/// Constructor
/// </summary>
public NavTableViewDataSource (List<NavItem> navItems)
{
this._navItems = navItems;
}
/// <summary>
/// Called by the TableView to determine how man cells to create for that particular section.
/// </summary>
public override int RowsInSection (UITableView tableView, int section)
{
return this._navItems.Count;
}
/// <summary>
/// Called by the TableView to actually build each cell.
/// </summary>
public override UITableViewCell GetCell (UITableView tableView, NSIndexPath indexPath)
{
//---- declare vars
string cellIdentifier = "SimpleCellTemplate";
//---- try to grab a cell object from the internal queue
var cell = tableView.DequeueReusableCell (cellIdentifier);
//---- if there wasn't any available, just create a new one
if (cell == null)
{
cell = new UITableViewCell (UITableViewCellStyle.Default, cellIdentifier);
}
//---- set the cell properties
cell.TextLabel.Text = this._navItems[indexPath.Row].Name;
cell.Accessory = UITableViewCellAccessory.DisclosureIndicator;
//---- return the cell
return cell;
}
}
//====================================================================
}

快速浏览一下代码。第一部分是我们的 List集合。就是一个 NavItem 对象的集合。接着会看到一个基本的构造器,使用传入的 NavItems 参数来初始化NavTableViewDataSource

接着,我们重写了RowsInSection方法。Table Views 能具有多个分段,在每个分段上都可以放置条目。RowsInSection 基于 section 参数传递进来的分段索引来返回条目的数量。在我们的例子中,只具有一个分段,那么我们就返回 NavItem 集合的 Count 属性。

最后一个方法是 GetCell,这里就是数据绑定实际发生的地方。这个方法被 UITableView 在构建每行数据的时候所调用。你可以利用这个方法来构建出 Table 中的每行数据,以显示出你期望的内容。

此处,我们所做的第一件事情就是通过DequeueReusableCell 方法从TableView 中得到UITableViewCell 对象。TableView 保持着一个UITableViewCell 对象的内部对象池,其基于CellIdentifiers来进行查找。它让你可以为UITableViewCell 创建自定义模板(只用创建一次),并重用这个模板,而不是 GetCell 每次被调用的时候都重复创建模板,这样就提高了性能。我们第一次调用DequeueReusableCell,它不会返回任何东西,那么就要创建一个新的UITableViewCell。之后的每次调用,UITableViewCell已经存在,就只需直接重用它就行。

我们使用 Default 的单元格样式(cell style),其只为我们提供了很少的自定义选项,所以接下来的事情就是把TextLabel.Text 属性设置为NavItemName 属性值。接着,我们设置Accessory 属性来使用DisclosureIndicator,其只是一个显示在 Navigation Item 右边的简单箭头。

现在,我们已经得到了创建好的UITableViewDataSource ,是时候使用它了。在 MonoDevelop 中打开 Main.cs,把如下的代码行添加到AppDelegate 类中:

复制代码
protected List<NavItem> _navItems = new List<NavItem> ();

它将保存我们的 NavItem 对象。

接下来,添加如下代码到FinishedLaunching 方法中,在Window.MakeKeyAndVisible()之后:

复制代码
//---- create our list of items in the nav
this._navItems.Add (new NavItem ("Hello World", typeof(HelloWorldScreen)));
this._navItems.Add (new NavItem ("Hello Universe", typeof(HelloUniverseScreen)));
//---- configure our datasource
this.mainNavTableView.DataSource = new NavTableViewDataSource (this._navItems);

在这里我们做的所有这些事情,就是创建两个 NavItem 对象,并把它们添加到 _navItems 集合中。接着,我们创建一个NavTableViewDataSource 对象,把它绑定到 Navigation Table View。

把之前代码加入后,我们的 AppDelegate 类看上去如下所示:

复制代码
// The name AppDelegate is referenced in the MainWindow.xib file.
public partial class AppDelegate : UIApplicationDelegate
{
protected List<NavItem> _navItems = new List<NavItem> ();
// This method is invoked when the application has loaded its UI and its ready to run
public override bool FinishedLaunching (UIApplication app, NSDictionary options)
{
// If you have defined a view, add it here:
window.AddSubview (this.mainNavigationController.View);
window.MakeKeyAndVisible ();
//---- create our list of items in the nav
this._navItems.Add (new NavItem ("Hello World", typeof(HelloWorldScreen)));
this._navItems.Add (new NavItem ("Hello Universe", typeof(HelloUniverseScreen)));
//---- configure our datasource
this.mainNavTableView.DataSource = new NavTableViewDataSource (this._navItems);
return true;
}
// This method is required in iPhoneOS 3.0
public override void OnActivated (UIApplication application)
{
}
}

如果你现在运行应用程序,你将看到如下所示的样子:

我们现在拥有了构建好的导航条目,不过在点击它们的时候不会发生任何事情。在你点击一个条目的时候,UITableView 会引发一个事件,不过需要我们传递给它一个特别的类,叫作UITableViewDelegate ,它是检测这些事件实际处理类。要实现这个步骤,就在项目中创建一个新类,命名为“NavTableDelegate”,并写入如下代码:

复制代码
using MonoTouch.Foundation;
using MonoTouch.UIKit;
using System;
using System.Collections.Generic;
using System.Reflection;
namespace Example_HelloWorld_2
{
//========================================================================
//
// This class receives notifications that happen on the UITableView
//
public class NavTableDelegate : UITableViewDelegate
{
//---- declare vars
UINavigationController _navigationController;
List<NavItem> _navItems;
//========================================================================
/// <summary>
/// Constructor
/// </summary>
public NavTableDelegate (UINavigationController navigationController, List<NavItem> navItems)
{
this._navigationController = navigationController;
this._navItems = navItems;
}
//========================================================================
//========================================================================
/// <summary>
/// Is called when a row is selected
/// </summary>
public override void RowSelected (UITableView tableView, NSIndexPath indexPath)
{
//---- get a reference to the nav item
NavItem navItem = this._navItems[indexPath.Row];
//---- if the nav item has a proper controller, push it on to the NavigationController
// NOTE: we could also raise an event here, to loosely couple this, but isn't neccessary,
// because we'll only ever use this this way
if (navItem.Controller != null)
{
this._navigationController.PushViewController (navItem.Controller, true);
//---- show the nav bar (we don't show it on the home page)
this._navigationController.NavigationBarHidden = false;
} else
{
if (navItem.ControllerType != null)
{
//----
ConstructorInfo ctor = null;
//---- if the nav item has constructor aguments
if (navItem.ControllerConstructorArgs.Length > 0)
{
//---- look for the constructor
ctor = navItem.ControllerType.GetConstructor (navItem.ControllerConstructorTypes);
} else
{
//---- search for the default constructor
ctor = navItem.ControllerType.GetConstructor (System.Type.EmptyTypes);
}
//---- if we found the constructor
if (ctor != null)
{
//----
UIViewController instance = null;
if (navItem.ControllerConstructorArgs.Length > 0)
{
//---- instance the view controller
instance = ctor.Invoke (navItem.ControllerConstructorArgs) as UIViewController;
} else
{
//---- instance the view controller
instance = ctor.Invoke (null) as UIViewController;
}
if (instance != null)
{
//---- save the object
navItem.Controller = instance;
//---- push the view controller onto the stack
this._navigationController.PushViewController (navItem.Controller, true);
} else
{
Console.WriteLine ("instance of view controller not created");
}
} else
{
Console.WriteLine ("constructor not found");
}
}
}
}
//==================================================================
}
//========================================================================
}

这个类的第一部分是针对UINavigationControllerNavItem 对象的集合的一对声明,下面的构造器会需要用到它们。在下面的方法——RowSelected中我们将看到,为什么需要它。

RowSelected 在用户点击某行的时候UITableView 会调用它,并会返回给我们一个UITableView 的引用,以及用户点击条目的NSIndexPath 。首先,我们要根据NSIndexPath 来找到相应的NavItem 。接着,我们把NavItemUIViewController 传递给NavigationController。如果Controller 是空的,那么我们就会基于它的类型进行实例化。

最后的两个操作,就是我们为什么需要NavItem 集合和NavigationController引用的原因。

现在,我们有了UITableViewDelegate,就可以来组合在一起。返回到 Main.cs 文件中,在AppDelegate 类中添加如下代码行到设置DataSource 属性的后面:

复制代码
this.mainNavTableView.Delegate = new NavTableDelegate (this.mainNavigationController, this._navItems);

这样就创建了一个新的NavTableDelegate 类,以及指向 Navigation Controller 和NavItems集合的引用,且会告知mainNavTable 使用它来处理事件。

Main.cs 文件中的AppDelegate 类将会如下面代码所示:

复制代码
// The name AppDelegate is referenced in the MainWindow.xib file.
public partial class AppDelegate : UIApplicationDelegate
{
protected List<NavItem> _navItems = new List<NavItem> ();
// This method is invoked when the application has loaded its UI and its ready to run
public override bool FinishedLaunching (UIApplication app, NSDictionary options)
{
// If you have defined a view, add it here:
window.AddSubview (this.mainNavigationController.View);
window.MakeKeyAndVisible ();
//---- create our list of items in the nav
this._navItems.Add (new NavItem ("Hello World", typeof(HelloWorldScreen)));
this._navItems.Add (new NavItem ("Hello Universe", typeof(HelloUniverseScreen)));
//---- configure our datasource
this.mainNavTableView.DataSource = new NavTableViewDataSource (this._navItems);
this.mainNavTableView.Delegate = new NavTableDelegate (this.mainNavigationController, this._navItems);
return true;
}
// This method is required in iPhoneOS 3.0
public override void OnActivated (UIApplication application)
{
}
}

现在,我们运行一下应用程序,看一下会发生什么,点击“Hello World”你将看到如下的效果:

注意,我们会自动地在顶部得到一个“Hello World Home”按钮,这样就能让我们返回到主界面上。点击“Hello Universe”将得到如下界面:

恭喜你!你现在应该已经对 MonoTouch iPhone 应用程序中多个界面是如何工作的有了一个基本的概念,以及对UINavigationController 的工作原理有了一定了解。

示例代码

查看英文原文: An Intro to the Model-View-Controller in MonoTouch


感谢马国耀对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家加入到 InfoQ 中文站用户讨论组中与我们的编辑和其他读者朋友交流。

2010-08-09 00:003411
用户头像

发布了 254 篇内容, 共 57.7 次阅读, 收获喜欢 2 次。

关注

评论

发布
暂无评论
发现更多内容

JAVA主流锁

颇风

Java 多线程

NIO看破也说破(四)—— Java的NIO

小眼睛聊技术

Java 学习 开源 架构 后端

ZooKeeper,到底如何选主?

奈学教育

识别代码中的坏味道(三)

Page

敏捷开发 面向对象 重构 代码质量 代码坏味道

设计模式前传——为什么要学设计模式

大头星

Java 面试 设计模式

Live2D for Unity入门篇 4.x

波波

编程 游戏开发 Live2D Unity

半小时手工解决的活,让我意外学会了 python 的 pdfkit 库

小匚

Python python教程

东哥和刘亦菲的故事

张利东

R

回“疫”录(20):世界从来不会欺负听话的人

小天同学

疫情 回忆录 现实纪录 纪实

程序员的晚餐 | 5 月 18 日 瓠子,年少时的味道

清远

美食

如何更自信的写作

董一凡

写作

给苹果提醒APP配个助手

BabyKing

提醒助手 TODO 奇妙清单 Reminders Helper

Kotlin 协程实践(2)之 异步和Callback地狱

陈吉米

Java kotlin 协程

Kafka系列第7篇:你必须要知道集群内部工作原理的一些事!

z小赵

大数据 kafka 实时计算

Redis缓存三大问题

Bruce Duan

redis 缓存穿透 缓存击穿 缓存雪崩

Spring Security 中的授权操作原来这么简单

江南一点雨

Java spring Spring Boot spring security

Vue+SpringBoot+SpreadJS 实现的在线文档

葡萄城技术团队

Spring Boot Vue SpreadJS

Web3极客日报#127

谢锐 | Frozen

区块链 开源 技术社区 Rebase Web3 Daily

谈谈控制感(7):底线思维与控制感

史方远

职场 心理 成长

重新强调完成的定义

Bob Jiang

Scrum 完成的定义 DoD definition of done

游戏夜读 | Two Sum问题的八个解

game1night

产品周刊 | 第 15 期(20200517)

八味阁

产品 设计 产品经理 产品设计

MacOS 下使用VSCode进行GoLang Test报错

北纬32°

macos vscode Unit Test debug Go 语言

Web3极客日报 #128

谢锐 | Frozen

区块链 开源 技术社区 Rebase Web3 Daily

单核小鸡上的Minikube实践(一)

摩登土狗

Docker Linux DevOps k8s minikube

Deno 入门手册:附大量 TypeScript 代码实例

寇云

node.js typescript

换脸新潮流:BIGO风靡全球的人脸风格迁移技术

DT极客

npm下载electron缓慢的问题

玏佾

npm Electron

项目提升服务过程与总结稿

Geek_bc0aff

从零开始制作一台计算机-概述

小兵

计算机基础

DDD 实践手册(番外篇: 事件风暴-概念)

Joshua

领域驱动设计 DDD 事件风暴 事件驱动 Event Storming

MonoTouch中的MVC简介_.NET_Bryan Costanich_InfoQ精选文章