面向UI编程:ui.js 1.0 粗糙版本发布,分布式开发+容器化+组件化+配置化框架,从无到有的艰难创造-阿里云开发者社区

开发者社区> 仲强> 正文

面向UI编程:ui.js 1.0 粗糙版本发布,分布式开发+容器化+组件化+配置化框架,从无到有的艰难创造

简介: 有朋友提出一看来是懵逼的,根本不知道什么是面向UI编程的思想,下面是我之前写的博客,描述的这中思想,下面为地址,参考下就明了很多了。 1. 前端思想实现:面向UI编程        2. 面向UI编程框架:ui.js框架思路详细设计         时隔第一次被UI思路激励,到现在1.0的粗糙版本发布,掐指一算整整半年了。
+关注继续查看

有朋友提出一看来是懵逼的,根本不知道什么是面向UI编程的思想,下面是我之前写的博客,描述的这中思想,下面为地址,参考下就明了很多了。

1. 前端思想实现:面向UI编程       

2. 面向UI编程框架:ui.js框架思路详细设计

     

  时隔第一次被UI思路激励,到现在1.0的粗糙版本发布,掐指一算整整半年了。半年之间,有些细节不断推翻重做,再推翻再重做。时隔今日,终于能先出来个东西了,这个版本很粗糙,主体功能大概能实现了,但是还是有很多很多的问题。不过有问题没事,可以进行修改完善,这是相对轻松的问题,最艰难的从无到有的创造才是最艰难的。好了,不废话了,我们直接进入正题 --- UI.js功能介绍。

 

首先介绍几个概念

  • 分布式:原来的概念只有分布式计算,它研究如何把一个需要非常巨大的计算能力才能解决的问题分成许多小的部分,然后把这些部分分配给许多计算机进行处理,最后把这些计算结果综合起来得到最终的结果。这边要实现的概念是,
  • 分布式协作开发就是将一个项目分拆多个组件,可从不同的服务器上拉取,分布协作开发
  • 容器容器用来包装或装载物品的贮存器,从web角度来讲,web容器是应用服务器中位于组件和平台之间的接口集合。
  • 组件供装配整台机器、构件或元件的零件组合或者在电子或机械设备中组装在一起形成一个功能单元的一组元件,我们这里讨论的组件是由html+js+css乃至其他资源组合,完善的能够实现一个功能且能重复使用的组合元件。
  • 配置管理:提供一套基础机制,通过配置文件修改,来进行对项目的管理
  • 统一管理和注入:本框架中将项目中所有的接口进行统一配置和管理,包括组件所需组件的配置和注入。

 

首先为推荐的项目布局:

  app                   ---存放容器的目录

  component           ---存放组件的目录

    testCom            ---组件名称目录

      css             ---组件样式

      img           ---组件使用图片

      js             ---组件使用的脚本

      tpl            ---组件的html标签

      description.txt      ---组件的描述说明

  rely              ---存放ui.js和ui.config文件的目录

  resources           ---存放静态资源的目录

    css             ---全局使用样式库

    img            ---全局使用的图片库

    lib              ---第三方引动库

图片:

 

结构布局的思想主要使用的分类和拆分思想

  • 将类似结构的构造进行分离归纳,这样对于每个布局的查找和分布都很好
  • 将组件和容器分开来,是为了防止高耦合,改动容器和组件,不会对页面其他布局和组件影响

 

配置文件如下:

