如何设计我的 C# jQuery API,使其使用起来不会混乱?

发布于 2024-09-30 16:29:31 字数 2217 浏览 12 评论 0原文

我正在为 C# 制作 jquery 克隆。现在我已经将其设置为每个方法都是 IEnumerable 上的扩展方法,因此它可以很好地与已经使用 HtmlAgilityPack 的现有项目配合使用。我以为我可以在不保留状态的情况下逃脱......但是,然后我注意到 jQuery 有两个方法 .andSelf.end ,它们“弹出”最近匹配的元素一个内部堆栈。如果我更改我的类,使其始终在 SharpQuery 对象而不是可枚举对象上运行,我可以模仿此功能,但仍然存在问题。

使用 JavaScript 时,您会自动获得 Html 文档,但在使用 C# 时,您必须显式加载它,并且如果需要,您可以使用多个文档。看起来,当您调用 $('xxx') 时,您实际上是在创建一个新的 jQuery 对象,并从一个空堆栈开始。在 C# 中,您不想这样做,因为您不想从 Web 重新加载/重新获取文档。因此,您可以将其加载到 SharpQuery 对象或 HtmlNode 列表中(您只需要 DocumentNode 即可开始)。

在 jQuery 文档中,他们给出了这个例子

$('ul.first').find('.foo')
  .css('background-color', 'red')
.end().find('.bar')
  .css('background-color', 'green')
.end();

,我没有初始化方法,因为我无法重载 () 运算符,所以你只需从 sq.Find() 相反,它对文档的根进行操作,本质上做同样的事情。但是人们会尝试在一行上编写 sq.Find() ,然后在接下来的某个地方编写 sq.Find() ,并且(正确地)期待它再次对文档的根目录进行操作...但是如果我正在维护状态,那么您刚刚在第一次调用后修改了上下文。

那么...我应该如何设计我的 API?我是否添加另一个所有查询都应以重置堆栈开始的 Init 方法(但如何强制它们以此开始?),或者添加一个 Reset() 他们必须在线路末尾拨打电话?我是否要重载 [] 并告诉他们从那里开始?我是否会说“算了,反正没有人使用这些状态保存的函数”?

基本上,您希望如何用 C# 编写 jQuery 示例?

  1. sq["ul.first"].Find(".foo") ...
    缺点:滥用 [] 属性。

  2. sq.Init("ul.first").Find(".foo") ...
    缺点:没有什么能真正迫使程序员从 Init 开始,除非我添加一些奇怪的“初始化”机制;用户可能尝试从 .Find 开始,但没有得到他期望的结果。此外,InitFind 无论如何都几乎相同,只是前者也会重置堆栈。

  3. sq.Find("ul.first").Find(".foo") ... .ClearStack()
    缺点:程序员可能忘记清除堆栈。

  4. 做不到。
    end() 未实现。

  5. 使用两个不同的对象。
    也许使用 HtmlDocument 作为所有查询的基础,然后每个方法都返回一个可以链接的 SharpQuery 对象。这样,HtmlDocument 始终保持初始状态,但 SharpQuery 对象可能具有不同的状态。不幸的是,这意味着我必须两次实现一堆东西(一次用于 HtmlDocument,一次用于 SharpQuery 对象)。

  6. new SharpQuery(sq).Find("ul.first").Find(".foo") ...
    构造函数复制对文档的引用,但重置堆栈。

I'm making a jquery clone for C#. Right now I've got it set up so that every method is an extension method on IEnumerable<HtmlNode> so it works well with existing projects that are already using HtmlAgilityPack. I thought I could get away without preserving state... however, then I noticed jQuery has two methods .andSelf and .end which "pop" the most recently matched elements off an internal stack. I can mimic this functionality if I change my class so that it always operates on SharpQuery objects instead of enumerables, but there's still a problem.

With JavaScript, you're given the Html document automatically, but when working in C# you have to explicitly load it, and you could use more than one document if you wanted. It appears that when you call $('xxx') you're essentially creating a new jQuery object and starting fresh with an empty stack. In C#, you wouldn't want to do that, because you don't want to reload/refetch the document from the web. So instead, you load it once either into a SharpQuery object, or into an list of HtmlNodes (you just need the DocumentNode to get started).

In the jQuery docs, they give this example

$('ul.first').find('.foo')
  .css('background-color', 'red')
.end().find('.bar')
  .css('background-color', 'green')
.end();

I don't have an initializer method because I can't overload the () operator, so you just start with sq.Find() instead, which operates on the root of the document, essentially doing the same thing. But then people are going to try and write sq.Find() on one line, and then sq.Find() somewhere down the road, and (rightfully) expect it to operate on the root of the document again... but if I'm maintaining state, then you've just modified the context after the first call.

So... how should I design my API? Do I add another Init method that all queries should begin with that resets the stack (but then how do I force them to start with that?), or add a Reset() that they have to call at the end of their line? Do I overload the [] instead and tell them to start with that? Do I say "forget it, no one uses those state-preserved functions anyway?"

