时间: 2020-09-03 00:08:26 人气: 2283 评论: 0
前段时间做了一个非常有意思的模拟终端的展示页:http://ursb.me/terminal/(没有做移动端适配,请在PC端访问),这个页面非常有意思,它可以作为个人博客系统或者给 Linux 初学者学习终端命令,现分享给大家~
开源地址:airingursb/terminal
打开页面效果如下图所示:

其实这里的样式就直接 Copy 了自己 Mac 上 Terminal 的界面,当然界面上的参数都是自己写的,表示穷人没有钱买这么高配的电脑…
注:截图里面的 logo 是通过archey打印出来的,mac直接输入 brew install archey 即可安装。
命令输入其实只用了一个 input标签实现的:
<span class="prefix">[<span id='usr'>usr</span>@<span class="host">ursb.me</span> <span id="pos">~</span>]% </span> <input type="text" class="input-text">
当然,原始的样式太丑了,肯定要对 input标签做美化:
.input-text { display: inline-block; background-color: transparent; border: none; -moz-appearance: none; -webkit-appearance: none; outline: 0; box-sizing: border-box; font-size: 17px; font-family: Monaco, Cutive Mono, Courier New, Consolas, monospace; font-weight: 700; color: #fff; width: 300px; padding-block-end: 0 }
虽然是在浏览器访问,但毕竟我们要模拟终端的效果,因此对鼠标的样式最好也修改一下:
* { cursor: text; }
每次打印新的内容其实是一个在之前 html 的基础上拼接新的内容再重新绘制的过程。渲染时机是用户按下回车键,因此需要监听keydown事件;渲染函数是mainFunc,传入用户输入的内容和用户当前的目录,后者是全局变量,在很多命令中都需要判断用户当前的位置。
e_main.html($('#main').html() + '[<span id="usr">' + usrName + '</span>@<span class="host">ursb.me</span> ' + position + ']% ' + input + '<br/>Nice to Meet U : )<br/>') e_html.animate({ scrollTop: $(document).height() }, 0)
每次渲染之后记得加个滚动动画,让浏览器尽可能真实地模拟终端的行为。
$(document).bind('keydown', function (b) { e_input.focus() if (b.keyCode === 13) { e_main.html($('#main').html()) e_html.animate({ scrollTop: $(document).height() }, 0) mainFunc(e_input.val(), nowPosition) hisCommand.push(e_input.val()) isInHis = 0 e_input.val('') } // Ctrl + U 清空输入快捷键 if (b.keyCode === 85 && b.ctrlKey === true) { e_input.val('') e_input.focus() } })
同时,还实现了一个快捷键 Ctrl + U 清空当前输入,有其他的快捷键读者也可以这样类似去实现。
我们知道,Linix 命令的规范是 command[Options...],以防有用户不了解,首先,我实现了一个最简单的 help命令字。效果如下:
直接看代码,这是直接打印的内容,实现起来非常简单。
switch (command) { case 'help': e_main.html($('#main').html() + '[<span id="usr">' + usrName + '</span>@<span class="host">ursb.me</span> ' + position + ']% ' + input + '<br/>' + '[sudo ]command[ Options...]<br/>You can use following commands:<br/><br/>cd<br/>ls<br/>cat<br/>clear<br/>help<br/>exit<br/><br/>Besides, there are some hidden commands, try to find them!<br/>') e_html.animate({ scrollTop: $(document).height() }, 0) break }
其中 command取 input 标签第一个空格前的元素即可:
command = input.split(' ')[0]
既然知道了怎么取命令字,那各种打印类型的命令字都是可以自己作为小彩蛋实现~ 这里就不一一举例了,读者可以阅读源码自行了解。
clear是清空控制台,实现起来非常简单,根据我们的渲染逻辑,直接清空外层div中的内容即可。
case 'clear': e_main.html('') e_html.animate({ scrollTop: $(document).height() }, 0) break
既然是博客系统,总不能全部的内容都放在前端页面的代码上进行渲染,固定的 help命令或者简单的打印命令是这样做是可以的。但如果我们的目录结构变动了,或者想写一篇新文章,或者修改文件的内容,那则需要我们大幅度去修改静态 html 文件的代码,这显然是不现实的。
本系统还配套实现了相应的后台,服务端的作用是用来读取存放在服务端的目录和文件内容,并提供对应的接口以便将数据返回给前端。
服务器存储的文件层级如下:
接下来,来看几个稍有难度的功能吧。
ls命令用来显示目标列表,在 Linux 中是使用率较高的命令。 ls命令的输出信息可以进行彩色加亮显示,以分区不同类型的文件。
因此,我们的实现该功能的三个重点是:
对于第一点,在 mainFunc中的第二参数是必传的,它是我们精心维护的一个全局变量(在 cd命令中进行维护)。
对于第二点,我们在后端提供了一个接口:
router.get('/ls', (req, res) => { let { dir } = req.query glob(`src/file${dir}**`, {}, (err, files) => { if (dir === '/') { files = files.map(i => i.replace('src/file/', '')) files = files.filter(i => !i.includes('/')) // 过滤掉二级目录 } else { // 如果不在根目录,则替换掉当前目录 dir = dir.substring(1) files = files.map(i => i.replace('src/file/', '').replace(dir, '')) files = files.filter(i => !i.includes('/') && !i.includes(dir.substring(0, dir.length - 1))) // 过滤掉二级目录和当前目录 } return res.jsonp({ code: 0, data: files.map(i => i.replace('src/file/', '').replace(dir, '')) }) }) })
文件遍历这里我们用到了第三方的开源库glob。如果用户在主目录,我们需要过滤掉二级目录下的文件,因为ls只能看到本目录下的内容;如果用户在其他目录,我们还需要过滤掉当前目录,因为glob返回的数据包含有当前目录的名字。
之后,前端直接调用就好:
case 'ls': // dir: /dir/ $.ajax({ url: host + '/ls', data: { dir: position.replace('~', '') + '/' }, dataType: 'jsonp', success: (res) => { if (res.code === 0) { let data = res.data.map(i => { if (!i.includes('.')) { // 目录 i = `<span class="ls-dir">${i}</span>` } return i }) e_main.html($('#main').html() + '[<span id="usr">' + usrName + '</span>@<span class="host">ursb.me</span> ' + position + ']% ' + input + '<br/>' + data.join(' ') + '<br/>') e_html.animate({ scrollTop: $(document).height() }, 0) } } }) break
前端这里我们根据是否文件名中是否具有'.'来区分是目录和文件的,给目录加上新的样式。但我们这样区分其实并不严谨,因为目录名其实也可以具备'.',目录本质上也是一个文件。严谨的方法应该根据系统的 ls-l命令判断,我们要实现的博客系统没有这么复杂,因此就简单根据'.'判断也是适用的。
实现效果如下:
服务端提供接口,pos为用户当前的位置,dir是用户想要切换的相对路径。需要注意的是,这里过滤了文件,因为cd命令后面的参数只能接目录;同时这里并没有过滤掉二级目录,因为cd命令后续接的是目录的路径,有可能是深层级的。对于目录不存在的情况,只需要返回一个错误码和提示即可。
router.get('/cd', (req, res) => { let { pos, dir } = req.query glob(`src/file${pos}**`, {}, (err, files) => { pos = pos.substring(1) files = files.filter(i => !i.includes('.')) // 过滤掉文件 files = files.map(i => i.replace('src/file/', '').replace(pos, '')) dir = dir.substring(0, dir 技术沙龙 教程文章 热点综合