ui.config({
    //注入页面容器
    container:{
        //名称:容器地址+是否装载(PS:如果都为true只会选择第一个模板容器)
        "layout1":["app/demoPage1.tpl",true],
        "layout2":["app/demoPage2.tpl",false]
    },
    //注入接口
    interface:{
        "interface1":"www.123.com/interface1111111",
        "interface2":"www.123.com/interface2",
        "interface3":"www.123.com/interface3",
        "interface4":"www.123.com/interface4",
        "interface5":"www.123.com/interface555555555555",
    },
    //注入组件
    component:{
        //组件名:组件模板+组件样式+组件脚本+接口注入+组件是否装载   ===>  该处可优化针对本地项目和分布式开发进行优化
        "test":["component/test/tpl/test.tpl","component/test/css/test.css","component/test/js/test.js",["interface1","interface2"],true],
        "test1":["component/test1/tpl/test1.tpl","component/test1/css/test1.css","component/test1/js/test1.js",["interface5","interface2"],true],
        "test2":["component/test2/tpl/test2.tpl","component/test2/css/test2.css","component/test2/js/test2.js",["interface1","interface2"],true],
        "test3":["component/test3/tpl/test3.tpl","component/test3/css/test3.css","component/test3/js/test3.js",["interface1","interface2"],true],
        "test4":["component/test4/tpl/test4.tpl","component/test4/css/test4.css","component/test4/js/test4.js",["interface1","interface2"],true],
        "test5":["component/test5/tpl/test5.tpl","component/test5/css/test5.css","component/test5/js/test5.js",["interface1","interface2"],true]
    },
    //容器组件映射关系   选择的容器名称:{"页面容器":"所加载的组件"}
    con_com:{
        layout1:{
            con1:"test",
            con2:"test1",
            con3:"test2",
            con4:"test3",
            con5:"test4",
            con6:"test5",
        },
        layout2:{
            con1:"test",
            con2:"test",
            con3:"test",
            con4:"test",
            con5:"test",
            con6:"test",
        }
    }
});

针对配置文件的解释:

1. container

  为页面布局容器,该tpl模板中全部为ui设计好的容器,框架加载的时候首先会去查找开发者设置使用的哪一种布局,然后解析模板中的容器。该容器是为了整个页面换布局设计的。如果整个网站需要转换页面布局,传统的做法就是重新设计重新编码,然后搞完了重新部署。而该框架的设计思路,就是你重新设计一套布局容器,然后在该注入选项中注入你的新的布局容器,然后重新配置新的容器和适配组件的绑定关系,然后就会自动加载新的布局。

  代码实例(全部使用ui-con的值来设置容器名称):

<div style="" ui-con="con1"></div>
<div style="" ui-con="con2"></div>
<div style="margin:0 auto;width:1000px;padding:0;border:0;">
    <div style="display: inline-block;width: 755px;" ui-con="con3"></div>
    <div style="display: inline-block;vertical-align: top;width:200px;padding:10px;" ui-con="con4"></div>
</div>
<div style="" ui-con="con5"></div>
<div style="" ui-con="con6"></div>

 

2. interface

  该项为了全局统一管理接口。因为传统的页面开发中,如果一个接口换了,整个项目中所有用到该接口的ajax等各种请求都必须一个一个的换,所以该处将所有的接口提取出来,做一个全局管理。并且只要组件所需解口进行了注入配置,开发者只需要在每个组件的js中在use的第一个data参数中就可以拿到该接口地址。

  组件注入接口实例(data中会有注入的接口地址):

use(function(data,that){
    var tempObj ={
        //reader为一些初始化需要的操作,有时候会有注册事件等,或者一些预操作,该方法加载完成后会直接跑起来
        reader:function(){
            that = this;
            that.firm.testLoad();
        },
        //注入所有的选择器,方便选择器变化,直接修改该对象中的选择器,而不需要全局去更改
        selector:{
            testBtn:"#testBtn",  //按钮
        },
        //注入page中所有的事件,统一管理,建议命名规范:事件_命名,例 click_login
        registerEle:{
        },
        //注入所有ajax请求,页面所有请求,将在这里统一管理,建议命名规范:ajax_命名,例 ajax_login
        ajaxRequest:{
        },
        //处理所有回调函数,针对一个请求,处理一个回调
        callback:{
        },
        //临时缓存存放区域,仅针对本页面,如果跨页面请存放cookie或者localstorage等
        //主要解决有时候会使用页面控件display来缓存当前页面的一些数据
        temp:{
        },
        /*
         * 业务使用区域,针对每个特别的业务去串上面所有的一个个原子
         *   因为上面所有的方法,只是做一件事,这边可以根据业务进行串服务,很简单的
         * */
        firm:{
            testLoad:function(){
                console.log("获取接口的值:"+data.interface.interface1)
            }
        }
    };
    ui.component.reader(tempObj);
});

