使用JQUERY Tabs插件宿主IFRAMES


Posted in Javascript onJanuary 01, 2010

必备的东西:

Windows XP/Vista/7/2003/2008
Visual Studio 2005 or 2008 (download the correct version of Home Site project above)
.NET Framework 2.0 and ASP.NET AJAX 1.0
今天,很多浏览器提供了使用tab的能力去浏览更多的网页和网站。当然这是一个非常有用的功能来替代同时打开几个浏览器,但如果提供在一个页面中浏览多个网页也非常的不错。

例如,如果你的主页是由很多不同的有用的Web工具或者站点组成,一个tab页面将可能非常的有用。使用框架集,IFRAME等等,都是宿主外部内容的典型方式。这些方法允许你在单个页面上宿主多个网页。 但是使他们能正确的布局却非常不容易。 更不用说去处理页面和IFRAME的scrollbars等问题。

这篇文章中,尝试去宿主外部数据,提供了一个基本的解决方案,利用ASP.NET,AJAX和javascript 去处理一些遇到的麻烦。

计划

主旨是提供一个简单的宿主外部数据的方案,这个解决方案有下面这些简单的需求。

1、提供一个tab界面,方便浏览。

2、提供一个配置方法来添加tab

3、使每个tab页都能宿主配置的页面

基本的技术需要是:

1、仅当tab被选中的时候,加载外部的数据内容

2、保证纵向的scrollbars的设置成显示,而且仅当需要处理的数据溢出的时候,才显示scrollbars 。

3、保证该解决方案是能跨浏览器工作

解决方案的名字和主页面的名字都是 Home Site

分析

对于这个解决方案,我决定使用JQuery UI Tabs 来实现表格式的导航功能。我以前也使用过商业的开源的tab控件。但是JQuery UI Tabs 是轻量级的,实现非常地简单,而且是免费的。

除了JQuery 和tab控件以及.net提供的功能,不需要其它的控件。 VS2005 将足以结合整个项目的开发环境,选择C#作为开发语言。

我将使用一个IFRAME的宿主网站内容,由于跨站点(又名跨域)的安全限制,使用JQuery UI Tabs去宿主外部网页将无法直接工作。

设计

这里有一个为客户提供视觉上的最低限度的需求:
使用JQUERY Tabs插件宿主IFRAMES
该解决方案,将需要三种不同的功能模块:
1、配置模块
2、使用JQuery UI Tabs 插件的tab界面
3、使用IFRAME元素宿主网页内容办法。
配置模块:
一个需求的功能是是使tab可配置。 我选择最低限度,将tab的配置信息放到一个xml文件之中。虽然我可以更进一步的深入,使tab能的动态增加和删除,我决定在本文的第二篇中提供此功能。
XML文件的格式如下:
代码

<?xml version="1.0" encoding="utf-8" ?> 
<configuration> 
<tab id="TabOne" displayName="Tab One" path="www.msn.com" /> 
<tab id="TabTwo" displayName="Tab Two" path="www.amazon.com" /> 
</configuration>

参数描述:
id = tab的唯一ID。这个ID不能包含空格
displayName =显示在tab头部的名字
path = 带查询参数的URL,"http://"是可选的。
配置文件的名字是:TabConfig.xml。现在必须手动添加或删除tab来更新解决方案的配置文件。
内容加载器:
可以说,没有内容加载模块是需要用IFRAME去为tab页面设置内部的列表项。但是如果IFRAME在一个通过使用锚作为每个tab列表项元素的子元素的独立的宿主网页中,我觉得在功能和测试方面没有比IFRAME的更好的控件了:
使用JQUERY Tabs插件宿主IFRAMES
如果你愿意,将内容装载器做成一个通用的模块,它接受查询字符串参数,以便正确设置IFRAME元素;参数为元素的唯一的ID,和源属性值,也就是加载该网页的URL。

另一个内容加载器的设计要求是,必须使该IFRAME负载整个页面(scrolling设置为auto)。此外,该网页body必须隐藏溢出(通过设置样式属性),以避免重复滚动条。特别是当改变浏览器的大小时。最后,滚动条的处理必须能跨浏览器。

tab界面