Basically, how would you like that jQuery example to be written in C#?

  1. sq["ul.first"].Find(".foo") ...
    Downfalls: Abuses the [] property.

  2. sq.Init("ul.first").Find(".foo") ...
    Downfalls: Nothing really forces the programmer to start with Init, unless I add some weird "initialized" mechanism; user might try starting with .Find and not get the result he was expecting. Also, Init and Find are pretty much identical anyway, except the former resets the stack too.

  3. sq.Find("ul.first").Find(".foo") ... .ClearStack()
    Downfalls: programmer may forget to clear the stack.

  4. Can't do it.
    end() not implemented.

  5. Use two different objects.
    Perhaps use HtmlDocument as the base that all queries should begin with, and then every method thereafter returns a SharpQuery object that can be chained. That way the HtmlDocument always maintains the initial state, but the SharpQuery objects may have different states. This unfortunately means I have to implement a bunch of stuff twice (once for HtmlDocument, once for the SharpQuery object).

  6. new SharpQuery(sq).Find("ul.first").Find(".foo") ...
    The constructor copies a reference to the document, but resets the stack.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

栀子花开つ 2024-10-07 16:29:31

我认为您在这里遇到的主要障碍是您试图为每个文档只使用一个 SharpQuery 对象。 jQuery 不是这样工作的;一般来说,jQuery 对象是不可变的。当您调用更改元素集的方法(例如 findendadd)时,它不会更改现有对象,但返回一个新的:(

var theBody = $('body');
// $('body')[0] is the <body>
theBody.find('div').text('This is a div');
// $('body')[0] is still the <body>

有关详细信息,请参阅 end 的文档)

SharpQuery 应该以相同的方式操作。使用文档创建 SharpQuery 对象后,方法调用应返回新的 SharpQuery 对象,引用同一文档的不同元素集。例如:

var sq = SharpQuery.Load(new Uri("http://api.jquery.com/category/selectors/"));
var header = sq.Find("h1"); // doesn't change sq
var allTheLinks = sq.Find(".title-link") // all .title-link in the whole document; also doesn't change sq
var someOfTheLinks = header.Find(".title-link"); // just the .title-link in the <h1>; again, doesn't change sq or header

这种方法的好处有很多。因为 sqheaderallTheLinks 等都是同一个类,因此每个方法只有一个实现。然而,这些对象中的每一个都引用相同的文档,因此您没有每个节点的多个副本,并且节点的更改会反映在该文档上的每个 SharpQuery 对象中(例如在 allTheLinks 之后) .text("foo"), someOfTheLinks.text() == "foo".)。

实现 end 和其他基于堆栈的操作也变得很容易。当每个方法从另一个方法创建一个新的、经过过滤的 SharpQuery 对象时,它会保留对该父对象的引用(allTheLinksheaderheadersq)。然后 end 就像返回一个新的 SharpQuery 一样简单,其中包含与父级相同的元素,例如:(

public SharpQuery end()
{
    return new SharpQuery(this.parent.GetAllElements());
}

或者不管你的语法如何变化。)

我认为这种方法会让你最像 jQuery 的行为,并且实现相当简单。我一定会关注这个项目;这是个好主意。

I think the major stumbling block you're running into here is that you're trying to get away with just having one SharpQuery object for each document. That's not how jQuery works; in general, jQuery objects are immutable. When you call a method that changes the set of elements (like find or end or add), it doesn't alter the existing object, but returns a new one:

var theBody = $('body');
// $('body')[0] is the <body>
theBody.find('div').text('This is a div');
// $('body')[0] is still the <body>

(see the documentation of end for more info)

SharpQuery should operate the same way. Once you create a SharpQuery object with a document, method calls should return new SharpQuery objects, referencing a different set of elements of the same document. For instance:

var sq = SharpQuery.Load(new Uri("http://api.jquery.com/category/selectors/"));
var header = sq.Find("h1"); // doesn't change sq
var allTheLinks = sq.Find(".title-link") // all .title-link in the whole document; also doesn't change sq
var someOfTheLinks = header.Find(".title-link"); // just the .title-link in the <h1>; again, doesn't change sq or header

The benefits of this approach are several. Because sq, header, allTheLinks, etc. are all the same class, you only have one implementation of each method. Yet each of these objects references the same document, so you don't have multiple copies of each node, and changes to the nodes are reflected in every SharpQuery object on that document (e.g. after allTheLinks.text("foo"), someOfTheLinks.text() == "foo".).

Implementing end and the other stack-based manipulations also becomes easy. As each method creates a new, filtered SharpQuery object from another, it retains a reference to that parent object (allTheLinks to header, header to sq). Then end is as simple as returning a new SharpQuery containing the same elements as the parent, like:

public SharpQuery end()
{
    return new SharpQuery(this.parent.GetAllElements());
}

(or however your syntax shakes out.)

I think this approach will get you the most jQuery-like behavior, with a fairly easy implementation. I'll definitely be keeping an eye on this project; it's a great idea.

揽清风入怀 2024-10-07 16:29:31

我倾向于选项 2 的变体。在 jQuery 中,$() 是一个函数调用。 C# 没有全局函数,静态函数调用是最接近的。我会使用一种方法来指示您正在创建一个包装器,例如..

SharpQuery.Create("ul.first").Find(".foo")

我不会担心将 SharpQuery 缩短为 sq,因为智能感知意味着用户不必输入整个内容(如果他们有 resharper,他们只需要无论如何输入 SQ)。

I would lean towards a variant on option 2. In jQuery $() is a function call. C# doesn't have global functions, a static function call is the closest. I would use a method that indicates you're creating a wrapper like..

SharpQuery.Create("ul.first").Find(".foo")

I wouldn't be concerned about shortening SharpQuery to sq since intellisense means users won't have to type the whole thing (and if they have resharper they only need to type SQ anyways).

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文