本文由QQ音乐前端团队发表

前段时间做了一个非常有意思的模拟终端的展示页:http://ursb.me/terminal/(没有做移动端适配,请在PC端访问),这个页面非常有意思,它可以作为个人博客系统或者给 Linux 初学者学习终端命令,现分享给大家~

开源地址:airingursb/terminal

0x01 样式

打开页面效果如下图所示:

其实这里的样式就直接 Copy 了自己 Mac 上 Terminal 的界面,当然界面上的参数都是自己写的,表示穷人没有钱买这么高配的电脑…

注:截图里面的 logo 是通过archey打印出来的,mac直接输入 brew install archey 即可安装。

命令输入其实只用了一个 input标签实现的:

  1. <span class="prefix">[<span id='usr'>usr</span>@<span class="host">ursb.me</span> <span id="pos">~</span>]% </span>
  2. <input type="text" class="input-text">

当然,原始的样式太丑了,肯定要对 input标签做美化:

  1. .input-text {
  2. display: inline-block;
  3. background-color: transparent;
  4. border: none;
  5. -moz-appearance: none;
  6. -webkit-appearance: none;
  7. outline: 0;
  8. box-sizing: border-box;
  9. font-size: 17px;
  10. font-family: Monaco, Cutive Mono, Courier New, Consolas, monospace;
  11. font-weight: 700;
  12. color: #fff;
  13. width: 300px;
  14. padding-block-end: 0
  15. }

虽然是在浏览器访问,但毕竟我们要模拟终端的效果,因此对鼠标的样式最好也修改一下:

  1. * {
  2. cursor: text;
  3. }

0x02 渲染逻辑

每次打印新的内容其实是一个在之前 html 的基础上拼接新的内容再重新绘制的过程。渲染时机是用户按下回车键,因此需要监听keydown事件;渲染函数是mainFunc,传入用户输入的内容和用户当前的目录,后者是全局变量,在很多命令中都需要判断用户当前的位置。

  1. e_main.html($('#main').html() + '[<span id="usr">' + usrName + '</span>@<span class="host">ursb.me</span> ' + position + ']% ' + input + '<br/>Nice to Meet U : )<br/>')
  2. e_html.animate({ scrollTop: $(document).height() }, 0)

每次渲染之后记得加个滚动动画,让浏览器尽可能真实地模拟终端的行为。

  1. $(document).bind('keydown', function (b) {
  2. e_input.focus()
  3. if (b.keyCode === 13) {
  4. e_main.html($('#main').html())
  5. e_html.animate({ scrollTop: $(document).height() }, 0)
  6. mainFunc(e_input.val(), nowPosition)
  7. hisCommand.push(e_input.val())
  8. isInHis = 0
  9. e_input.val('')
  10. }
  11. // Ctrl + U 清空输入快捷键
  12. if (b.keyCode === 85 && b.ctrlKey === true) {
  13. e_input.val('')
  14. e_input.focus()
  15. }
  16. })

同时,还实现了一个快捷键 Ctrl + U 清空当前输入,有其他的快捷键读者也可以这样类似去实现。

0x03 help

我们知道,Linix 命令的规范是 command[Options...],以防有用户不了解,首先,我实现了一个最简单的 help命令字。效果如下:

直接看代码,这是直接打印的内容,实现起来非常简单。

  1. switch (command) {
  2. case 'help':
  3. 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/>')
  4. e_html.animate({ scrollTop: $(document).height() }, 0)
  5. break
  6. }

其中 command取 input 标签第一个空格前的元素即可:

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

既然知道了怎么取命令字,那各种打印类型的命令字都是可以自己作为小彩蛋实现~ 这里就不一一举例了,读者可以阅读源码自行了解。

0x04 clear

clear是清空控制台,实现起来非常简单,根据我们的渲染逻辑,直接清空外层div中的内容即可。

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

既然是博客系统,总不能全部的内容都放在前端页面的代码上进行渲染,固定的 help命令或者简单的打印命令是这样做是可以的。但如果我们的目录结构变动了,或者想写一篇新文章,或者修改文件的内容,那则需要我们大幅度去修改静态 html 文件的代码,这显然是不现实的。

本系统还配套实现了相应的后台,服务端的作用是用来读取存放在服务端的目录和文件内容,并提供对应的接口以便将数据返回给前端。

服务器存储的文件层级如下:

接下来,来看几个稍有难度的功能吧。

0x05 ls

ls命令用来显示目标列表,在 Linux 中是使用率较高的命令。 ls命令的输出信息可以进行彩色加亮显示,以分区不同类型的文件。

因此,我们的实现该功能的三个重点是:

  1. 获取用户当前的位置
  2. 获取当前位置下的所有文件和目录
  3. 需要区分出文件和目录,以便区分样式

对于第一点,在 mainFunc中的第二参数是必传的,它是我们精心维护的一个全局变量(在 cd命令中进行维护)。

对于第二点,我们在后端提供了一个接口:

  1. router.get('/ls', (req, res) => {
  2. let { dir } = req.query
  3. glob(`src/file${dir}**`, {}, (err, files) => {
  4. if (dir === '/') {
  5. files = files.map(i => i.replace('src/file/', ''))
  6. files = files.filter(i => !i.includes('/')) // 过滤掉二级目录
  7. } else {
  8. // 如果不在根目录,则替换掉当前目录
  9. dir = dir.substring(1)
  10. files = files.map(i => i.replace('src/file/', '').replace(dir, ''))
  11. files = files.filter(i => !i.includes('/') && !i.includes(dir.substring(0, dir.length - 1))) // 过滤掉二级目录和当前目录
  12. }
  13. return res.jsonp({ code: 0, data: files.map(i => i.replace('src/file/', '').replace(dir, '')) })
  14. })
  15. })

文件遍历这里我们用到了第三方的开源库glob。如果用户在主目录,我们需要过滤掉二级目录下的文件,因为ls只能看到本目录下的内容;如果用户在其他目录,我们还需要过滤掉当前目录,因为glob返回的数据包含有当前目录的名字。

之后,前端直接调用就好:

  1. case 'ls':
  2. // dir: /dir/
  3. $.ajax({
  4. url: host + '/ls',
  5. data: { dir: position.replace('~', '') + '/' },
  6. dataType: 'jsonp',
  7. success: (res) => {
  8. if (res.code === 0) {
  9. let data = res.data.map(i => {
  10. if (!i.includes('.')) {
  11. // 目录
  12. i = `<span class="ls-dir">${i}</span>`
  13. }
  14. return i
  15. })
  16. e_main.html($('#main').html() + '[<span id="usr">' + usrName + '</span>@<span class="host">ursb.me</span> ' + position + ']% ' + input + '<br/>' + data.join('&nbsp;&nbsp;') + '<br/>')
  17. e_html.animate({ scrollTop: $(document).height() }, 0)
  18. }
  19. }
  20. })
  21. break

前端这里我们根据是否文件名中是否具有'.'来区分是目录和文件的,给目录加上新的样式。但我们这样区分其实并不严谨,因为目录名其实也可以具备'.',目录本质上也是一个文件。严谨的方法应该根据系统的 ls-l命令判断,我们要实现的博客系统没有这么复杂,因此就简单根据'.'判断也是适用的。

实现效果如下:

0x06 cd

服务端提供接口,pos为用户当前的位置,dir是用户想要切换的相对路径。需要注意的是,这里过滤了文件,因为cd命令后面的参数只能接目录;同时这里并没有过滤掉二级目录,因为cd命令后续接的是目录的路径,有可能是深层级的。对于目录不存在的情况,只需要返回一个错误码和提示即可。

  1. router.get('/cd', (req, res) => {
  2. let { pos, dir } = req.query
  3. glob(`src/file${pos}**`, {}, (err, files) => {
  4. pos = pos.substring(1)
  5. files = files.filter(i => !i.includes('.')) // 过滤掉文件
  6. files = files.map(i => i.replace('src/file/', '').replace(pos, ''))
  7. dir = dir.substring(0, dir.length - 1)
  8. if (files.indexOf(dir) === -1) {
  9. // 目录不存在
  10. return res.jsonp({ code: 404, message: 'cd: no such file or directory: ' + dir })
  11. } else {
  12. return res.jsonp({ code: 0 })
  13. }
  14. })
  15. })

前端直接调用就好,但是这里要区分几种情况:

  1. 回退到主目录:cd || cd ~ || cd ~/
  2. 切换到其他目录
    1. 切换到绝对路径的其他层级:cd ~/dir
    2. 切换为相对路径的更深层级: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