tab界面代码非常的简单,可以从 JQuery UI Tabs documentation说明文档中得到详细的演示代码。JQuery UI Tabs说明文档中的和我们JQuery UI Tabs具体的实现不同的地方在于:每个tab项锚的href指向内容加载页面,随后加载所需的网页到IFRAME里面。

一些额外的东西

上面的标签,我认为它很方便去用一个div来显示头,logo,甚至一些链接或菜单项。一个更好的需求,我想要头部能够折叠,能使每个标签宿主的页面能有一个最大的视觉效果。

最终的设计布局如下:
使用JQUERY Tabs插件宿主IFRAMES
代码/开发
我们从内容加载器开始,下面是标记:
代码

<%@ Page Language="C#" AutoEventWireup="true" 
CodeBehind="ContentLoader.aspx.cs" Inherits="HomeSite.ContentLoader" %> 
<html xmlns="http://www.w3.org/1999/xhtml"> 
<head runat="server"> 
<title>ContentLoader</title> 
<style type="text/css"> 
.contentsBody { margin:0; overflow:hidden; } 
.contentsTable { width:100%; height:92%; valign:top; 
cellspacing:0; cellpadding:0; border:0 } 
.contentsIframe { height:100%; width:100%; marginwidth:0; 
marginheight:0; scrolling:auto } 
</style> 
</head> 
<body class="contentsBody"> 
<table class="contentsTable"> 
<tr> 
<td> 
<asp:Literal ID="Literal1" 
runat="server"></asp:Literal> 
</td> 
</tr> 
</table> 
</body> 
</html>

真正神奇的地方是css代码,我设置body 的margin 为0,设置overflow 为hidden。防止scrollbars 出现在页面的body上。
IFRAME的scrolling设置为auto,因此,如果需要滚动条,也只有IFRAME提供给它。因为在IFRAME的周围有大量不好看的空白空间,将margins 也设置为0,IFRAME的height和width被设置成100%,来确保网页内容占尽可能多的空间。请注意html标签中使用了Literal控件。正如你将看到以下的隐藏代码, 使用Literal的目的是允许后台代码注入东西到IFRAME元素中,它能构建了正确的querystring的ID和Path参数。
代码
using System; 
using System.Data; 
using System.Configuration; 
using System.Collections; 
using System.Web; 
using System.Web.Security; 
using System.Web.UI; 
using System.Web.UI.WebControls; 
using System.Web.UI.WebControls.WebParts; 
using System.Web.UI.HtmlControls; 
namespace HomeSite 
{ 
/// <summary> 
/// Content Loader code behind class 
/// </summary> 
public partial class ContentLoader : System.Web.UI.Page 
{ 
/// <summary> 
/// On Page Load we need to capture query string parameters, construct 
/// an IFRAME element, and inject the IFRAME element into our Literal control 
/// </summary> 
/// <param name="sender"></param> 
/// <param name="e"></param> 
protected void Page_Load(object sender, EventArgs e) 
{ 
string id = ""; 
string path = ""; 
// Validate we have valid querystring parameters 
// namely "ID" and "Path" 
if (HasValue(Request["ID"]) && 
HasValue(Request["Path"])) 
{ 
// Set our local variables 
id = Request["ID"].Trim().ToString(); 
path = Request["Path"].Trim().ToString(); 
// Prepend the path URL with http:// if needed 
if (!path.ToLowerInvariant().StartsWith("http://")) 
path = "http://" + path; 
// Construct the IFRAME element and set the Text value of the Literal control 
Literal1.Text = "<iframe class=\"contentsIframe\" " + 
"id=\"contentFrame" + id + "\" " + 
"frameborder=\"0\" src=\"" + path + 
"\"></iframe>"; 
} 
else 
{ 
// Either query parameter or both are not set or do not 
// exist (not passed as request parameters) 
Literal1.Text = "<span id=\"contentFrame\">An " + 
"error occurred while attempting to load a web page.</span>"; 
} 
} 
/// <summary> 
/// Simple static class used to validate the value of querystring 
/// parameter is not null or an empty string 
/// </summary> 
/// <param name="o">The object to check</param> 
/// <returns>Returns true if the object (string) 
/// has a value; false otherwise.</returns> 
public static bool HasValue(object o) 
{ 
if (o == null) 
return false; 
if (o is String) 
{ 
if (((String) o).Trim() == String.Empty) 
return false; 
} 
return true; 
} 
} 
}

