我觉得有点无聊,所以就把我的网站用 Node 包做了个插件。方法如下。
更新:我已经修改了我的网站,从终端设计开始,但你可以在这里找到该版本。
啊哈,明白了!你上当了,傻瓜。好了,我该停止胡闹了。文章标题说的没错,但有一些需要注意的地方。我的网站实际是这样的:
这篇文章会有点长,所以如果你不想读的话,这里有你需要的链接:
我为什么要这么做?
我讨厌建立网站。
好吧,这有点太夸张了。我写网络软件,但我不喜欢建网站。我更喜欢应用程序。我知道,语义化。
但说真的,我不喜欢担心这张图片应该放在哪里,横幅应该放在哪里,以及一堆链接应该放在哪里。
嘿,这并不意味着我不喜欢漂亮的网站。我喜欢——尤其是当它们是大量功能的关键时。
我更喜欢 Web 应用,那些不仅仅是展示信息的网站,还能接受你的输入并用它来做一些很酷的事情。我尤其喜欢开发它们的后端。
为了向您展示我有多不喜欢网站,以下是我的网站先前版本的样子:
好了,各位。就是这样。一些文字,几个图标。我想你会说它看起来不错,以一种极简主义的方式。我非常想在里面加入一些工程挑战,所以我加了一个 hack 来获取并显示我最近的一些 Medium 文章。
我一直在考虑重建我的网站,尤其是在我不断提升前端技能的时候。但是重建一个网站的想法让我感到无聊,所以我问自己:“我该如何把它做成某种应用程序呢?”
你说过度工程,我说 po-tah-to。
然后我想起几周前我曾尝试用 CSS 和 jQuery 构建一个命令行 UI。为什么不把我的网站变成一个终端呢?我见过几个类似的网站,它们真的很酷。从那时起,我开始思考,我一直想构建一个命令行应用,所以下一个想法就是“动手做!做一个也能在浏览器中运行的命令行应用!”太棒了!
所以我开始工作。
CLI 工具
首先,我知道我将向 CLI 和浏览器公开相同的功能,因此我从以下内容开始src/shalvah.js
(为保持相关性而截断):
const shalvah = {
bio: "Hi, I'm Shalvah. I'm a backend software engineer based in Lagos, Nigeria.",
prompt: 'Want to know more about me? Check me out on these sites:',
links: [
{
name: 'GitHub',
value: 'https://github.com/shalvah',
},
{
name: 'dev.to',
value: 'https://dev.to/shalvah',
},
{
name: 'Twitter',
value: 'https://twitter.com/theshalvah',
}
],
email: 'hello@shalvah.me'
};
module.exports = shalvah;
这个对象保存着我的所有信息。网站或 CLI 中的其他内容只是呈现和与它交互的一种方式。
然后我开始编写 CLI 界面。我使用了三个主要工具:
- commander.js - 用于创建命令行应用程序
- inquirer.js - 用于创建交互式 CLI 提示
- opn - 用于从终端打开东西
在我的 package.json 中:
{
"main": "src/shalvah.js",
"bin": "cli.js"
}
main
:这是我之前创建的文件,所以每次运行require('shalvah')
,最终都会得到这个对象。这样我就可以用浏览器 UI(或者我选择的任何其他 UI)来包裹它。bin
:Node 将链接为可执行文件的文件。因此,当您npm install -g shalvah
,然后运行 时shalvah
,Node 将执行此文件。
以下是内容cli.js
(再次为了相关性而截断):
#!/usr/bin/env node
const program = require('commander');
const inquirer = require('inquirer');
const opn = require('opn');
const shalvah = require('./src/shalvah');
program.description('Shalvah in your command-line')
.parse(process.argv);
console.log(shalvah.bio);
inquirer.prompt({
name: 'link',
type: 'list',
message: shalvah.prompt,
choices: shalvah.links
}).then(answers => {
console.log(`Opening ${answers.link}`);
opn(answers.link);
});
最终,非常简单。得益于这三个很棒的工具,只需几行代码就能创建一个功能齐全的 CLI 应用。
之后,剩下要做的就是将包发布到 NPM,然后使用 安装它npm install -g shalvah
,瞧:
建立网站
这有点复杂。我原本打算安装我的 NPM 包,然后创建一个index.js
作为浏览器入口的程序,也就是cli.js
CLI 的入口。它index.js
会设置终端环境并向包发送调用。进展如何?
出色地...
创建终端 UI
我要做的第一件事就是处理终端 UI。我最近一直在提升我的前端技能,所以很想自己做。最终我决定使用库,因为我意识到我需要很多东西(比如事件处理程序和自定义按键处理程序),这些都需要我花时间编写、测试和重构。而且我对终端、缓冲区和 I/O 流的工作原理也了解不够。
我做了一些研究,发现最可行的选择是xterm.js。Xterm.js是一款功能非常强大的 Web 终端模拟器。可惜的是,它的文档亟待完善,所以我花了很长时间才弄清楚如何使用它。此外,它支持很多功能,但很多功能都是在底层实现的,所以我不得不围绕这些功能编写自定义处理程序。
将控制台移植到浏览器
接下来我想到的是,我非常喜欢控制台上显示的 Inquirer.js 提示符。我也想在网站上也用上它们。同样,我的选择是:自己写代码或者找个库。出于同样的原因,我再次选择了库。我决定在浏览器中使用我在 CLI 中使用过的库(Inquirer.js 和 Commander)。
我面临的一些挑战:
如何在浏览器中使用为命令行设计的包?
这时,Browserify就派上用场了。如果您不熟悉,Browserify 是一款很棒的工具,它允许您在浏览器中使用 Node 包。它还为 Node 提供了诸如 和process
之类的“shim”(shim 就像一个伪造的) __dirname
。
插入指挥官
这相对容易,因为它的 API 要求你传入命令行参数(通常是process.argv
)。在命令行中,运行shalvah help
会填充process.argv
类似于 的内容['/usr/bin/node', 'shalvah', 'help']
,因此在浏览器中我执行了以下操作:
commander.parse([''].concat(args));
集成 Inquirer.js
这是一个更大的问题。它的代码主要读写process.stdout
/ process.stdin
,而 / 是 的实例Readline.Interface
。好消息是:
- 该库依赖于行为(某些方法的存在),而不是继承(
x instanceof Readline.Interface
),并且 - Xterm.js 已经支持 readline 的大部分功能。我编写了一个 shim 函数,假装它
xterm.js
是 readline 接口的一个实例,然后使用Browserify 的别名转换功能,将 Inquirer 期望的 shim 替换成readline
我自己的 shim 函数。这个 shim 函数的简化版本如下所示:
module.exports = {
createInterface({ input, output }) {
// so we don't redefine these properties
if (input.readlineified) {
return input;
}
// normally, input and output should be the same xterm.Terminal instance
input.input = input;
input.output = input;
input.pause = function () {};
input.resume = function () {};
input.close = function () {};
input.setPrompt = function () {};
input.removeListener = input.off.bind(input);
Object.defineProperty(input, 'line', {
get: function () {
return input.textarea.value;
}
});
input.readlineified = true;
return input;
}
};
Xterm.js 已经有一个write
函数,因此不需要定义它。
我还必须做一些非常具体的垫片:
// The most important shim. Used by both Commander and Inquirer.
// We're tricking them into thinking xterm is a TTY
// (see https://nodejs.org/api/tty.html)
term.isTTY = true;
// Xterm is both our input and output
process.stdout = process.stdin = process.stderr = term;
// Shim process.exit so calling it actually halts execution. Used in Commander
process.exit = () => {
term.emit('line-processed');
throw 'process.exit';
};
// catch the process.exit so no error is reported
window.onerror = (n, o, p, e, error) => {
if (error === 'process.exit') {
console.log(error);
return true;
}
};
// For inquirer.js to exit when Ctrl-C is pressed (SIGINT)
process.kill = () => {
process.running = false;
term.writeln('');
term.writeThenPrompt('');
};
适当调整终端尺寸
我面临的另一个挑战是调整终端的大小,使其在桌面端和移动设备上都能很好地显示,并且不出现难看的滚动条。以下是我希望它在移动设备上的效果:
实现起来有点困难,因为终端窗口的大小不仅受 CSS 规则的影响,还受每行行数和列数的影响,而行数和列数又受字体大小的影响。这非常棘手。即使列数多出一个单位,也会出现滚动条。在研究和尝试了多种方法之后,我最终决定采用以下方案:
const term = new Terminal({
cursorBlink: true,
convertEol: true,
fontFamily: "monospace",
fontSize: '14',
rows: calculateNumberOfTerminalRows(),
cols: calculateNumberOfTerminalCols(),
});
// This measures the height of a single character using a div's height
// and uses that to figure out how many rows can fit in about 80% of the screen
function calculateNumberOfTerminalRows() {
let testElement = document.createElement('div');
testElement.innerText = 'h';
testElement.style.visibility = 'hidden';
document.querySelector('.term-container').append(testElement);
testElement.style.fontSize = '14px';
let fontHeight = testElement.clientHeight + 1;
testElement.remove();
return Math.floor(screen.availHeight * 0.8 / fontHeight) - 2;
}
// This measures the width of a single character using canvas
// and uses that to figure out how many columns can fit in about 60% (80% for mobile) of the screen
function calculateNumberOfTerminalCols() {
const ctx = document.createElement("canvas").getContext('2d');
ctx.font = '14px monospace';
const fontWidth = ctx.measureText('h').width + 1;
const screenWidth = screen.availWidth;
return Math.floor(screenWidth * ((screenWidth > 600) ? 0.6 : 0.8) / fontWidth) + 3;
}
这可能看起来有点过度设计,但这是我能想到的最可靠的方法。
颜色支持
我希望我的终端能显示颜色(谁不想呢?),我用的程序是chalk。可惜的是,chalk 似乎无法与 Xterm.js 兼容,所以经过几个小时的调试,我发现问题出在用于检测颜色的外部依赖项chalk 上,于是我把它替换成了我的 shim 程序:
module.exports = {
stdout: {
level: 2,
hasBasic: true,
has256: true,
has16m: false
}
};
移动设备上的切换提示
你会注意到,在我之前的例子中,我使用了一个名为 的 Inquirer 提示符list
,它允许你使用箭头键来选择选项。然而,在移动设备上,通常没有箭头键。所以我不得不切换到rawlist
移动设备上的提示符(使用数字输入):
inquirer.prompt({
name: 'link',
type: (screen.width > 600) ? 'list' : 'rawlist'
});
经过漫长的时间,终端终于可以运行了!
值得吗?
总的来说,这对我来说是一段紧张却有趣的经历,我学到了很多新东西。这是我第一次接触,甚至可以说是第一次学习我在这里描述的大部分内容,所以这真是个胜利。我甚至最终能够使用一些基本的 Unix 实用程序,比如cd
和ls
(试试看!😁😁)
仍然有一些 bug,尤其是在移动设备上,但我实在不想等到一切都完美,所以就直接发布了。希望你们喜欢!
文章来源:https://dev.to/shalvah/i-was-bored-so-i-made-my-website-into-a-node-package-heres-how-2id3