成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專欄INFORMATION COLUMN

web模擬終端博客系統(tǒng)

番茄西紅柿 / 3451人閱讀

摘要:每次渲染之后記得加個(gè)滾動(dòng)動(dòng)畫,讓瀏覽器盡可能真實(shí)地模擬終端的行為。我們的模擬終端暫時(shí)只是文件和目錄的讀取操作,所以自動(dòng)補(bǔ)全的前提是,系統(tǒng)存儲(chǔ)有完整的目錄和文件。

本文由QQ音樂前端團(tuán)隊(duì)發(fā)表

前段時(shí)間做了一個(gè)非常有意思的模擬終端的展示頁(yè):http://ursb.me/terminal/(沒有做移動(dòng)端適配,請(qǐng)?jiān)赑C端訪問),這個(gè)頁(yè)面非常有意思,它可以作為個(gè)人博客系統(tǒng)或者給 Linux 初學(xué)者學(xué)習(xí)終端命令,現(xiàn)分享給大家~

開源地址:airingursb/terminal

0x01 樣式

打開頁(yè)面效果如下圖所示:

其實(shí)這里的樣式就直接 Copy 了自己 Mac 上 Terminal 的界面,當(dāng)然界面上的參數(shù)都是自己寫的,表示窮人沒有錢買這么高配的電腦…

注:截圖里面的 logo 是通過archey打印出來的,mac直接輸入 brew install archey 即可安裝。

命令輸入其實(shí)只用了一個(gè) input標(biāo)簽實(shí)現(xiàn)的:

[usr@ursb.me ~]% 

當(dāng)然,原始的樣式太丑了,肯定要對(duì) input標(biāo)簽做美化:

.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
}

雖然是在瀏覽器訪問,但畢竟我們要模擬終端的效果,因此對(duì)鼠標(biāo)的樣式最好也修改一下:

* {
    cursor: text;
}

0x02 渲染邏輯

每次打印新的內(nèi)容其實(shí)是一個(gè)在之前 html 的基礎(chǔ)上拼接新的內(nèi)容再重新繪制的過程。渲染時(shí)機(jī)是用戶按下回車鍵,因此需要監(jiān)聽keydown事件;渲染函數(shù)是mainFunc,傳入用戶輸入的內(nèi)容和用戶當(dāng)前的目錄,后者是全局變量,在很多命令中都需要判斷用戶當(dāng)前的位置。

e_main.html($('#main').html() + '[' + usrName + '@ursb.me ' + position + ']% ' + input + '
Nice to Meet U : )
') e_html.animate({ scrollTop: $(document).height() }, 0)

每次渲染之后記得加個(gè)滾動(dòng)動(dòng)畫,讓瀏覽器盡可能真實(shí)地模擬終端的行為。

$(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()
  }
})

同時(shí),還實(shí)現(xiàn)了一個(gè)快捷鍵 Ctrl + U 清空當(dāng)前輸入,有其他的快捷鍵讀者也可以這樣類似去實(shí)現(xiàn)。

0x03 help

我們知道,Linix 命令的規(guī)范是 command[Options...],以防有用戶不了解,首先,我實(shí)現(xiàn)了一個(gè)最簡(jiǎn)單的 help命令字。效果如下:

直接看代碼,這是直接打印的內(nèi)容,實(shí)現(xiàn)起來非常簡(jiǎn)單。