只要你将querystring的ID和Path参数传递给它,装载的页面能够单独的运行。通过VS2005浏览网页,有URL的示例:http://localhost:49573/ContentLoader.aspx?ID=1234&Path=www.amazon.com.
using System;
using System.Data;
using System.Configuration;
using System.Collections;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using System.IO;
using System.Xml;
using System.Text;
namespace HomeSite
{
    /// <summary>
    /// Tab configuration static handling class
    /// </summary>
    public static class TabConfiguration
    {
        /// <summary>
        /// This class returns a collection of TabDefinition classes created from
        /// parsing the tab definitions defined in the TabConfig.xml file.
        /// </summary>
        /// <param name"page">The Page reference
        ///         calling this class</param>
        /// <returns>ArrayList of TabDefinition classes</returns>
        public static ArrayList LoadConfiguration(Page page)
        {
            // Local container for tab definitions
            ArrayList tabList = new ArrayList();
            try
            {
                // Read the contents of the TabConfig.xml file
                StreamReader reader = new StreamReader(new FileStream(
                   page.MapPath("./TabConfig.xml"), 
                   FileMode.Open, FileAccess.Read));
                string xmlContent = reader.ReadToEnd();
                reader.Close();
                reader.Dispose();
                // Create an XML document and load the tab configuration file contents
                XmlDocument xmlDoc = new XmlDocument();
                xmlDoc.LoadXml(xmlContent);
                // Iterate through each tab definition, create a TabDefinition class,
                // and add the TabDefinition to the local ArrayList container
                foreach (XmlNode node in xmlDoc.SelectNodes("/configuration/tab"))
                {
                    TabDefinition tab = new TabDefinition();
                    tab.ID = node.Attributes["id"].Value;
                    tab.DisplayName = node.Attributes["displayName"].Value;
                    tab.Path = node.Attributes["path"].Value;
                    tabList.Add(tab);
                }
            }
            catch
            {
                // Do nothing
            }
            // Return the tab definition
            return tabList;
        }
    }
    /// <summary>
    /// This class serves as the container for a tab definition
    /// </summary>
    public class TabDefinition
    {
        /// <summary>
        /// Member variable for the Unique ID for the tab
        /// </summary>
        private string _id;
        /// <summary>
        /// Member variable for the displayed name of the tab
        /// </summary>
        private string _displayName;
        /// <summary>
        /// Member variable for the web page URL to host in the tab (IFRAME)
        /// </summary>
        private string _path;
        /// <summary>
        /// Property for the Unique ID for the tab
        /// </summary>
        public string ID
        {
            get { return _id; }
            set { _id = value; }
        }
        /// <summary>
        /// Property for the displayed name of the tab
        /// </summary>
        public string DisplayName
        {
            get { return _displayName; }
            set { _displayName = value; }
        }
        /// <summary>
        /// Property for the web page URL to host in the tab (IFRAME)
        /// </summary>
        public string Path
        {
            get { return _path; }
            set { _path = value; }
        }
    }
}

    请注意页面实例必须提供LoadConfiguration方法来正确引入TabConfig.xml的位置。我本可以使用XmlTextReader,但选择使用StreamReader读取整个配置文件的内容和使用XmlDocument对象解析tab的配置信息。因为我觉得快速转储整个配置文件比通过流程解析打开配置文件要好很多。使用XmlTextReader正属于这种情况。

    现在,让我们看看Home Site 的主页的标记

代码

