ASP.NET AJAX 驱动的源代码浏览器






4.87/5 (12投票s)
ASP.NET AJAX 驱动的源代码浏览器
这个项目始于我开始寻找一种快速的方式将源代码放在我的网站上,以便访客能够轻松地浏览项目。时间非常宝贵,所以我并不总是喜欢下载一个 zip 压缩包然后解压来查看它——我更希望有一个在线的空间。
(注意:在自己安装此应用程序时,您应该将其发布为 Web 应用程序,然后更新 web.config 文件,将 "root
" appSetting
指向您的源代码的根目录。Web 应用程序应该具有读取指定文件夹的权限。)
<appSettings><add key="root" value="c:\dev\"/></appSettings>
那时我意识到这是一个完美的迷你项目。它将展示一个稳健架构的灵活性,提供了大量的重构机会,并解决了我在显示源代码方面的问题,同时展示了一些基本的编程范例!
我的最初要求是,我可以在几个小时内完成 "第一版" 的开发。我本身就花了很多时间在编码上,不希望一个失控的项目会占用我主要目标的任何时间和精力。所以,需求相当简单:
- 显示一个树形视图,可以向下浏览到各种节点、目录或源文件
- 以某种方式显示源代码,并进行语法高亮
- 不要显示所有内容,只显示相关内容
不算差,那么从哪里开始呢?
首先,我想创建一个灵活的模型。我第一版迭代的想法是解析文件系统并显示节点,但考虑到将来,我可能最终需要链接到第三方存储库,如 SVN。因此,我决定对我的模型进行接口化。
我有了结构中最通用的部分,即 IFileSystemNode
,它基本上包含一个名称和一个路径。这可以进一步解析为 IDirectory
,它包含其他 IFileSystemNode
的集合,或者一个由扩展名(也许我应该将其更改为类型)定义的 IFile
,并且可能包含要渲染的内容。
下一步是实现这些模型。我为节点创建了一个基类 abstract
,然后创建了一个 DirectoryModel
和一个 FileModel
。
关键在于,我可以有一个保存 Uri 并将路径公开为 URL 等实现的。遵循敏捷原则,我不会想得太远,暂时坚持我目前所拥有的。
接下来是数据访问层。第一步是一个通用的 IDataAccess
,它简单地允许 "Load
" 一个实体。
namespace Interface.DataAccess
{
/// <summary>
/// Data access method
/// </summary>
/// <typeparam name="T">The type of data access</typeparam>
public interface IDataAccess<T> where T: IFileSystemNode
{
/// <summary>
/// Loads the selected node
/// </summary>
/// <param name="path">The path to the node</param>
/// <returns>The node</returns>
T Load(string path);
}
}
由于我不需要在请求时获取文件内容,因此我创建了一个 IFileDataAccess
,它专门接收一个文件对象的引用,然后加载内容。
实现相对简单。DirectoryDataAccess
递归地遍历结构,为每个目录构建一个集合,或者调用文件数据访问来加载文件内容。
public DirectoryModel Load(string path)
{
if (string.IsNullOrEmpty(path))
{
throw new ArgumentNullException("path");
}
DirectoryModel retVal = ModelFactory.GetInstance<directorymodel>();
if (Directory.Exists(path))
{
retVal.Path = path;
DirectoryInfo dir = new DirectoryInfo(path);
retVal.Name = dir.Name;
// recursively add subdirectories
foreach(DirectoryInfo subDir in dir.GetDirectories())
{
DirectoryModel subDirectory = Load(subDir.FullName);
subDirectory.ParentDirectory = retVal;
retVal.Contents.Add(subDirectory);
}
foreach(FileInfo file in dir.GetFiles())
{
FileModel fileModel = _fileAccess.Load(file.FullName);
fileModel.ParentDirectory = retVal;
retVal.Contents.Add(fileModel);
}
}
return retVal;
}</directorymodel>
这是我给自己的第一个提示…与其递归,不如我轻松地只加载顶级目录项的名称,并设置一个 "isLoaded
" 标志。然后,只有当用户深入到较低级别时,我才需要开始在那里进行递归。这将是稍后进行的重构……现在,我将整个东西一次性加载到内存中,但这显然无法扩展到大量项目,也无法通过网络连接到源代码存储库。我们会实现的!
FileDataAccess
提取文件名和扩展名信息。它通过读取文件的所有字节来实现 "LoadContents
"。
...
if (File.Exists(file.Path))
{
file.Content = File.ReadAllBytes(file.Path);
}
...
我喜欢在数据访问之上有一个服务层。该层可以协调多个数据访问(例如,将来当我添加 SVN 实现时,服务层可以实例化一个文件系统和一个 SVN 数据访问处理程序,然后聚合结果传递给表示层)。在此项目中,服务层主要充当数据访问的传递者。但是,它还包含一个我们将在用户界面中显示的有效扩展名列表,因此它根据扩展名是否符合条件来管理对 "LoadContents
" 的调用。这可以防止我们尝试加载 DLL 等文件。
现在我们已经把这些都整理好了,让我们转向控件。我可以利用 AJAX 提供的广泛的控件库,但我觉得自己动手创建控件会更有趣。我确实决定使用 JQuery,因为它使客户端操作非常容易。
首先是控件的架构。我想要一个 "树" ,它有分支(目录或文件),然后是一个 "叶子",用于显示代码。这很容易可视化和规划。
我决定将分支作为一个基于 Panel
的服务器控件。然后,分支可以控制子分支的可见性。分支包含一个 IFileSystemNode
,并为其节点生成一个锚点标签。然后,根据它是 IDirectory
还是 IFile
,它会生成一个点击事件来展开子节点或加载代码内容。我决定让分支递归地加载自身。在此阶段,整个树结构都被输出,然后第二级分支默认折叠。同样,这是第一个迭代:UI 的下一个重构将是延迟加载子节点。目前的状态无法处理大量项目。
您会在分支的 JavaScript 代码中发现一些有趣的代码。例如,我发现最初隐藏 DIV
元素在某种程度上与内部 DOM 状态不同步,迫使用户点击两次才能展开一个节点。为了解决这个问题,我创建了一个附加到节点的自定义属性,并调用了一个冗余的 "hide" 操作来使 DOM 同步。
// for some reason the visibility isn't sorted out, so
// the first time we'll hide it again to get the flags set
// and then the toggle works fine
if (!$(child).attr('init')) {
$(child).hide();
$(child).attr('init','true');
}
其余的只是一个简单的切换子节点及其同级节点的可见性。整个架构最终看起来是这样的:
分支是一个服务器控件,树是相关分支的容器。分支暴露一个事件,其他客户端控件可以在用户点击节点查看时监听。这就是 "叶子" 的作用。它基本上是一个标题和一个 DIV
,并注册监听点击事件。当发生这种情况时,它会进行回调以获取代码内容,然后将其渲染在 DIV
中。
对于语法高亮,我使用了 Alex Gorbatchev 的 Syntax Highlighter。它做得很好,也是我在我的 C#er : IMage 博客 中使用的。一旦 div
被渲染,并在 Leaf
的帮助下(这是表示逻辑,不是服务逻辑)确定了基于文件扩展名的正确画笔,就会调用 Syntax
对象,它会进行高亮显示代码的工作。
我在 Leaf
中 "按预期" 使用了 Callback
函数,即通过调用 GetCallbackEventReference
来生成代码,它会生成适当的 JavaScript。为了防止用户在节点仍在加载时频繁点击,我们设置了一个全局标志,并在当前叶子完全加载之前显示一个错误。最终效果如下:
function leafCallback(path,name) {
if (window.loading) {
alert('An existing request is currently loading.
Please wait until the code is loaded before clicking again.');
return;
}
window.loading = true;
$("#_codeName").html(name);
$("#_divCode").html("Loading...");
var arg = path + '|' + name;
;
}
我们将路径和名称传递下去,以便在服务器上找到正确的节点,然后服务器返回要渲染的字节。回调函数只需注入源代码,然后调用语法高亮器。
function leafCallbackComplete(args, ctx) {
$("#_divCode").html(args);
window.loading = false;
window.scrollTo(0, 0);
setTimeout('SyntaxHighlighter.highlight()', 1);
}
我决定构建一个静态缓存,并将整个结构的副本保留在内存中。每小时刷新一次,因此当添加新项目时,它会获取新的源代码。Cache
只是使用 ASP.NET Cache
对象,然后从缓存中拉取树,或者调用服务来递归加载树。显然,当重构以支持延迟加载时,此策略需要更改。
namespace CodeBrowser.Cache
{
/// <summary>
/// Caches a global copy of the tree and refreshes it every hour
/// </summary>
public static class TreeCache
{
/// <summary>
/// Key for storing in the cache
/// </summary>
private const string CACHE_KEY = "Master.Node.Key";
/// <summary>
/// The expiration (1 hour)
/// </summary>
private static readonly TimeSpan _expiration = new TimeSpan(0,1,0,0);
/// <summary>
/// Gets the master tree node
/// </summary>
/// <returns>The root <seealso cref="IDirectory"/></returns>
public static IDirectory GetMasterNode()
{
IDirectory retVal = (IDirectory) HttpContext.Current.Cache.Get(CACHE_KEY);
if (retVal == null)
{
IService<DirectoryModel> service = ServiceFactory.GetService<DirectoryModel>();
retVal = service.Load(ConfigurationManager.AppSettings["root"]);
HttpContext.Current.Cache.Insert
(CACHE_KEY, retVal, null, DateTime.Now.Add(_expiration), TimeSpan.Zero);
}
return retVal;
}
}
}
请注意,主 Web 项目仅处理接口和工厂(以及具体的模型),而不处理服务的实际实现(它甚至不知道服务背后的数据访问)。这为我们将来进行扩展提供了很大的灵活性。
我做的唯一其他事情是创建一个基页,该页面注册了项目所需的所有包含文件和 CSS。这使得默认页面整洁干净,因为所有设置都在后台完成。我使用了我 MVC for WebForms 架构的一个松散版本,但没有进行接口和控件/控制器工厂的范围界定。这是另一个可以让我将来重构并进一步抽象的地方。目前,控制器只是注入控件并使用缓存来拉取文件系统结构。
您可以通过点击这里查看浏览器的一个实时副本(并使用它来浏览代码)。要下载 zip 文件中的源代码,请点击这里。我希望您喜欢并从这个项目中有所收获,我期待在未来的帖子中进行一些重构并扩展它。