HT for Web 的 HTML5树组件 有延迟加载的功能,这个功能对于那些需要从服务器读取具有层级依赖关系数据时非常有用,需要获取数据的时候再向服务器发起请求,这样可减轻服务器压力,同时也减少了浏览器的等待时间,让页面的加载更加流畅,增强用户体验。
进入正题,今天用来做演示的Demo是,客户端请求服务器读取系统文件目录结构,通过 HT for Web 的 HTML5树组件 显示系统文件目录结构。
首先,我们先来设计下服务器,这次Demo的服务器采用 Node.js ,用到了Node.js的 express 、 socket.io 、fs和http这四个模块, Node.js 的相关知识,我在这里就不阐述了,网上的教材一堆,这里推荐下 socket.io 的相关入门 http://socket.io/get-started/chat/ 。
服务端代码代码:
var fs = require('fs'), express = require('express'), app = express(), server = require('http').createServer(app), io = require('socket.io')(server), root = ‘/Users/admin/Projects/ht-for-web/guide‘; io.on('connection', function(socket){ socket.on('explore', function(url){ socket.emit('file', walk(url || root)); }); }); app.use(express.static('/Users/admin/Projects/ht-for-web')); server.listen(5000, function(){ console.log('server is listening at port 5000'); });
io监听了connection事件,并获得一个socket;socket再监听一个叫explore的自定义事件,通过url参数获取到数据后,派发一个叫file的自定义事件,供客户端监听并做相应处理;通过app.use结合express.static设置项目路径;最后让server监听5000端口。
到此,一个简单的服务器就搭建好了,现在可以通过http://localhost:5000来访问服务器了。等等,好像缺了点什么。对了,获取系统文件目录结构的方法忘记给了,OK,那么我们就先来看看获取整站文件的代码是怎么写的:
function walk(pa) { var dirList = fs.readdirSync(pa), key = pa.substring(pa.lastIndexOf('/') + 1), obj = { name: key, path: pa, children: [], files: [] }; dirList.forEach(function(item) { var stats = fs.statSync(pa + '/' + item); if (stats.isDirectory()) { obj.children.push(walk(pa + '/' + item)); } else { obj.files.push({name: item, dir: pa + '/' + item}); } }); return obj; }
如大家所见,采用递归的方式,逐层遍历子目录,代码也没什么高深的地方,相信大家都看得懂。那我们来看看运行效果吧:
duang~文件目录结构出来了,是不是感觉酷酷的,这代码量不小吧。其实,代码并不多,贴出来大家瞅瞅:
<!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8"> <title>tree-loader</title> <script src="/socket.io/socket.io.js"></script> <script src="/lib/core/ht.js"></script> <script> var socket = io(), idMap = {}; function init() { var dm = window.dm = new ht.DataModel(), tree = new ht.widget.TreeView(dm); tree.addToDOM(); socket.on('file', function(data) { var root = dm.getDataById(idMap[data.path]); createChildren(data.children || [], root, dm); createFiles(data.files || [], root, dm); }); socket.emit('explore'); } function createChildren(children, parent, dm) { children.forEach(function(child) { var n = createData(child, parent); dm.add(n); createChildren(child.children || [], n, dm); createFiles(child.files || [], n, dm); }); } function createFiles(files, parent, dm){ files.forEach(function(file){ var n = createData(file, parent); dm.add(n); }); } function createData(data, parent){ var n = new ht.Data(); n.setName(data.name); n.setParent(parent); n.a('path', data.path); idMap[data.path] = n.getId(); return n; } </script> </head> <body onload="init();"> </body> </html>
这就是全部的HTML代码,加上空行总共也就50几行,怎么样,有没有感觉 HT for Web 很强大。废话不多说,来看看这些代码都干了些什么:
整体的思路是这样子的,当然这离我们要实现的树组件的延迟加载技术还有些差距,那么, HT for Web 的 HTML5树组件 的延迟加载技术是怎么实现的呢?不要着急,马上开始探讨。
首先我们需要改造下获取文件目录的方法walk,因为前面介绍的方法中,使用的是加载整站文件目录,所以我们要将walk方法改造成只获取一级目录结构,改造起来很简单,就是将递归部分改造成获取当前节点就可以了,具体代码如下:
obj.children.push(walk(pa + '/' + item)); // 将上面对代码改成下面的代码 obj.children.push({name: item, path: pa + '/' + item});
这样子服务器就只请求当前请求路径下的第一级文件目录结构。接下来就是要调整下客户端代码了,首先需要给tree设置上loader:
tree.setLoader({ load: function(data) { socket.emit('explore', data.a('path')); data.a('loaded', true); }, isLoaded: function(data) { return data.a('loaded'); } });
loader包含了两个方法,load和isLoaded,这两个方法的功能分别是加载数据和判断数据是否已经加载,在load方法中,对socket派发explore事件,当前节点的path为参数,向服务器请求数据,之后将当前节点的loaded属性设置为true;在isLoaded方法中,返回当前节点的loaded属性,如果返回为true,那么tree将不会在执行load方法向服务器请求数据。
接下来需要移除createChildren的两个回调方法,并且在createFiles方法中为创建出来的节点的loaded属性设置成true,这样在不是目录的节点前就不会有展开的图标。createChildren和createFiles两个方法修改后的代码如下:
function createChildren(children, parent, dm) { children.forEach(function(child) { var n = createData(child, parent); dm.add(n); }); } function createFiles(files, parent, dm){ files.forEach(function(file){ var n = createData(file, parent); n.a('loaded', true); dm.add(n); }); }
如此, HT for Web的HTML5树组件 延迟加载技术就设计完成了,我在服务器的控制台打印出请求路径,看看这个延迟加载是不是真的,如下图:
看吧,控制台打印的是4条记录,第一条是请求跟目录时打印的,我在浏览器中展开里三个目录,在控制台打印了其对应的目录路径。
等等,现在这个目录看起来好烦,只有文字,除了位子前的展开图标可以用来区别文件和目录外,没有其他什么区别,所以我决定对其进行一番改造,让每一级目录都有图标,而且不同文件对应不同的图标,来看看效果吧:
怎么样,是不是一眼就能看出是什么文件,这个都是样式上面的问题,我就不再一一阐述了,直接上代码:
<!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8"> <title></title> <script src="/socket.io/socket.io.js"></script> <script src="/build/ht-debug.js"></script> <script> var socket = io(), idMap = {}; function init() { var icons = ['css', 'dir-open', 'dir', 'file', 'flash', 'gif', 'html', 'jar', 'java', 'mp3', 'pdf', 'png', 'script', 'txt', 'video', 'xml', 'zip']; icons.forEach(function(c){ ht.Default.setImage(c, 16, 16, '/test/wyl/images/' + c + '.png'); }); var dm = window.dm = new ht.DataModel(), tree = new ht.widget.TreeView(dm); tree.setLoader({ load: function(data) { socket.emit('explore', data.a('path')); data.a('loaded', true); }, isLoaded: function(data) { return data.a('loaded'); } }); tree.getLabelFont = function(data){ return '13px Helvetica, Arial, sans-serif'; }; tree.getLabelColor = function (data) { return this.isSelected(data) ? 'white' : 'black'; }; tree.getSelectBackground = function (data) { return '#408EDB'; }; tree.getIcon = function (data) { var icon = data.getIcon() || 'file'; if (data.a('isdir')) { if (this.isExpanded(data)) { icon = 'dir-open'; } else { icon = 'dir'; } } return icon; }; tree.addToDOM(); socket.on('file', function(data) { var root = dm.getDataById(idMap[data.path]); createChildren(data.children || [], root, dm); createFiles(data.files || [], root, dm); }); socket.emit('explore'); } function createChildren(children, parent, dm) { children.forEach(function(child) { var n = createData(child, parent); n.a('isdir', true); dm.add(n); }); } function createFiles(files, parent, dm){ files.forEach(function(file){ var n = createData(file, parent); n.a('loaded', true); dm.add(n); }); } function createData(data, parent){ var name = data.name, icon = 'file'; if (/.jar$/.test(name)) icon = 'jar'; else if (/.css$/.test(name)) icon = 'css'; else if (/.gif$/.test(name)) icon = 'gif'; else if (/.png$/.test(name)) icon = 'png'; else if (/.js$/.test(name)) icon = 'script'; else if (/.html$/.test(name)) icon = 'html'; else if (/.zip$/.test(name)) icon = 'zip'; var n = new ht.Data(); n.setName(data.name); n.setParent(parent); n.setIcon(icon); n.a('path', data.path); idMap[data.path] = n.getId(); return n; } </script> </head> <body onload="init();"> </body> </html>
在最后,附上完整的服务器代码:
var fs = require('fs'), express = require('express'), app = express(), server = require('http').createServer(app), io = require('socket.io')(server), root = '/Users/admin/Projects/ht-for-web/guide'; io.on('connection', function(socket){ socket.on('explore', function(url){ socket.emit('file', walk(url || root)); }); }); app.use(express.static('/Users/admin/Projects/ht-for-web')); server.listen(5000, function(){ console.log('server is listening at port 5000'); }); function walk(pa) { var dirList = fs.readdirSync(pa), key = pa.substring(pa.lastIndexOf('/') + 1), obj = { name: key, path: pa, children: [], files: [] }; dirList.forEach(function(item) { var stats = fs.statSync(pa + '/' + item); if (stats.isDirectory()) { obj.children.push({name: item, path: pa + '/' + item}); } else { obj.files.push({name: item, dir: pa + '/' + item}); } }); return obj; }