<%@ Page Language="C#" AutoEventWireup="true" 
  CodeBehind="Default.aspx.cs" Inherits="HomeSite._Default" %>
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>Home Site</title>
    <link href="css/jquery-ui-1.7.2.custom.css" 
          type="text/css" rel="stylesheet" />
    <link href="css/Main.css" 
          type="text/css" rel="stylesheet" />
    <script src="JavaScript/jquery-1.3.2.min.js" 
          type="text/javascript"></script>
    <script src="Javascript/jquery-ui-1.7.2.custom.min.js" 
          type="text/javascript"></script>
    <script src="Javascript/jquery.hijack.min.js" 
          type="text/javascript"></script>
    <script type="text/javascript">
        // JQuery scripting
        $(document).ready(function()
        {
            var browser = navigator.appName;
            var heightAdjust = 23;
            var widthAdjust = 7;
            // Make height and width offset adjusts for non-IE browsers 
            if (browser != "Microsoft Internet Explorer")
            {
                heightAdjust = 18;
                widthAdjust = 9;
            }
            // Show the panelList UL element so we can setup the tabs
            // Please note this approach eliminates Flash of Unstyled Content (FOUC)
            $('#panelList').show();
            // Setup the jQuery UI tabs
            $('#tabPage').tabs({
                cache: true, // This ensures selecting a tab does not refresh the page
                load: function(event, ui)
                {
                    // Keep links, form submissions, etc. contained within the tab
                    $(ui.panel).hijack();
                    // Adjust the IFRAME size correctly in the browser window
                    $('.contentsIframe').width((ViewPortWidth() - widthAdjust));
                    $('.contentsIframe').height((ViewPortHeight() - 
                       $('.menuRow').height() - $('.tabs').height() - heightAdjust));
                }
            });
            // Toggle arrow button image and hide/show menu area
            $('#collapseArrow').click(function()
            {
                if ($(this).hasClass('ui-icon-circle-triangle-s'))
                {
                    $(this).removeClass('ui-icon-circle-triangle-s');
                    $(this).addClass('ui-icon-circle-triangle-n');
                    $('#menuDiv').show();
                }
                else
                {
                    $(this).removeClass('ui-icon-circle-triangle-n');
                    $(this).addClass('ui-icon-circle-triangle-s');
                    $('#menuDiv').hide();
                }
            // Adjust the IFRAME size correctly in the browser window
            $('.contentsIframe').width((ViewPortWidth() - widthAdjust));
            $('.contentsIframe').height((ViewPortHeight() - 
              $('.menuRow').height() - $('.tabs').height() - heightAdjust));
        });
        // Adjust tab header width and visible iframe window
        // height and width after the window is resized
        $(window).resize(function(){
        $('.contentsIframe').width((ViewPortWidth() - widthAdjust));
        $('.contentsIframe').height((ViewPortHeight() - 
          $('.menuRow').height() - $('.tabs').height() - heightAdjust));
        $('.ui-widget-header').width(ViewPortWidth() - widthAdjust);
        });
        // Adjust tab header height and width according to the IE client viewing area
        $('.ui-widget-header').width(ViewPortWidth() - widthAdjust);
        // Adjust the IFRAME height correctly in the browser window
        $('.contentsIframe').height((ViewPortHeight() - 
          $('.menuRow').height() - $('.tabs').height() - heightAdjust));
        });
        // Returns width of viewable area in the browser
        function ViewPortWidth()
        {
            var width = 0;
            if ((document.documentElement) && 
                (document.documentElement.clientWidth))
            {
                width = document.documentElement.clientWidth;
            }
            else if ((document.body) && (document.body.clientWidth))
            {
                width = document.body.clientWidth;
            }
            else if (window.innerWidth)
            {
                width = window.innerWidth;
            }
            return width;
        }
        // Returns height of viewable area in the browser
        function ViewPortHeight()
        {
            var height = 0;
            if (window.innerHeight)
            {
                height = window.innerHeight;
            }
                else if ((document.documentElement) && 
                         (document.documentElement.clientHeight))
            {
                height = document.documentElement.clientHeight;
            }
            return height;
        }
    </script>
</head>
<body class="mainBody" style="margin:0">
    <form id="form1" runat="server">
        <asp:ScriptManager id="ScriptManager1" runat="server" />
        <div>
            <table id="mainTable" cellpadding="0" cellspacing="0">
                <tr class="menuRow">
                    <td align="left" valign="top">
                        <span id="collapseArrow" 
                           title="Show/Hide Header" 
                           class="menuSpan ui-icon ui-icon-circle-triangle-n"></span>
                        <div id="menuDiv" 
                          class="menuDiv">This is the header area.
                                  <br /><i>Please customize this area as you set 
                                  fit; i.e. add a logo, menu options, links, 
                                  etc.</i><br /><br /></div>
                    </td>
                </tr>
                <tr>
                    <td class="tabPageCell" colspan="2" 
                             valign="top" align="left">
                        <div id="tabPage" class="contents">
                            <ul id="panelList" 
                                class="tabs" runat="server" />
                        </div>
                    </td>
                </tr>
            </table>
        </div>
    </form>
