自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

ASP.NET MVC控件項目開發(fā)的簡單分析

開發(fā) 后端
作者從一個ASP.NET MVC控件項目開始,對ASP.NET MVC控件進(jìn)行了簡單的分析,十分深入透徹,希望對大家有所幫助。

【51CTO編者按】對于ASP.NET MVC框架大家一定不會陌生,但是對于很多人來說,弄好一個ASP.NET MVC控件項目實在是費(fèi)勁的事情。這里由作者來介紹他的一個ASP.NET MVC控件項目經(jīng)歷。51CTO編輯推薦《ASP.NET MVC框架視頻教程

在寫本文之前,本人一直抱著‘不宜’在ASP.NET MVC框架下搞什么控件開發(fā)的想法,因為一提到控件就會讓人想起‘事件’,‘VIEWSTATE’等一些問題,而ASP.NET MVC下是Controller, Action, Viewpage, Filter等特性的‘天下’。所以總感覺‘驢唇對不上馬嘴’。

但直到前陣子在郵箱中收到了關(guān)于telerik關(guān)于MVC框架擴(kuò)展的一些信息之后,才發(fā)現(xiàn)這家商業(yè)控件公司也開始打MVC的主意了。而這個項目(開源)就是該公司在理解了asp.net mvc的基礎(chǔ)上所做的一些嘗試,當(dāng)然其所實現(xiàn)的所謂控件與之前我們在項目中所開發(fā)或使用的web服務(wù)器控件有很大的不同,可以說是拋棄了以往的設(shè)計方式。盡管目前它的這種做法我心里還打著問號,但必定是一種嘗試(不管你贊同還是不贊同)。下面就做一個簡單的分析,希望能給研究MVC架構(gòu)的朋友提供一些的思考。

首先要聲明的是該開源項目中所使用的js就是jquery,而那些顯示效果也基本上就是基于jQuery中的那件插件為原型,并進(jìn)行相應(yīng)的屬性封裝,以便于在viewpage中用C#等語言進(jìn)行聲明綁定。下面就其中一些控件的顯示截圖:

控件的顯示截圖

控件顯示截圖

ASP.NET

進(jìn)度條控件

在該開源項目中,所有控件均基于jQueryViewComponentBase (abstract 類型),但其自身屬性并不多,而所有的控件基類屬性都被jQueryViewComponentBase 的父類ViewComponentBase所定義,下面以控件中的“Accordion(屬性頁控件)”為例進(jìn)行說明,見下圖:

Accordion(屬性頁控件)

