本文描述了如何利用 jQuery、jQuery 插件、以及 Django 来实现基于 web 的电子表格。并不是为了与 Google Docs 进行竞争,而是要演示如果创建 “office” 风格的应用程序,并给出大量可用的 jQuery 插件与工具。我采用 SQLite/Python/Django 栈作为后端,您也可以通过很小的工作量,来实现到其他框架的端口,比如 Ruby on Rails。
本文采用如下 Python 技术(见 参考资料 中的链接):
- Python 2.5+
- simplejson
- Django 1.2.3
注意: Python 2.5 不包括 simplejson,但在 Python 后续的版本中包含。
要想避免获取所有 jQuery 依赖项过程中的麻烦,可通过 参考资料 中的链接来下载完整的演示。在前端,需要如下技术:
- jQuery 1.4.3
- jQuery UI 1.8.5
- SlickGrid
- jQuery JSON
所有的第三方库能处理大部分的工作量,特别是 SlickGrid。我选择 SlickGrid 是因为,它支持突出显示/选择单元格组 — 适用于优化单元格的数学操作与分析功能。它还支持在滚动时加载数据。还有很多优秀的 jQuery 网格插件可供使用,包括 Flexigrid、jQuery Grid、jqGridView、以及 Ingrid。此外,jQuery 项目已宣布提供官方 jQuery Grid 插件的计划。
每个电子表格都包含单个工作簿,每个工作簿包含一个或多个数据表。当首次输入的字符是等号(=)时,表中的每个单元格应当执行算术运算。否则,输入的文本应保持原样。数据加载到 JSON 对象中,异步发送给后端,并保存到数据库中。电子表格将处理 Open、New、以及 Save 操作,并且工作簿的名称将出现在顶部的可编辑文本框内。
单击 Open 打开一个 jQuery UI 窗口,显示数据库中的现有工作簿。选择工作簿后,利用 Asynchronous JavaScript and XML(Ajax)来检索所存储的 JSON 数据,并呈现给网格。异步地将以 JSON 格式发送的网格数据存储到后端。New 操作会清除所有引用,并重新加载干净的工作簿。
最后,工作簿的表格将被分成不重复的 jQuery 选项卡。同任何其他电子表格一样,选项卡将在底部展示,并通过单击底部的按钮来动态增加。
将所有的 CSS/JavaScript/images 放置到项目顶级目录中的 resources 文件夹中。Django 应用程序将包含名为 index.html 的模板,那只是一些标记,用于保持 HTML 语义与 JavaScript 代码语法。组件的生成,比如网格,是动态完成的。电子表格的定义包含在文件 spreadsheet.js 中。
首先,通过执行以下命令来创建 Django 项目:
django-admin startproject spreadsheet |
然后,cd 到新创建的项目中,通过调用以下内容来创建应用程序:
django-admin startapp spreadsheet_app |
本文采用 SQLite3 来避免过多的数据库相关工作,但是,您可随意选择任何关系型数据库系统(RDBS)。修改 settings.py 文件来应用清单 1 中的代码。
import os
APPLICATION_DIR = os.path.dirname( globals()[ '__file__' ] )
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': 'db',
'USER': '',
'PASSWORD': '',
'HOST': '',
'PORT': '',
}
}
MEDIA_ROOT = os.path.join( APPLICATION_DIR, 'resources' )
MEDIA_URL = 'http://localhost:8000/resources/'
ROOT_URLCONF = 'spreadsheet.urls'
TEMPLATE_DIRS = (
os.path.join( APPLICATION_DIR, 'templates' ),
)
INSTALLED_APPS = (
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.sites',
'django.contrib.messages',
'spreadsheet_app',
)
|
不必修改 settings.py 文件中的任何其他变量。现在需要配置 URL 映射。在本例中,仅需要两个映射:一个用于静态传送的文件,另一个用于指向索引。清单 2 展示了相关代码。
from django.conf.urls.defaults import *
from django.conf import settings
import spreadsheet.spreadsheet_app.views as views
urlpatterns = patterns('',
( r'^resources/(?P<path>.*)$',
'django.views.static.serve',
{ 'document_root': settings.MEDIA_ROOT } ),
url( r'^spreadsheet_app/', views.index, name="index" ) ,
)
|
在项目的顶级目录中创建目录 resources,并创建 css、js、与 images 子目录。SlickGrid 之类的下载依赖项,以及用于应用程序的自定义 JavaScript 代码存储在此处。如果觉得麻烦,可以下载演示版并复制 resources 目录即可。
接下来,创建域模型(见清单 3)。该模型将仅包含 3 个字段:workbook_name、 sheet_name 与 data。 Django Object-Relational Mapper(ORM)会自动创建关键字段 id。
# file: spreadsheet_app/models.py
from django.db import models
class Workbooks(models.Model):
workbook_name = models.CharField(max_length=30)
sheet_name = models.CharField(max_length=30)
data = models.TextField()
|
其实,您并没有完全发挥 Django 的优势。您只是想在处理后端工作的同时提升 jQuery 前端的性能。
最后,创建索引视图。索引视图处理电子表格相关的创建/读取/更新操作。不必深入探讨索引视图的细节问题,清单 4 展示了如何处理进入的请求。
# file:spreadsheet_app/views.py
from django.template.context import RequestContext
from spreadsheet.spreadsheet_app.models import Workbooks
import simplejson as json
from django.http import HttpResponse
def index(request):
app_action = request.POST.get('app_action')
posted_data = request.POST.get('json_data')
if posted_data is not None and app_action == 'save':
...
elif app_action == 'get_sheets':
...
elif app_action == 'list':
...
|
import 完成后,可以看到索引视图接受了包含客户端所发送 post 数据的请求对象。可以得到两个参数:app_action 与posted_data。app_action 参数说明客户端请求什么动作,比如创建新的工作表。posted_data 参数是客户端发送的,针对单个表的 JSON 数据。不同的动作通过 if 语句来处理;可以保存表,取得工作簿的所有表,或者取得数据库中工作簿的清单。
您将在后面看到索引视图的相关细节。此时,要在 spreadsheets_app 目录中增加名为 templates 的目录。在 templates 子目录中,增加文件 index.html,这是本项目唯一的模板。清单 5 展示了相关代码。
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1"i>
<title>Overly Simple Spreadsheet</title>
<link rel="stylesheet" href="{{MEDIA_URL}}css/smoothness/jquery-ui-1.8.5.custom.css"
type="text/css" media="screen" charset="utf-8" />
<link rel="stylesheet" href="{{MEDIA_URL}}css/slick.grid.css" type="text/css"
media="screen" charset="utf-8" />
<link rel="stylesheet" href="{{MEDIA_URL}}css/examples.css" type="text/css"
media="screen" charset="utf-8" />
<link rel="stylesheet" href="{{MEDIA_URL}}css/spreadsheet.css" type="text/css"
media="screen" charset="utf-8" />
<script type="text/javascript" src="{{MEDIA_URL}}js/jquery-1.4.3.min.js"></script>
<script type="text/javascript" src="{{MEDIA_URL}}js/jquery.json.js"></script>
<script type="text/javascript" src="{{MEDIA_URL}}js/jquery-ui-1.8.5.custom.min.js">
</script>
<script type="text/javascript" src="{{MEDIA_URL}}js/jquery.event.drag-2.0.min.js">
</script>
<script type="text/javascript" src="{{MEDIA_URL}}js/ui/jquery.ui.tabs.js"></script>
<script type="text/javascript" src="{{MEDIA_URL}}js/slick.editors.js"></script>
<script type="text/javascript" src="{{MEDIA_URL}}js/slick.grid.js"></script>
<script type="text/javascript" src="{{MEDIA_URL}}js/spreadsheet.js"></script>
</headi>
<body>
</body>
</html>
|
如您所见,在 HTML 中没有控制逻辑或样式 — 仅有标记。事实上,甚至主体中也没有任何元素。这些都通过 JavaScript 代码来动态生成,也就是通过方法调用来增加或删除元素。
加载完成后,电子表格呈现 UI 并加载单个选项卡。清单 6 展示了用于 render_ui 的代码。
清单 6. spreadsheet.js 中的 render_ui 方法
function render_ui(){
insert_menu_markup();
insert_grid_markup();
make_grid_component();
add_newtab_button();
insert_open_dialog_markup();
make_open_dialog();
}
|
我们来看一下每个方法的工作方式,从 insert_menu_markup 开始。该方法仅向顶部菜单增加 HTML 代码,如清单 7 所示。该菜单由三个按钮 — new、open、和 save — 以及用于展示并输入工作簿名称的文本字段组成。可采用 jQuery prepend 来确定何时增加了标记,它将作为第一个元素插入到主体中。
// OK, it's not really a menu...yet :-)
function insert_menu_markup(){
$("body").prepend(
'<input type="text" id="workbook_name" name="workbook_name" value="">');
$("body").prepend('<input id="save" type="button" value="save"/>');
$("body").prepend('<input id="open" type="button" value="open"/>');
$("body").prepend('<input id="new" type="button" value="new"/>');
}
|
图 1 展示了一个非常简单的菜单,它仅包含一些按钮与文本框。
insert_grid_markup 方法与 insert_menu_markup 类似,只是这一次采用了 append 方法。jQuery UI 请求 <div> 中的一个清单来生成选项卡组件:
function insert_grid_markup(){
var workbook_widget = '<div id="tabs" class="tabs-bottom"><ul><li></li></ul></div>';
$('body').append(workbook_widget);
}
|
此时,通过调用方法 make_grid_component 来使选项卡生效。清单 8 展示了相关代码。
function make_grid_component(){
$("#tabs").tabs();
$(".tabs-bottom .ui-tabs-nav, .tabs-bottom .ui-tabs_nav > *")
.removeClass("ui-corner-all ui-corner-top")
.addClass("ui-corner-bottom");
}
|
可采用 jQuery id 选择器来获取到选项卡 <div> 的引用,然后调用 tabs() 方法将 <div> 转换成选项卡组件。默认 CSS 类 ui-corner-top 被移除,然后增加了类 ui-corner-bottom,因此选项卡出现在底部,如图 2 所示。
该组件是所有数据网格的容器。此时,在选项卡的下面增加按钮,它将会在每次单击时动态增加选项卡。可通过方法add_newtab_button 来完成此任务:
function add_newtab_button(){
$('body').append('<input id="new_tab_button" type="button" value="+"/>');
}
|
最后一个可视化组件是创建 Open 窗体,可通过清单 9 中展示的 insert_open_dialog_markup 方法来创建。同其他插入标记方法一样,它创建包含标记信息的字符串,并将其附加到主体中。
function insert_open_dialog_markup(){
var dlg = '<div id="dialog_form" title="Open">' +
'<div id="dialog_form" title="Open">' +
'<p>Select an archive.</p><form>'+
'<select id="workbook_list" name="workbook_list">' +
'</select></form></div>';
$("body").append(dlg);
}
|
此时已有了用于窗体的标记,可通过清单 10 中展示的 make_open_dialog 方法来增加其功能 — render_ui 中的最后一个方法。通过调用 .dialog() 方法,并向其传递参数 autoOpen:false,只有当表单被明确地打开时,它才会在 web 页面中展示。对话框表单包含选择清单,该清单中包含了所加载工作簿的名称。
function make_open_dialog(){
$('#dialog_form').dialog({
autoOpen: false,
modal: true,
buttons: {
"OK":function(){
selected_wb = $('option:selected').attr('value');
$(this).dialog('close');
// remove grid, existing forms, and recreate
$('body').html('');
render_ui();
// load grids and create forms with invisible inputs
load_sheets(selected_wb);
// place workbook name in text field
$('#workbook_name').val(selected_wb);
},
"Cancel":function(){
$(this).dialog('close');
}
}
});
}
|
再次使用 jQuery 选择器来对 dialog_form 进行处理并调用 dialog() 方法,该方法将 html 元素转换为 jQuery 窗体。Open 窗体是个模式,不会随着页面加载而打开。它还包含两个按钮 — OK 与 Cancel。后者的功能是关闭窗体。OK 函数在选择清单中找到所选项目,并获取其值。然后它会关闭窗体,移除主体中的任何子元素,重新呈现 GUI 组件,并加载表格(术语称为 SlickGrids)。正如前面提到的,因为标记的生成全在方法中实现,所以增加与移除这些组件已无关紧要。
呈现 UI 基础之后,现在编写方法来打开具有网格的选项卡。继续采用这种自上而下的方法,清单 11 展示了 openTab 方法,这是本应用程序的关键功能。
注意: 应用程序中的每个表都包含一个 ID,它符合一个简单的命名约定:tabs_ 后面加上表中的选项卡号。
function openTab(sheet_id) {
numberOfTabs = $("#tabs").tabs("length");
tab_name = "tabs_" + numberOfTabs;
$("#tabs").tabs("add","#" + tab_name,"Sheet " + numberOfTabs, numberOfTabs);
$("#" + tab_name).css("display","block");
$("#tabs").tabs("select",numberOfTabs);
$('#'+tab_name ).css('height','80%');
$('#'+tab_name ).css('width','95%');
$('#'+tab_name ).css('float','left');
add_grid(tab_name, numberOfTabs);
// add form for saving this tabs data
if(!sheet_id){
$('body').append(
'<form method="post" action="?" id="'+tab_name +'_form" name="'+tab_name+'_form">'+
'<input type="hidden" id="data'+numberOfTabs+'" name="data'+numberOfTabs+'" value="">'+
'<input type="hidden" id="sheet_id" name="sheet_id" value="">' +
'</form>');
} else {
$('body').append(
'<form method="post" action="?" id="'+tab_name +'_form" name="'+tab_name+'_form">' +
'<input type="hidden" id="data'+numberOfTabs +'" name="data'+numberOfTabs+'" value="">'+
'<input type="hidden" id="sheet_id" name="sheet_id" value="'+sheet_id+'">' +
'</form>');
}
}
|
如果您有兴趣,那么可以选择比较优雅的方法来编写标记字符串,那就是采用一些 JavaScript 模板,而不是将字符串连接在一起。唯一的表 ID 保存在隐藏的 input 元素中。为了保存 JSON 数据,还增加了另一个隐藏元素。这一隐藏 input 遵循一个简单的命名规则 — 数据加上选项卡号。另一点需要注意的是,新增加的选项卡具有在实际增加 SlickGrid 之前修改的 CSS 属性。如果不这样做,那么网格将无法正确呈现。
方法 openTab 调用方法 add_grid,它完成 SlickGrid 对象的一个真实实例。清单 12 显示出应用程序任务繁重。可创建两个 JavaScript 对象 — workbook 与 grid_references。Workbook 对象包含到当前 workbook 对象的引用,而 grid_references 包含到每个 SlickGrid 对象的引用。add_grid 方法接受两个参数:网格名与网格号。
在列定义中,需要将 16 列作为默认值 — a 到 p — 采用 TextCellEditor 来在一个 SlickGrid 示例中提供。完成列定义后,要向 SlickGrid 提供参数定义。单元格可编辑、可调整、可选择。要注意确保 asyncEditorLoading 选项已设置为 True ,这用于在网格上实现 Ajax 操作。
var workbook = {};
var grid_references = {};
function add_grid(grid_name, gridNumber){
var grid;
var current_cell = null;
// column definitions
var columns = [
{id:"row", name:"#", field:"num", cssClass:"cell-selection", width:40,
cannotTriggerInsert:true, resizable:false, unselectable:true },
{id:"a", name:"a", field:"a", width:70, cssClass:"cell-title",
editor:TextCellEditor},
{id:"b", name:"b", field:"b", width:70, cssClass:"cell-title",
editor:TextCellEditor},
{id:"c", name:"c", field:"c", width:70, cssClass:"cell-title",
editor:TextCellEditor},
{id:"d", name:"d", field:"d", width:70, cssClass:"cell-title",
editor:TextCellEditor},
{id:"e", name:"e", field:"e", width:70, cssClass:"cell-title",
editor:TextCellEditor},
{id:"f", name:"f", field:"f", width:70, cssClass:"cell-title",
editor:TextCellEditor},
{id:"g", name:"g", field:"g", width:70, cssClass:"cell-title",
editor:TextCellEditor},
{id:"h", name:"h", field:"h", width:70, cssClass:"cell-title",
editor:TextCellEditor},
{id:"i", name:"i", field:"i", width:70, cssClass:"cell-title",
editor:TextCellEditor},
{id:"j", name:"j", field:"j", width:70, cssClass:"cell-title",
editor:TextCellEditor},
{id:"k", name:"k", field:"k", width:70, cssClass:"cell-title",
editor:TextCellEditor},
{id:"l", name:"l", field:"l", width:70, cssClass:"cell-title",
editor:TextCellEditor},
{id:"m", name:"m", field:"m", width:70, cssClass:"cell-title",
editor:TextCellEditor},
{id:"n", name:"n", field:"n", width:70, cssClass:"cell-title",
editor:TextCellEditor},
{id:"o", name:"o", field:"o", width:70, cssClass:"cell-title",
editor:TextCellEditor},
{id:"p", name:"p", field:"p", width:70, cssClass:"cell-title",
editor:TextCellEditor},
];
var options = {
editable: true,
autoEdit: true,
enableAddRow: true,
enableCellNavigation: true,
enableCellRangeSelection : true,
asyncEditorLoading: true,
multiSelect: true,
leaveSpaceForNewRows : true,
rerenderOnResize : true
};
eval("var data" + gridNumber + " = [];");
workbook["data" + gridNumber] = [];
for( var i=0; i < 100 ; i++ ){
var d = (workbook["data"+gridNumber][i] = {});
d["num"] = i;
d["value"] = "";
}
grid = new Slick.Grid($("#"+grid_name),workbook["data"+gridNumber], columns, options);
...
|
以 “类似黑客” 的方式使用 eval 语句,动态创建变量名。变量 data 将以空字符串数据方式加载,并完成 SlickGrid 实例的创建。
此时,针对事件将 attach 增加到网格,如清单 13 所示。当 onCurrentCellChanged 事件发生时,它获取网格中的数据并更新单元格内容。在完成单元格编辑之前,调用 onBeforeCellEditorDestroy 事件。然后事件触发,将会获取单元格数据,但这一次要确定第一个字符是等号。如果是,则采用 eval 方法来计算输入的表达式。
注意: 不要在生产环境中使用此代码。它会使您的系统完全开放给各类注入攻击。始终要注意净化您的数据。
最后,到网格的引用将保存以供其他方法使用。
// file: resources/js/spreadsheet.js continued
// Events
grid.onCurrentCellChanged = function(){
d = grid.getData();
row = grid.getCurrentCell().row;
cell = grid.getCurrentCell().cell;
this_cell_data = d[row][grid.getColumns()[cell].field];
};
grid.onBeforeCellEditorDestroy = function(){
d = grid.getData();
row = grid.getCurrentCell().row;
cell = grid.getCurrentCell().cell;
this_cell_data = d[row][grid.getColumns()[cell].field];
if(this_cell_data && this_cell_data[0] === "="){
// evaluate JavaScript expression, don't use
// in production!!!!
eval("var result = " + this_cell_data.substring(1));
d[row][grid.getColumns()[cell].field] = result;
}
};
grid_references[grid_name] = grid;
};
|
根据前面的规范,您的电子表格必须创建新的工作簿,以及保存并打开现有工作簿。现在实现新工作簿函数,这是三个任务中最简单的一个。在此处,所要做的就是毁掉 UI 以及任何引用,然后重新创建,如清单 14 所示。
$('#new').live('click', function(){
// delete any existing references
workbook = {};
grid_references = {};
// remove grid, existing forms, and recreate
$('body').html('');
// recreate
render_ui();
openTab();
});
|
“save” 函数有点复杂。可采用 jQuery 选择器来获取名称属性以 data 开头的每个元素,然后采用 each 方法来循环处理结果集。需要关注的重点是采用用于 “线上” 发送的插件 jquery.json 来将网格数据编码成 JSON 格式。采用 $.post 方法来异步发送数据。传递给索引视图的参数是需要执行的动作(在本例中为 save,如清单 15 所示)、唯一的表 ID、工作簿名、以及 JSON 格式的网格数据。
$('#save').live('click',function(){
// Do a foreach on all the grids. The ^= operator gets all
// the inputs with a name attribute that begins with data
$("[name^='data']").each(function(index, value){
var data_index = "data"+index;
var sheet_id = $('#tabs_'+index+'_form').find('#sheet_id').val();
if(sheet_id !== ''){
sheet_id = eval(sheet_id);
}
// convenience variable for readability
var data2post = $.JSON.encode(workbook[data_index]);
$("#"+data_index).val(data2post);
$.post( '{% url index %}', {'app_action':'save', 'sheet_id': sheet_id,
'workbook_name':$('#workbook_name').val(),
'sheet':data_index, 'json_data':data2post});
});
});
|
如果重新调用,那么打开操作需要 load_sheets 方法。现在就添加该方法(见清单 16)。此方法必须调用后端,请求表,并将工作簿的所有数据传递给方法。然后将数据加载到相应的 SlickGrid 对象中。注意,在将数据插入到对象之前,必须利用 decode 方法来反序列化 JSON 数据。然后重新呈现网格。
function load_sheets(workbook_name){
$('#workbook_list').load('{% url index %}',
{'app_action':'get_sheets','workbook_name':workbook_name},
function(sheets, resp, t){
sheets = $.JSON.decode(sheets);
workbook = {}; // reset
grid_references = {};
$.each(sheets, function(index, value){
// add to workbook object
var sheet_id = value["sheet_id"];
openTab(sheet_id);
// By calling eval, we translate value from
// a string to a JavaScript object
workbook[index] = eval(value["data"]);
// insert data into hidden
$("#data"+index).attr('value', workbook[index]);
grid_references["tabs_"+index].setData(workbook[index]);
grid_references["tabs_"+index].render();
});
});
}
|
最后,需要实现 open 函数。再次进行异步调用,此次发送 list 动作,然后再次反序列化 JSON 发送的数据。接下来,利用数据库中所有工作簿的名称来更新 select 清单。对话框表单将打开。清单 17 展示了相关代码。
$('#open').live('click',function(){
// load is used for doing asynchronous loading of data
$('#workbook_list').load('{% url index %}', {'app_action':'list'},
function(workbooks,success){
workbooks = $.JSON.decode(workbooks);
$.each(workbooks, function(index, value){
$('#workbook_list').append(
'<option value="'+ value +'">'+value +'*lt;/option>');
});
});
$('#dialog_form').dialog('open');
});
|
此时已完成客户端 post 编码,可以看到完整的索引视图了。清单 18 展示了除导入之外的完整索引视图。
想要序列化(反序列化)JSON 代码,可采用 simplejson 模块与命令转储来对其进行序列化并为反转进行加载。对于 Save 动作,如果 ID 是针对某个表的,那么该表将被更新。否则将创建新表。
与此相反,获取表动作将创建具有特定工作簿的所有表的 JSON 对象。它利用命令 filter(),针对某个工作簿来检索所有表(QuerySet 对象)。这等效于 select * from spreadsheet_app_workbooks,其中 workbook_name = wb_name"。检索完这些设置后,再次将其转换为 JSON 格式并发回客户端。
List 也采用 Django ORM,但此次与 values() 方法一起使用。在这里,您正在指示 Django “获取 workbook_name 列”。通过在QuerySet 对象上调用 distinct 方法,等于在说明,不想要任何重复。可再次采用清单解析,从结果创建清单。对于 get_sheets 和list,必须返回 HttpResponse 对象,以便 jQuery 处理 Ajax 响应。
def index(request):
template = 'index.html'
app_action = request.POST.get('app_action')
posted_data = request.POST.get('json_data')
if posted_data is not None and app_action == 'save':
this_sheet = request.POST.get("sheet")
this_workbook = request.POST.get("workbook_name")
sheet_id = request.POST.get("sheet_id")
posted_data = json.dumps(posted_data)
if(sheet_id):
wb = Workbooks(id=sheet_id, workbook_name=this_workbook,
sheet_name=this_sheet, data=posted_data)
else:
wb = Workbooks(workbook_name=this_workbook,
sheet_name=this_sheet, data=posted_data)
wb.save()
elif app_action == 'get_sheets':
wb_name = request.POST.get('workbook_name')
sheets = Workbooks.objects.filter(workbook_name=wb_name)
# use list comprehension to create python list which is like a JSON object
sheets = [{ "sheet_id":i.id, "workbook_name": i.workbook_name.encode("utf-8"),
"sheet_name": i.sheet_name.encode("utf-8"),
"data": json.loads(i.data.encode("utf-8"))} for i in sheets ]
# dumps -> serialize to JSON
sheets = json.dumps(sheets)
return HttpResponse( sheets, mimetype='application/javascript' )
elif app_action == 'list':
workbooks = Workbooks.objects.values('workbook_name').distinct()
# use list comprehension to make a list of just the work books names
workbooks = [ i['workbook_name'] for i in workbooks ]
# encode into json format before sending to page
workbooks = json.dumps(workbooks)
# We need to return an HttpResponse object in order to complete
# the ajax call
return HttpResponse( workbooks, mimetype='application/javascript' )
return render_to_response(template, {},
context_instance = RequestContext( request ))
|
此时已经定义了所有的 JavaScript 方法,可以利用两个方法调用来生成应用程序了,如清单 19 所示。
$(document).ready(function(){
render_ui();
openTab();
});
|
完成页面加载后,UI 呈现出来,新的选项卡插入到了工作簿中。
现在可以通过在命令行运行 python manage syncdb 创建数据库,来展示电子表格应用程序的功能了。当操作完成后,执行命令python manage.py runserver 并导航到 http://localhost:8000/spreadsheet_app。应当能看到最终版本,如图 3 所示。
可向项目添加很多内容,比如:
- 图表
- 针对单元格中输入的公式的、更安全、功能更好的解析器
- 导出为其他格式,比如 Microsoft® Office Excel
- 采用附加 jQuery 插件的真实菜单
虽然这一 web 应用程序还不能用于生产环境,但它演示了如何将多项技术组合在一起。采用 JavaScript、语义 HTML、JSON 对象来实现服务器数异步传输据,最重要的是,有很多现成的 jQuery 插件可供使用,这就大大简化了您的工作。
本文介绍如何结合使用jQuery、Django及SlickGrid等工具构建基于Web的电子表格应用。涵盖前端用户界面的设计与实现,以及后端数据处理的详细步骤。



491

被折叠的 条评论
为什么被折叠?