</body>
</html>

     这些代码标记非常的繁琐,我用了很多内部的注释来解释他们。请注意出现在标头区的左上角的箭头按钮,其实就是来自我选择的JQuery的主题中的图像文件,使用ui-icon 和ui-icon-circle-triangle-n causes 设置collapseArrow span ,造成JQuery 显示一个名字为ui-icon-circle-triangle-n的ico的图像。在文档头部的脚本区中,我创建一个函数,当我们单击它的时候,改变向上箭头图标为向下箭头图标,此外,同样的单击事件处理程序将显示或隐藏标头部域的div(menuDiv)。

Home Site 页面的隐藏代码如下:

代码

using System;
using System.Data;
using System.Configuration;
using System.Collections;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
namespace HomeSite
{
    /// <summary>
    /// Home Site (default) web page code behind class
    /// </summary>
    public partial class _Default : System.Web.UI.Page
    {
        /// <summary>
        /// On page load we need to create the tab
        /// list items for tab interface construction
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        protected void Page_Load(object sender, EventArgs e)
        {
            if (!IsPostBack)
                AddTabsToForm();
        }
        /// <summary>
        /// This method calls to our business logic
        /// class to read the tab configuration file,
        /// which will return an ArrayList of TabDefinition
        /// classes. This method iterates
        /// through the ArrayList building HTML controls to add to the tab panel.
        /// </summary>
        protected void AddTabsToForm()
        {
            foreach (TabDefinition tab in TabConfiguration.LoadConfiguration(this.Page))
            {
                HtmlGenericControl tabListItem = new HtmlGenericControl();
                tabListItem.TagName = "li";
                tabListItem.InnerHtml = "<a title=\"" + 
                  tab.DisplayName + "\" href=\"ContentLoader.aspx?ID=" + 
                  tab.ID + "&Path=" + tab.Path + 
                  "\">" + tab.DisplayName + "</a>";
                panelList.Controls.Add(tabListItem);
            }
        }
    }
}

隐藏代码不需要做过多的解释,关键的动作是创建和设置HtmlGenericControl列表项,最后它通过编程被添加tab panel中。

遇到的问题

我遇到的主要的问题是跨浏览器的情况下自动适应IFRAME的大小,该方案在IE 8,Firefox v3.5.6,和谷歌v3.0.195.38浏览器进行测试。

我必须进行浏览器检测,根据IFRAME在三个浏览器测试的尺寸,调整相应的宽度和高度。当浏览器改变大小的时候,Chrome 和FireFox看起来IFRAME有个固定的高度。 但是, IE8看来来会丢失在IFRAME和浏览器顶部之间的padding。调整宽度和高度特别是IE似乎应该尽可能的减少IFRAME到IE浏览器窗口的底部“蜷缩”影响。

限制

1、以下JavaScript将使你加载的网页跳出IFRAME,我不知道任何解决办法此(如果存在)。Code Project 网站上目前拥有类似下面的代码,这样配置选项指向http://www.codeproject.com/非常的容易,这里重现描述的动作。

<script type="text/javascript" language="javascript">
    if (top!=self) top.location.href = location.href;
</script>
2、在浏览器中,Web网页被迫改变页面本身的大小,有可能跳出IFRAME窗体,从而取代了前父窗口。

3、我没有使用Safari,Opera,早期版本的IE浏览器,或任何其他浏览器的早期版本测试的此解决方案,所以要在Home Site中适当地调整heightAdjust和widthAdjust变量,适应没有测试的IE浏览器或低于IE8浏览器的版本。

总结和兴趣点
虽然这种解决方案不是很复杂,通过一个标签界面宿主外部网站内容。这是我所见过的许多网络论坛和博客要求的功能。请注意:您也可以配置标签可显示自己相关的域名或网站(在同一台服务器)。