对于情境1,实现比较简单,直接将当前目录切回'~'即可。

  1. if (!input.split(' ')[1] || input.split(' ')[1] === '~' || input.split(' ')[1] === '~/') {
  2. // 回退到主目录:cd || cd ~ || cd ~/
  3. nowPosition = '~'
  4. e_main.html($('#main').html() + '[<span id="usr">' + usrName + '</span>@<span class="host">ursb.me</span> ' + position + ']% ' + input + '<br/>')
  5. e_html.animate({ scrollTop: $(document).height() }, 0)
  6. e_pos.html(nowPosition)
  7. }

对于情境2之所以还判断是否在主目录,是因为解析规则不一样。其实也可以做个兼容合并成一种情况。由于代码比较长,这里只列出最复杂的情境2.2.2的代码:

  1. let pos = '/' + nowPosition.replace('~/', '') + '/'
  2. let backCount = input.split(' ')[1].match(/\.\.\//g) && input.split(' ')[1].match(/\.\.\//g).length || 0
  3. pos = nowPosition.split('/') // [~, blog, img]
  4. nowPosition = pos.slice(0, pos.length - backCount) // [~, blog]
  5. nowPosition = nowPosition.join('/') // ~/blog
  6. pos = '/' + nowPosition.replace('~', '').replace('/', '') + '/'
  7. dir = dir + '/'
  8. dir = dir.startsWith('./') && dir.substring(1) || dir // 适配:cd ./dir
  9. $.ajax({
  10. url: host + '/cd',
  11. data: { dir, pos },
  12. dataType: 'jsonp',
  13. success: (res) => {
  14. if (res.code === 0) {
  15. nowPosition = '~' + pos.substring(1) + dir.substring(0, dir.length - 1) // ~/blog/img
  16. e_main.html($('#main').html() + '[<span id="usr">' + usrName + '</span>@<span class="host">ursb.me</span> ' + position + ']% ' + input + '<br/>')
  17. e_html.animate({ scrollTop: $(document).height() }, 0)
  18. e_pos.html(nowPosition)
  19. } else if (res.code === 404) {
  20. e_main.html($('#main').html() + '[<span id="usr">' + usrName + '</span>@<span class="host">ursb.me</span> ' + position + ']% ' + input + '<br/>' + res.message + '<br/>')
  21. e_html.animate({ scrollTop: $(document).height() }, 0)
  22. }
  23. }
  24. })

核心环节是计算回退层数,并根据回退层数判断出回退后的路径应该是什么。回退层数用正则匹配出路径中'../'的数量即可,而路径计算则通过数组和字符串的相互转换可以轻易实现。

效果如下:

0x07 cat

cat 命令的实现和 cd 基本一致,只需要将目录处理换成文件处理即可。

服务端提供接口:

  1. router.get('/cat', (req, res) => {
  2. let { filename, dir } = req.query
  3. // 多级目录拼接: 位于 ~/blog/img, cat banner/menu.md
  4. dir = (dir + filename).split('/')
  5. filename = dir.pop() // 丢弃最后一级,其肯定是文件
  6. dir = dir.join('/') + '/'
  7. glob(`src/file${dir}*.md`, {}, (err, files) => {
  8. dir = dir.substring(1)
  9. files = files.map(i => i.replace('src/file/', '').replace(dir, ''))
  10. filename = filename.replace('./', '')
  11. if (files.indexOf(filename) === -1) {
  12. return res.jsonp({ code: 404, message: 'cat: no such file or directory: ' + filename })
  13. } else {
  14. fs.readFile(`src/file/${dir}/${filename}`, 'utf-8', (err, data) => {
  15. return res.jsonp({ code: 0, data })
  16. })
  17. }
  18. })
  19. })

这里的目录拼接计算放在了服务端完成,和之前的拼接方法基本一样,因为与 cd 命令不同,这里 nowPosition 不会发生改变,所以可放在服务端计算。

若文件存在,读取文件内容返回即可;文件不存在,则返回一个错误码和提示。

与 cd 不同的是, cat 更加简单,前端不需要区分那么多种情况了,直接调用就好。因为我们不需要再维护 nowPosition 去计算当前路径,glob 支持相对路径。

  1. case 'cat':
  2. file = input.split(' ')[1]
  3. $.ajax({
  4. url: host + '/cat',
  5. data: { filename: file, dir: position.replace('~', '') + '/' },
  6. dataType: 'jsonp',
  7. success: (res) => {
  8. if (res.code === 0) {
  9. e_main.html($('#main').html() + '[<span id="usr">' + usrName + '</span>@<span class="host">ursb.me</span> ' + position + ']% ' + input + '<br/>' + res.data.replace(/\n/g, '<br/>') + '<br/>')
  10. e_html.animate({ scrollTop: $(document).height() }, 0)
  11. } else if (res.code === 404) {
  12. e_main.html($('#main').html() + '[<span id="usr">' + usrName + '</span>@<span class="host">ursb.me</span> ' + position + ']% ' + input + '<br/>' + res.message + '<br/>')
  13. e_html.animate({ scrollTop: $(document).height() }, 0)
  14. }
  15. }
  16. })
  17. break

实现效果如下:

0x08 自动补全

熟悉命令行的童鞋应该都知道命令行的效率其实大部分情况都比图形界面快得多,最主要的一点是因为命令行工具支持 Tab 自动补全命令,这使得用户只需短短几个字符就可以敲出一大串命令。如此使用且基础的功能,我们当然也是需要实现的。

所谓自动补全,前提必然是系统知道补全之后的完整内容是啥。我们的模拟终端暂时只是文件和目录的读取操作,所以自动补全的前提是,系统存储有完整的目录和文件。

这里用两个全局变量来分别存储目录和文件的数据就好,在页面一打开时调用:

  1. $(document).ready(() => {
  2. // 初始化目录和文件
  3. $.ajax({
  4. url: host + '/list',
  5. data: { dir: '/' },
  6. dataType: 'jsonp',
  7. success: (res) => {
  8. if (res.code === 0) {
  9. directory = res.data.directory
  10. directory.shift(); // 去掉第一个 ~
  11. files = res.data.files
  12. }
  13. }
  14. })
  15. })

服务端接口实现如下:

  1. router.get('/list', (req, res) => {
  2. // 用于获取所有目录和所有文件
  3. let { dir } = req.query
  4. glob(`src/file${dir}**`, {}, (err, files) => {
  5. if (dir === '/') {
  6. files = files.map(i => i.replace('src/file/', ''))
  7. }
  8. files[0] = '~' // 初始化主目录
  9. let directory = files.filter(i => !i.includes('.')) // 过滤掉文件
  10. files = files.filter(i => i.includes('.')) // 只保留文件
  11. // 文件根据层级排序(默认为首字母排序),以便前端实现最短层级优先匹配
  12. files = files.sort((a, b) => {
  13. let deapA = a.match(/\//g) && a.match(/\//g).length || 0
  14. let deapB = b.match(/\//g) && b.match(/\//g).length || 0
  15. return deapA - deapB
  16. })
  17. return res.jsonp({ code: 0, data: {directory, files }})
  18. })
  19. })

额,注释写的比较详尽,看注释就好了…最后得到的两个数组结构如下:

需要注意的是,对于目录而言,我们用的是默认的字符表的顺序排序的,因为 cd 到某目录的自动补全,应该遵循最短路径匹配;而对于文件而言,我们根据层级深度拍排序的,因为 cat 某文件,是根据最浅路径匹配的,即应优先匹配当前目录下的文件。

前端需要监听 Tab 键的 keydown 事件:

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

对于pressTab函数,分成了三类情况(因为我们实现的带参数的命令只有cat和cd):

  1. 补全命令
  2. 补全 cat 命令后的参数
  3. 补全 cd 命令后的参数

情况1的实现有点蠢萌蠢萌的:

  1. command = input.split(' ')[0]
  2. if (command === 'l') e_input.val('ls')
  3. if (command === 'c') {
  4. e_main.html($('#main').html() + '[<span id="usr">' + usrName + '</span>@<span class="host">ursb.me</span> ' + nowPosition + ']% ' + input + '<br/>cat&nbsp;&nbsp;cd&nbsp;&nbsp;claer<br/>')
  5. }
  6. if (command === 'ca') e_input.val('cat')
  7. if (command === 'cl' || command === 'cle' || command === 'clea') e_input.val('clea')

对于情况2,cat 命令自动补全只适配文件,即适配我们全局变量files里面的元素,需要注意的是处理好前缀'./'的情况。直接贴代码了:

  1. if (input.split(' ')[1] && command === 'cat') {
  2. file = input.split(' ')[1]
  3. let pos = nowPosition.replace('~', '').replace('/', '') // 去除主目录的 ~ 和其他目录的 ~/ 前缀
  4. let prefix = ''
  5. if (file.startsWith('./')) {
  6. prefix = './'
  7. file = file.replace('./', '')
  8. }
  9. if (nowPosition === '~') {
  10. files.every(i => {
  11. if (i.startsWith(pos + file)) {
  12. e_input.val('cat ' + prefix + i)
  13. return false
  14. }
  15. return true
  16. })
  17. } else {
  18. pos = pos + '/'
  19. files.every(i => {
  20. if (i.startsWith(pos + file)) {
  21. e_input.val('cat ' + prefix + i.replace(pos, ''))
  22. return false
  23. }
  24. return true
  25. })
  26. }
  27. }

对于情况3,实现和情况2基本一致,但是 cd 命令自动补全只适配目录,即配我们全局变量directory 里面的元素。由于篇幅问题,且此处实现和以上代码基本重复,就不贴了。

0x09 历史命令

Linux 的终端按上下方向键可以翻阅用户历史输入的命令,这也是一个很重要很基础的功能,所以我们来实现一下。

先来几个全局变量,以便存储用户输入的历史命令。

  1. let hisCommand = [] // 历史命令
  2. let cour = 0 // 指针
  3. let isInHis = 0 // 是否为当前输入的命令,0是,1否

isInHis 变量用于判断输入内容是否在历史记录里,即用户输入了内容哪怕没有按回车,按了上键之后再按下键也依然可以复现刚才自己输入的内容,不至于清空。(在按回车之后,isInHis = 0)

在监听keydown事件绑定的时候新增上下方向键的监听:

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

historyCmd 函数接受的参数则表明用户的翻阅顺序,是前一条还是后一条。

  1. let historyCmd = (k) => {
  2. $('body,html').animate({ scrollTop: $(document).height() }, 0)
  3. if (k !== 'up' || isInHis) {
  4. if (k === 'up' && isInHis) {
  5. if (cour >= 1) {
  6. cour--
  7. e_input.val(hisCommand[cour])
  8. }
  9. }
  10. if (k === 'down' && isInHis) {
  11. if (cour + 1 <= hisCommand.length - 1) {
  12. cour++
  13. $(".input-text").val(hisCommand[cour])
  14. } else if (cour + 1 === hisCommand.length) {
  15. $(".input-text").val(inputCache)
  16. }
  17. }
  18. } else {
  19. inputCache = e_input.val()
  20. e_input.val(hisCommand[hisCommand.length - 1])
  21. cour = hisCommand.length - 1
  22. isInHis = 1
  23. }
  24. }

代码实现比较简单,根据上下键移动数组的指针即可。

本代码已开源(airingursb/terminal),有兴趣的小伙伴可以提交 PR,让我们一起把模拟终端做的更好~

此文已由作者授权腾讯云+社区发布,更多原文请点击

搜索关注公众号「云加社区」,第一时间获取技术干货,关注后回复1024 送你一份技术课程大礼包!

web模拟终端博客系统的更多相关文章

  1. web开发-Django博客系统

    项目界面图片预览 项目代码github地址 项目完整流程 项目流程: 1 搞清楚需求(产品经理) (1) 基于用户认证组件和Ajax实现登录验证(图片验证码) (2) 基于forms组件和Ajax实现 ...

  2. CentOS 6.2编译安装Nginx1.2.0+MySQL5.5.25+PHP5.3.13+博客系统WordPress3.3.2

    说明: 操作系统:CentOS 6.2 32位 系统安装教程:CentOS 6.2安装(超级详细图解教程): http://www.osyunwei.com/archives/1537.html 准备 ...

  3. Asp.net博客系统收集和简单介绍

    国内Asp.net博客系统收集和简单介绍       [转载文章,仅供个人参考,引自http://www.soyaoo.com/Blog/post/92.html] 1.ZJ-Blog程序简介:基于A ...

  4. 基于开源博客系统(mblog)搭建网站

    基于开源博客系统(mblog)搭建网站 上一章讲了基于jpress部署的博客系统,这一章了解一下 mblog这个开源的基于springboot的博客系统,相比与jpress 的热度fork数量要少一些 ...

  5. 欢迎阅读daxnet的新博客:一个基于Microsoft Azure、ASP.NET Core和Docker的博客系统

    2008年11月,我在博客园开通了个人帐号,并在博客园发表了自己的第一篇博客.当然,我写博客也不是从2008年才开始的,在更早时候,也在CSDN和系统分析员协会(之后名为"希赛网" ...

  6. 一个基于Microsoft Azure、ASP.NET Core和Docker的博客系统

    2008年11月,我在博客园开通了个人帐号,并在博客园发表了自己的第一篇博客.当然,我写博客也不是从2008年才开始的,在更早时候,也在CSDN和系统分析员协会(之后名为“希赛网”)个人空间发布过一些 ...

  7. 从零开始,搭建博客系统MVC5+EF6搭建框架(5),博客详情页、留言、轮播图管理、右侧统计博文

    一.博客系统进度回顾 上一遍博客介绍到,系统已经实现到了发布以及前台布局展示,接下来就是实现一些,详情页,留言.轮播图管理.右侧博文统计信息实现. 二.博客系统详情页实现 2.1先来看看详情页展示的效 ...

  8. 从零开始,搭建博客系统MVC5+EF6搭建框架(4)上,前后台页面布局页面实现,介绍使用的UI框架以及JS组件

    一.博客系统进度回顾以及页面设计 1.1页面设计说明 紧接前面基础基本完成了框架搭建,现在开始设计页面,前台页面设计我是模仿我博客园的风格来设计的,后台是常规的左右布局风格. 1.2前台页面风格 主页 ...

  9. 从零开始,搭建博客系统MVC5+EF6搭建框架(3),添加Nlog日志、缓存机制(MemoryCache、RedisCache)、创建控制器父类BaseController

    一.回顾系统进度以及本章概要 目前博客系统已经数据库创建.以及依赖注入Autofac集成,接下来就是日志和缓存集成,这里日志用的是Nlog,其实还有其他的日志框架如log4,这些博客园都有很多介绍,这 ...

随机推荐

  1. kubernetes namespace Terminating

    1.kubectl get namespace annoying-namespace-to-delete -o json > tmp.jsonthen edit tmp.json and rem ...

  2. linux ">/dev/null 2>&1 &"

    0:表示键盘输入(stdin)1:表示标准输出(stdout),系统默认是1 2:表示错误输出(stderr) command >/dev/null 2>&1 &  == ...

  3. 记一次Django报错Reverse for 'indextwo' with no arguments not found. 1 pattern(s) tried: ['$index/$']

    启动python manage.py runserver 打开127.0.0.1:8000,报错信息如下: Reverse for 'indextwo' with no arguments not f ...

  4. TCP编程

    Socket是网络编程的一个抽象概念,通常我们用一个Socket表示“打开了一个网络连接”,而打开一个Socket需要知道目标计算机的IP地址和端口号,在指定协议类型即可. 客户端 大多数连接就是考的 ...

  5. 【Java】代理模式、反射机制-动态代理

    关于代理模式和动态代理参考自:https://www.cnblogs.com/gonjan-blog/p/6685611.html 这里通过参考博客中的例子整理个人理解. 代理模式: 访问某个类的方法 ...

  6. VS 2015 Android 环境设置

    一般有3个地方需要设置(否则新建项目时会弹出值不能为空 null 参数名:path1.参见:http://www.cnblogs.com/fang8206/p/5020942.html) 1.Tool ...

  7. JavaScript 变量的作用域名

    在JavaScript中,用var申明的变量实际上是有作用域的. 如果一个变量在函数体内部申明,则该变量的作用域为整个函数体,在函数体外不可引用该变量: 'use strict'; function ...

  8. 201771010142 张燕《面向对象程序设计(java)》第三周学习总结

    实验三 Java基本程序设计(2) 实验时间 2018-9-13 1.实验目的与要求 (1)进一步掌握Eclipse集成开发环境下java程序开发基本步骤: (2)熟悉PTA平台线上测试环境: (3) ...

  9. Visual Studio2013 配置opencv3.3.0 x64系统

    注:小白一个,第一次写博客,可能会有一些理解上的错误,只此记录自己测试成功的坎坷之路,已备以后查看,同时给有需要之人. 我是win10 64 位,之前安装了visual studio 2013, 现在 ...

  10. 参考文献bib管理

    比如在IEEE模板中,在当前目录添加 bib 文件reference.bib 在 \end{document} 之前加入 \bibliographystyle{IEEEtran} \bibliogra ...