文件管理——文件系统管理 [TOC]
开发环境 开发语言:Javascript+html+css
开发框架:Vue.js 3.0+Element-plus
开发工具:Vue-cli、Vue-devtools、VScode、Edge
实现方法 引入Element-plus组件作为UI,采用Vue3框架进行组件化开发
采用 JS 在内存中开辟的空间作为文件资源管理器所需的内存部分,使用浏览器缓存(localStorage)作为外部磁盘,将文件写入的数据存至其中。
在退出资源管理器时(即关闭浏览器页面)将必要的目录文件结构(如位图、文件目录等)也一并存入浏览器缓存中,模拟关闭系统时文件存入磁盘。在下次访问时从浏览器缓存中读取目录文件结构数据,模拟进入系统后从磁盘中取出文件目录等操作。
项目浏览
请注意:
由于使用了浏览器缓存来存放数据,不同浏览器之间、同一浏览器不同域名下缓存不能共享,因此 请使用同一浏览器打开项目 ,以顺利读取上一次退出时保存的内容
请确保浏览器不处于无痕模式/安全模式 ,以确保浏览器允许缓存写入
本项目在Edge、FireFox、Chrom、Mac端Safari均进行过测试,可正常读写缓存
主要变量及数据结构介绍 实现功能的主要文件为DocumentPage.vue
文件预览
文件名称
负责内容
子组件
App.vue
页面根组件
DocumentPage.vue
DocumentPage.vue
实现页面UI展示、整体逻辑
null
源码变量介绍 DocumentPage.vue 维护变量 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 data ( ) { return { cur_path : [], cur_dir : [], totol_dir : [ { name : "Data(D:)" , last_edit_timestr : "2001/10/23" , last_edit_time : -1 , type : 2 , size : "64" , used_space : "" , path : "" , children : [], p_begin : -1 , p_end : -1 , }, ], disk_bitmap : [ { disk : "D" , bolck_size : 512 , block_free_num : 512 , bitmap : "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" , }, ], physical_disk : [], ... }; },
基本介绍 组件内部维护了文件资源管理器所需的状态变量及必要数据结构,除了基本的状态参数外,使用了对象数组 total_dir 作为文件目录表,其中每个对象相当于一个 FCB , disk_bitmp 模拟使用位图来管理空闲块信息;physical_disk 模拟外部磁盘空间,存放文件内容
在退出系统前会将以上信息转换为 JSON 格式,存放入浏览器本地缓存中(使用 windows.localStorage 对象),在每次进入系统时将会读取缓存,恢复上次离开时的状态。
主要对象属性介绍
totol_dir 使用树的数据结构 组织 FCB 项,采用链接结构管理文件存储空间 ,其中每一项 FCB 主要包含以下属性:
disk_bitmp 是一个数组,原本考虑到了多个磁盘(C、D、E、F),每个磁盘均有一个位图管理空闲空间,但时间原因最终只写了一个磁盘:
physical_disk 是一个数组,模拟外部磁盘,其中的每一个对象元素为一个磁盘块,每个元素的属性为:
block_num:块号
content:存放内容
disk_next:采用链接结构管理文件存储空间,此处指向下一块块号
由于块数比较多,不便于以字面量形式初始化,因此在 created 钩子中调用相应函数进行初始化,初始化代码如下:
1 2 3 4 5 6 7 8 9 10 physicalDiskInit ( ) { for (let i = 0 ; i < 64 ; ++i) { this .physical_disk .push ({ block_num : i, content : "" , des_content : "" , disk_next : -1 , }); } },
主要功能设计及实现 管理方式
文件目录——多级目录
文件存储空间管理——链接结构
空闲空间管理——位图
新建文件
由于文件目录以多级目录方式组织,因此直接遍历树找到当前目录下新建的文件所属的直接父文件夹(父结点)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 let dir = this .totol_dir ; for (let i = 0 ; i < this .cur_path .length - 1 ; ++i) { for (let j = 0 ; j < dir.length ; ++j) { if (dir[j].name == this .cur_path [i] && dir[j].type != 1 ) { dir = dir[j].children ; } } } let index = -1 ; console .log (222222 ); console .log (dir); for (let i = 0 ; i < dir.length ; ++i) { if ( dir[i].name == this .cur_path [this .cur_path .length - 1 ] && (dir[i].type == 2 || dir[i].type == 0 ) ) { index = i; } }
检查文件名是否冲突及合法性(同一目录下已存在同类型同名文件)
若非法则输出提示,等待新的文件名输入
若合法则进行下一步
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 if (this .new_doc_name == "" ) { if (this .new_doc_type == 0 ) { ElMessage .error ("文件夹名不能为空!" ); } else { ElMessage .error ("文件名不能为空!" ); } return ; } for (let i = 0 ; i < dir[index].children .length ; ++i) { if ( dir[index].children [i].name == this .new_doc_name && dir[index].children [i].type == this .new_doc_type ) { if (this .new_doc_type == 0 ) { ElMessage .error ("已存在同名文件夹!" ); } else { ElMessage .error ("已存在同名文件!" ); } this .new_doc_name = "" ; this .new_doc_type = -1 ; return ; } }
生成文件其他信息,在文件目录下创建相应FCB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 let new_doc_path = "" ; for (let i = 0 ; i < this .cur_path .length ; ++i) { new_doc_path += this .cur_path [i] + "\\" ; } let doc_size = 0 ; if (this .new_doc_type == 0 ) { doc_size = "-" ; } dir[index].children .push ({ name : this .new_doc_name , last_edit_timestr : this .new_doc_timestr , last_edit_time : this .new_doc_time , type : this .new_doc_type , size : doc_size, path : new_doc_path, children : [], p_begin : -1 , p_end : -1 , }); dir[index].last_edit_timestr = this .new_doc_timestr ; dir[index].last_edit_time = this .new_doc_time ; this .cur_dir = [].concat (dir[index].children ); } else { alert ("下标查找有问题" ); return ; } this .new_doc_name = "" ; this .new_doc_type = -1 ; this .show_dialog = false ; ElMessage ({ message : "创建成功!" , type : "success" , }); return ; },
创建成功(P.S windows 11文件管理系统不会在创建文件时要求输入文本内容,因此此处也不要求,初始化文件大小均为 0 KB)
打开文件 根据前端传入的打开文件名称,查找文件目录表,根据其对应的 p_begin,p_end 指针从”外部磁盘”中读取数据
1 2 3 4 5 6 7 8 9 openFile (index ) { this .open_doc_name = this .cur_dir [index].name ; this .open_doc_index = index; this .show_content = true ; this .open_doc_content = this .readDisk ( this .cur_dir [index].p_begin , this .cur_dir [index].p_end ); },
由于打开文件事件由鼠标双击触发,因此此处传入的文件名称必然合法,且一定在 FCB 中存在(若不存在则不会显示在页面上,自然 无法被点击),因此无需检测文件名
下面介绍 readDisk() 函数,其主要功能为根据文件块指针从“磁盘”读取数据
判断 p_begin 是否为 -1 ,若为 -1 则说明是一个空文件,直接返回即可;若不为 -1,则从起始块读数据,依据起始块中的指向下一块的指针找到下一块……直至读取完毕,即 p_begin 与 p_end 相等
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 readDisk (p_begin, p_end ) { if (p_begin == -1 ) { return ; } let content = "" ; let p_cur = p_begin; while (1 ) { content += this .physical_disk [p_cur].content ; if (p_cur == p_end) { break ; } p_cur = this .physical_disk [p_cur].disk_next ; } return content; },
保存文件 将文件数据存入磁盘,若磁盘有剩余空间则返回 true,记录相关信息并输出提示;若无剩余空间则输出提示,等待文本内容修改
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 saveDoc ( ) { if (this .writeOutDisk ()) { this .cur_dir [this .open_doc_index ].size = this .open_doc_content .length / 1024 ; let a = new Date ().toLocaleDateString (); this .cur_dir [this .open_doc_index ].last_edit_time = new Date (a) / 1000 ; this .cur_dir [this .open_doc_index ].last_edit_timestr = a; ElMessage ({ message : "更改已保存!" , type : "success" , }); this .show_content = false ; } else { ElMessage .error ("磁盘空间不足!" ); } },
下面主要介绍下writeOutDisk()函数,其主要功能为根据文件块指针从“磁盘”读取数据
查看 p_begin 是否为-1,若为-1则说明该文件之前在磁盘上没有存储的数据,则直接进行第 3 步
若 p_begin 不为-1,则说明该文件之前在磁盘上存储有数据。此处设计为将磁盘上该文件的旧数据擦除,再存储新数据
1 2 3 4 5 6 7 8 9 10 if (this .cur_dir [this .open_doc_index ].p_begin != -1 ) { this .deleteFromDisk ( this .cur_dir [this .open_doc_index ].p_begin , this .cur_dir [this .open_doc_index ].p_end ); this .cur_dir [this .open_doc_index ].p_begin = -1 ; this .cur_dir [this .open_doc_index ].p_end = -1 ; }
计算保存的数据所需要的磁盘块数,并将数据按块大小分隔成相应份
1 2 3 4 5 6 7 let size = this .open_doc_content .length ; let block_size = this .disk_bitmap [0 ].bolck_size ; let block_need_num = Math .ceil (size / block_size); var block_content_ary = []; for (let i = 0 ; i < this .open_doc_content .length ; i += block_size) { block_content_ary.push (this .open_doc_content .slice (i, i + block_size)); }
通过位图磁盘是否有剩余空间,若不足,则返回 false 输出提示并等待文本内容修改;若充足则进行磁盘块空间分配。分配规则为通过位图寻找前N个(假设需要 N 块)空闲块,依次将数据存入,并建立块指针链接。完成后返回 true
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 if (this .disk_bitmap [0 ].block_free_num >= block_need_num) { this .disk_bitmap [0 ].block_free_num -= block_need_num; let count = 0 ; let last_block_index = -1 ; let bitmap_change_index = []; for (let i in this .disk_bitmap [0 ].bitmap ) { if (count == block_need_num) { this .cur_dir [this .open_doc_index ].p_end = last_block_index; break ; } if (this .disk_bitmap [0 ].bitmap [i] == "0" ) { if (count == 0 ) { this .cur_dir [this .open_doc_index ].p_begin = i; bitmap_change_index.push (i); this .physical_disk [i].content = block_content_ary[count]; this .physical_disk [i].des_content = block_content_ary[ count ].slice (0 , 10 ); count++; last_block_index = i; } else { this .physical_disk [last_block_index].disk_next = i; bitmap_change_index.push (i); this .physical_disk [i].content = block_content_ary[count]; this .physical_disk [i].des_content = block_content_ary[ count ].slice (0 , 10 ); count++; last_block_index = i; } } } ...
删除文件
由于文件目录以多级目录方式组织,因此直接遍历树找到当前目录下要删除的文件所属的直接父文件夹及删除过程中所需参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 let delete_cur_path = delete_doc_path.split ("\\" ); delete_cur_path.pop ();let dir = this .totol_dir ; for (let i = 0 ; i < delete_cur_path.length - 1 ; ++i) { for (let j = 0 ; j < dir.length ; ++j) { if (dir[j].name == delete_cur_path[i] && dir[j].type != 1 ) { dir = dir[j].children ; } } }let index = -1 ;for (let i = 0 ; i < dir.length ; ++i) { if ( dir[i].name == delete_cur_path[delete_cur_path.length - 1 ] && dir[i].type != 1 ) { index = i; } }let delete_cur_dir = [].concat (dir[index].children ); if (index == -1 ) { alert (-1 ); return ; }let delete_index = -1 ;for (let i = 0 ; i < delete_cur_dir.length ; ++i) { if ( delete_cur_dir[i].name == delete_doc_name && delete_cur_dir[i].type == type ) { delete_index = i; break ; } }
判断删除的文件类型
若不为文件夹,且在磁盘内存有数据(p_begin 不为-1),则先根据文件目录表中 FCB 所记录的块指针,调用 deleteFromDisk(p_begin,p_end)
将磁盘对应块中的数据擦除,并取消块之间的链接
1 2 3 4 5 6 if (delete_cur_dir[delete_index].p_begin != -1 ) { this .deleteFromDisk ( delete_cur_dir[delete_index].p_begin , delete_cur_dir[delete_index].p_end ); }
在从对应的文件目录中将对应的 FCB 项去除,即从树中去除相应结点
1 2 3 4 5 6 7 8 9 10 for (let i = 0 ; i < dir[index].children .length ; ++i) { if ( dir[index].children [i].name == delete_cur_dir[delete_index].name && dir[index].children [i].type == delete_cur_dir[delete_index].type ) { dir[index].children .splice (i, 1 ); this .cur_dir = [].concat (dir[index].children ); return ; } }
若为文件夹,则递归调用本函数,将所有的子文件删除后,再将其删除
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 ...else if (type == 0 ) { let i = 0 ; while (i < delete_cur_dir[delete_index].children .length ) { this .deleteFile ( delete_cur_dir[delete_index].children [0 ].type , delete_cur_dir[delete_index].children [0 ].path , delete_cur_dir[delete_index].children [0 ].name ); } for (let i = 0 ; i < dir[index].children .length ; ++i) { if ( dir[index].children [i].name == delete_doc_name && dir[index].children [i].type == 0 ) { dir[index].children .splice (i, 1 ); this .cur_dir = [].concat (dir[index].children ); break ; } } return ; }
数据记录/恢复 使用 localStorage 对象,其允许在浏览器中存储 key/value 对的数据,可长久保存整个网站的数据,保存的数据没有过期时间,直到手动去删除,所以很适合在本项目使用
保存数据 在页面关闭/刷新/前进/后退时,将文件目录表、位图、用内存空间模拟的磁盘空间数据转换为 JSON 格式,存入缓存中。需要在 mounted 钩子中设置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 mounted ( ) { window .onbeforeunload = (e ) => { e = e || window .event ; if (e) { e.returnValue = "关闭提示" ; } localStorage .clear (); let physical_disk_JSON = JSON .stringify (this .physical_disk ); let totol_dir_JSON = JSON .stringify (this .totol_dir ); let disk_bitmap_JSON = JSON .stringify (this .disk_bitmap ); localStorage .setItem ("physical_disk" , physical_disk_JSON); localStorage .setItem ("total_dir" , totol_dir_JSON); localStorage .setItem ("disk_bitmap" , disk_bitmap_JSON); return "关闭提示" ; }; ... }
读取数据 在页面打开后,变量创建完成但页面未渲染时,检测本地缓存中是否有相应数据,若有则读取数据,将读到的 JSON 格式转换回对象,完成相应状态的恢复。在created 钩子设置:
1 2 3 4 5 6 7 8 9 10 created ( ) { this .physicalDiskInit (); if (localStorage .length != 0 ) { this .totol_dir = JSON .parse (localStorage .getItem ("total_dir" )); this .physical_disk = JSON .parse (localStorage .getItem ("physical_disk" )); this .disk_bitmap = JSON .parse (localStorage .getItem ("disk_bitmap" )); } this .cur_dir = []; this .cur_dir .push (this .totol_dir [0 ]); },