3. component

  注入的所有组件。该组件有2中形态。针对本项目(待优化中)和针对分布式开发项目。该组件的配置为:组件模板+组件样式+组件脚本+接口注入+组件是否装载。配置好了这5个参数之后,框架会针对配置文件,进行是否装载,接口注入,和组件装载,该方式已经实现。如果作为本地项目的话,可以配置更精简一点,只要格式按照我的布局文件写,可以直接配置一个组件的根目录就好了,就会自动寻找,不过该设计待优化中,还没有做好。这块加载组件模板用的技术就是ajax技术,css和script用的动态标签。所以如果做分布式,请求组件的时候,就必须做跨域处理了,这边就不详细描述,如果该组件做的功能请求的接口也支持跨域,那么这个功能就在其他项目中就可以实现了,做到了真正的分布式协作开发了,而本项目就是作为一个流量入口,接入互联网上所有分布式开发的资源了

4. con_com

  该配置文件为容器-组件绑定关系,为你每个注入的容器配置所需要加载的组件,该项可以根据容器进行配置,也必须配置。如果所配置的组件,在组件配置文件中未注入,框架不会直接停止解析,而是会在控制台抛错,告诉你该组件配置的问题,然后继续加载其他正确的容器-组件映射正确的模块功能,详细配置见配置文件。

 

ui.js框架核心代码粗略解析

  1. 代码结构使用,因为在一个核心的代码中肯定会遇到封装很多的工具,比如dom操作,一般的tool,ajax等工具。我的选择结构就是将所有的工具,以参数的形式注入到匿名函数中,然后匿名函数执行的就是ui.js的核心功能。代码结构如下:
(function(window,document,rely){
    //rely就是注入的所有使用的工具,修改起来相对轻松,核心中使用这些工具,不需要在乎这些方法怎么实现,只需要知道这些方法会达成我的目的就好了
})(window,document,(function(){
    var tool={};
    var ajax={};
    return {
        tool=tool,
        ajax=ajax
    }
})());

 

2.  ui.config为入口进行配置文件的解析

