分类 软件开发 下的文章

为你的用户提供选项是任何应用的一个重要功能,而 Commander.js 使它变得容易做到。你最喜欢的 JavaScript 命令行构建器是什么?

 title=

JavaScript 是一种为 Web 开发的语言,但它的用处已经远远超出了互联网的范畴。由于 Node.js 和 Electron 这样的项目,JavaScript 既是一种通用的脚本语言,也是一种浏览器组件。有专门设计的 JavaScript 库来构建命令行界面。是的,你可以在你的终端中运行 JavaScript。

现在,当你在终端中输入一个命令时,一般都有 选项,也叫 开关标志,你可以用来修改命令的运行方式。这是由 POSIX 规范 定义的一个有用的惯例,所以作为一个程序员,知道如何检测和解析这些选项是很有帮助的。要从 JavaScript 获得此功能,使用旨在简化构建命令行界面的库很有用。我最喜欢的是 Commander.js。它很简单,很灵活,而且很直观。

安装 node

要使用 Commander.js 库,你必须安装 Node.js。在 Linux 上,你可以用你的包管理器安装 Node。例如,在 Fedora、CentOS、Mageia 和其他系统上:

$ sudo dnf install nodejs

在 Windows 和 macOS 上,你可以 从 nodejs.org 网站下载安装程序

安装 Commander.js

要安装 Commander.js,请使用 npm 命令:

$ npm install commander

在你的 JavaScript 代码中添加一个库

在 JavaScript 中,你可以使用 require 关键字在你的代码中包含(或者导入,如果你习惯于 Python)一个库。创建一个名为 example.js 的文件,并在你喜欢的文本编辑器中打开它。在顶部添加这一行,以包括 Commander.js 库:

const { program } = require('commander');

JavaScript 中的选项解析

要解析选项,你必须做的第一件事是定义你的应用可以接受的有效选项。Commander.js 库可以让你定义短选项和长选项,同时还有一个有用的信息来澄清每个选项的目的。

program
  .description('A sample application to parse options')
  .option('-a, --alpha', 'Alpha')
  .option('-b, --beta <VALUE>', 'Specify a VALUE', 'Foo');

第一个选项,我称之为 --alpha(简写 -a),是一个布尔型开关:它要么存在,要么不存在。它不需要任何参数。第二个选项,我称之为 --beta(简写 -b),接受一个参数,甚至在你没有提供任何参数的情况下指定一个默认值。

访问命令行数据

当你定义了有效的选项,你就可以使用长的选项名称来引用这些值:

program.parse();

const options = program.opts();
console.log('Options detected:');

if (options.alpha) console.log('alpha');
 
const beta = !options.beta ? 'no' : options.beta;
console.log('beta is: %s', beta);

运行应用

试着用 node 命令来运行它,首先不使用选项:

$ node ./example.js 
Options detected: 
beta is: Foo

在用户没有覆盖的情况下,beta 的默认值被使用。

再次运行它,这次使用选项:

$ node ./example.js --beta hello --alpha
Options detected: 
alpha
beta is: hello

这次,测试脚本成功检测到了选项 --alpha,以及用户提供的 --beta 选项的值。

选项解析

下面是完整的演示代码供你参考:

const { program } = require('commander');

program
  .description('A sample application to parse options')
  .option('-a, --alpha', 'Alpha')
    .option('-b, --beta <VALUE>', 'Specify a VALUE', 'Foo');

program.parse();

const options = program.opts();
console.log('Options detected:');

console.log(typeof options);

if (options.alpha) console.log(' * alpha');
const beta = !options.beta ? 'no' : options.beta;
console.log(' * beta is: %s', beta);

在该项目的 Git 仓库 中还有更多例子。

对任何应用来说,包括用户的选项都是一个重要的功能,而 Commander.js 使它很容易做到。除了 Commander.js,还有其他库,但我觉得这个库使用起来很方便快捷。你最喜欢的 JavaScript 命令行构建器是什么?


via: https://opensource.com/article/21/11/javascript-command-line-apps

作者:Seth Kenlon 选题:lujun9972 译者:geekpi 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

使用 argparse 模块为应用程序设置命令行选项。

 title=

有一些第三方库用于命令行解析,但标准库 argparse 与之相比也毫不逊色。

无需添加很多依赖,你就可以编写带有实用参数解析功能的漂亮命令行工具。

Python 中的参数解析

使用 argparse 解析命令行参数时,第一步是配置一个 ArgumentParser 对象。这通常在全局模块内完成,因为单单\_配置\_一个解析器没有副作用。

import argparse

PARSER = argparse.ArgumentParser()

ArgumentParser 中最重要的方法是 .add_argument(),它有几个变体。默认情况下,它会添加一个参数,并期望一个值。

PARSER.add_argument("--value")

查看实际效果,调用 .parse_args()

PARSER.parse_args(["--value", "some-value"])
Namespace(value='some-value')

也可以使用 = 语法:

PARSER.parse_args(["--value=some-value"])
Namespace(value='some-value')

为了缩短在命令行输入的命令,你还可以为选项指定一个短“别名”:

PARSER.add_argument("--thing", "-t")

可以传入短选项:

PARSER.parse_args("-t some-thing".split())
Namespace(value=None, thing='some-thing')

或者长选项:

PARSER.parse_args("--thing some-thing".split())
Namespace(value=None, thing='some-thing')

类型

有很多类型的参数可供你使用。除了默认类型,最流行的两个是布尔类型和计数器。布尔类型有一个默认为 True 的变体和一个默认为 False 的变体。

PARSER.add_argument("--active", action="store_true")
PARSER.add_argument("--no-dry-run", action="store_false", dest="dry_run")
PARSER.add_argument("--verbose", "-v", action="count")

除非显式传入 --active,否则 active 就是 Falsedry-run 默认是 True,除非传入 --no-dry-run。无值的短选项可以并列。

传递所有参数会导致非默认状态:

PARSER.parse_args("--active --no-dry-run -vvvv".split())
Namespace(value=None, thing=None, active=True, dry_run=False, verbose=4)

默认值则比较单一:

PARSER.parse_args("".split())
Namespace(value=None, thing=None, active=False, dry_run=True, verbose=None)

子命令

经典的 Unix 命令秉承了“一次只做一件事,并做到极致”,但现代的趋势把“几个密切相关的操作”放在一起。

gitpodmankubectl 充分说明了这种范式的流行。argparse 库也可以做到:

