CRUD生成器DBuilder设计与实现
源码位于github:https://github.com/lvyahui8/dbuilder.git 。文中图片如果太小看不清楚,请右键点击“在新标签页中打开”即可看到原图
有兴趣还可以加QQ群交流:146103720 DBuilder交流群
第一章 引言
1.1 研究背景及意义
计算机软件技术发展至今,数据库已成为最广泛使用的存储格式化数据的媒介,数据库程序开发技术也日趋完善,大型的ORM框架使得数据库程序开发变得简单,并已成为操作关系型数据库的主流方式。数据库程序主要代码为CRUD(create, retrieve, update, delete)代码,随着ORM框架功能的完善,编写CRUD代码也衍生其固定的流程,对不同数据库表进行操作的CRUD代码也存在高度可重用性。当前编写重复性的CRUD代码成为开发人员的常态,不仅严重降低其积极性,而且损失其开发效率,所以迫切需要一种能够快速生成CRUD代码的产品,以期减少这方面的工作,提高开发效率。
1.2 研究现状
目前国外已经诞生一些解决上述需求的、具有很高可用性的CRUD生成器产品:CrudKit,CRUD-Admin-Generator,Dadabik,GroceryCrud,SximoBuilder。这些产品各有其特点,但也有一共同点:都是基于PHP进行开发(这在一定程度上决定于PHP语法的灵活性及其解析性)。SximoBuilder是其中的典型代表,尽管SximoBuilder设计独特、可用性高、流行度高,但也存在如下不足之处:
- 不支持自定义表单控件;
- 不支持多数据库;
- 验证规则不完善,不支持异步验证;
- 代码冗余度极大。
然而对于当今日益复杂的web程序,上述几点是开发过程必须考虑的问题,因此,开发一款既具有SximoBuilder现有功能、又完善其不足之处的CRUD生成器产品,势在必行。
1.3 研究内容
基于国内外CRUD生成器研究现状,笔者开发一款名为DBuilder(D为DataAdministrator的简写)的CRUD 生成器。
DBuilder借鉴SximoBuilder的模块为代码单元、由模板生成代码的思想,但拥有与SximoBuilder完全不同的代码生成器逻辑。它在实现SximoBuilder核心的代码生成、通用CRUD两种功能的基础上,通过重写代码逻辑完善其不足之处:代码冗余度大、缺少前端验证。
第二章 DBuilder系统分析
DBuilder面向的主要用户人群为web后台管理员以及开发人员,因此其系统分析过程,将更多的站在web后台管理员及开发人员的角度考虑问题。
2.1 需求分析
项目需要实现如下几点核心功能。
1) 数据源管理
用户可以在界面为项目配置多个数据源。配置的数据源信息包括数据库类型、地址、数据库名、端口、用户名、密码等信息。支持对数据源的增删改查,保证对数据源的增删改查不轻易造成系统问题。
2) 代码生成
此功能是DBuilder的核心要实现的功能,用户在选择数据源和数据表之后,能够对数据库表的字段做简单配置,配置包括Form表单配置、List(Table)列表配置、关系配置、全局属性配置。配置完成后DBuilder要能生成对数据库表的CRUD的MVC代码,即需要实现CRUD可用功能,但不用编写代码。
3) 数据库表CRUD
生成的代码必须支持数据表记录的新建、删除、更新、查询操作。
4) 菜单管理
DBuilder要能将生成的代码跟一个菜单项绑定,让用户点击菜单项之后,就可以使用DBuilder生成的CRUD功能。此菜单必须包括后台菜单,前台菜单不是必须的。
5) 用户管理
用户要实现多种角色。必须能够以邮箱为用户唯一标识,并作为登录参数。未来还要实现支持QQ、微信、新浪微博基于OAuth2.0的互联登录。
6) 权限管理
DBuilder要能实现不同用户角色对不同CRUD代码的执行、访问权限做到三维的可配置。譬如,现有一个文章管理的CRUD功能模块,用户角色分为系统管理员(SuperAdmin),管理员(Admin),访客(Guest),那么DBuilder要能实现如下的三维权限配置,且将之作为所有Module的默认权限。
表2-1 Module权限配置表
用户组与权限 |
查看 |
编辑 |
删除 |
导出 |
SuperAdmin |
√ |
√ |
√ |
√ |
Admin |
√ |
√ |
√ |
|
Guest |
√ |
7) 站点参数配置
DBuilder作为一个网站的web后台程序,对站点的全局参数配置也是必须的,这些参数包括网站名字、关键词、联系地址、友情链接等等。
8) 操作日志
DBuilder要记录用户的操作信息,包括访问的页面、执行的CRUD类型、时间等等信息。日志的记录形式支持数据库和文件两种方式。
9) 多语言支持
DBuilder要支持多国语言的切换。至少应该支持中文和英语两种语言,且以中文为默认。
10) 多数据库类型支持
DBuilder要支持多种类型数据库,暂时主要支持关系型数据库,包括mysql,MS SqlServer,oracle,PostGreSQL等等。
2.2 数据原型分析
按照需求提取可得实体有:用户、用户组、数据源、代码模块、菜单,关系有:权限、日志。实体与关系的含义如下:
- 用户:表示使用DBuilder的用户;
- 用户组:表示用户的类型分组,用户类型应该至少包括访客、管理员、超级管理员三种;
- 数据源:表示DBuilder包含的数据库配置,一个数据源的配置包含连接一个数据库所需的基本参数;
- 代码模块:表示DBuilder生成的代码模块,描述了代码文件和配置;
- 菜单:表示DBuilder的左侧菜单项;
- 权限:表示用户组对每个代码模块的各种操作权限;
- 日志:表示用户对每个代码模块的CRUD访问日志。
实体与关系的ER图如下:
图2-1 ER图
2.3 原则性要求
DBuilder应该要做成一套高性能、高可用的CRUD生成器,为此DBuilder设计中应该符合下面几项原则:
- DBuilder要精确到每个数据库字段可配置;
- 应具备一个WEB后台应用的雏形,使用户可在此基础上快速建立完整的WEB后台应用;
- DBuilder要尽可能减少SQL操作,必要时可借助缓存、异步等技术,减少请求的处理逻辑,提高页面效率,减少用户等待时间;
- DBuilder要有美观、简洁、直观的用户界面;
- DBuilder要留有大量的扩展接口,能够让用户通过二次开发快速实现较为复杂的功能。
第三章 DBuilder系统设计
3.1 系统架构
DBuilder有下面2个核心的构件Core CRUD 模块和GModule,GModule对Core CRUD 模块有继承依赖的关系,GModule由MVC Code和CRUD Config组成;Core CRUD模块是手工编写的代码,而GModule是DBuilder生成的代码;Core CRUD 模块实现CRUD操作,GModule实现扩展功能。下图表示了这两个构件的组成和关系
图3-1概念与构件
下面对图中设计的概念、构件、模块关系以及Build与CRUD流程做详细阐述。
3.1.1 Core CRUD 模块
Core CRUD 模块实现核心CRUD操作,一切对GModule MVC中Controller的CRUD请求,最终转交至Core CRUD 模块进行处理。Core CRUD 模块会开放一些预处理和后处理接口交由GModule实现,这些接口会在Model,Controller,View上都有体现。
Core CRUD 模块主要包括如下文件
- app/controllers/admin/AdminController.php
- app/models/BaseModel.php
- app/config/crud/admin.php
- app/views/admin/core/list.blade.php
- app/views/admin/core/form.blade.php
Core CRUD 模块读取GModule Configuration实现真正的CRUD操作。
3.1.2 GModule
GModule(Generated Module)不但实现了Core CRUD Module接口(MVC代码),而且具有自己配置文件(CRUD Configuration)。每一GModule表示以一张数据库表为主表,具备CRUD功能的代码文件合集(包括对应的MVC + Configuration代码)。譬如,DBuilder生成的一个GModule, 主表为core数据源user表,名字为User,那么User GModule应包含下面代码文件:
- controllers/UserController.php
- models/User.php
- views/user/_list.blade.php
- views/user/_form.blade.php
- views/user/view.blade.php
- config/crud/user.php
代码文件命名取决于GModule的名字,故为保证生成的代码文件不冲突,取GModule的名字(GModule Key,GModule Name)作为GModule的唯一标识。每一个GModule的信息都被保存在数据库中。一次新建 GModule操作将会新建上述所有代码文件,更新相关文件,并插入一条GModule记录到数据库。一次更新 GModule操作将只会更新Configuration文件。
GModule 由MVC代码和CRUD Configuration代码组成,下面分别进行阐述:
- MVC代码:用来实现扩展接口。CRUD请求应最先路由到GModule MVC的中的Controller(控制器)。并且GModule MVC 应与Core CRUD Module的MVC代码有继承关系。
- CRUD Configuration代码:实现对GModule主表增删改查参数的配置。该文件放置在app/config/crud/目录下,以php array的格式定义。它包含对所有字段的表单,列表,视图,关系等参数的配置,以及全局的参数配置。
GModule并不表示具体某一个模块,而是代指一类模块,这种模块可以由DBuilder生成,或者由开发人员手工建立。它主要用来实现Core CRUD Module的接口,主要包括下述几部分
1) Controller接口
假设GModule模块的 Controller为A,Core CRUD Module 的Controller为B,则A应继承自B。CRUD请求会先路由到A,而实际的处理者是B。A会实现B开放的下列接口。
- beforeListExcuteQuery(&querier):该接口在List查询器执行查询之前调用,传递的参数为查询器引用。用来在查询之前,绑定特殊的查询参数。
- beforeList(&data):该接口在List查询器执行之后,渲染List视图之前调用。传递的参数为视图参数引用,其中包括查询出的model集合。用来对查询的model 集合做后处理,或者对list视图绑定一些Module专有的参数。
- beforeEditExcuteQuery(&querier):该接口在Edit请求中Model查询器执行查询之前调用,传递的是查询器引用。用来绑定查询model需要的特殊参数。
- beforeEdit(&data):该接口在Edit中Model查询器执行之后,渲染视图之前调用,传递的是视图参数引用,其中包括查询器查询出的model。用来做渲染前的预处理。
- afterSave(&model):该接口在Edit中,保存编辑的之后调用,传递的是保存在数据库中,最新的数据库记录持久化的model。用来对model做一些复杂的后级联处理。
- beforeView(data): 该接口在View请求中,View 查询器查询之后调用,传递的是视图参数的引用。用来对视图显示做预处理。
2) Model 接口
GModule MVC代码中的Model也继承自BaseModel,实现 BaseModel类开放的一些接口可以完成扩展。
formatXXXAttribute():该接口用来格式化某个字段。本产品基于Laravel,其已经具备类似的接口,就是getXXXXAttribute()。但这样的接口的优先级比字段优先级高,这在特殊的情况下为开发带来了不便,所以再设计一个类似的接口,该接口的优先级低于字段本身。
3) View 接口
视图的扩展接口与前两者不同,主要体现在子视图与视图块上,也就是在Core CURD模块的视图基础上,扩展视图组件。默认Core CRUD MVC视图生成的是一个表格或者一个表单,占满页面。而View接口将提供在该表格上下左右扩展页面组件的能力。
4) Configuration
每一个GModule对应一个Configuration文件,其中包含GModule对主表各个字段的配置参数,以及布局参数。
3.1.3 模块关系
CRUD请求路由到GModule的Controller,GModule代码实现Core CRUD MVC开放的接口,而由Core CRUD Module去真正实现对数据库的CRUD操作。每一个GModule的信息应该被记录在数据库表中,以便给GModule关联菜单,控制权限,记录操作日志等等。一些主要模块之间的关系如下图所示。
图3-2模块关系
从图2-2中可以看到,由GModule管理模块根据用户配置来生成一个GModule A,当用户的CRUD请求到达GModule A时,GModule 会讲请求转交Core CRUD进行处理,Core CRUD 模块再以SQL对数据库进行CRUD操作。
3.1.4 Build 与 CRUD流程
DBuilder项目的方案,将真正的CRUD操作交给了Core CRUD Module去执行,CRUD参数由GET或者POST请求参数与GModule Configuration构成,而GModule的MVC代码只是去实现Core CRUD MVC开放的一些预处理或者后处理接口。
图2-3是DBuilder最核心的流程图,包含Module的生成和处理CRUD请求的过程,图2-4是SximoBuilder 中Module的生成和处理CRUD请求的流程图。
图3-3 DBuilder 代码生成和处理CRUD的流程
图3-4 SximoBuilder 代码生成和处理CRUD的流程
对比两者,可以看到两者的最大区别,是DBuilder复用一份CRUD代码,而不是像Sximo那样为每一个Module生成一套可以当独执行的CRUD代码。这样做的好处是提高了复用性,并通过Module CRUD MVC实现预处理/后处理接口达到扩展性的目的。
3.2 Core数据源
Core数据源是DBuilder的默认数据源,其类型为mysql,数据库名为dbuilder,本节按照《数据原型分析》一节进行详细的数据库设计。为提高程序性能,数据源信息保存在代码文件app/config/datasource.php中,文件内容如下:
<?php return array ( 'core' => array ( 'driver' => 'mysql', 'host' => 'localhost', 'database' => 'dbuilder', 'username' => 'root', 'password' => 'root', 'charset' => 'utf8', 'collation' => 'utf8_unicode_ci', 'prefix' => '', 'edit' => false, 'port' => 3306, ), // more data source ); |
其中Core数据源有下述数据表:
1) d_menu 表:表示后台左侧树形菜单,每一个可点击跳转的菜单项必须与一个Module进行关联。
表3-1 web后台左侧菜单表
field |
type |
default |
info |
id |
int |
auto_increment |
PRI |
module_id |
int |
||
module_name |
varchar(12) |
||
parent_id |
null |
父菜单项 |
|
title |
varchar(12) |
module_title |
显示名称 |
_order |
int |
0 |
排序字段 |
2) d_module 表:记录了module信息,每一条d_module表的记录代表了DBuilder生成的一个Module。
表3-2 module信息描述
field |
type |
default |
info |
id |
int |
auto_increment |
PRI |
name |
varchar(32) |
UIN |
|
title |
varchar(32) |
module标题 |
|
note |
varchar(32) |
module 说明 |
|
db_source |
varchar(16) |
core |
数据源名称 |
db_table |
varchar(16) |
module主表 |
|
db_table_key |
varchar(16) |
主表PRI |
3) d_user 表:保存着使用后台程序的用户。
表3-3 web后台用户
field |
type |
default |
info |
id |
int |
auto_increment |
PRI |
username |
varhcar(64) |
用户名 |
|
|
varchar(64) |
邮箱 |
|
password |
varchar(64) |
HASH |
|
salt |
varchar(64) |
盐 |
|
last_login |
timestamp |
最后登录时间 |
|
remember_token |
记住密码口令 |
||
group_id |
int |
组ID |
4) d_group表:表示对后台用户的分组信息。
表3-4 用户分组表
field |
type |
default |
info |
id |
int |
auto_increment |
PRI |
name |
varhcar(64) |
组名 |
|
note |
varchar(128) |
组说明 |
|
level |
int |
组级别 |
5) d_group_access表:记录了每个GModule、不同后台用户组与各种操作权限的三维权限信息。
表3-5 用户组对Module权限表
field |
type |
default |
info |
id |
int |
auto_increment |
PRI |
group_id |
int |
组id |
|
module_id |
int |
Module模块ID |
|
edit |
int |
1 |
可编辑 |
view |
int |
1 |
可查看 |
delete |
int |
1 |
可删除 |
export |
int |
1 |
可导出 |
6) d_log表:记录了每个用户的操作日志。
表3-6 用户操作日志
field |
type |
default |
info |
id |
int |
auto_increment |
PRI |
user_id |
int |
用户id |
|
ip_addr |
varchar(15) |
客户端IP |
|
module_id |
int |
访问的moduleid |
|
module_title |
varchar(16) |
||
task |
varchar(16) |
操作 |
|
created_at |
timestamp |
可导出 |
3.3 数据源管理模块
DBuilder需要支持多数据源,多种类型数据库。数据源信息保存在d_database表中。考虑到数据库操作是频繁操作,如果将数据源信息保存在数据库中,则每次数据库操作将多一次数据源查询操作,这样做浪费性能。那么DBuilder不应该把数据源信息保存在数据库中,而应该保存在代码文件中。数据源管理的信息包括数据源名称(数据源的唯一标识,DBuilder默认的数据源名为core)、数据库类型、地址、端口、数据库名、用户名、密码等等信息。因为数据源管理模块并不对表进行增删改查操作,所以数据源管理模块并不是一个GModule模块。该模块的代码完全手工编写。
3.4 GModule 管理模块
DBuilder将以基于名字为“Module”的GModule作为生成GModule的用户接口,该模块称作GModule管理模块,换言之GModule管理模块本身就是一个GModule,该GModule的主表即是core数据源中保存GModule信息的数据库表,改GModule的名字为“Module”。GModule 管理模块包含创建,更新和删除GModule 的所有代码文件以及数据库记录。GModule的新建和删除需要更新全局的GModule路由。
1) GModule 路由
GModule路由定义在一个独立的代码文件中,为一个以GModule名字进行减号分词并全部小写的字符串为键(譬如:GModule名字为OrderItem,则键值为order-item)、以Module中Controller类的类名为值的map字典,GModule路由是全局的。
2) GModule 新建&更新
新建GModule将在数据库中生成一条记录、生成所有的module文件、并更新路由。更新操作只修改配置文件。新建与更新都使用相同的编辑视图,此编辑视图是对GModule Configuration的图形化配置界面。
3) GModule 删除
GModule删除将删除所有的GModule MVC代码,删除GModule Configuration代码,删除数据库表记录,并更新GModule路由。
3.5 Core CRUD 模块
Core CRUD 模块是DBuilder处理CRUD请求的实际处理者,它由下述几部分组成:
1) 参数解析初始化
初始化Model,实例化一个Module的Model对象作为初始化查询器。加载Module Configuration,对未设置的值进行设置默认值,对参数进行汇聚。
2) 表单Form
主要包括新建和更新功能。根据GModule主表主键primaryKey是否设置判断是新建还是更新操作。下图是Form模块的流程
图3-5 Form执行流程
Form 分两部分,第一部分渲染Form页面给用户填写。第二部分为Form保存。
渲染Form页面需要考虑的有Form控件和有外键关系的字段要怎么处理。Form控件需要支持类型包括text、text_date、text_datetime、textarea、select、radio、checkbox、file、hidden、address以及custom,自定义控件应该继承FormControl类,自定义控件的渲染由控件的render方法完成。Form渲染需要判断有关系的字段做辅助加载。比如对post(文章)表进行编辑,post表有一个字段为category_id,表示文章的栏目ID,对应category(栏目)表的id字段。这时需要对category_id使用select,radio,checkbox控件进行加载,方便用户输入。比如使用select控件,那么应该将category.id作为option的value,将category.name作为option中的text。这样做也是为了方便用户输入。此步骤与List中搜索时有共性,因此代码可复用。
Form 保存需要考虑一些自定义控件的保存,自定义控件的数保存由自定义控件类的onSave方法完成。Form 保存还需要考虑关系的保存,默认应该级联更新附属表。Form 表单在用户输入完成点击保存之后,要分下面几步:
- 根据字段配置的验证规则进行验证;
- 应判断Module Configuration 中的relation进行分析,进行必要的级联操作;
- 并要调用自定义控件的onSave方法;
- 最后才应更新或新建主表数据;
- 跳转:更新或新建成功跳转至List,失败跳转至Form。
Form 还需要开放对应的预处理和后处理接口。
3) 列表List(Table)
List是一个分页Table,按照Module Configuration 中的字段配置显示分页数据。支持列表搜索,排序,勾选删除,导出等功能;
- 分页展现数据以InitQuerier模块得到的Model作为查询器,结合分页,查询出基本的数据列表。分页类型为全页刷新类型(非异步分页);
- List搜索:支持在Module Configuration中定义了search不等于false的字段作为搜索条件。搜索关系为逻辑与的关系。并反映在GET参数上。搜索输入控件根据字段的form type来定。在Form 中定义为select,radio,checkbox控件的字段,在List中都将使用select控件作为输入控件;
- List 排序:以在Module Configuration中定义了form.sort 不等于 false的字段作为可排序字段。排序只支持按单一字段排序,降序方式含升序和降序;
- List 多选操作主要支持多选删除,多选复制操作,任何删除操作都需确认;
- List 数据每行记录的支持的操作按Module Configuration中的配置给出,默认支持编辑,删除,查看三项操作;
- List 也要开放预处理/后处理接口给Module CRUD MVC。
4) 查看View
View 暂时以Form为基础,提供预处理后处理接口,但不允许编辑。
第四章 DBuilder系统实现
4.1 目录结构
代码按照前段资源、MVC、Configuration、Library等概念进行了分目录存放。下面表格中给出了主要目录的说明:
表4-2 代码主要目录
目录 |
作用 |
assets |
此目录存放着各种各样的前端资源。包括bootstrap,以及自定义的css和js文件。 |
plugins |
存放特殊前端插件的目录,比如富文本编辑器,视音频插件等等。 |
app/controllers/admin |
存放着MVC中控制器的目录。其中,DBuilder的核心在admin目录下。 |
app/models |
存放着MVC中模型(Model)的目录。用来做数据库查询用。 |
app/views |
存放着MVC中视图的目录。文件名以*.blade.php的格式命名。 |
app/library |
存放PHP辅助类,PHP库的目录。 |
app/config/crud |
存放Module Configuration的目录。 |
4.2 GModule 配置文件
GModule配置文件定义了GModule的参数,该文件保存在app/config/crud/下,是以GModule Name进行蛇形分词得到的字符串命名的php文件(譬如:一GModule的名字为OrderItem,则GModule配置文件为order_item.php)。配置参数以数组格式返回。
考虑到PHP数组在表格中呈现的美观性,对参数以配置中的Key=>Value形式,以点分形式Key.Value表示。
表4-3 root配置
Configuration Key |
类型 |
默认值 |
含义 |
fields |
array |
array() |
字段列表 |
fields.field_name |
array |
array() |
对field_name字段的配置 |
fields.field_name.label |
string |
UP(field_name) |
显示在列表表格的表头的内容,和form控件旁边的内容 |
fields.field_name.form |
array |
array() |
field_name字段的表单配置,具体参考 fields.field_name.form配置 |
fields.field_name.list |
array |
array() |
field_name 字段的列表配置,具体参考 fields.field_name.list配置 |
fields.field_name. relation |
array |
array() |
field_name 字段的关系 |
表4-3中每个字段的表单配置说明如下表所示:
表4-4 fields.field_name.form配置
Configuration Key |
值类型 |
默认 |
含义 |
type |
string |
text |
Form控件类型 |
show |
bool |
true |
是否出现在表单 |
hidden |
bool |
false |
是否以隐藏的空间在表单中 |
rule |
string |
required |
验证规则 |
ajax_validate |
bool |
false |
是否异步验证 |
placeholder |
string |
控件中的提示 |
表4-3中每个字段的列表配置说明如下表所示:
表4-5 fields.field_name.list配置
Configuration Key |
值类型 |
默认 |
含义 |
show |
bool |
true |
是否出现在表单 |
sort |
bool |
true |
字段是否可以排序,默认可排序 |
search |
bool,array |
array() |
是否可搜索以及搜索规则 |
string |
控件中的提示 |
表4-3中每个字段的关系配置说明如下表所示:
表4-6 fields.field_name.relation配置
Configuration Key |
值类型 |
默认 |
含义 |
table |
string |
关联表 |
|
foreign_key |
string |
id |
对应关联表里的字段 |
show |
string |
关联表里的一个字段,当需要转义时,将用该字段代替field_nae字段显示 |
|
as |
string |
table_show |
转义查询出的值用哪个字段表示,主要为了防止主表和关联表有重复字段 |
下面是一个名为post的GModule的Configuration文件
<?php return array (
'data_source' => 'core',
'table' => 'post',
'fields' =>
array (
'id' =>
array (
'label' => 'ID',
'form' =>
array (
'show' => true,
'hidden' => true,
'type' => 'text',
'rule' => 'required',
'ajax_validate' => false,
'placeholder' => '',
),
'list' =>
array (
'show' => true,
'sort' => true,
'search' => '=',
'lookup' => false,
),
'relation' =>
array (
'type' => '',
'table' => '',
'foreign_key' => '',
'show' => '',
'as' => '',
),
),
'title' =>
array (
'label' => '标题',
'form' =>
array (
'show' => true,
'hidden' => false,
'type' => 'text',
'rule' => 'required',
'ajax_validate' => false,
'placeholder' => '',
),
'list' =>
array (
'show' => true,
'sort' => true,
'search' => '=',
'lookup' => false,
),
'relation' =>
array (
'type' => '',
'table' => '',
'foreign_key' => '',
'show' => '',
'as' => '',
),
),
'short' =>
array (
'label' => '摘要',
'form' =>
array (
'show' => true,
'hidden' => false,
'type' => 'textarea',
'rule' => 'required',
'ajax_validate' => false,
'placeholder' => '',
),
'list' =>
array (
'show' => true,
'sort' => true,
'search' => '=',
'lookup' => false,
),
'relation' =>
array (
'type' => '',
'table' => 'category',
'foreign_key' => 'id',
'show' => 'id',
'as' => '',
),
),
'content' =>
array (
'label' => '正文',
'form' =>
array (
'show' => true,
'hidden' => false,
'type' => 'wysiwyg',
'rule' => 'required',
'ajax_validate' => false,
'placeholder' => '',
),
'list' =>
array (
'show' => false,
'sort' => true,
'search' => '=',
'lookup' => false,
),
'relation' =>
array (
'type' => '',
'table' => 'category',
'foreign_key' => 'id',
'show' => 'id',
'as' => '',
),
),
'view_ct' =>
array (
'label' => '查看次数',
'form' =>
array (
'show' => false,
'hidden' => false,
'type' => 'text',
'rule' => 'required',
'ajax_validate' => false,
'placeholder' => '',
),
'list' =>
array (
'show' => true,
'sort' => true,
'search' => '=',
'lookup' => false,
),
'relation' =>
array (
'type' => '',
'table' => '',
'foreign_key' => '',
'show' => '',
'as' => '',
),
),
'created_at' =>
array (
'label' => '创建时间',
'form' =>
array (
'show' => false,
'hidden' => false,
'type' => 'text',
'rule' => 'required',
'ajax_validate' => false,
'placeholder' => '',
),
'list' =>
array (
'show' => false,
'sort' => true,
'search' => '=',
'lookup' => false,
),
'relation' =>
array (
'type' => '',
'table' => '',
'foreign_key' => '',
'show' => '',
'as' => '',
),
),
'updated_at' =>
array (
'label' => '更新时间',
'form' =>
array (
'show' => false,
'hidden' => false,
'type' => 'text',
'rule' => 'required',
'ajax_validate' => false,
'placeholder' => '',
),
'list' =>
array (
'show' => true,
'sort' => true,
'search' => '=',
'lookup' => false,
),
'relation' =>
array (
'type' => '',
'table' => '',
'foreign_key' => '',
'show' => '',
'as' => '',
),
),
'category_id' =>
array (
'label' => '栏目',
'form' =>
array (
'show' => true,
'hidden' => false,
'type' => 'select',
'rule' => 'required',
'ajax_validate' => false,
'placeholder' => '',
),
'list' =>
array (
'show' => true,
'sort' => true,
'search' => '=',
'lookup' => false,
),
'relation' =>
array (
'type' => 'belongsTo',
'table' => 'category',
'foreign_key' => 'id',
'show' => 'title',
'as' => 'category_title',
),
),
),
'list_options' =>
array (
'page' => 10,
'create' => true,
'update' => true,
'delete' => true,
),
'form_options' =>
array (
'layout' =>
array (
'cols' => 12,
'label_cols' => 1,
'input_cols' => 11,
),
),
'relations' =>
array (
'id' =>
array (
'type' => '',
'table' => '',
'foreign_key' => '',
'show' => '',
'as' => '',
),
'title' =>
array (
'type' => '',
'table' => '',
'foreign_key' => '',
'show' => '',
'as' => '',
),
'short' =>
array (
'type' => '',
'table' => '',
'foreign_key' => '',
'show' => '',
'as' => '',
),
'content' =>
array (
'type' => '',
'table' => '',
'foreign_key' => '',
'show' => '',
'as' => '',
),
'view_ct' =>
array (
'type' => '',
'table' => '',
'foreign_key' => '',
'show' => '',
'as' => '',
),
'created_at' =>
array (
'type' => '',
'table' => '',
'foreign_key' => '',
'show' => '',
'as' => '',
),
'updated_at' =>
array (
'type' => '',
'table' => '',
'foreign_key' => '',
'show' => '',
'as' => '',
),
'category_id' =>
array (
'type' => '',
'table' => '',
'foreign_key' => '',
'show' => '',
'as' => '',
),
),
);
GModule Configuration
4.3 数据源管理模块实现
数据源管理模块完成基于网页界面对app/config/datasource.php文件的配置。包含数据源列表页,数据源新建与编辑页。
- 数据源列表页:对应请求路径为admin/data-source/list,此页面读取DataSource文件渲染List(table)页面,密码不显示。
- 数据源编辑页面:对应请求路径为 admin/data-source/edit,GET请求读取DataSource问价内容渲染表单,POST请求将数据源参数输出值DataSource文件。此页面提供测试连接功能,实现方法为基于表单的数据源参数连接数据库,执行一条SQL语句,连接是否成功取决于SQL是否执行成功。
实现数据源管理的核心控制器代码放在DataSourceController.php文件中。
<?php
/**
* Created by PhpStorm.
* User: lvyahui
* Date: 2016/5/12
* Time: 15:35
*/ namespace admin; use BaseModel;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Redirect;
use SiteHelpers;
use Illuminate\Support\Facades\Response;
use Illuminate\Support\Facades\Input; use PDOException;
use PDO;
class DataSourceController extends AdminController
{
/**
* 呈现数据源列表
*/
public function getList()
{
$datasources = SiteHelpers::loadDataSources();
$this->makeView(array(
'datasources' => $datasources,
));
} /**
* 异步加载某数据源的所有数据表
* @return \Illuminate\Http\JsonResponse
*/
public function getTables()
{
$dataSourceName = Input::get("data_source");
$dataSources = SiteHelpers::loadDataSources(); $dataSource = $dataSources[$dataSourceName];
$tables = BaseModel::getTableList($dataSource['database'], $dataSourceName);
return Response::json(array(
'success' => true,
'data' => array(
'tables' => $tables,
'selected' => Input::get('table'),
),
));
} /**
* 呈现数据源编辑或者新建FORM
* @param null $slug
*/
public function getEdit($slug = null)
{
$dataSource = null;
if ($slug) {
// 更新
$dataSource = $slug === 'core' ? Config::get('database.connections.core')
: Config::get('datasource.' . $slug);
$dataSource['name'] = $slug;
} else {
// 新建
$dataSource = array(
'name' => '',
'driver' => 'mysql',
'host' => 'localhost',
'port' => 3306,
'database' => '',
'username' => 'root',
'password' => '',
'charset' => 'utf8',
'collation' => 'utf8_unicode_ci'
);
} $this->makeView(array(
'dataSource' => $dataSource
));
} /**
* 测试数据源连接是否可靠
* @return \Illuminate\Http\JsonResponse
*/
public function postTest(){
$success = true;
try{
$dsn = Input::get('driver').':'.Input::get('host').':'.Input::get('port').';dbname='.Input::get('database');
$dbh = new \PDO($dsn,Input::get('username'),Input::get('password'));
// $connection = new Connection($dbh,Input::get('database'));
// $key = md5(date("Y-m-d H:i:s"));
// DB::addConnection($key,$connection);
// if(!DB::connection($key)->getDatabaseName()){
// $success = false;
// }
$dbh = null;
}catch(PDOException $e){
$success = false;
}
return Response::json(array(
'success' => $success,
));
} /**
* 保存编辑好的数据源信息
* @param null $primaryKeyValue
* @return mixed
*/
public function postEdit($primaryKeyValue = null)
{
$dataSources = SiteHelpers::loadDataSources();
$name = Input::get('name');
$dataSources[$name] = Input::all();
SiteHelpers::saveDataSources($dataSources); return Redirect::action(get_class($this).'@getList');
} public function getTableFields(){
$connection = Input::get('connection');
$table = Input::get('table'); $rawFields = BaseModel::getTableColumns($table,$connection); $fields = array();
$pri = null;
foreach($rawFields as $field){
if($field->Key === 'PRI'){
$pri = $field->Field;
}
$fields [] = $field->Field;
} return Response::json(array(
'success' => true,
'data' => array(
'fields' => $fields,
'pri' => $pri,
),
));
}
}
DataSourceController
4.4 CoreCRUD 模块实现
CoreCRUD模块涉及的代码文件极其作用如下说明。
- app/controllers/admin/AdminController.php:CoreCRUD模块的控制器,是CRUD操作核心的逻辑代码。主要分析请求参数和Module参数,调用Model层,渲染视图层,实现List呈现、List搜索排序、Form呈现、Form保存等功能;
- app/models/BaseModel.php:CoreCRUD模块中的模型,是ModuleCRUD模块中模型的基类。未与表格关联。定义了一些公共的Model默认属性,以及一些静态的数据库操作方法,比如拉取数据库表字段列表;
- app/config/crud/admin.php:CoreCRUD模块中的默认crud参数配置文件,但ModuleCRUD模块中的配置文件未定义某些参数时,将使用admin.php中的默认参数;
- app/views/admin/core/list.blade.php:CoreCRUD模块中的列表视图文件,用来呈现数据列表;
- app/views/admin/core/form.blade.php:CoreCRUD模块中的数据记录编辑视图文件,用来呈现数据编辑的表单。
代码文件如下
<?php namespace admin; use Illuminate\Support\Facades\Request;
use Illuminate\Support\Facades\Response;
use Illuminate\Support\Facades\Redirect;
use Illuminate\Support\Facades\Input;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\View;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\URL;
use SiteHelpers;
use Module; class AdminController extends \BaseController
{
protected $layout = 'layouts.admin.main'; /**
* AdminController constructor.
*/
public function __construct()
{
parent::__construct();
View::share('stdName',$this->getStdName());
View::share('reducName',SiteHelpers::reducCase($this->getStdName()));
View::share('routeParams',$this->getRouteParams());
if($this->model){
$this->assignModel($this->model);
}
View::share('config',$this->savedConfig);
if(!Cache::has('modules')){
Cache::forever('modules',Module::all());
}
} public function getList(){
$models = $this->paginateModels();
$view = $this->getRouteParam('c').'._list';
if(!View::exists($view)){
$view = 'admin.core.list';
}
if(Request::ajax() || Input::has('isAjax')){
return Response::json(array(
'success' => true,
'data' => array(
'models' => $models->toArray()
)
));
}else{
$this->makeView(array(
'models' => $models,
$this->getStdName().'s' => $models,
),$view);
}
} public function getEdit($id = null)
{
if($id){
$this->model = $this->model->find($id);
}
$data = array(
$this->model->getKeyName() => $id,
'model' => $this->model,
$this->modelName=>$this->model,
);
$this->beforeEdit($data); $view = $this->getRouteParam('c').'._form';
if(!View::exists($view)){
$view = 'admin.core.form';
}
$this->makeView($data,$view);
} protected function config(){
$config = 'crud/'.$this->getStdName();
if(!file_exists(app_path('config/').$config.'.php')){
$config = 'crud/admin';
}
return Config::get($config);
} protected function assignModel($model)
{
$this->model = $model; $config = $this->config();
$defaultConfig = Config::get('crud/admin');
$relations = array();
/* 将默认参数传递给module config */
foreach($config['fields'] as $field => &$fieldConfig){
// if(isset($fieldConfig['value'])){
// $this->model->$field = $fieldConfig['value'];
// }
$fieldConfig['form'] = array_merge(
$defaultConfig['fields']['field_name']['form'],
isset($fieldConfig['form']) ? $fieldConfig['form'] : array()
);
$fieldConfig['list'] = array_merge(
$defaultConfig['fields']['field_name']['list'],
isset($fieldConfig['list']) ? $fieldConfig['list'] : array()
);
if(isset($fieldConfig['relation']) &&
isset($fieldConfig['relation']['type']) && $fieldConfig['relation']['type'] !== '' ){
$relations[$field] = $fieldConfig['relation'];
}
} $config['list_options'] = array_merge(
$defaultConfig['list_options'],
isset($config['list_options']) ? $config['list_options'] : array()
); $config['form_options'] = array_merge(
$defaultConfig['form_options'],
isset($config['form_options']) ? $config['form_options'] : array()
); /* 将字段的relation汇聚出来,是为了后面的代码方便,同时减少循环 */
$config['relations'] = $relations; $this->savedConfig = $config;
} protected function paginateModels()
{
$models = array();
if($this->model){
$query = $this->model->newQuery();
$this->handleListQuery($query);
$selects = array($this->model->getTable().'.*'); foreach($this->savedConfig['relations'] as $field=>&$params){
$query->join($params['table'],$params['table'].'.'.$params['foreign_key'],'=',$this->model->getTable().'.'.$field);
if(!isset($params['as'])){
$params['as'] = $params['table'].'_'.$params['show'];
}
$selects[] = $params['table'].'.'.$params['show'] . ' as '.$params['as'];
}
$query->select($selects);
$orderBy = Input::get('list_order_by');
if( $orderBy){
$query->orderBy($this->model->getTable().'.'.$orderBy,Input::get('list_sort_asc') ? 'asc' : 'desc');
}else{
$query->orderBy($this->model->getTable().'.'.$this->model->getKeyName(),'desc');
}
$page = Input::has('_page') ? Input::get('_page') : 10;
$models = $query->paginate($page);
}
return $models;
} public function postEdit($primaryKeyValue = null){ $primaryKeyName = $this->model->getKeyName();
if($primaryKeyValue == null){
$primaryKeyValue = Input::get($primaryKeyName);
} $fields = $this->savedConfig['fields'];
$datas = array();
foreach($fields as $field => $fieldConfig){
if(Input::has($field)){
$datas[$field] = Input::get($field);
}
} if($primaryKeyValue){
$this->model->where($primaryKeyName,$primaryKeyValue)->update($datas);
}else{
$this->model->fill($datas);
$this->model->save();
}
$this->afterSave($this->model);
$resp = Redirect::action(get_class($this).'@getList')->withMessage('save success!'); return $resp;
} public function getIndex()
{
$this->makeView(null,'admin.index');
} public function postDelete(){
$ids = explode(',',Input::get('ids'));
$data = array();
$success = true;
$data['ids'] = $ids;
$ids = array_filter($ids,function($id){
return $id;
});
$this->model->whereIn($this->model->getKeyName(),$ids)->delete();
$data['redirect_url'] = URL::to(action(get_class($this).'@getList'));
return Response::json(array(
'success' => $success,
'data' => $data,
));
} public function getDelete($id){
$this->beforeDelete($id);
$this->model->where($this->model->getKeyName(),$id)->delete();
return Redirect::action(get_class($this).'@getList');
} public function missingMethod($parameters = array())
{
//
$this->makeView(null,'site.404');
} protected function handleListQuery(&$query)
{
$searchFields = array_intersect_key($this->savedConfig['fields'],Input::all());
foreach($searchFields as $field=> $fieldConfig){
if(isset($fieldConfig['list']['search'])){
$value = Input::get($field);
$operator = $fieldConfig['list']['search'];
if($value !== ''){
if($operator){
if($operator === 'like'){
$value = '%'.$value.'%';
}
$query = $query->where($this->model->getTable().'.'.$field,$operator,$value);
}else{
$query = $query->where($this->model->getTable().'.'.$field,$value);
}
}
}
} } protected function beforeDelete($id)
{ } protected function beforeEdit(&$data)
{ } protected function afterSave($model)
{ } public function getHelp(){
$this->makeView(null,'admin.help');
} }
AdminController
<?php /**
* Created by PhpStorm.
* User: Administrator
* Date: 2015/10/10 0010
* Time: 17:58
*/
class BaseModel extends Eloquent
{
protected $table = '';
protected $guarded = array('id');
public $timestamps = false;
public static function getTranslates($translate){
$rows = DB::table($translate['table'])->select(array($translate['foreign_key'],$translate['show']))->get();
return $rows;
} static function getTableList( $db ,$connection = null)
{
$t = array();
$dbname = 'Tables_in_'.$db ;
$tables = $connection ? DB::connection($connection)->select("SHOW TABLES FROM {$db}") : DB::select("SHOW TABLES FROM {$db}");
foreach($tables as $table)
{
$t[$table->$dbname] = $table->$dbname;
}
return $t;
} static function getTableColumns( $table,$connection = false)
{
// $columns = array();
$sql = "SHOW COLUMNS FROM $table";
$rawColumns = $connection ? DB::connection($connection)->select($sql)
: DB::select($sql);
// foreach($rawColumns as $column)
// $columns[$column->Field] = $column->Field;
return $rawColumns;
} function getColoumnInfo( $result )
{
$pdo = DB::getPdo();
$res = $pdo->query($result);
$i =0; $coll=array();
while ($i < $res->columnCount())
{
$info = $res->getColumnMeta($i);
$coll[] = $info;
$i++;
}
return $coll; } function builColumnInfo( $statement )
{
$driver = Config::get('database.default');
$database = Config::get('database.connections');
$db = $database[$driver]['database'];
$dbuser = $database[$driver]['username'];
$dbpass = $database[$driver]['password'];
$dbhost = $database[$driver]['host']; $data = array();
$mysqli = new mysqli($dbhost,$dbuser,$dbpass,$db);
if ($result = $mysqli->query($statement)) { /* Get field information for all columns */
while ($finfo = $result->fetch_field()) {
$data[] = (object) array(
'Field' => $finfo->name,
'Table' => $finfo->table,
'Type' => $finfo->type
);
}
$result->close();
} $mysqli->close();
return $data; } static function findPrimarykey( $table, $db = null)
{
$query = "SHOW KEYS FROM `{$table}` WHERE Key_name = 'PRIMARY'";
$primaryKey = '';
$keys = $db ? DB::connection($db)->select($query) : DB::select($query); foreach($keys as $key)
{
$primaryKey = $key->Column_name;
} return $primaryKey;
}
}
BaseModel
<?php
$formOption = $config['form_options'];
$layout = $formOption['layout'];
$labelCols = $layout['label_cols'];
$inputCols = $layout['input_cols'];
$labelCss = "col-sm-$labelCols";
$inputCss = "col-sm-$inputCols";
// 插件是否加载
$loadUE = false;
$loadSBox = false;
$loadDatePicker = false;
?>
<div class="panel panel-primary">
<div class="panel-heading">
<h3 class="panel-title">@if($model->id) 编辑<code>#{{$model->id}}</code>@else 新建 @endif</h3>
</div>
<div class="panel-body">
<div class="row">
<div class="col-sm-{{$layout['cols']}} col-sm-offset-{{(12-$layout['cols'])/2}}">
<form class="form-horizontal validate" action="{{URL::to('admin/'.$stdName.'/edit')}}" method="post">
<input type="hidden" name="{{$model->getKeyName()}}" value="{{$model->getKey()}}">
<?php foreach($config['fields'] as $field => $settings):?>
<?php
if ($field === $model->getKeyName() || !$settings['form']['show']) continue;
$type = $settings['form']['type'];
$rule = $settings['form']['rule'];
?>
<?php if($settings['form']['type'] === 'hidden'):?>
<input type="hidden" name="{{$field}}" value="{{$model->$field}}">
<?php continue;?>
<?php endif; ?>
<div class="form-group">
<label for="{{$field}}"
class="{{$labelCss}} control-label">{{isset($settings['label']) ? $settings['label'] : strtoupper($field)}}</label>
<div class="{{$inputCss}}">
<?php if($type === 'textarea'):?>
<textarea name="{{$field}}" id="{{$field}}" rows="10"
{{SiteHelpers::inputValidate($rule)}}
class="form-control">{{$model->$field}}</textarea>
<?php elseif($type === 'select'):?>
<?php $loadSBox = true;?>
<select name="{{$field}}" id="{{$field}}" class="selectboxit" {{SiteHelpers::inputValidate($rule)}}>
@if (isset($settings['form']['options']))
@if(is_array($settings['form']['options']))
@foreach($settings['form']['options'] as $value => $text)
<option value="{{$value}}"
@if($value == $model->$field) selected @endif>{{$text}}</option>
@endforeach
@elseif(is_string($settings['form']['options']))
@foreach($$settings['form']['options'] as $value => $text)
<option value="{{$value}}">{{$value}}</option>
@endforeach
@endif
@elseif(isset($settings['relation']['type']) && $settings['relation']['type'] )
<?php
$fieldTranslate = $config['relations'][$field];
$options = BaseModel::getTranslates($fieldTranslate);
foreach($options as $option):
?>
<option value="<?=$option->$fieldTranslate['foreign_key']?>"
@if($option->$fieldTranslate['foreign_key'] == $model->$field) selected @endif
><?=$option->$fieldTranslate['show']?>
</option>
<?php endforeach;?>
@endif
</select>
<?php elseif($type === 'wysiwyg'):?>
<?php
$loadUE = true;
?>
<script type="text/plain" name="{{$field}}" id="wysiwyg-edit"
style="width:100%;height:240px;">{{$model->$field}}</script>
<?php elseif ($type === 'radio' || $type === 'checkbox'): ?> @if(isset($settings['form']['options']))
@foreach($settings['form']['options'] as $option => $text)
<div class="{{$type}} {{$type}}-replace">
<input type="{{$type}}" value="{{$option}}" name="{{$field}}"
id="{{$field}}" @if($model->field === $option) checked @endif>
<label>{{$text}}</label>
</div>
@endforeach
@endif
<?php elseif($type === 'date'):?>
<?php $loadDatePicker = true;?>
<div class="input-group">
<input type="text" name="{{$field}}" id="{{$field}}" class="form-control datepicker" data-format="yyyy-MM-dd" {{SiteHelpers::inputValidate($rule)}}>
<div class="input-group-addon">
<a href="#"><i class="entypo-calendar"></i></a>
</div>
</div>
<?php elseif($type === 'password'):?>
<input type="password" class="form-control" name="{{$field}}" id="{{$field}}"
value="{{$model->$field}}"
{{SiteHelpers::inputValidate($rule)}}
>
<?php elseif($type === 'file'):?>
<input type="file"
class="form-control file2 inline btn btn-primary"
data-label="<i class='glyphicon glyphicon-file'></i> 选择文件" >
<?php else:?>
<input type="text" class="form-control" name="{{$field}}" id="{{$field}}"
{{SiteHelpers::inputMask($rule)}}
{{SiteHelpers::inputValidate($rule)}}
value="{{$model->$field}}">
<?php endif;?>
</div>
</div>
<?php endforeach;?>
<div class="form-group">
<div class="{{$inputCss}} col-sm-offset-{{$labelCols}}">
<button type="submit" class="btn btn-primary">保存</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
@yield('form.bottom','')
@section('styles')
@if($loadSBox)
{{HTML::style('assets/js/selectboxit/jquery.selectBoxIt.css')}}
@endif
@append
@section('scripts')
<?php if($loadUE):?>
{{HTML::script('plugins/ue-utf8-php/ueditor.config.js')}}
{{HTML::script('plugins/ue-utf8-php/ueditor.all.min.js')}}
{{HTML::script('plugins/ue-utf8-php/lang/zh-cn/zh-cn.js')}}
<?php endif;?>
@if($loadSBox)
{{HTML::script('assets/js/selectboxit/jquery.selectBoxIt.min.js')}}
@endif
@if($loadDatePicker)
{{HTML::script('assets/js/bootstrap-datepicker.js')}}
@endif
{{HTML::script('assets/js/jquery.inputmask.bundle.min.js')}}
{{HTML::script('assets/js/jquery.validate.min.js')}}
@append @section('footScript')
<script>
var ue = null,
ueId = 'wysiwyg-edit';
if (document.getElementById(ueId)) {
ue = UE.getEditor(ueId);
}
</script>
@append
app/views/admin/core/form.blade.php
@section('headStyle')
<style>
.sort.sort-active {
color: #000;
font-weight: bold;
}
</style>
@append
<?php
$list_options = $config['list_options'];
$loadSBox = false;
$loadDatePicker = false;
?>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title"><?=isset($navMap[$stdName]['text']) ? $navMap[$stdName]['text'] : strtoupper($stdName)?>
列表</h3>
</div>
<div class="panel-body">
<div class="row">
<div class="col-sm-12">
<div class="btn-group btn-group-sm" role="group">
@if($list_options['create'])
<a href="{{URL::to('admin/'.$stdName.'/edit')}}" class="btn btn-primary">新建</a>
@endif
<a class="btn btn-danger delete-selected">删除</a>
<a class="btn btn-default">导出</a>
</div>
</div>
</div>
<br>
<form class="list-form" action="" method="get">
<input type="hidden" name="list_sort_asc" value="{{Input::get('list_sort_asc') !== null ? Input::get('list_sort_asc') : 1}}">
<input type="hidden" name="list_order_by" value="">
<table class="table table-bordered responsive table-hover table-striped">
<thead>
<tr>
<th>
<div class="checkbox checkbox-replace">
<input type="checkbox" class="item-all">
</div>
</th>
<?php foreach($config['fields'] as $field=>$settings):?>
<?php if($settings['list']['show']):?>
<th @if($settings['list']['sort'])
class="sort @if($field === Input::get('list_order_by')) sort-active @endif"
data-field="{{$field}}" @endif
><?=is_array($settings) && isset($settings['label']) ? $settings['label'] : strtoupper($field)?>
<span class="pull-right">
@if($field === Input::get('list_order_by')) @if(Input::get('list_sort_asc') == 1) <i class="fa fa-sort-asc"></i> @else <i class="fa fa-sort-desc"></i> @endif @endif
</span>
</th>
<?php endif;?>
<?php endforeach;?>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr>
<td></td>
@foreach($config['fields'] as $field=>$fieldConfig)
@if($fieldConfig['list']['show'])
@if(isset($fieldConfig['list']['search']) && $fieldConfig['list']['search'] !== false)
<td>
@if($fieldConfig['form']['type'] == 'select' || ($fieldConfig['form']['type'] === 'radio' || $fieldConfig['form']['type'] == 'checkbox'))
<?php $loadSBox = true;?>
@if(isset($fieldConfig['form']['options']) && $fieldConfig['form']['options'])
<select name="{{$field}}" id="{{$field}}" class="selectboxit">
<option value="" class="default-value">请选择</option>
@foreach($fieldConfig['form']['options'] as $option => $text)
<option value="{{$option}}" @if(Input::get($field) && Input::get($field) === $option) selected @endif>{{$text}}</option>
@endforeach
</select>
@elseif(isset($fieldConfig['relation']['type']) && $fieldConfig['relation']['type'] )
{{View::make('components.relation_select',array(
'fieldConfig'=>$fieldConfig,'field' => $field, ))}}
@endif
@elseif($fieldConfig['form']['type'] === 'date')
<?php $loadDatePicker = true;?>
<input type="text" name="{{$field}}" id="{{$field}}" class="form-control datepicker" data-format="yyyy-MM-dd" value="{{Input::get($field)}}">
@else
<input type="text" name="{{$field}}" id="{{$field}}"
value="{{Input::get($field)}}" class="form-control input-sm">
@endif
</td>
@else
<td></td>
@endif
@endif
@endforeach
<td>
<div class="btn-group btn-group-sm" role="group">
<button type="submit" class="btn btn-primary">搜索</button>
<button type="reset" onclick="resetForm(this)" class="btn btn-warning hidden">重置</button>
</div>
</td>
</tr>
<?php foreach($models as $model):?>
<tr>
<td width="18px">
<div class="checkbox checkbox-replace">
<input type="checkbox" name="d_delete_select" class="item" value="{{$model->id}}">
</div>
</td>
<?php foreach($config['fields'] as $filed=>$settings):?>
<?php if($settings['list']['show']):?>
<?php
$value = $model->$filed;
/* 字段在列表中需要翻译 */
if (array_key_exists($filed, $config['relations'])) {
$value = $model->$config['relations'][$filed]['as'];
}
?>
<td>{{$value}}</td>
<?php endif;?>
<?php endforeach;?>
<td>
<div class="btn-group btn-group-sm" role="group">
@if($list_options['update'])
<a href="{{URL::to('admin/'.$stdName.'/edit/'.$model->id)}}"
class="btn btn-primary">编辑</a>
@endif
@if($list_options['delete'])
<a href="{{URL::to('admin/'.$stdName.'/delete/'.$model->id)}}"
class="btn btn-danger">删除</a>
@endif
@if(View::exists('admin.'.snake_case($stdName).'.list_item_links'))
@include('admin.'.snake_case($stdName).'.list_item_links',array('model'=>$model))
@endif
</div>
</td>
</tr>
<?php endforeach;?>
</tbody>
</table>
</form>
<div class="pull-right">
{{$models->appends(Input::all())->links()}}
</div>
</div>
</div> @section('styles')
{{HTML::style('assets/js/datatables/responsive/css/datatables.responsive.css')}} @if($loadSBox)
{{HTML::style('assets/js/selectboxit/jquery.selectBoxIt.css')}}
@endif
@append @section('scripts')
{{HTML::script('assets/js/jquery.dataTables.min.js')}}
{{HTML::script('assets/js/datatables/jquery.dataTables.columnFilter.js')}}
@if($loadSBox)
{{HTML::script('assets/js/selectboxit/jquery.selectBoxIt.min.js')}}
@endif
@if($loadDatePicker)
{{HTML::script('assets/js/bootstrap-datepicker.js')}}
@endif
@append @section('footScript')
<script>
$(document).ready(function(){
$('th.sort').click(function(){
var $th = $(this);
$('input[name="list_order_by"]').val($th.data('field'));
$('input[name="list_sort_asc"]').val($th.find('i').hasClass('fa-sort-asc') ? 0 : 1);
$('form.list-form').submit();
}); $('input.item-all').change(function(){
var $this = $(this),
$items = $('input.item');
if($this.is(':checked')){
$items.prop('checked','checked');
}else{
$items.removeProp('checked');
}
$items.trigger('change');
}); $('a.delete-selected').click(function(){
var ids = [],
$items = $('input.item:checked');
$items.each(function(i){
ids.push($(this).val());
});
var idsStr = ids.join(',');
confirmModal({
message : '确认删除:'+idsStr,
onOk: function(){
$.post('{{URL::to('admin/'.snake_case($stdName).'/delete')}}',{"ids":idsStr},function(resp){
if(resp.success){
window.location.href = resp.data.redirect_url;
}
},'json');
}
});
return false;
});
});
</script>
@append @section('modals')
<div class="modal fade" id="confirm-modal" data-backdrop="static">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">操作确认</h4>
</div>
<div class="modal-body"> </div>
<div class="modal-footer">
<button type="button" class="btn btn-default cancel" data-dismiss="modal">取消</button>
<button type="button" class="btn btn-info ok">确认</button>
</div>
</div>
</div>
</div>
@stop
app/views/admin/core/list.blade.php
<?php
/**
* 说明:
* 1. 以下配置项,不设置便是默认
* Created by PhpStorm.
* User: lvyahui
* Date: 2016/5/2
* Time: 12:33
*/ return array( /**
* 所有字段配置
*/
'fields' => array(
'field_name' => array(
/* 显示在列表表格的表头的内容,和form控件旁边的内容*/
'label' => '字段中文名',
/* 字段缺省值 */
'value' => false,
/* 针对表单的设置 */
'form' => array(
'show' => true,
'hidden' => false,
/*
* 字段对应表单的控件类型,默认text,
* 还支持常用的控件类型
* textarea
* radio
* checkbox
* number
* ipaddr
* wyswyg
* select
* date
* file
* 以及自定义类型
* */
'type' => 'text',
/*
'type' => array(
'select' => array(
'options' => function(){
return array();
}
),
),
'type' => array(
'radio' => array(),
),
*/
/* 提交表单后的验证规则 */
'rule' => 'required',
'ajax_validate' => false,
'placeholder' => 'xx', ),
// 针对列表的设置
'list' => array(
/* 字段在列表是否显示,默认为显示 */
'show' => true,
/* 字段是否可以排序,默认不能排序 */
'sort' => true,
/* 是否能够按这个字段搜索 */
'search' => true,
/* 字段进行翻译,比如栏目Id字段,一般要转成栏目名称显示 */
'lookup' => false,
), ),
// more fields
), /**
* 全局form配置,优先级小于字段配置
*/
'form_options' => array(
'layout' => array(
'cols' => 12,
'label_cols' => 1,
'input_cols' => 11,
),
), /**
* 全局list配置,优先级小于字段配置
*/
'list_options' => array(
'page' => 10,
'create' => true,
'update' => true,
'delete' => true,
), );
app/config/crud/admin.php
4.5 GModule 管理模块实现
GModule是一类由DBuilder生成的模块,它有一组模板定义在app/template目录下:
- app/template/_form.tpl
- app/template/_list.tpl
- app/template/controller.tpl
- app/template/model.tpl
前面设计中指出,GModule管理模块本身是一个名为“Module”,主表为d_module,且手工建立的GModule,故其代码组成也是符合GModule规范的,笔者编写的代码主要为扩展代码。GModule管理模块对应了下述代码文件:
- app/controllers/admin/ModuleController.php:控制器(Controller)代码,其实现CoreCRUD模块的接口,以及扩展的url接口;
- app/models/Module.php:GModule管理模块的模型;
- app/views/admin/module/_form.blade.php: FORM视图代码,其在原有的CoreCRUD 模块的FORM表单下部,扩展了一组Tab,其中第一个Tab中显示了所有字段的详细配置,通过以上扩展就能实现在CoreCRUD生成的Form表单页面中对GModule进行配置;
- app/views/admin/module/_list.blade.php: LIST视图代码;
- app/views/admin/module/fields_config.blade.php:字段配置表格视图代码;
- app/views/admin/module/list_item_links.blade.php:扩展链接视图代码;
- app/config/crud/module.php:GModule Configuration文件。
下面贴上主要的代码文件ModuleController.php
<?php
/**
* Created by PhpStorm.
* User: lvyahui
* Date: 2016/5/12
* Time: 15:28
*/ namespace admin; define('MODULE_ROUTES', json_encode(include(app_path() . '/module_routes.php'))); use Illuminate\Support\Facades\Redirect;
use SiteHelpers;
use BaseModel;
use Module;
use ConfigUtils;
use Illuminate\Support\Facades\Input;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Response; class ModuleController extends AdminController
{ protected function beforeEdit(&$data)
{
$data ['dataSources'] = SiteHelpers::loadDataSources();
if ($data['model']->id) {
$data ['moduleConf'] = ConfigUtils::get($data['model']->name);
}
} protected function afterSave($module)
{
/* 生成代码文件 */
$codes = array(
'moduleName' => $module->name,
'moduleTitle' => $module->title,
'tablePrimaryKey' => BaseModel::findPrimarykey($module->db_table, $module->db_source),
'moduleNote' => $module->note,
'date' => date('Y-m-d'),
'dbSource' => $module->db_source,
'dbTable' => $module->db_table,
);
$this->removeFiles($codes['moduleName']);
/* 生成默认module Configuration*/
$moduleConfs = $this->buildConfiguration($module->db_table, $module->db_source);
SiteHelpers::saveArrayToFile(app_path('config/crud/') . snake_case($codes['moduleName']) . '.php', $moduleConfs); $controller = file_get_contents(app_path('template') . '/controller.tpl');
$model = file_get_contents(app_path('template') . '/model.tpl');
$formView = file_get_contents(app_path('template') . '/_form.tpl');
$listView = file_get_contents(app_path('template') . '/_list.tpl');
$codes['timestamps'] = isset($moduleConfs['fields']['created_at']) && isset($moduleConfs['fields']['updated_at'])
? 'true' : 'false';
$buildController = SiteHelpers::blend($controller, $codes);
$buildModel = SiteHelpers::blend($model, $codes);
/* 生成 MVC 文件*/
file_put_contents(app_path() . "/controllers/admin/{$codes['moduleName']}Controller.php", $buildController);
file_put_contents(app_path() . "/models/{$codes['moduleName']}.php", $buildModel);
$viewPath = app_path('/views/admin/') . snake_case($codes['moduleName']);
if (!file_exists($viewPath)) mkdir($viewPath);
file_put_contents($viewPath . "/_form.blade.php", $formView);
file_put_contents($viewPath . "/_list.blade.php", $listView); /* 更新路由 */
$moduleRoutes = json_decode(MODULE_ROUTES, true); //require(app_path().'/module_routes.php');
if (is_array($moduleRoutes)) {
$moduleRoutes[SiteHelpers::reducCase($codes['moduleName'])] = 'admin\\' . "{$codes['moduleName']}Controller";
SiteHelpers::saveArrayToFile(app_path() . '/module_routes.php', $moduleRoutes);
}
} protected function beforeDelete($id)
{
$module = Module::find($id);
$moduleName = $module->name;
$this->removeFiles($moduleName);
} /**
* 删除GModule相关文件文件
* @param $moduleName
*/
public function removeFiles($moduleName)
{
$controller = app_path('admin/controllers') . "/{$moduleName}Controller.php";
if (file_exists($controller)) {
unlink($controller);
}
$model = app_path('models') . "/{$moduleName}.php";
if (file_exists($model)) {
unlink($model);
}
$moduleConf = app_path('config/crud/') . snake_case($moduleName) . '.php';
if (file_exists($moduleConf)) {
unlink($moduleConf);
} $viewPath = app_path('/views/admin/') . snake_case($moduleName);
$formFile = $viewPath . '/_form.blade.php';
$listFile = $viewPath . '/_list.blade.php';
if (file_exists($formFile)) unlink($formFile);
if (file_exists($listFile)) unlink($listFile);
} private function buildConfiguration($table, $connection)
{
$rawColumns = BaseModel::getTableColumns($table, $connection);
$fields = ConfigUtils::build($rawColumns);
return array(
'data_source' => $connection,
'table' => $table,
'fields' => $fields,
);
} /**
* 获取字段配置列表
* @return bool
*/
public function getFieldsConfig()
{
$filedsConfig = null;
if (Input::has('module_name')) {
$filedsConfig = ConfigUtils::get(Input::get('module_name'))['fields'];
} else {
$table = Input::get('table');
$connection = Input::get('connection'); $filedsConfig = $this->buildConfiguration($table, $connection)['fields'];
} $resp = $this->makeView(array(
'fieldsConfig' => $filedsConfig,
));
if ($resp) {
return $resp;
}
} /**
* 保存字段列表配置
* @return mixed
*/
public function postSaveFieldsConf()
{
$resp = Redirect::action(get_class($this) . '@getEdit', Input::get('id'));
$postFields = Input::get('fields');
$moduleName = Input::get('module_key');
$confKey = SiteHelpers::reducCase($moduleName);
$savedConfig = ConfigUtils::get($confKey);
foreach ($savedConfig['fields'] as $fieldName => &$savefield) {
$postField = $postFields[$fieldName];
$savefield['label'] = $postField['label'];
$savefield['form']['show'] = isset($postField['form']['show']);
$savefield['list']['show'] = isset($postField['list']['show']);
}
ConfigUtils::saveGModuleConf($confKey, $savedConfig);
return $resp;
} /**
* 呈现某一字段的配置参数FORM
* @return bool
*/
public function getFieldConfig()
{
$moduleKey = Input::get('module_key');
$field = Input::get('field'); $moduleConfig = Config::get('crud/' . snake_case($moduleKey));
$fieldConf = &$moduleConfig['fields'][$field];
$dbSource = SiteHelpers::loadDataSources()[$moduleConfig['data_source']];
$tables = BaseModel::getTableList($dbSource['database'],$moduleConfig['data_source']); $resp = $this->makeView(array(
'field' => $field,
'fieldConfig' => $fieldConf,
'moduleKey' => $moduleKey,
'tables' => $tables,
'connection' => $moduleConfig['data_source'],
)); if ($resp) return $resp;
} /**
* 保存某一字段的配置参数
* @return \Illuminate\Http\JsonResponse
*/
public function postFieldConfig()
{
$data = array(
'success' => true,
);
$moduleKey = Input::get('module_key');
$field = Input::get('field');
$moduleConfig = Config::get('crud/' . snake_case($moduleKey));
$fieldConf = &$moduleConfig['fields'][$field]; $postFormConf = Input::get('form');
$postListConf = Input::get('list');
$postRelationConf = Input::get('relation');
$fieldConf['form']['type'] = $postFormConf['type'];
if((
$postFormConf['type'] === 'select'
|| $postFormConf['type'] === 'radio'
|| $postFormConf['type'] === 'checkbox'
)
&& isset($postFormConf['options'])
&& $postFormConf['options']
){
$rawOptions = explode(',',$postFormConf['options']);
$options = array();
foreach($rawOptions as $option){
$options[$option] = $option;
}
$fieldConf['form']['options'] = $options;
}
$fieldConf['form']['placeholder'] = $postFormConf['placeholder'];
$fieldConf['form']['rule'] = $postFormConf['rule'];
$fieldConf['list']['sort'] = isset($postListConf['sort']);
$fieldConf['list']['search'] = $postListConf['search']; $fieldConf['relation'] = $postRelationConf; SiteHelpers::saveArrayToFile(app_path('config/crud/' . snake_case($moduleKey) . '.php'), $moduleConfig); return Response::json($data);
}
}
ModuleController
4.6 部署
DBuilder部署运行的操作系统可以是Windows或Linux,本文将基于LNMP(Linux+Nginx+MySQL+PHP)环境进行部署,详细部署环境要求:
- PHP Version > 5.4
- MCrypt PHP 必须安装
- OpenSSL 必须安装
- MySQL Version > 5.4
- Nginx、Apache等服务器
首先,需要将DBuilder放置到Nginx的Default Server或者Vhost中,这里以Default Server为例。本文中DBuilder的根目录为
/home/wwwroot/dbuilder/ |
编辑nginx.conf文件,修改server节点:
server { listen 80 default_server; #listen [::]:80 default_server ipv6only=on; #server_name www.lnmp.org; index index.html index.htm index.php; root /home/wwwroot/dbuilder; #error_page 404 /404.html; include enable-php.conf; location / { try_files $uri $uri/ /index.php?$query_string; }
location ~ [^/]\.php(/|$) { # comment try_files $uri =404; to enable pathinfo try_files $uri =404; fastcgi_pass unix:/tmp/php-cgi.sock; #fastcgi_pass 127.0.0.1:9000; fastcgi_index index.php; include fastcgi.conf; #include pathinfo.conf; } location /nginx_status { stub_status on; access_log off; } location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$ { expires 30d; } location ~ .*\.(js|css)?$ { expires 12h; } location ~ /\. { deny all; } access_log /home/wwwlogs/access.log access; } |
修改DBuilder项目文件所属用户,保证nginx http进程对文件有读权限,本文部署环境中,nginx http进程为www用户进程;同时需要给部分DBuilder目录完全的写入权限,执行下列命令:
cd /home/wwwroot chown –R www dbuilder chgrp –R www dbuilder cd dbuilder chmod –R 777 app/storage chmod -R 665 app/controllers/admin app/config/crud app/models/ app/views |
建立数据库,在mysql中创建名为dbuilder的数据库,并source Dbuilder根目录下的dbuilder.sql,具体执行如下命令
# 首先进入msyql mysql –uroot –pyour_root_password # 进入mysql之后 create database dbuilder default char set utf8; use dbuilder; source /home/wwwroot/dbuilder/dbuilder.sql; |
至此DBuilder部署完成,通过浏览器访问http://hostname/admin (hostname为主机域名或ip地址)即可以访问到DBuilder。
4.7 案例
设定:在不编写代码的基础上,以DBuilder生成一个简单可用的博客后台,博客后台有post表和category表,位于core数据源。
CREATE TABLE post ( id INT(11) PRIMARY KEY NOT NULL, category_id INT(11) NOT NULL, title VARCHAR(64) NOT NULL, short VARCHAR(256) NOT NULL, content TEXT NOT NULL, view_ct INT(11) DEFAULT '0' NOT NULL, created_at TIMESTAMP DEFAULT 'CURRENT_TIMESTAMP' NOT NULL, updated_at TIMESTAMP DEFAULT '0000-00-00 00:00:00' NOT NULL ) DEFAULT CHAR SET utf8; CREATE TABLE category ( id INT(11) PRIMARY KEY NOT NULL, title VARCHAR(32) NOT NULL, level INT(11), weight INT(11) DEFAULT '0' NOT NULL COMMENT '排序字段', parent_id INT(11), post_ct INT(11) DEFAULT '0' NOT NULL, ) DEFAULT CHAR SET utf8; |
4.7.1 新建GModule
准备好数据库表即可新建GModule,下面新建名为“Post”的GModule。进入GModule管理->新建界面,按图填写保存。
图4-1 新建GModule页面
编辑新建的Post GModule,可以看到在下部多出一个含有表格的tab。
图4-2 GModule Configuration字段配置页面
现在对于post表的所有字段都是默认配置,分别查看List和Form,可以看到List和Form都能正常读取数据库数据。
图4-3 GMoudle 列表页面
图4-4 GModule表单页面
上面两图呈现的List和Form并不具有可用性,因此需要对字段做配置。
4.7.2 GModule配置
首先修改字段的中文名、是否包含在form、是否包含在List等属性。
图4-5 GModule Configuration字段配置页面
保存之后,再次刷新Post列表和Form。对比图4-3、图4-4发现内容发生了变化
图4-6 GModule列表页面
图4-7 GModule表单页面
下面对每个字段做更详细的配置以得到更符合我们需求的页面,修改控件类型:short(摘要)字段为textarea(多行文本)类型,content(正文)字段为wysiwyg(富文本)类型,category_id字段为select(下拉列表)类型,updated_at(修改时间)为date(日期)类型。修改category_id(栏目外键)的关系为所属关系,并填写如下:
图4-8 GModule 字段详细配置表单
修改short(摘要)字段、title(标题)字段为不可排序与like模糊搜索,修改updated_at搜索方式为“>=”搜索
4.7.3 List&Form效果
刷新Post列表,可看到如下两个控件:date和select控件。
图4-9 GModule 列表搜索日期与下拉列表控件
输入搜索条件为修改日期:2016-03-03、栏目:C++、摘要:收到。结果按阅读次数排序。得到下面的列表结果。
图4-10 GModule 列表搜索与排序
点击其中一条记录进行编辑,测试Form功能。
图4-11 GModule编辑表单
修改之后点击保存也是正常可用的。
整个配置过程,只需几分钟,但却实现了上述较为复杂的功能。而如果换成开发人员手工编写类似功能模块,至少需要两三个小时的时间,相比之下,DBuilder极大的提高了开发效率。
第五章 总结与展望
本文基于WEB技术基本实现了一款可用的CRUD生成器,其内核实现比SximoBuilder更精简,在代码高度复用的前提下,提供更强的扩展性,并支持多数据库、前端验证、自定义表单控件等等。
由于时间原因,DBuilder尚未实现诸如用户管理、权限控制、操作日志记录、站点配置、多语言化等功能。另外,随着技术进步和网络普及,如何做到高并发、高性能以及支持数据集群的web系统是当前web项目开发需要着重考虑的问题。笔者将在DBuilder的后续改进中实现上述功能,并对高并发提供支持。同时,为了更好的推广和发展DBuilder,笔者已将DBuilder开源至Github:https://github.com/lvyahui8/dbuilder.git 。
CRUD生成器DBuilder设计与实现的更多相关文章
- Java设计模式:生成器模式
问题的提出: 有些类很容易创建对象,直接调用其构造方法,例如Student student = new Student("1001","zhang",21); ...
- [设计篇]01.RESTFUL URI 简单入门设计
1. HTTP Methods HTTP 常用方法: GET: 获取某个资源. POST: 创建一个新的资源. PUT: 替换某个已有的资源. PATCH: 修改某个已有的资源.-->这个自己没 ...
- 小白学 Python(21):生成器基础
人生苦短,我选Python 前文传送门 小白学 Python(1):开篇 小白学 Python(2):基础数据类型(上) 小白学 Python(3):基础数据类型(下) 小白学 Python(4):变 ...
- ES6入门十一:Generator生成器、async+await、Promisify
生成器的基本使用 生成器 + Promise async+await Promise化之Promisify工具方法 一.生成器的基本使用 在介绍生成器的使用之前,可以简单理解生成器实质上生成的就是一个 ...
- 利用mybatis-generator自动生成数据持久化的代码
MyBatis生成器简介 MyBatis Generator(MBG)是MyBatis MyBatis 和iBATIS的代码生成器.它将生成所有版本的MyBatis的代码,以及版本2.2.0之后的iB ...
- 谈谈yii2-gii如何自定义模板
作者:白狼 出处:http://www.manks.top/article/yii2_gii_custom_template本文版权归作者,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位 ...
- 移动APP的IM后台架构浅析
IM(InstantMessaging 即时通讯)作为一项基础功能,很多APP都有,比如:手机QQ.微信.易信.钉钉.飞信.旺旺.咚咚.陌陌等.而IM如同我们日常生活中的水和电一样,必不可少,也是很多 ...
- 实现自己的Linq to Sql
之前写过一篇<统一的仓储接口>,为了方便使用不同的仓储.在我们的项目中使用的是EF4.0,但是这个版本的EF有一些性能问题没有解决,又不想升级到EF6,具体EF6有没有解决暂时不清楚.我们 ...
- .net EntityFramework用法探索系列 1
EntityFramework用法探索系列 (一)DatabaseFirst (二)CodeFirst (三)CodeFirst流畅API (四)Repository和UnitOfWork (五)引入 ...
随机推荐
- 使用air进行移动app开发常见功能和问题(二)
1. Air如何判断android.ios 平台网络连接状态? Android,使用as3原生api: if(NetworkInfo.isSupported)//只有android支持 Networ ...
- mysql performance schema的即时诊断工具-邱伟胜
https://github.com/noodba http://www.noodba.com
- OpenCMS integration with Spring MVC--reference
ref from:http://blogs.indrajitpingale.com/?p=8 http://blog.shinetech.com/2013/04/09/integrating-spri ...
- Java基础知识强化之网络编程笔记19:Android网络通信之 HttpClient和传统Post、Get方式的区别
1. HttpClient是什么 ? HTTP 协议可能是现在 Internet 上使用得最多.最重要的协议了,越来越多的 Java 应用程序需要直接通过 HTTP 协议来访问网络资源.虽然在 ...
- javascript笔记07:使用Object类为实例定义方法和属性
function Person() {} Person.prototype = { nickName:"john", , showInfo:function() { return ...
- linux内核编程学习——草稿
第一章 1.1 文件IO c标准函数与系统函数的区别 FILE文件类型是一个结构体类型,包括文件描述符(inode).位置指针(f_pos).缓冲器(buffer)(8192byte). c标准文件函 ...
- javascript中new Date浏览器兼容性处理
看下面的代码 <script type="text/javascript"> var dt1 = new Date('2016-3-4 11:06:12'); aler ...
- [原]Eclipse 安装SVN、Maven插件(补充)
参考雨之殇的文章:Eclipse 安装SVN.Maven插件 1.SVN可以按文章介绍的正常安装 2.Maven的Eclipse插件地址有变化 文章中的安装链接已经失效:m2e - http://m2 ...
- javascript 获取下一个节点
下一个节点: nextElementSibling 上一个节点 previousElementSibling <div> <select onchange="alert(t ...
- java中的拷贝文件FileChannel
以前用Java拷贝文件,只知道写byte数组循环拷贝,今天知道了可以用FileChannel进行拷贝,上代码: 下边是传统的byte数组拷贝方法 </pre><pre name=&q ...