xterm 是一个使用 TypeScript 编写的前端终端组件,可以直接在浏览器中实现一个命令行终端应用。

主要特性:

终端应用程序正常工作:Xterm.js适用于大多数终端应用程序,如bash,vim和tmux,这包括对基于curses的应用程序和鼠标事件支持的支持
Performant:Xterm.js 非常快,它甚至还包括一个GPU加速的渲染器。

丰富的 unicode 支持:支持CJK,表情符号和IME。

自包含:零依赖性。

可访问:可以使用screenReaderMode选项打开屏幕阅读器支持。

还有更多:链接,主题,插件,记录良好的API等。

使用方法:

一、安装:

cnpm i xterm@4.6.0 -S
cnpm i xterm-addon-fit@0.4.0 -S

二、组件中使用:

import React from 'react';
import { Terminal } from 'xterm';
import 'xterm/css/xterm.css';

class WebSSH extends React.Component {
    constructor(props) {
        super(props);
        this.socket = null;
        this.term = new Terminal();
        this.container = null;
        this.input = null;
        this.state = {
            visible: false,
            uploading: false,
            managerDisabled: true,
            host: {},
            percent: 0
        }
    }

    componentDidMount() {
        //alert(document.getElementById("terminal").offsetHeight);

        var term = new Terminal({
            rendererType: "canvas", //渲染类型
            rows: Math.ceil((document.getElementById("terminal").clientHeight-100)/14), //行数
            convertEol: true, //启用时,光标将设置为下一行的开头
            scrollback: 10,//终端中的回滚量
            disableStdin: false, //是否应禁用输入。
            cursorStyle: 'underline', //光标样式
            cursorBlink: true, //光标闪烁
            theme: {
                foreground: 'yellow', //字体
                background: '#060101', //背景色
                cursor: 'help',//设置光标
            }
        });
        term.open(document.getElementById('terminal'));
        term.write('Hello from \x1B[1;3;31mxterm.js\x1B[0m $ ');

        if (term._initialized) {
            return
        }

        term._initialized = true

        term.prompt = () => {
            term.write('\r\n$ ')
        }

        term.writeln('Welcome to xterm.js')
        term.writeln('This is a local terminal emulation, without a real terminal in the back-end.')
        term.writeln('Type some keys and commands to play around.')
        term.writeln('')
        term.prompt()

        const webSocket = new WebSocket('ws://localhost:9000');//建立通道  

        // xterm.4.x 输入
        term.onKey(e => {
            const ev = e.domEvent
            const printable = !ev.altKey && !ev.altGraphKey && !ev.ctrlKey && !ev.metaKey
            if (ev.keyCode === 13) {
                term.prompt()
            } else if (ev.keyCode === 8) {
                // Do not delete the prompt
                if (term._core.buffer.x > 2) {
                    term.write('\b \b')
                }
            } else if (printable) {
                term.write(e.key);
                webSocket.send(e.key);
            }
        })

        //返回
        webSocket.onmessage = function (evt) {
            term.write(evt.data);
        };

        // xterm3.x
        // function runFakeTerminal() {
        //     if (term._initialized) {
        //       return;
        //     }
       
        //     term._initialized = true;
       
        //     term.prompt = () => {
        //       term.write('\r\n$ ');
        //     };
       
        //     term.writeln('Welcome to xterm.js');
        //     term.writeln('This is a local terminal emulation, without a real terminal in the back-end.');
        //     term.writeln('Type some keys and commands to play around.');
        //     term.writeln('');
        //     term.prompt();
       
        //     term.on('key', function (key, ev) {
        //       const printable = !ev.altKey && !ev.altGraphKey && !ev.ctrlKey && !ev.metaKey;
        //       console.log(key,ev.keyCode);
        //       console.log(term._core.buffer.x);
       
        //       if (ev.keyCode === 13) {
        //         term.prompt();
        //       } else if (ev.keyCode === 8) {
        //         // Do not delete the prompt
        //         if (term._core.buffer.x > 2) {
        //           term.write('\b \b');
        //         }
        //       } else if (printable) {
        //         term.write(key);
        //       }
        //     });
       
        //     term.on('paste', function (data) {
        //       term.write(data);
        //     });
        //   }
        //   runFakeTerminal();
    }

    render() {
        return (
            <div id="terminal" style={{height: "100%"}}></div> //terminal容器
        )
    }
}

export default WebSSH
import React from 'react';
import { Button } from 'antd';
import { AuthDiv } from 'components';
import { Terminal } from 'xterm';
import { FitAddon } from 'xterm-addon-fit';
import FileManager from './FileManager';
import { http, X_TOKEN } from 'libs';
import 'xterm/css/xterm.css';
import styles from './index.module.css';


class WebSSH extends React.Component {
  constructor(props) {
    super(props);
    this.id = props.match.params.id;
    this.socket = null;
    this.term = new Terminal();
    this.container = null;
    this.input = null;
    this.state = {
      visible: false,
      uploading: false,
      managerDisabled: true,
      host: {},
      percent: 0
    }
  }

  componentDidMount() {
    this._fetch();
    const fitPlugin = new FitAddon();
    this.term.loadAddon(fitPlugin);
    const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
    this.socket = new WebSocket(`${protocol}//${window.location.host}/api/ws/ssh/${this.id}/?x-token=${X_TOKEN}`);
    this.socket.onmessage = e => this._read_as_text(e.data);
    this.socket.onopen = () => {
      this.term.open(this.container);
      this.term.focus();
      fitPlugin.fit();
    };
    this.socket.onclose = e => {
      if (e.code === 3333) {
        window.location.href = "about:blank";
        window.close()
      } else {
        setTimeout(() => this.term.write('\r\nConnection is closed.\r\n'), 200)
      }
    };
    this.term.onData(data => this.socket.send(JSON.stringify({data})));
    this.term.onResize(({cols, rows}) => {
      this.socket.send(JSON.stringify({resize: [cols, rows]}))
    });
    window.onresize = () => fitPlugin.fit()
  }

  _read_as_text = (data) => {
    const reader = new window.FileReader();
    reader.onload = () => this.term.write(reader.result);
    reader.readAsText(data, 'utf-8')
  };

  handleShow = () => {
    this.setState({visible: !this.state.visible})
  };

  _fetch = () => {
    http.get(`/api/host/?id=${this.id}`)
      .then(res => {
        document.title = res.name;
        this.setState({host: res, managerDisabled: false})
      })
  };

  render() {
    const {host, visible, managerDisabled} = this.state;
    return (
      <div className={styles.container}>
        <div className={styles.header}>
          <div>{host.name} | {host.username}@{host.hostname}:{host.port}</div>
          <AuthDiv auth="host.console.manager">
            <Button disabled={managerDisabled} type="primary" icon="folder-open"
                    onClick={this.handleShow}>文件管理器</Button>
          </AuthDiv>
        </div>
        <div className={styles.terminal}>
          <div ref={ref => this.container = ref}/>
        </div>
        <FileManager id={this.id} visible={visible} onClose={this.handleShow}/>
      </div>
    )
  }
}

export default WebSSH
Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