MULTI_PARSER = argparse.ArgumentParser()
subparsers = MULTI_PARSER.add_subparsers()
get = subparsers.add_parser("get")
get.add_argument("--name")
get.set_defaults(command="get")
search = subparsers.add_parser("search")
search.add_argument("--query")
search.set_defaults(command="search")
MULTI_PARSER.parse_args("get --name awesome-name".split())
Namespace(name='awesome-name', command='get')
MULTI_PARSER.parse_args("search --query name~awesome".split())
Namespace(query='name~awesome', command='search')`

程序架构

使用 argparse 的一种方法是使用下面的结构:

## my_package/__main__.py
import argparse
import sys

from my_package import toplevel

parsed_arguments = toplevel.PARSER.parse_args(sys.argv[1:])
toplevel.main(parsed_arguments)
## my_package/toplevel.py

PARSER = argparse.ArgumentParser()
## .add_argument, etc.

def main(parsed_args):

    ...

    # do stuff with parsed_args

在这种情况下,使用 python -m my_package 运行。或者,你可以在包安装时使用 console\_scprits 入口点。

总结

argparse 模块是一个强大的命令行参数解析器,还有很多功能没能在这里介绍。它能实现你想象的一切。


via: https://opensource.com/article/21/8/python-argparse

作者:Moshe Zadka 选题:lujun9972 译者:MjSeven 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

我通常会抽象地总结我为他人所做的工作(出于显而易见的原因),但是我被允许公开谈论一个网站:Vocal 。我去年为它做了一些 SRE 工作。实际上早在 2 月份,我就在 GraphQL 悉尼会议上做过一次演讲,不过这篇博客推迟了一点才发表。

Vocal 是一个基于 GraphQL 的网站,它获得了人们的关注,然后就遇到了可扩展性问题,而我是来解决这个问题的。这篇文章会讲述我的工作。显然,如果你正在扩展一个 GraphQL 站点,你会发现这篇文章很有用,但其中大部分内容讲的都是当一个站点获得了足够的流量而出现的必须解决的技术问题。如果你对站点可扩展性有兴趣,你可能想先阅读 最近我发表的一系列关于可扩展性的文章

Vocal

Vocal 是一个博客平台,内容包括日记、电影评论、文章评论、食谱、专业或业余摄影、美容和生活小贴士以及诗歌,当然,还有可爱的猫猫和狗狗照片。

Vocal 的不同之处在于,它允许人们制作观众感兴趣的作品而获得报酬。作者的页面每次被浏览都可以获得一小笔钱,还能获得其他用户的捐赠。有很多专业人士在这个平台上展示他们的工作,但对于大多数普通用户来说,他们只是把 Vocal 当作一个兴趣爱好,碰巧还能赚些零花钱作为奖励。

Vocal 是新泽西初创公司 Jerrick Media 的产品,更新:Jerrick Media 已经更名为 Creatd,在纳斯达克上市。2015 年,他们与 Thinkmill 合作一起开发,Thinkmill 是一家悉尼中型软件开发咨询公司,擅长 JavaScript、React 和 GraphQL 开发。

剧透

不幸的是,有人告诉我,由于法律原因,我不能提供具体的流量数字,但公开的信息可以说明。Alexa 对所有网站按照流量进行排名。这是我演讲中展示的 Alexa 排名图,从 2019 年 11 月到今年 2 月,Vocal 流量增长到全球排名第 5567 位。

去年 11 月到今年 2 月 Vocal 的全球排名从 9574 名增长到 5567 名

曲线增长变慢是正常的,因为它需要越来越多的流量来赢得每个位置。Vocal 现在排名 4900 名左右,显然还有很长的路要走,但对于一家初创公司来说,这一点也不寒酸。大多数初创公司都很乐意与 Vocal 互换排名。

在网站升级后不久,Creatd 开展了一项营销活动,使流量翻了一番。在技术方面,我们要做的就是观察仪表盘上的上升的数字。自发布以来的 9 个月里,只有两个平台问题需要员工干预:3 月份每五年一次的 AWS RDS 证书轮换,以及一款应用推出时遇到的 Terraform 错误。作为一名 SRE,我很高兴看到 Vocal 不需要太多的平台工作来保持运行。更新:该系统也抗过了 2020 年的美国大选,没有任何意外。

以下是本文技术内容的概述:

  • 技术和历史背景
  • 从 MongoDB 迁移到 Postgres
  • 部署基础设施的改造
  • 使应用程序兼容扩展措施
  • 让 HTTP 缓存发挥作用
  • 其他一些性能调整

一些背景信息

Thinkmill 使用 Next.js(一个基于 React 的 Web 框架)构建了一个网站,和 Keystone 在 MongoDB 前面提供的 GraphQL API 进行交互。Keystone 是一个基于 GraphQL 的无头 CMS 库:在 JavaScripy 中定义一个模式,将它与一些数据存储挂钩,并获得一个自动生成的 GraphQL API 用于数据访问。这是一个自由开源软件项目,由 Thinkmill 提供商业支持。

Vocal V2

Vocal 的第一版就受到了关注,它找到了一个喜欢它的用户群,并不断壮大,最终 Creatd 请求 Thinkmill 帮助开发 V2,并于去年 9 月成功推出。Creatd 员工避免了 第二个系统效应,他们一般都是根据用户的反馈进行改变,所以他们 主要是 UI 和功能更改,我就不赘述了。相反,我将讨论下我的工作内容:使新站点更加健壮和可扩展。

声明:我很感谢能与 Creatd 以及 Thinkmill 在 Vocal 上的合作,并且他们允许我发表这个故事,但 我仍然是一名独立顾问,我写这篇文章没有报酬,甚至没有被要求写它,这仍然是我自己的个人博客。

迁移数据库

Thinkmill 在使用 MongoDB 时遇到了几个可扩展性问题,因此决定升级到 Keystone 5 以利用其新的 Postgres 支持。

如果你从事技术工作的时间足够长,那你可能还记得 00 年代末的 “NOSQL” 营销,这可能听起来很有趣。NoSQL 的一个重要特点是,像 Postgres 这样的关系数据库(SQL)不像 MongoDB 这样“网站级规模”的 NoSQL 数据库那样具有可扩展性。从技术上将,这种说法是正确的,但 NoSQL 数据库的可扩展性来自它可以有效处理各种查询的折衷。简单的非关系数据库(如文档数据库和键值数据库)有其一席之地,但当它们用作应用的通用后端时,应用程序通常会在超出关系数据库的理论扩展限制之前,就超出了数据库的查询限制。Vocal 的原本的大多数数据库查询在 MongoDB 上运行良好,但随着时间推移,越来越多的查询需要特殊技巧才能工作。

在技术要求方面,Vocal 与维基百科非常相似。维基百科是世界上最大的网站之一,它运行在 MySQL(或者说它的分支 MariaDB)上。当然,这需要一些重要的工程来实现,但在可预见的未来,我认为关系数据库不会对 Vocal 的扩展构成严重威胁。

我做过一个比较,托管的 AWS RDS Postgres 实例的成本不到旧 MongoDB 实例的五分之一,但 Postgres 实例的 CPU 使用率仍然低于 10%,尽管它提供的流量比旧站点多。这主要是因为一些重要的查询在文档数据库架构下一直效率很低。

迁移的过程可以新写一篇博客文章来讲述,但基本上是 Thinkmill 开发人员构建了一个 ETL 管道,使用 MoSQL 来完成这项繁重的工作。由于 Keystone 对于 Postgres 支持仍然比较基础,但它是一个 FOSS 项目,所以我能够解决在 SQL 生成性能方面遇到的问题。对于这类事情,我总是推荐 Markys Winand 的 SQL 博文:使用 Luke 索引现代 SQL。他的文章很友好,甚至对那些暂时不太关注 SQL 人来说也是容易理解的,但他拥有你大多数需要的理论知识。如果你仍然有问题,一本好的、专注于即可性能的书可以帮助你。

平台

架构

V1 是几个 Node.js 应用,运行在 Cloudflare(作为 CDN)背后的单个虚拟专用服务器(VPS)上。我喜欢把避免过度工程化作为一个高度优先事项,所以我对这一架构竖起了大拇指。然而,在 V2 开始开发时,很明显,Vocal 已经超越了这个简单的架构。在处理巨大峰值流量时,它没有给 Thinkmill 开发人员提供很多选择,而且它很难在不停机情况下安全部署更新。

这是 V2 的新架构:

Vocal V2 的技术架构,请求从 CDN 进入,然后经过 AWS 的负载均衡。负载均衡将流量分配到两个应用程序 “Platform” 和 “Website”。“Platform” 是一款 Keystone 应用程序,将数据存储在 Redis 和 Postgres 中。

基本上就是两个 Node.js 应用程序复制放在负载均衡器后面,非常简单。有些人认为可扩展架构要比这复杂得多,但是我曾经在一些比 Vocal 规模大几个数量级的网站工作过,它们仍然只是在负载均衡器后面复制服务,带有 DB 后端。你仔细想想,如果平台架构需要随着站点的增长而变得越来越复杂,那么它就不是真正“可扩展的”。网站可扩展性主要是解决那些破坏可扩展的实现细节。

如果流量增长得足够多,Vocal 的架构可能需要一些补充,但它变得更加复杂的主要原因是新功能。例如,如果(出于某种原因)Vocal 将来需要处理实时地理空间数据,那将是一个与博客文章截然不同的技术,所以我预期它会进行架构上的更改。大型网站架构的复杂性主要来自于复杂的功能。

不知道未来的架构应该是什么样子很正常,所以我总是建议你尽可能从简单开始。修复一个简单架构要比复杂架构更容易,成本也更低。此外,不必要的复杂架构更有可能出现错误,而这些错误将更难调试。

顺便说一下,Vocal 分成了两个应用程序,但这并不重要。一个常见的扩展错误是,以可扩展的名义过早地将应用分割成更小的服务,但将应用分割在错误的位置,从而导致更多的可扩展性问题。作为一个单体应用,Vocal 可以扩展的很好,但它的分割做的也很好。

基础设施

Thinkmill 有一些人有使用 AWS 经验,但它主要是一个开发车间,需要一些比之前的 Vocal 部署更容易上手的东西。我最终在 AWS Fargate 上部署了新的 Vocal,这是弹性容器服务(ECS)的一个相对较新的后端。在过去,许多人希望 ECS 作为一个“托管服务运行 Docker 容器”的简单产品,但人们仍然必须构建和管理自己的服务器集群,这让人感到失望。使用 ECS Fargate,AWS 就可以管理集群了。它支持运行带有复制、健康检查、滚动更新、自动缩放和简单警报等基本功能的 Docker 容器。

一个好的替代方案是平台即服务(PaaS),比如 App Engine 或 Heroku。Thinkmill 已经在简单的项目中使用它们,但通常在其他项目中需要更大的灵活性。在 PaaS 上运行的网站规模要大得多,但 Vocal 的规模决定了使用自定义云部署是有经济意义的。

另一个明显的替代方案是 Kubernetes。Kubernetes 比 ECS Fargate 拥有更多的功能,但它的成本要高得多 —— 无论是资源开销还是维护(例如定期节点升级)所需的人员。一般来说,我不向任何没有专门 DevOps 员工的地方推荐 Kubernetes。Fargate 具有 Vocal 需要的功能,使得 Thinkmill 和 Creatd 能专心于网站改进,而不是忙碌于搭建基础设施。

另一种选择是“无服务器”功能产品,例如 AWS Lambda 或 Google 云。它们非常适合处理流量很低或很不规则的服务,但是 ECS Fargate 的自动伸缩功能足以满足 Vocal 的后端。这些产品的另一个优点是,它们允许开发人员在云环境中部署东西,但无需了解很多关于云环境的知识。权衡的结果是,无服务器产品与开发过程、测试以及调试过程紧密耦合。Thinkmill 内部已经有足够的 AWS 专业知识来管理 Fargate 的部署,任何知道如何开发 Node.js 简单的 Hello World 应用程序的开发人员都可以在 Vocal 上工作,而无需了解无服务器功能或 Fargate 的知识。

ECS Fargate 的一个明显缺点是供应商锁定。但是,避免供应商锁定是一种权衡,就像避免停机一样。如果你担心迁移,那么在平台独立性花费比迁移上更多的钱是没有意义的。在 Vocal 中,依赖于 Fargate 的代码总量小于 500 行 Terraform。最重要的是 Vocal 应用程序代码本身与平台无关,它可以在普通开发人员的机器上运行,然后打包成一个 Docker 容器,几乎可以运行在任何可以运行 Docker 容器的地方,包括 ECS Fargate。

Fargate 的另一个缺点是设置复杂。与 AWS 中的大多数东西一样,它处于一个 VPC、子网、IAM 策略世界中。幸运的是,这类东西是相对静态的(不像服务器集群一样需要维护)。

制作一个可扩展的应用程序

如果你想轻松地运行一个大规模的应用程序,需要做一大堆正确的事情。如果你遵循 应用程序设计的十二个守则 the Twelve-Factor App design ,事情会变得容易,所以我不会在这里赘述。

如果员工无法规模化操作,那么构建一个“可扩展”系统就毫无意义 —— 就像在独轮车上安装喷气式发动机一样。使 Vocal 可扩展的一个重要部分是建立 CI/CD 和 基础设施即代码 之类的东西。同样,有些部署的思路也不值得考虑,因为它们使生产与开发环境相差太大(参阅 应用程序设计守则第十守则)。生产和开发之间的任何差异都会降低应用程序的开发速度,并且(在实践中)最终可能会导致错误。

缓存

缓存是一个很大的话题 —— 我曾经做过 一个关于 HTTP 缓存的演讲,相对比较基础。我将在这里谈论缓存对于 GraphQL 的重要性。

首先,一个重要的警告:每当遇到性能问题时,你可能会想:“我可以将这个值放入缓存中吗,以便再次使用时速度更快?”微基准测试 总是 告诉你:是的。 然而,由于缓存一致性等问题,随处设置缓存往往会使整个系统 变慢。以下是我对于缓存的检查表:

  1. 是否需要通过缓存解决性能问题
  2. 再仔细想想(非缓存的性能往往更加健壮)
  3. 是否可以通过改进现有的缓存来解决问题
  4. 如果所有都失败了,那么可以考虑添加新的缓存

在 Web 系统中,你经常使用的一个缓存是 HTTP 缓存系统,因此,在添加额外缓存之前,试着使用 HTTP 缓存是一个好主意。我将在这篇文章中重点讨论这一点。

另一个常见的陷阱是使用哈希映射或应用程序内部其他东西进行缓存。它在本地开发中效果很好,但在扩展时表现糟糕。最好的办法是使用支持显式缓存库,支持 Redis 或 Memcached 这样的可插拔后端。

基础知识

HTTP 规范中有两种类型缓存:私有和公共。私有缓存不会和多个用户共享数据 —— 实际上就是用户的浏览器缓存。其余的就是公共缓存。它们包括受你控制的(例如 CDN、Varnish 或 Nginx 等服务器)和不受你控制的(代理)。代理缓存在当今的 HTTPS 世界中很少见,但一些公司网络会有。

缓存查找键通常基于 URL,因此如果你遵循“相同的内容,相同的 URL;不同的内容,不同的 URL” 规则,即,给每个页面一个规范的 URL,避免从同一个 URL 返回不同的内容这样“聪明”的技巧,缓存就会强壮一点。显然,这对 GraphQL API 端点有影响(我将在后面讨论)。

你的服务器可以采用自定义配置,但配置 HTTP 缓存的主要方法是在 Web 响应上设置 HTTP 头。最重要的标头是 cache-control。下面这一行说明所有缓存都可以缓存页面长达 3600 秒(一小时):

cache-control: max-age=3600, public

对于针对用户的页面(例如用户设置页面),重要的是使用 private 而不是 public 来告诉公共缓存不要存储响应,防止其提供给其他用户。

另一个常见的标头是 vary,它告诉缓存,响应是基于 URL 之外的一些内容而变化。(实际上,它将 HTTP 头和 URL 一起添加到缓存键中。)这是一个非常生硬的工具,这就是为什么尽可能使用良好 URL 结构的原因,但一个重要的用例是告诉浏览器响应取决于登录的 cookie,以便在登录或注销时更新页面。

vary: cookie

如果页面根据登录状态而变化,你需要 cache-control:private(和 vary:cookie),即使是在公开的、已登出的页面版本上,以确保响应不会混淆。

其他有用的标头包括 etaglast-modified,但我不会在这里介绍它们。你可能仍然会看到一些诸如 expirespragma:cache 这种老旧的 HTTP 标头。它们早在 1997 年就被 HTTP/1.1 淘汰了,所以我只在我想禁用缓存或者我感到偏执时才使用它们。

客户端标头

鲜为人知的是,HTTP 规范允许在客户端请求中使用 cache-control 标头以减少缓存时间并获得最新响应。不幸的是,似乎大多数浏览器都不支持大于 0 的 max-age ,但如果你有时在更新后需要一个最新响应,no-cache 会很有用。

HTTP 缓存和 GraphQL

如上,正常的缓存键是 URL。但是 GraphQL API 通常只使用一个端点(比如说 /api/)。如果你希望 GraphQL 查询可以缓存,你需要查询参数出现在 URL 路径中,例如 /api/?query={user{id}}&variables={"x":99}(忽略了 URL 转义)。诀窍是将 GraphQL 客户端配置为使用 HTTP GET 请求进行查询(例如,apollo-link-http 设置为 useGETForQueries )。

Mutation 不能缓存,所以它们仍然需要使用 HTTP POST 请求。对于 POST 请求,缓存只会看到 /api/ 作为 URL 路径,但缓存会直接拒绝缓存 POST 请求。请记住,GET 用于非 Mutation 查询(即幂等),POST 用于 Mutation(即非幂等)。在一种情况下,你可能希望避免使用 GET 查询:如果查询变量包含敏感信息。URL 经常出现在日志文件、浏览器历史记录和聊天中,因此 URL 中包含敏感信息通常是一个坏主意。无论如何,像身份验证这种事情应该作为不可缓存的 Mutation 来完成,这是一个特殊的情况,值得记住。

不幸的是,有一个问题:GraphQL 查询往往比 REST API URL 大得多。如果你简单地切换到基于 GET 的查询,你会得到一些非常长的 URL,很容易超过 2000 字节的限制,目前一些流行的浏览器和服务器还不会接受它们。一种解决方案是发送某种查询 ID,而不是发送整个查询,即类似于 /api/?queryId=42&variables={"x":99}。Apollo GraphQL 服务器对此支持两种方式:

一种方法是 从代码中提取所有 GraphQL 查询,并构建一个服务器端和客户端共享的查询表。缺点之一是这会使构建过程更加复杂,另一个缺点是它将客户端项目与服务器项目耦合,这与 GraphQL 的一个主要卖点背道而驰。还有一个缺点是 X 版本和 Y 版本的代码可能识别一组不同的查询,这会成为一个问题,因为 1:复制的应用程序将在更新推出或回滚期间提供多个版本,2:客户端可能会使用缓存的 JavaScript,即使你升级或降级服务器。

另一种方式是 Apollo GraphQL 所宣称的 自动持久查询(APQ)。对于 APQ 而言,查询 ID 是查询的哈希值。客户端向服务器发出请求,通过哈希查询。如果服务器无法识别该查询,则客户端会在 POST 请求中发送完整的查询,服务器会保存此次查询的散列值,以便下次识别。

HTTP 缓存和 Keystone 5

如上所述,Vocal 使用 Keystone 5 生成 GraphQL API,而 Keystone 5 和 Apollo GraphQL 服务器配合一起工作。那么我们是如何设置缓存标头的呢?

Apollo 支持 GraphQL 模式的 缓存提示 cache hint 。巧妙地是,Apollo 会收集查询涉及的所有内容的所有缓存提示,然后它会自动计算适当的缓存标头值。例如,以这个查询为例:

query userAvatarUrl {
    authenticatedUser {
        name
        avatar_url
    }
}

如果 name 的最长期限为 1 天,而 avatar_url 的最长期限为 1 小时,则整体缓存最长期限将是最小值,即 1 小时。authenticatedUser 取决于登录 cookie,因此它需要一个 private 提示,它会覆盖其他字段的 public,因此生成的 HTTP 头将是 cache-control:max-age=3600,private

对 Keystone 列表和字段添加了缓存提示支持。以下是一个简单例子,在文档的待办列表演示中给一个字段添加缓存提示:

const keystone = new Keystone({
  name: 'Keystone To-Do List',
  adapter: new MongooseAdapter(),
});

keystone.createList('Todo', {
  schemaDoc: 'A list of things which need to be done',
  fields: {
    name: {
      type: Text,
      schemaDoc: 'This is the thing you need to do',
      isRequired: true,
      cacheHint: {
        scope: 'PUBLIC',
        maxAge: 3600,
      },
    },
  },
});

另一个问题:CORS

令人沮丧的是, 跨域资源共享 Cross-Origin Resource Sharing (CORS)规则会与基于 API 网站中的缓存产生冲突。

在深入问题细节之前,让我们跳到最简单的解决方案:将主站点和 API 放在一个域名上。如果你的站点和 API 位于同一个域名上,就不必担心 CORS 规则(但你可能需要考虑 限制 cookie)。如果你的 API 专门用于网站,这是最简单的解决方案,你可以愉快地跳过这一节。

在 Vocal V1 中,网站(Next.js)和平台(Keystone GraphQL)应用程序处于不同域(vocal.mediaapi.vocal.media)。为了保护用户免受恶意网站的侵害,现代浏览器不会随便让一个网站与另一个网站进行交互。因此,在允许 vocal.mediaapi.vocal.media 发出请求之前,浏览器会对 api.vocal.media 进行“预检”。这是一个使用 OPTIONS 方法的 HTTP 请求,主要是询问跨域资源共享是否允许。预检通过后,浏览器会发出最初的正常请求。

令人沮丧的是,预检是针对每个 URL 的。浏览器为每个 URL 发出一个新的 OPTIONS 请求,服务器每次都会响应。服务器没法说 vocal.media 是所有 api.vocal.media 请求的可信来源 。当所有内容都是对一个 API 端点的 POST 请求时,这个问题并不严重,但是在为每个查询提供 GET 式 URL 后,每个查询都因预检而延迟。更令人沮丧的是,HTTP 规范说 OPTIONS 请求不能被缓存,所以你会发现你所有的 GraphQL 数据都被完美地缓存在用户身旁的 CDN 中,但浏览器仍然必须向源服务器发出所有的预检请求。

如果你不能只使用一个共享的域,有几种解决方案。

如果你的 API 足够简单,你或许可以利用 CORS 规则的例外

某些缓存服务器可以配置为忽略 HTTP 规范,任何情况都会缓存 OPTIONS 请求。例如,基于 Varnish 的缓存和 AWS CloudFrone。这不如完全避免预检那么有效,但比默认的要好。

另一个很魔改的选项是 JSONP。当心:如果做错了,那么可能会创建安全漏洞。

让 Vocal 更好地利用缓存

让 HTTP 缓存在底层工作之后,我需要让应用程序更好地利用它。

HTTP 缓存的一个限制是它在响应级别上要么是全有要么是全无的。大多数响应都可以缓存,但如果一个字节不能缓存,那整个页面就无法缓存。作为一个博客平台,大多数 Vocal 数据都是可缓存的,但在旧网站上,由于右上角的菜单栏,几乎没有页面可以缓存。对于匿名用户,菜单栏将显示邀请用户登录或创建账号的链接。对于已登录用户,它会变成用户头像和用户个人资料菜单,因为页面会根据用户登录状态而变化,所以无法在 CDN 中缓存任何页面。

Vocal 的一个典型页面。该页面的大部分内容都是高度可缓存的内容,但在旧网站中,由于右上角有一个小菜单,实际上没有一个内容是可缓存的。

这些页面是由 React 组件的服务器端渲染(SSR)的。解决方法是将所有依赖于登录 cookie 的 React 组件,强制它们 只在客户端进行延迟呈现。现在,服务器会返回完全通用的页面,其中包含用于个性化组件(如登录菜单栏)的占位符。当页面在浏览器中加载时,这些占位符将通过调用 GraphQL API 在客户端填充。通用页面可以安全地缓存到 CDN 中。

这一技巧不仅提高了缓存命中率,还帮助改善了人们感知的页面加载时间。空白屏幕和旋转动画让我们不耐烦,但一旦第一个内容出现,它会分散我们几百毫秒的注意力。如果人们在社交媒体上点击一个 Vocal 帖子的链接,主要内容就会立即从 CDN 中出现,很少有人会注意到,有些组件直到几百毫秒后才会完全出现。

顺便说一下,另一个让用户更快地看到第一个内容的技巧是 流式渲染,而不是等待整个页面渲染完成后再发送。不幸的是,Node.js 还不支持这个功能

拆分响应来提高可缓存性也适用于 GraphQL。通过一个请求查询多个数据片段的能力通常是 GraphQL 的一个优势,但如果响应的不同部分具有差别很大的缓存,那么最好将它们分开。举个简单的例子,Vocal 的分页组件需要知道当前页面的页数和内容。最初,组件在一个查询中同时获取两个页面,但由于页面的总数是所有页面的一个常量,所有我将其设置为一个单独的查询,以便缓存它。

缓存的好处

缓存的明显好处是它减轻了 Vocal 后端服务器的负载。这很好。但是依赖缓存来获得容量是危险的,你仍然需要一个备份计划,以便当有一天你不可避免地放弃缓存。

提高页面响应速度是使用缓存是一个更好的理由。

其他一些好处可能不那么明显。峰值流量往往是高度本地化的。如果一个有很多社交媒体粉丝的人分享了一个页面的链接,那么 Vocal 的流量就会大幅上升,但主要是指向分享的那个页面及其元素。这就是为什么缓存擅长吸收最糟糕的流量峰值,它使后端流量模式相对更平滑,更容易被自动伸缩处理。

另一个好处是 优雅的退化 graceful degradation 。即使后端由于某些原因出现了严重的问题,站点最受欢迎的部分仍然可以通过 CDN 缓存来提供服务。

其他的性能调整

正如我常说的,可扩展的秘诀不是让事情变得更复杂。它只是让事情变得不比需要的更复杂,然后彻底解决所有阻碍扩展的东西。扩展 Vocal 的规模涉及到许多小事,在这篇文章中无法一一说明。

一个经验:对于分布式系统中难以调试的问题,最困难的部分通常是获取正确的数据,从而了解发生的原因。我能想到很多时候,我被困住了,只能靠猜测来“即兴发挥”,而不是想办法找到正确的数据。有时这行得通,但对复杂的问题却不行。

一个相关技巧是,你可以通过获取系统中每个组件的实时数据(甚至只是 tail -F 的日志),在不同的窗口中显示,然后在另一个窗口中单击网站来了解很多信息。比如:“为什么切换这个复选框会在后台产生这么多的 DB 查询?”

这里有个修复的例子。有些页面需要几秒钟以上的时间来呈现,但这个情况只会在部署环境中使用 SSR 时会出现。监控仪表盘没有显示任何 CPU 使用量峰值,应用程序也没有使用磁盘,所以这表明应用程序可能正在等待网络请求,可能是对后端的请求。在开发环境中,我可以使用 sysstat 工具来记录 CPU、RAM、磁盘使用情况,以及 Postgres 语句日志和正常的应用日志来观察应用程序是如何工作的。Node.js 支持探测跟踪 HTTP 请求,比如使用 bpftrace,但处于某些无聊的原因,它们不能在开发环境中工作,所以我在源代码中找到了探针,并创建了一个带有请求日志的 Node.js 版本。我使用 tcpdump 记录网络数据,这让我找到了问题所在:对于网站发出的每一个 API 请求,都要创建一个新的网络连接到 “Platform”。(如果这都不起作用,我想我会在应用程序中添加请求跟踪功能。)

网络连接在本地机器上速度很快,但在现实网络上却不可忽略。建立加密连接(比在生产环境中)需要更长时间。如果你向一个服务器(比如一个 API)发出大量请求,保持连接打开并重用它是很重要的。浏览器会自动这么做,但 Node.js 默认不会,因为它不知道你是否发出了很多请求,所以这个问题只出现在 SSR 上。与许多漫长的调试过程一样,修复却非常简单:只需将 SSR 配置为 保持连接存活,这样会使页面的呈现时间大幅下降。

如果你想了解更多这方面的知识,我强烈建议你阅读《高性能浏览器网络》这本书(可免费在线阅读),并跟进 Brendan Gregg 发表的指南

你的站点呢?

实际上,我们还可以做很多事情来提升 Vocal 的速度,但我们没有全做。这是因为在初创公司和在大公司身为一个固定员工做 SRE 工作还是有很大区别的。我们的目标、预算和发布日期都很紧张,但最终我们的网站得到了很大改善,给了用户他们想要的东西。

同样的,你的站点有它自己的目标,并且可能与 Vocal 有很大的不同。然而,我希望这篇文章和它的链接至少能给你一些有用的思路,为用户创造更好的东西。


via: https://theartofmachinery.com/2020/06/29/scaling_a_graphql_site.html

作者:Simon Arneaud 选题:lujun9972 译者:MjSeven 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

如果你 在 Ubuntu 上运行 Java 程序 ,使用 Eclipse、Maven 或 Netbeans 等等,你将需要将 JAVA_HOME 环境变量设置为正确的路径。否则,你的系统将会向你控诉 “java\_home 环境变量没有设置”。

在这篇初学者教程中,我将向你展示在 Ubuntu 上正确地设置 JAVA_HOME 变量的步骤。这些步骤应该也适用于大多数的其它的 Linux 发行版。

设置过程包含这些步骤:

  • 确保已安装 Java 开发工具包(JDK)。
  • 查找 JDK 可执行文件的正确的位置。
  • 设置 JAVA_HOME 环境变量,并永久更改它。

步骤 1: 核查 JDK 是否已经安装

核查 Java 开发工具包(JDK)是否已经安装在你的 Linux 系统上的最简单的方法是运行这个命令:

javac --version

上面的命令将核查 Java 编译器的版本。如果已经安装了 Java 编译器,它将显示 Java 版本:

Java Compiler is installed

如果上面的命令显示像这样未找到 javac 命令的错误信息,你得先安装 JDK :

Java Compiler is not installed

如果在你的系统上并没有安装 Java 编译器,使用这条命令来安装 Java 开发工具包 (JDK):

sudo apt install default-jdk

这将在你当前的 Ubuntu 版本中安装默认的 Java 版本。如果你需要一些其它版本的 Java 版本,那么你必须 在 Ubuntu 中安装 Java 时 具体指出它的版本。

在你确保 Java 编译器存在于你的系统之中后,接下来就到了查找其位置的时候了。

步骤 2: 获取 JDK 可执行文件(Java 编译器)的位置

可执行文件通常位于 /usr/lib/jvm 目录之中。但我不会让你来玩一个猜谜游戏,让我们来找出 Java 可执行文件的路径。

使用 which 命令 来获取 Java 编译器可执行文件的位置:

which javac

在这里的问题是,它给出的位置实际上是一个 符号链接 。你将需要按照下图执行几次:

最简单的方法是直接使用下面这条命令跟随符号链接来以获取实际的可执行文件:

readlink -f `which java`

readlink 命令会跟随一个符号链接。我在 which java 的外侧使用 readlink 将会使用 which java 的输出来替换要检查的符号链接,这被称之为命令替换。因此,在这个实例中,上面的命令大体上相当于 readlink -f /usr/bin/java

在我的示例中,可执行文件的位置是 /usr/lib/jvm/java-11-openjdk-amd64/bin/java 。对你来说可能会不一样。在你的系统中,复制上述命令所获取的正确的路径。你知道,你可以 在 Ubuntu 的终端中复制和粘贴

步骤 3: 设置 JAVA\_HOME 变量

现在,你已经获取了位置,使用它来设置 JAVA_HOME 环境变量:

export JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64/bin/java

核查 JAVA_HOME 目录的值:

echo $JAVA_HOME

尝试在同一个终端中运行你的 Java 程序或工程,并查看它是否工作。

这尚未结束。你刚刚声明的 JAVA_HOME 环境变量是临时的。如果你关闭这个终端或开始一个新的会话,它将会再次变成空的。

为了“永久地”设置 JAVA_HOME 变量,你应该将其添加到你的家目录中的 .bashrc 文件中。

你可以 在 Linux 终端中使用 Nano 编辑器来编辑文件。 如果你不想使用它,并想采取一种简单的复制和粘贴的方法,使用下面的命令:

首先备份你的 .bashrc 文件(以防万一你把它弄坏了,你还可以将其再恢复回来):

cp ~/.bashrc ~/.bashrc.bak

接下来,使用 echo 命令来追加 在这一节开头使用的 export 命令。你应该适当地更改下面的命令,以便其正确地使用你的系统所显示的路径

echo "export JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64/bin/java" >> ~/.bashrc

验证它已经被正确地添加到文件的结尾处:

tail -3 ~/.bashrc

上面的 tail 命令 将显示所具体指定文件的最后 3 行。

这里是上面的三个命令的全部的输出:

现在,即使你退出会话或重新启动系统,JAVA_HOME 环境变量都仍将设置为你所具体指定的值。这就是你所想要的,对吧?

注意,如果你将来更改默认的 Java 版本,你将需要更改 JAVA_HOME 环境变量的值并将其指向正确的可执行文件的路径。

我希望这篇教程不仅会帮助你设置 JAVA_HOME 环境变量,也会教会你如何完成这项工作。

如果你仍然面临难题或者有一些疑问或建议,请在评论区告诉我。


via: https://itsfoss.com/set-java-home-ubuntu/

作者:Abhishek Prakash 选题:lujun9972 译者:robsean 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

让你的网站根据用户选择的主题进行调整的能力是一个很棒的辅助功能。

 title=

你可能已经熟悉 媒体查询 media query 了。它们被广泛地用于使网站具有响应性。widthheight 属性包含视区的尺寸。然后,你可以使用 CSS 在不同的尺寸下呈现不同的布局。

prefers-color-scheme 媒体查询 的工作方式与此相同。用户可以将他们的操作系统配置为使用浅色或深色主题。prefers-color-scheme 包含这个值。该值是 lightdark ,尽管 W3C 规范指出它可能支持未来的值,如 sepia。我为这两种模式指定不同的 CSS 变量值,让用户的操作系统来决定。

prefers-color-scheme 媒体查询

prefers-color-scheme 媒体查询的两种变化是:

/* Light mode */
@media (prefers-color-scheme: light) {
   :root {
       --body-bg: #FFFFFF;
       --body-color: #000000;
   }
}

/* Dark mode */
@media (prefers-color-scheme: dark) {
   :root {
       --body-bg: #000000;
       --body-color: #FFFFFF;
   }
}

在上面的 CSS 中,--body-bg--body-colorCSS 变量。正如你所看到的,它们对两种模式都包含不同的值。在浅色主题中,我设置了一个白色背景和黑色文本。在深色主题中,我设置了黑色背景和白色文本。

因为规范说 W3C 可能会引入未来的值,所以把这个 CSS 转换为默认值是有意义的。

/* Light mode */
:root {
   --body-bg: #FFFFFF;
   --body-color: #000000;
}

/* Dark mode */
@media (prefers-color-scheme: dark) {
   :root {
       --body-bg: #000000;
       --body-color: #FFFFFF;
   }
}

在上面的代码中,我默认定义了一个浅色主题,如果媒体查询是 dark,则将其转换为深色主题。这样一来,以后任何添加到媒体查询的值都会默认设置为浅色主题。

使用 CSS 变量

现在我为不同的主题设置了不同的值,我需要实际使用它们来设计页面。

body {
   background: var(--body-bg);
   color: var(--body-color);
}

var() 语法 是 CSS 使用变量的方式。在上面的代码中,我是说把 background 设置为 --body-bg 的值,把 color 设置为 --body-color 的值。注意,这些变量的值来自媒体查询。这意味着背景和前景的颜色是根据操作系统的设置而改变的!

这就是媒体查询的真正能力。提供一个从操作系统到网页的一致的用户体验。

如果你进入 findmymastodon.com,并切换你的操作系统的主题,你会看到从一个主题到另一个主题的过渡。

CSS 工作组 网站也使用同样的媒体查询。改变你的操作系统主题,网站就会切换主题来进行调整。

结论

请注意,使用 prefers-color-scheme 与使用普通的编程语言没有什么不同。我定义了一些变量,这些变量的值根据一些逻辑而改变。而这些变量然后被用于进一步的操作。

让你的网站根据用户选择的主题进行调整的能力是一个很棒的辅助功能。而且,为了用户的利益,它进一步模糊了桌面和网络之间的界限。最新的浏览器版本 支持 prefers-color-scheme,所以你今天就可以开始实验了。

编码愉快。

这篇文章最初发表在 作者的网站 上,经许可后重新发表。


via: https://opensource.com/article/21/10/dark-themes-websites

作者:Ayush Sharma 选题:lujun9972 译者:geekpi 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

使用 Linux 或 FreeDOS 从一个 C 程序中生成彩色的 ASCII 艺术。

 title=

利用扩展 ASCII 字符集和它的绘画元素集合的全彩 ASCII 艺术在 DOS 上曾经相当流行。你可以在你的下一个 FreeDOS 程序中加入 ASCII 艺术,作为一个很酷的“欢迎”屏幕,或者作为一个提供了更多程序信息的彩色“退出”屏幕,来增加一点视觉上的乐趣。

但是,这种 ASCII 艺术的风格并不仅仅局限于 FreeDOS 程序。你可以在 Linux 终端模式的程序中使用同样的方法。虽然 Linux 使用 ncurses 来控制屏幕,而不是 DOS 的 conio,但相关的概念也适用于 Linux 程序。本文探讨了如何从 C 语言程序中生成彩色 ASCII 艺术。

ASCII 艺术文件

你可以使用各种工具来绘制你的 ASCII 艺术。在这个例子中,我使用了一个叫做 TheDraw 的老式 DOS 应用程序,但是你可以在 Linux 上找到现代的开源 ASCII 艺术程序,比如 Moebius(Apache 许可证)或者 PabloDraw(MIT 许可证)。只要你知道保存的数据是什么样子的,你使用什么工具并不重要。

下面是一个 ASCII 艺术文件样本的一部分,以 C 源代码保存。请注意,这个代码片段定义了几个值。IMAGEDATA_WIDTHIMAGEDATA_DEPTH 定义了屏幕上的列数和行数。在这里,它是一个 80x25 的 ASCII 艺术“图像”。IMAGEDATA_LENGTH 定义了 IMAGEDATA 数组中的条目数量。ASCII 艺术画面中的每个字符可以用两个字节的数据表示。要显示的字符和包含该字符的前景和背景颜色的颜色属性。对于一个 80x25 的屏幕,每个字符都与一个属性配对,该数组包含 4000 个条目(即 80*25*2=4000)。

#define IMAGEDATA_WIDTH 80
#define IMAGEDATA_DEPTH 25
#define IMAGEDATA_LENGTH 4000
unsigned char IMAGEDATA [] = {
    '.', 0x08,  ' ', 0x08,  ' ', 0x08,  ' ', 0x08,  ' ', 0x08,  ' ', 0x08,
    ' ', 0x08,  ' ', 0x08,  '.', 0x0F,  ' ', 0x08,  ' ', 0x08,  ' ', 0x08,
    ' ', 0x08,  ' ', 0x08,  ' ', 0x08,  ' ', 0x08,  ' ', 0x08,  '.', 0x0F,
    ' ', 0x08,  ' ', 0x08,  ' ', 0x08,  ' ', 0x08,  ' ', 0x08,  ' ', 0x08,
    ' ', 0x08,  ' ', 0x08,  ' ', 0x08,  ' ', 0x08,  ' ', 0x08,  ' ', 0x08,

数组的其它部分依此类推。

为了在屏幕上显示这种 ASCII 艺术,你需要写一个小小的程序来读取数组并以正确的颜色打印每个字符。

设置一个颜色属性

这个 ASCII 艺术文件中的颜色属性在一个字节中定义了背景和前景的颜色,用十六进制的值表示,如 0x080x6E。十六进制是适合表达这样的颜色“对”的紧凑方式。

像 Linux 上的 ncurses 或 DOS 上的 conio 这样的字符模式系统 只能显示 16 种颜色。这就是十六种可能的文本颜色和八种背景颜色。用二进制计算十六个值(从 0 到 15)只需要四个二进制位。

1111 是二进制的 15

而且方便的是,十六进制可以用一个字符表示 0 到 15:0123456789ABCDEF。所以十六进制的值 F 是数字 15,或二进制的 1111

通过颜色对,你可以用一个八位的字节来编码背景和前景的颜色。这就是文本颜色的四个二进制位(十六进制中的 0 到 15 或 0 到 F)和背景颜色的三个二进制位(十六进制中的 0 到 7 或 0 到 E)。字节中剩余的二进制位在这里没有使用,所以我们可以忽略它。

为了将颜色对或属性转换成你的程序可以使用的颜色值,你需要 使用位掩码,只指定用于文字颜色或背景颜色的位。使用 FreeDOS 上的 OpenWatcom C 编译器,你可以编写这个函数,从颜色属性中适当地设置颜色。

void
textattr(int newattr)
{
  _settextcolor(newattr & 15);         /* 0000xxxx */
  _setbkcolor((newattr >> 4) & 7);     /* 0xxx0000 */
}

_settextcolor 函数只设置文本颜色,_setbkcolor 函数设置背景颜色。两者都定义在 graph.h 中。注意,由于颜色属性在一个字节值中包括了背景色和前景色,textattr 函数使用 &(二进制的“与”运算)来设置一个位掩码,只隔离了属性中的最后四个位。这就是颜色对存储前景颜色的值 0 到 15 的地方。

为了得到背景色,该函数首先执行了一个位移,将位“推”到右边。这就把“上”位放到了“下”位范围,所以任何像 0xxx0000 这样的位都变成了 00000xxx。我们可以用另一个的位掩码 7(二进制 0111)来挑选出背景颜色值。

显示 ASCII 艺术

IMAGEDATA 数组包含整个 ASCII 艺术屏幕和每个字符的颜色值。为了在屏幕上显示 ASCII 艺术,你的程序需要扫描该数组,设置颜色属性,然后一次在屏幕上显示一个字符。

让我们在屏幕的底部留出空间,以便向用户提供单独的信息或提示。也就是说,我不想显示一个 80 列 ASCII 屏幕的所有 25 行,而只想显示前 24 行。

  /* print one line less than the 80x25 that's in there:
     80 x 24 x 2 = 3840 */

  for (pos = 0; pos < 3840; pos += 2) {
...
  }

for 循环里面,我们需要设置颜色,然后打印字符。OpenWatcom C 编译器提供了一个函数 _outtext 来显示带有当前颜色值的文本。然而,这需要传递一个字符串,如果我们需要一个一个地处理每个字符,在一行中的每个字符需要不同颜色的情况下,效率就会很低。

相反,OpenWatcom 有一个类似的函数,叫做 _outmem,允许你指示要显示多少个字符。对于一次一个字符,我们可以在 IMAGEDATA 数组中提供一个字符值的指针,并告诉 _outtext 只显示一个字符。这将使用当前的颜色属性显示该字符,这就是我们需要的。

  for (pos = 0; pos < 3840; pos += 2) {
    ch = &IMAGEDATA[pos];              /* pointer assignment */
    attr = IMAGEDATA[pos + 1];
 
    textattr(attr);
    _outmem(ch, 1);
  }

这个更新的 for 循环通过向 IMAGEDATA 数组分配一个指针来设置字符 ch。接下来, 循环设置文本属性, 然后用 _outmem 显示字符.

整合起来

有了 textattr 函数和处理数组的 for 循环, 我们可以编写一个完整的程序来显示 ASCII 艺术文件的内容。对于这个例子,将 ASCII 艺术文件保存为 imgdata.inc,并用 #include 语句将其包含在源文件中。

#include <stdio.h>
#include <conio.h>
#include <graph.h>

#include "imgdata.inc"

void
textattr(int newattr)
{
  _settextcolor(newattr & 15);         /* 0000xxxx */
  _setbkcolor((newattr >> 4) & 7);     /* 0xxx0000 */
}

int
main()
{
  char *ch;
  int attr;
  int pos;

  if (_setvideomode(_TEXTC80) == 0) {
    fputs("Error setting video mode", stderr);
    return 1;
  }

  /* draw the array */

  _settextposition(1, 1);              /* top left */

  /* print one line less than the 80x25 that's in there:
     80 x 24 x 2 = 3840 */

  for (pos = 0; pos < 3840; pos += 2) {
    ch = &IMAGEDATA[pos];              /* pointer assignment */
    attr = IMAGEDATA[pos + 1];

    textattr(attr);
    _outmem(ch, 1);
  }

  /* done */

  _settextposition(25, 1);             /* bottom left */

  textattr(0x0f);
  _outtext("Press any key to quit");

  getch();

  textattr(0x00);
  return 0;
}

在 FreeDOS 上使用 OpenWatcom C 编译器编译该程序,你会得到一个显示这个节日信息的新程序。

ASCII艺术中的万圣节信息

万圣节快乐(Jim Hall, CC-BY-SA 4.0)

万圣节快乐,各位!


via: https://opensource.com/article/21/10/ascii-linux-halloween

作者:Jim Hall 选题:lujun9972 译者:wxy 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出