/*
    *   内部使用一些快捷标识符
    *       0. _  代表dom操作
    *       1. $  代表全局注入的工具类
    *       2. $1 代表注入的ajax类库
    *       3. $2 代表模板处理类库
    *       4. $3 代表所有错误处理信息
    *
    *
    * */

    var _ = relyObj.dom,$ = relyObj.tool,$1 = relyObj.ajax,$2 = relyObj.template,$3 =relyObj.errMsg,$4 =relyObj.htmlModule,that={},ui,UI_global;
    ui = UI_global = {
        //对所有参数进行处理
        config:function(configObject){
            //  1. 初始化数据池
            ui.dataPool.initPool();
            if(configObject === undefined){
                throw new Error($3.gloErr.noConfig);
            };

            //  2. 将配置文件存储到数据池中,做备用
            ui.dataPool.setData_glo("config",configObject);

            //  3. 检查加载页面容器,加载到body中
            $.each(configObject.container,function(value,key){
                if(value[1] === true){
                    ui.dataPool.setData_glo("private",{"pageConName":key});
                    ui.container.loadContainer(key,value[0],function(data){
                        _("body").html(data);
                        dealWithCom();
                        return;
                    });
                }
            });

            //处理组件方法
            var dealWithCom = function(){
                //  4. 处理配置容器和组件映射关系,取得所有容器所要加载组件的信息
                var temp = ui.dataPool.getData_glo("private","pageConName");
                //取得配置文件中关于当前容器中的容器-组件对应关系
                var tempS = ui.dataPool.getData_glo("config","con_com",temp);


                //  5. 判断组件是否存在,存在即加载组件
                $.each(tempS,function(value,key){
                    //判断组件是否配置
                    var getComInfo = ui.component.isExist_com(value);
                    if(getComInfo){
                        if (getComInfo[4]){
                            ui.component.loadComponent(value,getComInfo[0]);
                        }else {
                            var height =  _("[ui-con='"+key+"']").css("height");
                            _("[ui-con='"+key+"']").html($4.loadErr("line-height:"+height));
                        }

                    }else {
                        console.log($3.component.comConfig(value));
                    }
                });
            };
        },

3.  数据中转池的核心(以对象方式存数数据,而不是数组,因为数据需要遍历,增加很多压力),这个是最重要的,因为整个框架所依赖的数据处理都在这里面,它还没有很完善一些细节还是需要推敲的,比如说以后拒绝开发者修改一些内部使用中的数据,因为这些数据是框架内部处理使用,而非外部,修改不当容易造成整体崩溃。它规定一些重要的参数,以及向数据池增加参数和得到数据池的参数的方法和规定

/*
        *   数据中转池核心处理库(以对象方式去存储,这样方便快捷的取数据)
        *       数据格式:
        *           1. mapping(name + uuid )映射,每个name对应一个uuid  注:每加载一个页面容器生成一个uuid,每次加载一个组件,生成一个组件的uuid,尽量,按需创建和使用
        *           2. pool(uuid + data )映射,每个uuid对应储存一段数据
        *               a. interface,注入接口信息
        *               b. transfer,流转过来的数据
        *           3. pageContainer,存储当前加载的页面中所有的页面容器
        *           4. global_temp,全局存储的临时数据
        *           5. config,存储用户配置的参数
        *           6. private,存储UI.js本身处理过的数据,内部使用   (需要加载的组件)
        *
        * */
        dataPool:{
            //初始化数据池,将数据池都清空
            initPool:function(){
                that["dataCenter"]={
                    mapping:{},                 //数据例子: "module":"ffb71d7f-995b-4898-8e4b-b283a4fe6253"
                    pool:{},                    //数据例子:"ffb71d7f-995b-4898-8e4b-b283a4fe6253":{索要存的对象}
                    pageContainer:{},           //数据例子:"name":"http://xxxxx.com"
                    global_temp:{},             //数据例子:"Test":"123321"      做全局缓存用
                    config:{},                  //数据例子:值为config中的配置参数值
                    private:{}                 //数据例子:内部使用参数,处理ui.js的内部流程
                };
            },
            //检查是否为数据池全局拒绝修改的参数 (待定)
            checkParam_glo:function(name){
                var data =["interface","config","private"],len = data.length;
                while(len--> 0){
                    if(name === data[len]){
                        throw new Error($3.pool.forbidUpdate(name));
                    }
                }
                return true;
            },
            /*
            *   数据池设置数据池全局的值(该方法为了固定数据池的参数,防止数据错乱)
            *       key1 key2 .... value isNew
            * */
            setData_glo:function(){
                //参数检查处理,检查是否为数据池禁止修改数据,初始化一些参数
                //this.checkParam_glo(name);
                var arg = arguments,len = arg.length,funContent = "dataCenter.",temp;
                if (len < 2) throw new Error($3.pool.setValueErr);

                for (var i = 0;i<len-1;i++){
                    if (i === len-2){
                        funContent+=arg[i];
                    }else{
                        funContent+=(arg[i]+".");
                    }
                };
                try {
                    temp = (new Function("dataCenter","return "+funContent))(that.dataCenter);
                } catch (e){
                    temp = undefined;
                };
                if (temp === undefined){
                    //在全局数据池新增数据
                    (new Function("dataCenter","value",funContent+"=value"))(that.dataCenter,arg[len-1]);
                }else{
                    $.MergeObject(temp,arg[len-1]);
                }
            },
            /*
            * 获取全局对象的数据
            *   参数为: key1、key2、key3...
            * */
            getData_glo:function(){
                var arg = Array.prototype.slice.call(arguments),len = arg.length,temp;
                if (len === 0) return that.dataCenter;

                var funCon = "return dataCenter.";
                for (var i = 0;i<len;i++){
                    if (i === len-1) {
                        funCon += arg[i]
                    }else{
                        funCon += (arg[i]+".");
                    };
                };
                //获得数据容错判断,如果报错即查无此数据
                try {
                    temp = (new Function("dataCenter",funCon))(that.dataCenter);
                }catch (e){
                    temp = undefined
                };
                return temp;
            }
        },

4. 其他的暂时就不介绍了,有兴趣的可以去github上翻看源代码。

 

下面为我写的一个demo解析:

1.  首先将页面容器,按照设计进行规划和布局好,demo参考的是阿里巴巴的招聘网站,地址为:https://job.alibaba.com/zhaopin/positionList.htm

页面设计布局容器代码如下(6个容器):

<div style="" ui-con="con1"></div>
<div style="" ui-con="con2"></div>
<div style="margin:0 auto;width:1000px;padding:0;border:0;">
    <div style="display: inline-block;width: 755px;" ui-con="con3"></div>
    <div style="display: inline-block;vertical-align: top;width:200px;padding:10px;" ui-con="con4"></div>
</div>
<div style="" ui-con="con5"></div>
<div style="" ui-con="con6"></div>

 

2. 将页面组件每个单独设计,讲解一个。

比如上图一个组件

tpl代码如下:

@import test;  //该模板必须按照这个格式写,否则加载模板将会出问题,这样做是为了告诉框架我加载的是什么组件 @import + 组件名称
<div class="testBg">
    <img class="testImgLogo" src="/UI.js/component/test/img/logo.png"/>
    <div class="testItem">首页</div>
    <div class="testItem">社会招聘</div>
    <div class="testItem">校园招聘</div>
    <div class="testItem">个人中心</div>
    <div class="testItem testUserOption">欢迎来到阿里巴巴集团招聘! <a>登录</a> | <a>注册</a></div>
</div>

css样式设计,建议使用组件名称做前缀,不建议设置全局通用标签样式,防止和其他冲突。

.testBg{
    background-color: #FB9A00;
    text-align: center;
    color: white;
    height: 33px;
}
.testImgLogo{
    margin-top: 4px;
    margin-right: 20px;
}
.testItem{
    display: inline-block;
    height: 100%;
    line-height: 33px;
    position: relative;
    top: -10px;
    margin: 0 15px;
}
.testUserOption{
    font-size: 14px;
    margin-left: 180px;
}

js脚本,如果你在配置文件中注入了接口,在data中可以data.接口名称,拿到接口的地址

use(function(data,that){
    var tempObj ={
        //reader为一些初始化需要的操作,有时候会有注册事件等,或者一些预操作,加载完毕,会首先跑这个方法,这是一个入口
        reader:function(){
            that = this;
            that.firm.testLoad();
        },
        //注入所有的选择器,方便选择器变化,直接修改该对象中的选择器,而不需要全局去更改
        selector:{
            testBtn:"#testBtn",  //按钮
        },
        //注入page中所有的事件,统一管理,建议命名规范:事件_命名,例 click_login
        registerEle:{
        },
        //注入所有ajax请求,页面所有请求,将在这里统一管理,建议命名规范:ajax_命名,例 ajax_login
        ajaxRequest:{
        },
        //处理所有回调函数,针对一个请求,处理一个回调
        callback:{
        },
        //临时缓存存放区域,仅针对本页面,如果跨页面请存放cookie或者localstorage等
        //主要解决有时候会使用页面控件display来缓存当前页面的一些数据
        temp:{
        },
        /*
         * 业务使用区域,针对每个特别的业务去串上面所有的一个个原子
         *   因为上面所有的方法,只是做一件事,这边可以根据业务进行串服务,很简单的
         * */
        firm:{
            testLoad:function(){
                console.log("获取接口的值:"+data.interface.interface1)
            }
        }
    };
    ui.component.reader(tempObj);
});

 

3. 注入接口到配置文件中,代码如下

//注入组件
    component:{
        //组件名:组件模板+组件样式+组件脚本+接口注入+组件是否装载   ===>  该处可优化针对本地项目和分布式开发进行优化
        "test":["component/test/tpl/test.tpl","component/test/css/test.css","component/test/js/test.js",["interface1","interface2"],true],
        "test1":["component/test1/tpl/test1.tpl","component/test1/css/test1.css","component/test1/js/test1.js",["interface5","interface2"],true],
        "test2":["component/test2/tpl/test2.tpl","component/test2/css/test2.css","component/test2/js/test2.js",["interface1","interface2"],true],
        "test3":["component/test3/tpl/test3.tpl","component/test3/css/test3.css","component/test3/js/test3.js",["interface1","interface2"],true],
        "test4":["component/test4/tpl/test4.tpl","component/test4/css/test4.css","component/test4/js/test4.js",["interface1","interface2"],true],
        "test5":["component/test5/tpl/test5.tpl","component/test5/css/test5.css","component/test5/js/test5.js",["interface1","interface2"],true]
    },

 

4. 配置容器和组件的映射关系,让什么容器去加载哪个组件

layout1:{
            con1:"test",
            con2:"test1",
            con3:"test2",
            con4:"test3",
            con5:"test4",
            con6:"test5",
        },

 

5. 将配置文件ui.config和ui.js加载到页面中

<script src="rely/UI.js" type="text/javascript"></script>
<script src="rely/UI.config.js"></script>

 

6. 启动和执行该文件,下面为测试结果。

 

7. 比如我需要维护test3组件了,修改组件的配置文件为false,卸载组件。

"test3":["component/test3/tpl/test3.tpl","component/test3/css/test3.css","component/test3/js/test3.js",["interface1","interface2"],false],

页面结果如下

所有针对test3组件的配置都没加载

 

8. 我在配置容器组件映射关系配置了一个没注入的组件,页面其他组件仍旧正常工作,该组件不会加载,但是会有提示

layout1:{
            con1:"test",
            con2:"test1",
            con3:"test2",
            con4:"TTTTT",  //更换一个未知组件
            con5:"test4",
            con6:"test5",
        },

结果如下,其他组件正常加载,该组件则不加载:

控制台如下:

      以下就不介绍了,还有很多彩蛋,等待大家去发现,历时半年,第一版本已经出来,我知道很粗糙很粗糙,所以暂时框架的API也暂时不急着写,有兴趣的可以自己下载下来共同研究,有很多问题,很多方面的考虑肯定不是很全面,希望有兴趣的可以跟我一起开发这个分布式+容器化+组件化+配置化的框架的开发,为前端,为我们的梦想,加上一对翅膀。其实说实在话和窝心的话,我真的希望有志同道合的朋友一起开发,一起讨论,这半年,180多天的每个夜里,都是一个人在苦苦思考,不停的用纸和笔去推敲,也推翻了一个又一个细节,重塑了很多方案,灵魂的孤独,孤独的王者,心累。我很渴望战友!战友!!战友!!!

      下面是我的github地址,源代码和demo都在里面,运行index.html就好了,1.0版本肯定粗糙,如果你有什么改进方案,有什么新奇的思想,有什么问题,可以邮件发给我,github上提问题,博客联系我,我会记录所有的问题,进行分析和推敲的,也欢迎大家跟我一起开发这个框架,谢谢。点颗星,我将继续战斗下去!!!

 

github地址:https://github.com/GerryIsWarrior/UI.js

邮箱:gerry.zhong@outlook.com

 

我愿用我力所能及的力量,改变全世界!!!

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
使用IntelliJ IDEA开发SpringMVC网站(二)框架配置
原文:使用IntelliJ IDEA开发SpringMVC网站(二)框架配置 摘要 讲解如何配置SpringMVC框架xml,以及如何在Tomcat中运行 目录[-] 文章已针对IDEA 15做了一定的更新,部分更新较为重要,请重新阅读文章并下载最新源码。
989 0
本地使用SVN编辑发布远程SAE工程
一、通过SVN本地修改远程仓库代码1、复制SVN仓库地址 2、在本地PC上开一个空目录,右键找到SVN Checkout 在SVN地址栏输入仓库地址 3、输入SAE的安全邮箱和安全密码下载远程仓库工程到本地。
809 0
使用Portainer或者UI for Docker可视化管理你的树莓派容器
本文讲的是使用Portainer或者UI for Docker可视化管理你的树莓派容器【编者的话】Stefan Scherer是Docker海盗船长,微软MVP。专注于Docker技术在ARM体系中应用。
4148 0
阿里云新品发布会第36期 丨 移动开发平台mPaaS重磅发布
点击订阅新品发布会! 新产品、新版本、新技术、新功能、价格调整,评论在下方,下期更新!关注更多新品发布会! 新品发布会动态 移动开发平台mPaaS重磅发布 快速构建一款 App 并不难,但如何从容应对复杂机型及系统版本,构建快速迭代的端上架构,实现动态更新、稳定流畅的应用移动开发者的核心课题。
3060 0
duilib让不同的容器使用不同的滚动条样式
装载请说明原出处,谢谢~~:http://blog.csdn.net/zhuhongshu/article/details/42240569         以前在给一个容器设置横纵向的滚动条时,一直是通过设置xml的Default标签来完成的,但是这样做有个很大的缺点就是,这个窗体里面的所有滚动条的样式只有一种。
1383 0
+关注
仲强
一个不撞南墙不回头的傻孩子
59
文章
0
问答
文章排行榜
最热
最新
相关电子书
更多
《2021云上架构与运维峰会演讲合集》
立即下载
《零基础CSS入门教程》
立即下载
《零基础HTML入门教程》
立即下载