switch (command) {
    case 'help':
      e_main.html($('#main').html() + '[' + usrName + '@ursb.me ' + position + ']% ' + input + '
' + '[sudo ]command[ Options...]
You can use following commands:

cd
ls
cat
clear
help
exit

Besides, there are some hidden commands, try to find them!
') e_html.animate({ scrollTop: $(document).height() }, 0) break }

其中 command取 input 標(biāo)簽第一個(gè)空格前的元素即可:

command = input.split(' ')[0]

既然知道了怎么取命令字,那各種打印類型的命令字都是可以自己作為小彩蛋實(shí)現(xiàn)~ 這里就不一一舉例了,讀者可以閱讀源碼自行了解。

0x04 clear

clear是清空控制臺(tái),實(shí)現(xiàn)起來非常簡(jiǎn)單,根據(jù)我們的渲染邏輯,直接清空外層div中的內(nèi)容即可。

case 'clear':
  e_main.html('')
  e_html.animate({ scrollTop: $(document).height() }, 0)
  break

既然是博客系統(tǒng),總不能全部的內(nèi)容都放在前端頁(yè)面的代碼上進(jìn)行渲染,固定的 help命令或者簡(jiǎn)單的打印命令是這樣做是可以的。但如果我們的目錄結(jié)構(gòu)變動(dòng)了,或者想寫一篇新文章,或者修改文件的內(nèi)容,那則需要我們大幅度去修改靜態(tài) html 文件的代碼,這顯然是不現(xiàn)實(shí)的。

本系統(tǒng)還配套實(shí)現(xiàn)了相應(yīng)的后臺(tái),服務(wù)端的作用是用來讀取存放在服務(wù)端的目錄和文件內(nèi)容,并提供對(duì)應(yīng)的接口以便將數(shù)據(jù)返回給前端。

服務(wù)器存儲(chǔ)的文件層級(jí)如下:

接下來,來看幾個(gè)稍有難度的功能吧。

0x05 ls

ls命令用來顯示目標(biāo)列表,在 Linux 中是使用率較高的命令。 ls命令的輸出信息可以進(jìn)行彩色加亮顯示,以分區(qū)不同類型的文件。

因此,我們的實(shí)現(xiàn)該功能的三個(gè)重點(diǎn)是:

  1. 獲取用戶當(dāng)前的位置
  2. 獲取當(dāng)前位置下的所有文件和目錄
  3. 需要區(qū)分出文件和目錄,以便區(qū)分樣式

對(duì)于第一點(diǎn),在 mainFunc中的第二參數(shù)是必傳的,它是我們精心維護(hù)的一個(gè)全局變量(在 cd命令中進(jìn)行維護(hù))。

對(duì)于第二點(diǎn),我們?cè)诤蠖颂峁┝艘粋€(gè)接口:

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('/')) // 過濾掉二級(jí)目錄
    } else {
      // 如果不在根目錄,則替換掉當(dāng)前目錄
      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))) // 過濾掉二級(jí)目錄和當(dāng)前目錄
    }
    return res.jsonp({ code: 0, data: files.map(i => i.replace('src/file/', '').replace(dir, '')) })
  })
})

文件遍歷這里我們用到了第三方的開源庫(kù)glob。如果用戶在主目錄,我們需要過濾掉二級(jí)目錄下的文件,因?yàn)閘s只能看到本目錄下的內(nèi)容;如果用戶在其他目錄,我們還需要過濾掉當(dāng)前目錄,因?yàn)間lob返回的數(shù)據(jù)包含有當(dāng)前目錄的名字。

之后,前端直接調(diào)用就好:

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 = `${i}`
          }
          return i
        })
        e_main.html($('#main').html() + '[' + usrName + '@ursb.me ' + position + ']% ' + input + '
' + data.join('  ') + '
') e_html.animate({ scrollTop: $(document).height() }, 0) } } }) break

前端這里我們根據(jù)是否文件名中是否具有.來區(qū)分是目錄和文件的,給目錄加上新的樣式。但我們這樣區(qū)分其實(shí)并不嚴(yán)謹(jǐn),因?yàn)槟夸浢鋵?shí)也可以具備.,目錄本質(zhì)上也是一個(gè)文件。嚴(yán)謹(jǐn)?shù)姆椒☉?yīng)該根據(jù)系統(tǒng)的 ls-l命令判斷,我們要實(shí)現(xiàn)的博客系統(tǒng)沒有這么復(fù)雜,因此就簡(jiǎn)單根據(jù).判斷也是適用的。

實(shí)現(xiàn)效果如下:

0x06 cd

服務(wù)端提供接口,pos為用戶當(dāng)前的位置,dir是用戶想要切換的相對(duì)路徑。需要注意的是,這里過濾了文件,因?yàn)閏d命令后面的參數(shù)只能接目錄;同時(shí)這里并沒有過濾掉二級(jí)目錄,因?yàn)閏d命令后續(xù)接的是目錄的路徑,有可能是深層級(jí)的。對(duì)于目錄不存在的情況,只需要返回一個(gè)錯(cuò)誤碼和提示即可。

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.length - 1)
    if (files.indexOf(dir) === -1) {
      // 目錄不存在
      return res.jsonp({ code: 404, message: 'cd: no such file or directory: ' + dir })
    } else {
      return res.jsonp({ code: 0 })
    }
  })
})