Javascript 相关文章推荐
用JavaScript隐藏控件的方法
Sep 21 Javascript
js简单实现HTML标签Select联动带跳转
Oct 23 Javascript
在线一元二次方程计算器实例(方程计算器在线计算)
Dec 22 Javascript
jQuery中siblings()方法用法实例
Jan 08 Javascript
JavaScript实现彩虹文字效果的方法
Apr 16 Javascript
JavaScript和HTML DOM的区别与联系及Javascript和DOM的关系
Nov 15 Javascript
JS实现的仿淘宝交易倒计时效果
Nov 27 Javascript
轻松学习jQuery插件EasyUI EasyUI实现拖放商品放置购物车
Nov 30 Javascript
浅谈angularJS中的事件
Jul 12 Javascript
JS基于正则表达式实现的密码强度验证功能示例
Sep 21 Javascript
ES6中let 和 const 的新特性
Sep 03 Javascript
微信小程序3D轮播实现代码
Sep 19 Javascript
用jquery实现学校的校历(asp.net+jquery ui 1.72)
Jan 01 #Javascript
url 特殊字符 传递参数解决方法
Jan 01 #Javascript
JavaScript 数组循环引起的思考
Jan 01 #Javascript
javascript eval和JSON之间的联系
Dec 31 #Javascript
js下用gb2312编码解码实现方法
Dec 31 #Javascript
JavaScript 学习笔记(七)字符串的连接
Dec 31 #Javascript
JavaScript 学习笔记(六)
Dec 31 #Javascript
You might like
php格式化时间戳显示友好的时间实现思路及代码
2014/10/23 PHP
PHP制作用户注册系统
2015/10/23 PHP
Centos 6.5系统下编译安装PHP 7.0.13的方法
2016/12/19 PHP
一个不错的应用,用于提交获取文章内容,不推荐用
2007/03/03 Javascript
js 多种变量定义(对象直接量,数组直接量和函数直接量)
2010/05/24 Javascript
jQuery 表单验证扩展(四)
2010/10/20 Javascript
JavaScript中的console.group()函数详细介绍
2014/12/29 Javascript
JS对象序列化成json数据和json数据转化为JS对象的代码
2017/08/23 Javascript
轻松玩转BootstrapTable(后端使用SpringMVC+Hibernate)
2017/09/06 Javascript
JS实现点击下拉菜单把选择的内容同步到input输入框内的实例
2018/01/23 Javascript
深入理解使用Vue实现Context-Menu的思考与总结
2019/03/09 Javascript
详解Vue依赖收集引发的问题
2019/04/22 Javascript
CountUp.js实现数字滚动增值效果
2019/10/17 Javascript
Python中的匿名函数使用简介
2015/04/27 Python
python dict.get()和dict['key']的区别详解
2016/06/30 Python
Django返回json数据用法示例
2016/09/18 Python
关于Python中Inf与Nan的判断问题详解
2017/02/08 Python
Python魔法方法详解
2019/02/13 Python
Python寻找路径和查找文件路径的示例
2019/07/10 Python
python识别文字(基于tesseract)代码实例
2019/08/24 Python
Python中Unittest框架的具体使用
2019/08/27 Python
python openCV获取人脸部分并存储功能
2019/08/28 Python
python seaborn heatmap可视化相关性矩阵实例
2020/06/03 Python
Django CBV模型源码运行流程详解
2020/08/17 Python
python实现磁盘日志清理的示例
2020/11/05 Python
python制作一个简单的gui 数据库查询界面
2020/11/19 Python
新西兰便宜隐形眼镜购买网站:QUICKLENS New Zealand
2019/03/02 全球购物
Interflora澳大利亚:同日鲜花速递
2019/06/25 全球购物
Andrew Marc官网:设计师外套的领先制造商
2019/10/30 全球购物
Linux如何修改文件和文件夹的权限
2012/06/27 面试题
业务员薪酬管理制度
2014/01/15 职场文书
介绍长城的导游词
2015/01/30 职场文书
熟背这些句子,让您的英语口语突飞猛进(135句)
2019/09/06 职场文书
使用Djongo模块在Django中使用MongoDB数据库
2021/06/20 Python
教你如何让spark sql写mysql的时候支持update操作
2022/02/15 MySQL
MySQL添加索引特点及优化问题
2022/07/23 MySQL