如何设计我的 C# jQuery API,使其使用起来不会混乱?
我正在为 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 示例?
sq["ul.first"].Find(".foo") ...
缺点:滥用[]
属性。sq.Init("ul.first").Find(".foo") ...
缺点:没有什么能真正迫使程序员从 Init 开始,除非我添加一些奇怪的“初始化”机制;用户可能尝试从.Find
开始,但没有得到他期望的结果。此外,Init
和Find
无论如何都几乎相同,只是前者也会重置堆栈。sq.Find("ul.first").Find(".foo") ... .ClearStack()
缺点:程序员可能忘记清除堆栈。做不到。
end()
未实现。使用两个不同的对象。
也许使用HtmlDocument
作为所有查询的基础,然后每个方法都返回一个可以链接的SharpQuery
对象。这样,HtmlDocument
始终保持初始状态,但SharpQuery
对象可能具有不同的状态。不幸的是,这意味着我必须两次实现一堆东西(一次用于 HtmlDocument,一次用于 SharpQuery 对象)。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#?
sq["ul.first"].Find(".foo") ...
Downfalls: Abuses the[]
property.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
andFind
are pretty much identical anyway, except the former resets the stack too.sq.Find("ul.first").Find(".foo") ... .ClearStack()
Downfalls: programmer may forget to clear the stack.Can't do it.
end()
not implemented.Use two different objects.
Perhaps useHtmlDocument
as the base that all queries should begin with, and then every method thereafter returns aSharpQuery
object that can be chained. That way theHtmlDocument
always maintains the initial state, but theSharpQuery
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).new SharpQuery(sq).Find("ul.first").Find(".foo") ...
The constructor copies a reference to the document, but resets the stack.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
我认为您在这里遇到的主要障碍是您试图为每个文档只使用一个
SharpQuery
对象。 jQuery 不是这样工作的;一般来说,jQuery 对象是不可变的。当您调用更改元素集的方法(例如find
或end
或add
)时,它不会更改现有对象,但返回一个新的:(有关详细信息,请参阅
end
的文档)SharpQuery 应该以相同的方式操作。使用文档创建 SharpQuery 对象后,方法调用应返回新的
SharpQuery
对象,引用同一文档的不同元素集。例如:这种方法的好处有很多。因为
sq
、header
、allTheLinks
等都是同一个类,因此每个方法只有一个实现。然而,这些对象中的每一个都引用相同的文档,因此您没有每个节点的多个副本,并且节点的更改会反映在该文档上的每个SharpQuery
对象中(例如在allTheLinks 之后) .text("foo")
,someOfTheLinks.text() == "foo"
.)。实现
end
和其他基于堆栈的操作也变得很容易。当每个方法从另一个方法创建一个新的、经过过滤的SharpQuery
对象时,它会保留对该父对象的引用(allTheLinks
到header
、header
到sq
)。然后end
就像返回一个新的SharpQuery
一样简单,其中包含与父级相同的元素,例如:(或者不管你的语法如何变化。)
我认为这种方法会让你最像 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 (likefind
orend
oradd
), it doesn't alter the existing object, but returns a new one:(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: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 everySharpQuery
object on that document (e.g. afterallTheLinks.text("foo")
,someOfTheLinks.text() == "foo"
.).Implementing
end
and the other stack-based manipulations also becomes easy. As each method creates a new, filteredSharpQuery
object from another, it retains a reference to that parent object (allTheLinks
toheader
,header
tosq
). Thenend
is as simple as returning a newSharpQuery
containing the same elements as the parent, like:(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.
我倾向于选项 2 的变体。在 jQuery 中,$() 是一个函数调用。 C# 没有全局函数,静态函数调用是最接近的。我会使用一种方法来指示您正在创建一个包装器,例如..
我不会担心将 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..
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).