上圖中左側(cè)的就是ViewComponentBase類,其定義了多數(shù)控件屬性,比如js腳本名稱和路徑以及相關(guān)樣式以及最終的html元素輸出方法,因為其類也是抽象類,所以其中大部分方法均為定義,而未進(jìn)行具體實現(xiàn)。我們只要關(guān)注一下其構(gòu)造方法就可以了:

  1. /// <summary> 
  2. /// View component base class.  
  3. /// </summary> 
  4.     public abstract class ViewComponentBase : IStyleableComponent, IScriptableComponent  
  5. {  
  6. private string name;  
  7. private string styleSheetFilesLocation;  
  8. private string scriptFilesLocation;  
  9. /// <summary> 
  10. /// 初始化相關(guān)Initializes a new instance of the <see cref="ViewComponentBase"/> class.  
  11. /// </summary> 
  12. /// <param name="viewContext">當(dāng)前視圖的上下文,將會在子類中使用</param> 
  13. /// <param name="clientSideObjectWriterFactory">傳入當(dāng)前所使用的Writer工廠實例.通過子類注入,子類最終延伸到相對應(yīng)的控件實例</param> 
  14.         protected ViewComponentBase(ViewContext viewContext, IClientSideObjectWriterFactory clientSideObjectWriterFactory)  
  15. {  
  16. Guard.IsNotNull(viewContext, "viewContext");  
  17. Guard.IsNotNull(clientSideObjectWriterFactory, "clientSideObjectWriterFactory");  
  18. ViewContext = viewContext;  
  19. ClientSideObjectWriterFactory = clientSideObjectWriterFactory;  
  20. StyleSheetFilesPath = WebAssetDefaultSettings.StyleSheetFilesPath;  
  21. StyleSheetFileNames = new List<string>();  
  22. ScriptFilesPath = WebAssetDefaultSettings.ScriptFilesPath;  
  23. ScriptFileNames = new List<string>();  
  24. HtmlAttributes = new RouteValueDictionary();  
  25. }  

通過上述的構(gòu)造方法,就可以將控件的一些通用默認(rèn)屬性值進(jìn)行初始化了。

下面以“Accordion”的源碼來分析一下,這里還是從構(gòu)造方法入手:

  1. public class Accordion : jQueryViewComponentBase, IAccordionItemContainer  
  2. {  
  3. ……  
  4. /// <summary> 
  5. /// Initializes a new instance of the <see cref="Accordion"/> class.  
  6. /// </summary> 
  7. /// <param name="viewContext">The view context.</param> 
  8. /// <param name="clientSideObjectWriterFactory">The client side object writer factory.</param> 
  9.        public Accordion(ViewContext viewContext, IClientSideObjectWriterFactory clientSideObjectWriterFactory) : base(viewContext, clientSideObjectWriterFactory)  
  10. {  
  11. Items = new List<AccordionItem>();  
  12. autoHeight = true;  
  13. }  

注:上面的構(gòu)程方法后面加入了base(viewContext, clientSideObjectWriterFactory),以實現(xiàn)向基類構(gòu)造方法傳參,也就是實現(xiàn)了上面所說的將當(dāng)前控件所使用的viewContext,clientSideObjectWriterFactory傳遞到基類ViewComponentBase 中去。(注:最終的clientSideObjectWriterFactory為ClientSideObjectWriterFactory實例類型)。

當(dāng)然,因為該控件的中相應(yīng)屬性比較簡單,只是一些set,get語法,所以就不過多介紹了,相信做過控件開發(fā)的對這些再熟悉不過了。

下面主要介紹一下其write html元素時所使用的方法,如下:

 

  1. /// <summary> 
  2. /// 創(chuàng)建并寫入初始化腳本對象和相應(yīng)屬性.  
  3. /// </summary> 
  4. /// <param name="writer">The writer.</param> 
  5.       public override void WriteInitializationScript(TextWriter writer)  
  6. {  
  7. int selectedIndex = Items.IndexOf(GetSelectedItem());  
  8. IClientSideObjectWriter objectWriter = ClientSideObjectWriterFactory.Create(Id, "accordion", writer);  
  9. objectWriter.Start()  
  10. .Append("active", selectedIndex, 0)  
  11. .Append("animated", AnimationName)  
  12. .Append("autoHeight", AutoHeight, true)  
  13. .Append("clearStyle", ClearStyle, false)  
  14. .Append("collapsible", CollapsibleContent, false)  
  15. .Append("event", OpenOn)  
  16. .Append("fillSpace", FillSpace, false);  
  17. if (!string.IsNullOrEmpty(Icon) || !string.IsNullOrEmpty(SelectedIcon))  
  18. {  
  19. if (!string.IsNullOrEmpty(Icon) && !string.IsNullOrEmpty(SelectedIcon))  
  20. {  
  21. objectWriter.Append("icons:{'header':'" + Icon + "','headerSelected':'" + SelectedIcon + "'}");  
  22. }  
  23. else if (!string.IsNullOrEmpty(Icon))  
  24. {  
  25. objectWriter.Append("icons:{'header':'" + Icon + "'}");  
  26. }  
  27. else if (!string.IsNullOrEmpty(SelectedIcon))  
  28. {  
  29. objectWriter.Append("icons:{'headerSelected':'" + SelectedIcon + "'}");  
  30. }  
  31. }  
  32. objectWriter.Append("change", OnChange).Complete();  
  33. base.WriteInitializationScript(writer);  
  34. }  

可以看出,objectWriter (IClientSideObjectWriter 類型實例)中被綁定了相關(guān)的控件屬性,并通過其類的WriteInitializationScript(writer)進(jìn)行腳本的輸出。而基本類的相應(yīng)方法如下:    

  1.  /// <summary> 
  2. /// Writes the initialization script.  
  3. /// </summary> 
  4. /// <param name="writer">The writer.</param> 
  5.       public virtual void WriteInitializationScript(TextWriter writer)  
  6. {  
  7. }  

大家看到該方法為空,但其又是如何運(yùn)行起來的呢,這里先賣個關(guān)子,稍后再說。接著再看一下另一個方法:WriteHtml()

  1. /// <summary> 
  2. /// 輸出當(dāng)前的 HTML代碼.  
  3. /// </summary> 
  4.       protected override void WriteHtml()  
  5. {  
  6. AccordionItem selectedItem = GetSelectedItem();  
  7. TextWriter writer = ViewContext.HttpContext.Response.Output;  
  8. if (!string.IsNullOrEmpty(Theme))  
  9. {  
  10. writer.Write("<div class=\"{0}\">".FormatWith(Theme));  
  11. }  
  12. HtmlAttributes.Merge("id", Id, false);  
  13. HtmlAttributes.AppendInValue("class", " ", "ui-accordion ui-widget ui-helper-reset");  
  14. writer.Write("<div{0}>".FormatWith(HtmlAttributes.ToAttributeString()));  
  15. foreach (AccordionItem item in Items)  
  16. {  
  17. item.HtmlAttributes.AppendInValue("class", " ", "ui-accordion-header ui-helper-reset ui-state-default ");  
  18. item.ContentHtmlAttributes.AppendInValue("class", " ", "ui-accordion-content ui-helper-reset ui-widget-content ui-corner-bottom");  
  19. if (item == selectedItem)  
  20. {  
  21. item.ContentHtmlAttributes.AppendInValue("class", " ", "ui-accordion-content-active");  
  22. }  
  23. else  
  24. {  
  25. item.HtmlAttributes.AppendInValue("class", " ", "ui-corner-all");  
  26. }  
  27. writer.Write("<h3{0}><a href=\"#\">{1}</a></h3>".FormatWith(item.HtmlAttributes.ToAttributeString(), item.Text));  
  28. item.ContentHtmlAttributes.AppendInValue("style", ";", (item == selectedItem) ? "display:block" : "display:none");  
  29. writer.Write("<div{0}>".FormatWith(item.ContentHtmlAttributes.ToAttributeString()));  
  30. item.Content();  
  31. writer.Write("</div>");  
  32. }  
  33. writer.Write("</div>");  
  34. if (!string.IsNullOrEmpty(Theme))  
  35. {  
  36. writer.Write("</div>");  
  37. }  
  38. base.WriteHtml();  
  39. }  

該方法首先獲取當(dāng)前所選屬性頁標(biāo)簽(GetSelectedItem()方法),然后用foreach方法對屬性頁標(biāo)簽集合進(jìn)行遍歷,并判斷當(dāng)前屬性頁是否就是被選中的屬性頁,并綁定上相應(yīng)的css屬性。其最終也是調(diào)用相應(yīng)的基類方法進(jìn)行輸出。當(dāng)然這里基類方法也是為空,呵呵。

準(zhǔn)備好了這個控件類之后,Telerik還為Accordion控件‘準(zhǔn)備’了一些輔助組件,比如屬性頁組件(AccordionItem),以及相關(guān)的組件構(gòu)造器(AccordionItemBuilder,AccordionBuilder),這樣我們就可以通過這些構(gòu)造器很方便的創(chuàng)建相應(yīng)的控件和組件了,下面就以AccordionItemBuilder為例,解釋一下其構(gòu)造器結(jié)構(gòu):

  1. public class AccordionBuilder : ViewComponentBuilderBase<Accordion, AccordionBuilder>, IHideObjectMembers  
  2. {  
  3. /// <summary> 
  4. /// 初始化方法Initializes a new instance of the <see cref="AccordionBuilder"/> class.  
  5. /// </summary> 
  6. /// <param name="component">The component.</param> 
  7.       public AccordionBuilder(Accordion component) : base(component)  
  8. {}  
  9. /// <summary> 
  10. /// 指定一個屬性頁選項  
  11. /// </summary> 
  12. /// <param name="addAction">要添加的action.</param> 
  13. /// <returns></returns> 
  14.       public virtual AccordionBuilder Items(Action<AccordionItemFactory> addAction)  
  15. {  
  16. Guard.IsNotNull(addAction, "addAction");  
  17. AccordionItemFactory factory = new AccordionItemFactory(Component);  
  18. addAction(factory);  
  19. return this;  
  20. }  
  21. /// <summary> 
  22. /// 屬性頁動態(tài)效果顯示名稱(鼠標(biāo)在屬性頁移入移出時)  
  23. /// </summary> 
  24. /// <param name="effectName">Name of the effect.</param> 
  25. /// <returns></returns> 
  26.       public virtual AccordionBuilder Animate(string effectName)  
  27. {  
  28. Component.AnimationName = effectName;  
  29. return this;  
  30. }  
  31. /// <summary> 
  32. /// 是否高度自適用.  
  33. /// </summary> 
  34. /// <param name="value">if set to <c>true</c> value.</param> 
  35. /// <returns></returns> 
  36.       public virtual AccordionBuilder AutoHeight(bool value)  
  37. {  
  38. Component.AutoHeight = value;  
  39. return this;  
  40. }  
  41.  
  42. /// <summary> 
  43. /// 指定要觸發(fā)的屬性頁事件名稱.  
  44. /// </summary> 
  45. /// <param name="eventName">Name of the event.</param> 
  46. /// <returns></returns> 
  47.       public virtual AccordionBuilder OpenOn(string eventName)  
  48. {  
  49. Component.OpenOn = eventName;  
  50. return this;  
  51. }  
  52. /// <summary> 
  53. /// 所使用的Icons名稱.  
  54. /// </summary> 
  55. /// <param name="name">The name.</param> 
  56. /// <returns></returns> 
  57.       public virtual AccordionBuilder Icon(string name)  
  58. {  
  59. Component.Icon = name;  
  60. return this;  
  61. }  
  62. /// <summary> 
  63. /// 被選中的屬性頁所使用的Icons 名稱  
  64. /// </summary> 
  65. /// <param name="name">The name.</param> 
  66. /// <returns></returns> 
  67.       public virtual AccordionBuilder SelectedIcon(string name)  
  68. {  
  69. Component.SelectedIcon = name;  
  70. return this;  
  71. }  
  72. /// <summary> 
  73. /// 當(dāng)屬性頁發(fā)生變化時要傳遞的action 腳本.  
  74. /// </summary> 
  75. /// <param name="javaScript">The java script.</param> 
  76. /// <returns></returns> 
  77.       public virtual AccordionBuilder OnChange(Action javaScript)  
  78. {  
  79. Component.OnChange = javaScript;  
  80. return this;  
  81. }  
  82. /// <summary> 
  83. /// Specify the name of the theme applies to the accordion.  
  84. /// </summary> 
  85. /// <param name="name">The name.</param> 
  86. /// <returns></returns> 
  87.       public virtual AccordionBuilder Theme(string name)  
  88. {  
  89. Component.Theme = name;  
  90. return this;  
  91. }  
  92. }  

對于上面的OnChange方法,可以使用下面的方法將相應(yīng)的js腳本傳入并執(zhí)行

  1. .OnChange(() => 
  2. {%> 
  3. function(event, ui)  
  4. {  
  5. $('#trace').append('Change fired: ' + new Date() + '<br/>');  
  6. }  
  7. <%}  
  8. )  

這樣,當(dāng)屬性頁發(fā)生變化時,就會在頁面的指定區(qū)域?qū)⒆兓瘯r間顯示出來了,如下圖:

屬性頁發(fā)生變化

Telerik在jQueryViewComponentFactory中對項目中每一個控件提供了一個方法用以初始化相應(yīng)的構(gòu)造器,以便于創(chuàng)建相應(yīng)的控件,比如Accordion,形如: 

  1.  /// <summary> 
  2. /// Creates a accordion for ASP.NET MVC view.  
  3. /// </summary> 
  4. /// <returns></returns> 
  5.      [DebuggerStepThrough]  
  6. public virtual AccordionBuilder Accordion()  
  7. {  
  8. return new AccordionBuilder(Create(() => new Accordion(ViewContext, clientSideObjectWriterFactory)));  
  9. }  

而對于其在VIEW中的使用,則通過擴(kuò)展方法來加以聲明:

  1. public static class HtmlHelperExtension  
  2. {  
  3. private static readonly IClientSideObjectWriterFactory factory = new ClientSideObjectWriterFactory();  
  4. /// <summary> 
  5. /// Gets the jQuery view components instance.  
  6. /// </summary> 
  7. /// <param name="helper">The html helper.</param> 
  8. /// <returns>jQueryViewComponentFactory</returns> 
  9.        [DebuggerStepThrough]  
  10. public static jQueryViewComponentFactory jQuery(this HtmlHelper helper)  
  11. {  
  12. return new jQueryViewComponentFactory(helper, factory);  
  13. }  
  14. }  

這樣在頁面視圖中,我們這可以使用下面的寫法來構(gòu)造一個Accordion控件了:

  1. <% Html.jQuery().Accordion()  
  2. .Name("myAccordion")  
  3. .Animate("bounceslide")  
  4. .Items(parent => 
  5. ……  

上面只是介紹了前臺和底層代碼如果顯示的問題,但還沒有解釋之前所說的WriteInitializationScript(TextWriter writer)方法以及WriteHtml()
方法如何被調(diào)用的問題,正如之前所看到的,因為Accordion的基類ViewComponentBase中未實現(xiàn)具體的代碼,所以這里我們要將注意力轉(zhuǎn)移到 jQueryViewComponentFactory中,請看如下代碼: 

  1. private TViewComponent Create<TViewComponent>(Func<TViewComponent> factory) where TViewComponent : ViewComponentBase  
  2. {  
  3. TViewComponent component = factory();  
  4. if (component is jQueryViewComponentBase)  
  5. {  
  6. component.AssetKey = DefaultAssetKey;  
  7. }  
  8. htmlHelper.Telerik().StyleSheetRegistrar().ToRegistrar().Register(component);  
  9. htmlHelper.Telerik().ScriptRegistrar().ToRegistrar().Register(component);  
  10. return component;  
  11. }  

上面的方法其實就是之前在該類方法Accordion()中所調(diào)用并執(zhí)行的:

  1. return new AccordionBuilder(Create(() => new Accordion(ViewContext, clientSideObjectWriterFactory))); 
通過該方法,就可以將該控件及其相關(guān)組件信息注冊到相應(yīng)的視圖中。因為我們比較關(guān)注WriteHtml()方法,所以這里就直接分析一下這一行代碼:     
ScriptRegistrar().ToRegistrar().Register(component);
ScriptRegistrar類中的Register方法承擔(dān)著將當(dāng)前要創(chuàng)建的組件添加到當(dāng)前的腳本組件列表中的任務(wù)(scriptableComponents為list列表)    
  1.  
  2.        /// <summary> 
  3. /// Registers the scriptable component.  
  4. /// </summary> 
  5. /// <param name="component">The component.</param> 
  6.        public virtual void Register(IScriptableComponent component)  
  7. {  
  8. Guard.IsNotNull(component, "component");  
  9. if (!scriptableComponents.Contains(component))  
  10. {  
  11. scriptableComponents.Add(component);  
  12. }  
  13. }  

當(dāng)組件被成功添加到該list列表中后,系統(tǒng)就會調(diào)用Render()方法將其顯示出來(注:該方法與以前web控件開發(fā)中的顯示方法同名,所以比較好理解),如下:   

  1.     /// <summary> 
  2. /// Writes the scripts in the response.  
  3. /// </summary> 
  4.       public void Render()  
  5. {  
  6. if (hasRendered)  
  7. {  
  8. throw new InvalidOperationException(Resources.TextResource.YouCannotCallRenderMoreThanOnce);  
  9. }  
  10. if (ViewContext.HttpContext.Request.Browser.EcmaScriptVersion.Major >= 1)  
  11. {  
  12. Write(ViewContext.HttpContext.Response.Output);  
  13. }  
  14. hasRendered = true;  
  15. }  

注意上面的這一行代碼:

Write(ViewContext.HttpContext.Response.Output);

其所實現(xiàn)的功能如下:  

  1.     /// <summary> 
  2. /// 寫出所有腳本資源和腳本 statements.  
  3. /// </summary> 
  4. /// <param name="writer">The writer.</param> 
  5.       protected virtual void Write(TextWriter writer)  
  6. {  
  7. WriteScriptSources(writer);  
  8. WriteScriptStatements(writer);  
  9. }  

而就是WriteScriptStatements這行代碼開始執(zhí)行之前所說的那個WriteInitializationScript(TextWriter writer)。而WriteHtml()方法的執(zhí)行入口要更加復(fù)雜一些,因為Telerik提供了ViewComponentBuilderBase這個類來進(jìn)行視圖組件的構(gòu)造,而該類中的Render方法就是對相應(yīng)組件的Render方法(組件中已定義)進(jìn)行調(diào)用,如下:

  1.     /// <summary> 
  2. /// Renders the component.  
  3. /// </summary> 
  4.     public virtual void Render()  
  5. {  
  6. Component.Render();  
  7. }  

而之前的“Accordion”控件是繼承自ViewComponentBase類,所以相應(yīng)組件的Render方法就在該類中進(jìn)行了聲明定義,如下:

  1.   /// <summary> 
  2. /// Renders the component.  
  3. /// </summary> 
  4.      public void Render()  
  5. {  
  6. EnsureRequired();  
  7. WriteHtml();  
  8. }  

大家看到了第二行代碼了吧,這就是我們之前看到的那個方法,也就是Accordion組件中WriteHtml()重寫方法的調(diào)用入口。

繞了這么一大圈,才把這個流程理清,是不是有些暈了。的確,剛開始接觸時我也有點(diǎn)暈,但暈呀暈呀就‘暈過去了’,現(xiàn)在再回頭看起來感常見其整體的架構(gòu)思路還是很清晰的??梢哉f有了這瓶酒墊底,再分析該項目中的其它控件就‘如魚得水’了。

***不妨總結(jié)一下:

這是對ASP.NET MVC控件項目開發(fā)做的一次嘗試,但如果之前做過控件特別是web服務(wù)器端控件開發(fā)的朋友,可以看出項目中有非常重的web控件開發(fā)味道,基本連方法名稱都有一定的重疊。

另外就是其自身還是引用了組件對象模型的概念,就拿屬性頁控件來說,就將其分為Accordion和AccordionItem兩種類型,其中可以將Accordion看成是AccordionItem的集合封裝(包括遍歷操作),而這里AccordionItem就成了Accordion的一個組件,而Accordion又是當(dāng)前view中的一個組件。而組件開發(fā)一直是.NET平臺上所倡導(dǎo)的。其優(yōu)勢在于可復(fù)用,維護(hù)方便,簡化復(fù)雜問題等。

原文標(biāo)題:一個Asp.net MVC 控件項目分析---Telerik.Web.Mvc

鏈接:http://www.cnblogs.com/daizhj/archive/2009/09/09/1562966.html

【編輯推薦】

  1. 點(diǎn)評一下ASP.NET的WEB控件
  2. ASP.NET控件學(xué)習(xí)總結(jié)
  3. ASP.NET前臺控件點(diǎn)評:避免強(qiáng)迫癥,奔向簡潔高效
  4. ASP.NET 2.0環(huán)境下的Shell函數(shù)
  5. 在ASP.NET 2.0中向數(shù)據(jù)庫批量插入數(shù)據(jù)
責(zé)任編輯:彭凡 來源: 博客園
相關(guān)推薦

2009-07-28 14:47:18

ASP.NET MVC

2009-08-04 10:43:59

ASP.NET控件開發(fā)

2009-08-07 15:24:16

ASP.NET模板控件

2009-03-13 10:58:48

ASP.NetMVC框架編程

2009-07-24 13:20:44

MVC框架ASP.NET

2009-07-31 12:43:59

ASP.NET MVC

2009-04-20 09:43:37

ASP.NET MVC基礎(chǔ)開發(fā)

2014-06-30 09:22:38

ASP.NETBootstrap

2009-07-27 13:52:36

Panel控件ASP.NET

2009-07-29 09:04:36

JQRTEasp.net mvc

2009-07-24 15:46:00

ASP.NET登陸控件

2009-08-04 12:59:42

ASP.NET控件開發(fā)

2009-08-07 14:42:02

ASP.NET控件開發(fā)

2011-07-12 15:17:02

ASP.net服務(wù)器控件

2009-08-07 15:34:15

ASP.NET數(shù)據(jù)綁定

2009-08-06 18:18:27

ASP.NET控件開發(fā)ASP.NET復(fù)合控件

2009-07-22 10:34:37

ActionInvokASP.NET MVC

2009-07-20 15:44:32

ASP.NET MVC

2009-07-22 09:11:02

Action方法ASP.NET MVC

2009-07-29 02:40:00

asp.net mvc
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號