?
本文描述了如何利用 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 项目:
django-admin startproject spreadsheet |
然后,cd
?到新创建的项目中,通过调用以下内容来创建应用程序:
django-admin startapp spreadsheet_app |
本文采用 SQLite3 来避免过多的数据库相关工作,但是,您可随意选择任何关系型数据库系统(RDBS)。修改 settings.py 文件来应用清单 1 中的代码。
清单 1. Django settings.py 文件
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 展示了相关代码。
清单 2. urls.py 文件
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
。
清单 3. models.py 文件
# 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 展示了如何处理进入的请求。
清单 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 展示了相关代码。
清单 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 代码来动态生成,也就是通过方法调用来增加或删除元素。
回页首
spreadsheet.js 概览
加载完成后,电子表格呈现 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
?来确定何时增加了标记,它将作为第一个元素插入到主体中。
清单 7. 用于生成菜单标记的方法
// 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 展示了一个非常简单的菜单,它仅包含一些按钮与文本框。
图 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 展示了相关代码。
清单 8. 将网格 div 转换为选项卡
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 所示。
图 2. 工作簿选项卡
?
该组件是所有数据网格的容器。此时,在选项卡的下面增加按钮,它将会在每次单击时动态增加选项卡。可通过方法add_newtab_button
?来完成此任务:
function add_newtab_button(){ $('body').append('<input id="new_tab_button" type="button" value="+"/>'); } |
最后一个可视化组件是创建 Open 窗体,可通过清单 9 中展示的?insert_open_dialog_markup
?方法来创建。同其他插入标记方法一样,它创建包含标记信息的字符串,并将其附加到主体中。
清单 9. 用于生成对话框标记的方法
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 页面中展示。对话框表单包含选择清单,该清单中包含了所加载工作簿的名称。
清单 10. 使 Open 窗体生效
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_ 后面加上表中的选项卡号。
清单 11. 向工作簿增加新选项卡的方法
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 操作。
清单 12. 为应用程序增加 SlickGrids
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
?方法来计算输入的表达式。
注意:?不要在生产环境中使用此代码。它会使您的系统完全开放给各类注入攻击。始终要注意净化您的数据。
最后,到网格的引用将保存以供其他方法使用。
清单 13. 向网格增加事件处理
// 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 所示。
清单 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 格式的网格数据。
清单 15. 保存网格数据
$('#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 数据。然后重新呈现网格。
清单 16. 将数据加载到网格中
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 展示了相关代码。
清单 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 响应。
清单 18. 最终视图
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 所示。
清单 19. 页面加载时需要执行的代码
$(document).ready(function(){ render_ui(); openTab(); }); |
完成页面加载后,UI 呈现出来,新的选项卡插入到了工作簿中。
现在可以通过在命令行运行?python manage syncdb
?创建数据库,来展示电子表格应用程序的功能了。当操作完成后,执行命令python manage.py runserver
?并导航到?http://localhost:8000/spreadsheet_app
。应当能看到最终版本,如图 3 所示。
图 3. 完整电子表格应用程序
?
可向项目添加很多内容,比如:
- 图表
- 针对单元格中输入的公式的、更安全、功能更好的解析器
- 导出为其他格式,比如 Microsoft? Office Excel
- 采用附加 jQuery 插件的真实菜单
?
结束语
虽然这一 web 应用程序还不能用于生产环境,但它演示了如何将多项技术组合在一起。采用 JavaScript、语义 HTML、JSON 对象来实现服务器数异步传输据,最重要的是,有很多现成的 jQuery 插件可供使用,这就大大简化了您的工作。