前端直接調(diào)用就好,但是這里要區(qū)分幾種情況:

  1. 回退到主目錄:cd || cd ~ || cd ~/
  2. 切換到其他目錄
    1. 切換到絕對(duì)路徑的其他層級(jí):cd ~/dir
    2. 切換為相對(duì)路徑的更深層級(jí):cd dir || cd ./dir || cd ../dir || cd .. || cd ../ || cd ../../
    3. 用戶在主目錄:cd ~/dir || cd ./dir || cd dir
    4. 用戶在其他目錄:cd .. || cd ../ || cd ../dir || cd dir || cd ./dir

對(duì)于情境1,實(shí)現(xiàn)比較簡(jiǎn)單,直接將當(dāng)前目錄切回~即可。

if (!input.split(' ')[1] || input.split(' ')[1] === '~' || input.split(' ')[1] === '~/') {
    // 回退到主目錄:cd || cd ~ || cd ~/
    nowPosition = '~'
    e_main.html($('#main').html() + '[' + usrName + '@ursb.me ' + position + ']% ' + input + '
') e_html.animate({ scrollTop: $(document).height() }, 0) e_pos.html(nowPosition) }

對(duì)于情境2之所以還判斷是否在主目錄,是因?yàn)榻馕鲆?guī)則不一樣。其實(shí)也可以做個(gè)兼容合并成一種情況。由于代碼比較長(zhǎng),這里只列出最復(fù)雜的情境2.2.2的代碼:

let pos = '/' + nowPosition.replace('~/', '') + '/'
let backCount = input.split(' ')[1].match(/..//g) && input.split(' ')[1].match(/..//g).length || 0

pos = nowPosition.split('/') // [~, blog, img]
nowPosition = pos.slice(0, pos.length - backCount) // [~, blog]
nowPosition = nowPosition.join('/') // ~/blog

pos = '/' + nowPosition.replace('~', '').replace('/', '')  + '/'
dir = dir + '/'
dir = dir.startsWith('./') && dir.substring(1) || dir // 適配:cd ./dir
$.ajax({
    url: host + '/cd',
    data: { dir, pos },
    dataType: 'jsonp',
    success: (res) => {
      if (res.code === 0) {
        nowPosition = '~' + pos.substring(1) + dir.substring(0, dir.length - 1) // ~/blog/img
        e_main.html($('#main').html() + '[' + usrName + '@ursb.me ' + position + ']% ' + input + '
') e_html.animate({ scrollTop: $(document).height() }, 0) e_pos.html(nowPosition) } else if (res.code === 404) { e_main.html($('#main').html() + '[' + usrName + '@ursb.me ' + position + ']% ' + input + '
' + res.message + '
') e_html.animate({ scrollTop: $(document).height() }, 0) } } })

核心環(huán)節(jié)是計(jì)算回退層數(shù),并根據(jù)回退層數(shù)判斷出回退后的路徑應(yīng)該是什么。回退層數(shù)用正則匹配出路徑中../的數(shù)量即可,而路徑計(jì)算則通過數(shù)組和字符串的相互轉(zhuǎn)換可以輕易實(shí)現(xiàn)。

效果如下:

0x07 cat

cat 命令的實(shí)現(xiàn)和 cd 基本一致,只需要將目錄處理?yè)Q成文件處理即可。

服務(wù)端提供接口:

router.get('/cat', (req, res) => {
  let { filename, dir } = req.query

  // 多級(jí)目錄拼接: 位于 ~/blog/img, cat banner/menu.md
  dir = (dir + filename).split('/')
  filename = dir.pop() // 丟棄最后一級(jí),其肯定是文件
  dir = dir.join('/') + '/'

  glob(`src/file${dir}*.md`, {}, (err, files) => {
    dir = dir.substring(1)
    files = files.map(i => i.replace('src/file/', '').replace(dir, ''))
    filename = filename.replace('./', '')

    if (files.indexOf(filename) === -1) {
      return res.jsonp({ code: 404, message: 'cat: no such file or directory: ' + filename })
    } else {
      fs.readFile(`src/file/${dir}/${filename}`, 'utf-8', (err, data) => {
        return res.jsonp({ code: 0, data })
      })
    }
  })
})

這里的目錄拼接計(jì)算放在了服務(wù)端完成,和之前的拼接方法基本一樣,因?yàn)榕c cd 命令不同,這里 nowPosition 不會(huì)發(fā)生改變,所以可放在服務(wù)端計(jì)算。

若文件存在,讀取文件內(nèi)容返回即可;文件不存在,則返回一個(gè)錯(cuò)誤碼和提示。

與 cd 不同的是, cat 更加簡(jiǎn)單,前端不需要區(qū)分那么多種情況了,直接調(diào)用就好。因?yàn)槲覀儾恍枰倬S護(hù) nowPosition 去計(jì)算當(dāng)前路徑,glob 支持相對(duì)路徑。

case 'cat':
  file = input.split(' ')[1]
  $.ajax({
    url: host + '/cat',
    data: { filename: file, dir: position.replace('~', '') + '/' },
    dataType: 'jsonp',
    success: (res) => {
      if (res.code === 0) {
        e_main.html($('#main').html() + '[' + usrName + '@ursb.me ' + position + ']% ' + input + '
' + res.data.replace(/n/g, '
') + '
') e_html.animate({ scrollTop: $(document).height() }, 0) } else if (res.code === 404) { e_main.html($('#main').html() + '[' + usrName + '@ursb.me ' + position + ']% ' + input + '
' + res.message + '
') e_html.animate({ scrollTop: $(document).height() }, 0) } } }) break

實(shí)現(xiàn)效果如下:

0x08 自動(dòng)補(bǔ)全

熟悉命令行的童鞋應(yīng)該都知道命令行的效率其實(shí)大部分情況都比圖形界面快得多,最主要的一點(diǎn)是因?yàn)槊钚泄ぞ咧С?Tab 自動(dòng)補(bǔ)全命令,這使得用戶只需短短幾個(gè)字符就可以敲出一大串命令。如此使用且基礎(chǔ)的功能,我們當(dāng)然也是需要實(shí)現(xiàn)的。

所謂自動(dòng)補(bǔ)全,前提必然是系統(tǒng)知道補(bǔ)全之后的完整內(nèi)容是啥。我們的模擬終端暫時(shí)只是文件和目錄的讀取操作,所以自動(dòng)補(bǔ)全的前提是,系統(tǒng)存儲(chǔ)有完整的目錄和文件。

這里用兩個(gè)全局變量來分別存儲(chǔ)目錄和文件的數(shù)據(jù)就好,在頁(yè)面一打開時(shí)調(diào)用:

$(document).ready(() => {
  // 初始化目錄和文件
  $.ajax({
    url: host + '/list',
    data: { dir: '/' },
    dataType: 'jsonp',
    success: (res) => {
      if (res.code === 0) {
        directory = res.data.directory
        directory.shift(); // 去掉第一個(gè) ~
        files = res.data.files
      }
    }
  })
})

服務(wù)端接口實(shí)現(xiàn)如下:

router.get('/list', (req, res) => {
  // 用于獲取所有目錄和所有文件
  let { dir } = req.query
  glob(`src/file${dir}**`, {}, (err, files) => {
    if (dir === '/') {
      files = files.map(i => i.replace('src/file/', ''))
    }
    files[0] = '~' // 初始化主目錄
    let directory = files.filter(i => !i.includes('.')) // 過濾掉文件
    files = files.filter(i => i.includes('.')) // 只保留文件

    // 文件根據(jù)層級(jí)排序(默認(rèn)為首字母排序),以便前端實(shí)現(xiàn)最短層級(jí)優(yōu)先匹配
    files = files.sort((a, b) => {
      let deapA = a.match(///g) && a.match(///g).length || 0
      let deapB = b.match(///g) && b.match(///g).length || 0

      return deapA - deapB
    })

    return res.jsonp({ code: 0, data: {directory, files }})
  })
})

額,注釋寫的比較詳盡,看注釋就好了…最后得到的兩個(gè)數(shù)組結(jié)構(gòu)如下:

需要注意的是,對(duì)于目錄而言,我們用的是默認(rèn)的字符表的順序排序的,因?yàn)?cd 到某目錄的自動(dòng)補(bǔ)全,應(yīng)該遵循最短路徑匹配;而對(duì)于文件而言,我們根據(jù)層級(jí)深度拍排序的,因?yàn)?cat 某文件,是根據(jù)最淺路徑匹配的,即應(yīng)優(yōu)先匹配當(dāng)前目錄下的文件。

前端需要監(jiān)聽 Tab 鍵的 keydown 事件:

if (b.keyCode === 9) {
    pressTab(e_input.val())
    b.preventDefault()
    e_html.animate({ scrollTop: $(document).height() }, 0)
    e_input.focus()
  }

對(duì)于pressTab函數(shù),分成了三類情況(因?yàn)槲覀儗?shí)現(xiàn)的帶參數(shù)的命令只有cat和cd):

  1. 補(bǔ)全命令
  2. 補(bǔ)全 cat 命令后的參數(shù)
  3. 補(bǔ)全 cd 命令后的參數(shù)

情況1的實(shí)現(xiàn)有點(diǎn)蠢萌蠢萌的:

command = input.split(' ')[0]
if (command === 'l') e_input.val('ls')
if (command === 'c') {
  e_main.html($('#main').html() + '[' + usrName + '@ursb.me ' + nowPosition + ']% ' + input + '
cat  cd  claer
') } if (command === 'ca') e_input.val('cat') if (command === 'cl' || command === 'cle' || command === 'clea') e_input.val('clea')

對(duì)于情況2,cat 命令自動(dòng)補(bǔ)全只適配文件,即適配我們?nèi)肿兞縡iles里面的元素,需要注意的是處理好前綴./的情況。直接貼代碼了:

if (input.split(' ')[1] && command === 'cat') {
    file = input.split(' ')[1]
    let pos = nowPosition.replace('~', '').replace('/', '') // 去除主目錄的 ~ 和其他目錄的 ~/ 前綴
    let prefix = ''

    if (file.startsWith('./')) {
        prefix = './'
        file = file.replace('./', '')
    }

    if (nowPosition === '~') {
        files.every(i => {
          if (i.startsWith(pos + file)) {
            e_input.val('cat ' + prefix + i)
            return false
          }
          return true
        })
    } else {
        pos = pos + '/'
        files.every(i => {
          if (i.startsWith(pos + file)) {
            e_input.val('cat ' + prefix + i.replace(pos, ''))
            return false
          }
          return true
        })
    }
}

對(duì)于情況3,實(shí)現(xiàn)和情況2基本一致,但是 cd 命令自動(dòng)補(bǔ)全只適配目錄,即配我們?nèi)肿兞縟irectory 里面的元素。由于篇幅問題,且此處實(shí)現(xiàn)和以上代碼基本重復(fù),就不貼了。

0x09 歷史命令

Linux 的終端按上下方向鍵可以翻閱用戶歷史輸入的命令,這也是一個(gè)很重要很基礎(chǔ)的功能,所以我們來實(shí)現(xiàn)一下。

先來幾個(gè)全局變量,以便存儲(chǔ)用戶輸入的歷史命令。

let hisCommand = [] // 歷史命令
let cour = 0 // 指針
let isInHis = 0 // 是否為當(dāng)前輸入的命令,0是,1否

isInHis 變量用于判斷輸入內(nèi)容是否在歷史記錄里,即用戶輸入了內(nèi)容哪怕沒有按回車,按了上鍵之后再按下鍵也依然可以復(fù)現(xiàn)剛才自己輸入的內(nèi)容,不至于清空。(在按回車之后,isInHis = 0)

在監(jiān)聽keydown事件綁定的時(shí)候新增上下方向鍵的監(jiān)聽:

if (b.keyCode === 38) historyCmd('up')
if (b.keyCode === 40) historyCmd('down')

historyCmd 函數(shù)接受的參數(shù)則表明用戶的翻閱順序,是前一條還是后一條。

let historyCmd = (k) => {
  $('body,html').animate({ scrollTop: $(document).height() }, 0)

  if (k !== 'up' || isInHis) {
    if (k === 'up' && isInHis) {
      if (cour >= 1) {
        cour--
        e_input.val(hisCommand[cour])
      }
    }
    if (k === 'down' && isInHis) {
      if (cour + 1 <= hisCommand.length - 1) {
        cour++
        $(".input-text").val(hisCommand[cour])
      } else if (cour + 1 === hisCommand.length) {
        $(".input-text").val(inputCache)
      }
    }
  } else {
    inputCache = e_input.val()
    e_input.val(hisCommand[hisCommand.length - 1])
    cour = hisCommand.length - 1
    isInHis = 1
  }
}

代碼實(shí)現(xiàn)比較簡(jiǎn)單,根據(jù)上下鍵移動(dòng)數(shù)組的指針即可。

本代碼已開源(airingursb/terminal),有興趣的小伙伴可以提交 PR,讓我們一起把模擬終端做的更好~

此文已由作者授權(quán)騰訊云+社區(qū)發(fā)布,更多原文請(qǐng)點(diǎn)擊

搜索關(guān)注公眾號(hào)「云加社區(qū)」,第一時(shí)間獲取技術(shù)干貨,關(guān)注后回復(fù)1024 送你一份技術(shù)課程大禮包!

文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/1473.html

相關(guān)文章

發(fā)表評(píng)論

0條評(píng)論

最新活動(dòng)
閱讀需要支付1元查看
<