路人甲的世界

路人甲的世界 查看完整档案

武汉编辑武汉理工大学  |  软件工程 编辑  |  填写所在公司/组织 untitled.pw 编辑
编辑

在读软件工程专业本科生,博客untitled.pw。

个人动态

路人甲的世界 发布了文章 · 9月19日

[小技巧] 在zsh中使用鼠标定位光标,简单直接,节省时间,提升命令行操作效率

Linux下的命令行Shell由于其历史原因(需要兼容没有鼠标的设备如电传打字机),往往并不自带鼠标定位光标支持,这就造成一个比较麻烦的问题:如果我写了一个非常复杂的命令,想要修改里面的部分内容,再执行一次,就需要一直按下键盘上的方向键,找到想要替换的内容,狂按 backspace 删除,再回车执行。这一过程费时费力,且在服务器管理等存在延迟的情况下经常出现方向键按太多,又得按回去的问题。本文将为读者介绍在zsh中安装并配置 mouse.zsh 插件,使终端支持鼠标定位,提升命令行操作效率的小技巧,以及综合应用各种方法的最佳实践。
本文原载于未命名小站,由作者本人同步至SegmentFault,转载请注明原作者博客地址或本链接,谢谢!

0x01 现存方案1——快捷键大法

如果你是Shell/Emacs重度用户,看完引言后,你也许会回答Ctrl+A / Ctrl+E 大法好,甚至可能会祭出 Meta+F / Meta+B 绝招,可这些快捷键往往还是无法精准定位到所需位置,依旧没能摆脱重复操作。

的确,部分Shell或窗口管理器如Bash/Tmux支持Vim模式,允许用户使用Vim语法来进行替换/重复/搜索等操作,然而这一过程依旧无法摆脱重复的击键、陡峭的学习曲线与繁琐的记忆。

0x02 现存方案2——sed替换

快捷键的复杂性无法避免,但其实还有一种巧妙的方法可以在不输入快捷键的情况下对上一次输入的命令进行替换,变相满足我们的需求。

假设这里有一个命令:

./server.sh --local-port=8008 --remote-port=1233 --name="server-8008-1233" --quiet --daemon --no-restart --enable-compression --log="./server-8008-1233.log"

我们输入了这个命令,结果发现local-port应该是8088而非8008,这时我们无论是通过方向键还是通过快捷键都很难便捷地一次性将8008修改为8088。但如果使用Bash内建的 ! 语法配合sed,这一操作将会变得非常简单:

!:s/8008/8088/g

这时候Bash会另起一行,并立即将上次命令中的8008替换为8088。

如果你的命令并不在上一次输入,同样不用担心,! 语法提供了丰富的参数可选,这里简单列举两项:

  1. 通过索引定位
!-3:s/8008/8088/g

定位前面倒数第三次输入的命令

  1. 通过前缀定位
!./server:s/8008/8088/g

定位最近一次输入前缀为 ./server 的命令

全部内容可参考 GNU Bash的文档:https://www.gnu.org/software/bash/manual/html_node/History-Interaction.html#History-Interaction

这种方法尽管巧妙,但其替换的本质依旧存在局限性,有没有更好的办法能帮助我们快速定位呢?

0x03 使用鼠标快速定位

其实在鼠标发明之初是并没有指针的,当时的『鼠标指针』只是命令行界面中一个闪烁的光标,操作系统允许用户通过移动鼠标的方式来快速移动光标(如DOS中的命令行鼠标),实现更高效的操作。

随着GUI的普及,我们逐渐形成了『鼠标是为图形界面服务』的思维定式,但如果追溯过往,其实会发现并非如此。正如编辑文本时使用鼠标定位光标更方便,在命令行操作过程中使用鼠标进行定位也是提升操作效率最简单、最直接的方式。

比较可惜的是,大多数终端模拟器或Shell并没有自带鼠标支持,但如果读者们使用的是zsh,有一个很好用的插件 mouse.zsh 可以为zsh新增鼠标支持,安装方法如下:

wget http://stchaz.free.fr/mouse.zsh -O /usr/bin/mouse.zsh
echo ". /usr/bin/mouse.zsh" >> ~/.zshrc
echo "bindkey -M emacs '\em' zle-toggle-mouse" >> ~/.zshrc
source ~/.zshrc

这时我们按下 esc & m (不是同时按,是先按 esc 再按 m) 就会进入鼠标模式(模拟VT200终端),这时终端就已支持使用鼠标定位,我们可以在命令的任意位置点击鼠标左键进行定位,然后按下Ctrl+W删除光标前的单词,最后再按一次 esc & m 退出鼠标模式。这里笔者再举一个例子:

./server.sh --no-restrat --silent

假设笔者不小心把 --no-restart 输入成了 --no-restrat,这时我们按下 esc & m ,拿起鼠标点击 --no-restrat 后的空格,并按下 Ctrl+W 删除这个参数。

./server.sh  --silent

接下来我们输入正确的参数:

./server.sh --no-restart --silent

再按下 esc & m ,退出鼠标选择模式,这时我们就使用鼠标成功编辑了这一命令。

这里有几点需要注意:

  1. 如果觉得 esc & m 还是很累(毕竟要按两个键),可以将安装命令中的 echo "bindkey -M emacs '\em' zle-toggle-mouse" >> ~/.zshrc 替换为 echo "zle-toggle-mouse" >> ~/.zshrc,但缺点是在部分不支持独立编辑模式的终端下无法正常使用鼠标进行框选。
  2. 如果你使用的是MacOS自带 Terminal.app ,发现依旧无法使用鼠标进行定位,请开启自带终端的鼠标报告功能(快捷键:Command + R)。
(方法2可以配合方法1使用,利用鼠标报告功能的开关来区分框选与鼠标选择模式)
  1. \em代表 esc & m,这是ECMA-48所规定的转义序列表,你也可以使用满足ECMA-48规则的其他快捷键来触发鼠标选择模式。

0x04 最佳实践

上面介绍了纯命令行的不便之处,也介绍了使用鼠标进行快速定位的方法,但如果想问什么方法更高效,其实是没有一个固定规则的,不同的方法各自有其适用之处,笔者个人针对以上三种编辑方法总结了一些最佳实践:

1. 使用快捷键的场景

  • 需要编辑的内容在最开始(比如需要加一个sudo)
  • 需要立刻跳转本行到最开始或最后
  • 在本行最开始,想要删除整行内容
  • 需要删除某个单词或参数

2. 使用sed替换的场景

  • 需要重复旧命令,但新命令存在有规律的变化
  • 简单的Typo(如输错参数)

3. 使用鼠标定位的场景

  • 需要重复旧命令,但新命令变化较多/较复杂
  • 复杂的Typo(如漏掉关键参数等)

在读者实际操作过程中所面临的情况可能比本文所描述或总结得更为复杂,因此哪种方法更好其实没有一个定论,但在不同的情况下综合使用不同方法,一定能帮助读者提升命令行操作效率,而本文主要介绍的鼠标定位主要还是为了填补部分场景下的空缺。希望本文能对读者有所帮助、有所启发。
查看原文

赞 0 收藏 0 评论 0

路人甲的世界 发布了文章 · 8月23日

[小技巧]在Windows下使用sshfs-win挂载sftp磁盘到本地,便捷管理Linux中的文件

在进行Linux服务器维护时,文件管理一直是一个比较棘手的问题:FTP/SMB/WebDAV安装步骤繁琐,且用户权限等配置也较为复杂,最重要的是以上几种传输协议默认都是不加密的,配置加密等操作又要付出额外的时间与精力。OpenSSH自带的SFTP能做到开箱即用,且自带加密,但SFTP毕竟只是文件传输协议,在Linux下我们可以使用sshfs配合FUSE将其作为磁盘挂载,在Windows下同样有sshfs-win帮助我们实现这一功能。本文将为读者讲解sshfs-win的原理、安装、使用以及使用过程中一些需要注意的细节。
本文原载于未命名小站,由作者本人同步至知乎,转载请注明原作者博客地址或本链接,谢谢!

0x01 sshfs-win原理介绍

Windows下的sshfs-win与Linux下的sshfs原理基本一致,即它们都是建立在用户空间文件系统的基础上的应用。

sshfs将SFTP协议转换为FUSE的接口,FUSE再将这一接口以文件系统的方式暴露给内核;而sshfs-win则将SFTP协议转换为WinFSP(Windows文件系统代理)提供的接口,WinFSP所做的事情大致可以列举为以下三件:

  1. 注册一个设备驱动,让Windows能够挂载它
  2. 实现NT内核下CreateFile、ReadFile、WriteFile等API,管理读写队列
  3. 调用挂载到WinFSP上应用程序对应的FUSE接口

Windows的内核扩展与驱动程序非常复杂,因此为Windows开发内核态文件系统需要比在Linux下开发内核态文件系统更多的精力,而WinFSP作为FUSE接口到Windows文件系统的桥梁(这就是名字里代理的由来),能以更加全面与完整的API,以及对FUSE良好的兼容性,降低文件系统开发的难度。

WinFSP的具体原理与机制可以参考WinFSP的文档,此处不再赘述,接下来我们开始讲解如何安装与使用依赖WinFSP的sshfs-win。

0x02 sshfs-win安装与使用

上文提到,sshfs-win依赖WinFSP,因此我们要首先安装WinFSP:https://github.com/billziss-gh/winfsp

WinFSP安装结束后,我们再安装sshfs-win:https://github.com/billziss-gh/sshfs-win

两者都安装完毕后,我们在Windows的文件资源管理器中点击『映射网络驱动器』:

在弹出的对话框中,我们选定需要赋予的盘符,然后在文件夹中输入如下内容:

\\sshfs\用户名@IP地址!端口号

需要注意的是这里端口号不是我们通常使用的:,而是!,点击完成后,输入用户名对应的密码即可连接成功。

0x03 一些需要注意的细节

尽管sshfs-win的操作较为简单,但如果你想做更多操作,就会发现各种各样奇奇怪怪的『BUG』,因此这一节笔者将为大家列举两个需要注意的细节。

1. 如何挂载子目录

默认情况下,我们在输入第二节提到的连接串后,会将用户的家目录映射到Windows中,如果我们只是为了管理网站目录、做做备份或者是作为NAS使用,可能需要挂载其他子目录,但这时我们可能会发现,使用如下的连接串会导致连接时出现『位置不可用』的报错:

\\sshfs\用户名@IP地址!端口号\home

这是因为连接串中的路径是以用户家目录为参考的相对路径,即如果我们需要映射/home目录,则需要使用如下连接串

\\sshfs\用户名@IP地址!端口号\..\..\home

其中..\的数量需要根据实际情况决定,这样就能成功映射到我们所需的目录

这样的方式很容易造成误解,也让连接串变得复杂,因此sshfs-win支持另一种更为简明的连接串语法:

\\sshfs.r\用户名@IP地址!端口号\home

这里的r指的是root,即默认从根目录挂载而非家目录,这样就无需使用丑陋的..\回到根目录,我们可以直接从根目录开始,输入绝对路径来挂载我们所需的文件夹。

其实除了sshfs.r以外,还有其他的一些连接串规则能帮助sshfs-win变得更加方便,具体可以参考:https://github.com/billziss-gh/sshfs-win#unc-syntax

2. 如何取消挂载

笔者在使用sshfs-win的时候,经常出现无法卸载盘符的情况,无论通过文件资源管理器右键断开连接,还是通过cmd执行net use 盘符: /delete操作,均会在提示卸载成功后发现盘符依旧存在。

根据这篇Issue:billziss-gh/sshfs-win#119,这可能是由于和其他应用程序的冲突引起。冲突可能不便解决,但我们可以利用上文描述过的原理,即WinFSP为sshfs提供支持,从WinFSP的角度下手解决这个问题。

我们打开命令提示符(或者PowerShell,取决于你的喜好),定位目录到WinFSP的安装目录,默认应该是C:\Program Files (x86)\WinFsp\bin\,然后执行如下命令:

.\launchctl-x64.exe list

这时它会列出正在运行的挂载任务,如下图:

这里可以看到,第一个挂载任务的语法存在错误,这可能是导致它无法正常取消挂载的原因,这时候我们可以执行如下命令来取消挂载:

 .\launchctl-x64.exe stop sshfs root@xxx...

命令执行成功后,我们打开文件资源管理器就会发现之前无法取消挂载的盘符已经成功消失。

0x04 其他的替代品

尽管sshfs-win简单直接,但如果读者需要更多功能(如图形界面、缓存等),就需要其他的替代品来实现同样的功能。这里笔者推荐几款替代品:

  • rclone:一款号称『挂载任何存储服务』的跨平台开源软件,支持范围之广从Amazon S3到Google Drive,或者是更为传统的FTP、SFTP,甚至内存!rclone提供了超过30种存储目标,并提供充分的自定义选项支持,可以实现缓存、权限等复杂配置。美中不足的是rclone并未提供图形界面,而在Windows下编写服务配置文件较为复杂,因此该软件适合有较多自定义需求的用户使用。
  • raidrive: raidrive相对比rclone最大的特色就是提供了图形界面支持,可以更方便地管理挂载目录,但这是一款商业软件,免费套餐只支持较少的挂载目标,也无法支持缓存等高级功能。

类似的软件还有很多,如SFTP Drive等,读者可以根据自己的需求挑选适合自己的软件。

需要注意的是,这些软件的原理大多类似,其中还有不少是基于WinFSP的二次开发,因此在性能和稳定性方面,它们是相差不多的。

查看原文

赞 0 收藏 0 评论 0

路人甲的世界 发布了文章 · 6月17日

[源码级解析]阅读源码,分析并解决scrcpy无法正常输入中文的问题

移动互联网时代下,手机能干的事情越来越多,但如果想要让工作更高效,鼠标键盘依旧是必不可少的。可许多软件(点名阿里系)并没有提供对应的桌面版本,也不兼容基于x86架构的Android模拟器,这就使得我们要用投屏软件来在电脑上操作手机。scrcpy就是众多投屏软件中最具特色的一款,作为一款开源软件,它拥有极佳的性能和丰富的功能,但这款软件在中文输入方面却存在较大的问题。本文将为读者介绍如何让scrcpy正常输入中文,让这款非常好用的投屏软件变得更好用。
本文原载于未命名小站,由作者本人同步至知乎,转载请注明原作者博客地址或本链接,谢谢!
本文撰写时scrcpy最新为1.14版本,依旧存在下文所述的问题,当你阅读本文时也许scrcpy已经解决了这一问题,因此本文内容仅供思路参考和技术分享。

0x01 问题重现

scrcpy相对于其他仅依靠adb shell screencapadb shell input进行设备控制的软件,拥有更加优秀的性能,这得益于它的系统架构:

image.png

其中Server在每次启动scrcpy的时候运行于Android端,使用MediaCodec的API对采集到的画面进行编码,并使用多线程,通过Socket传输到PC。PC端则使用ffmpeg和SDL2对画面进行实时解码显示。其中Server使用Java开发,Client使用C开发。具体技术细节可以参考官方文档,此处不再赘述。

言归正传,scrcpy在Unicode文字输入方面一直存在巨大问题,从很久之前就有用户反馈无法输入ascii以外的文字(如#632,表现为PC端输入文字后,手机不显示,终端报错,见图1),而作者则在最近正式加入了对ascii字符的支持,但尚未合并到master分支(见#1426)。

图1. 无法正常输入汉字,并提示无法插入字符

笔者尝试拉取代码库,并按照开发文档#1426所在分支d613b10efcdf0d1cf76e30871e136ba0ff444e6e进行构建。

构建之后运行,我们会发现问题略有改善,但输入过程中的字母也被传入scrcpy,如图2所示:

图2. 笔者想输入“测试”,但输入过程中的ceshi 也被输入到了Android中

因为是尚未发布的功能,没有相关的资料可供参考,我们只能自行阅读源码查找原因。

0x02 问题分析

首先我们要了解一下SDL处理用户输入(以及其他事件)的流程:

  1. 使用SDL_WaitEvent(&event)获取事件队列中的事件
  2. 根据event->type对不同事件进行区分处理

这里的事件类型可以参考SDL2的官方文档:SDL_Event

顺着这样的思路,我们很快就能找到scrcpy处理事件的相关代码:app/src/scrcpy.c,这里截取一段:

static enum event_result
handle_event(SDL_Event *event, bool control) {
    switch (event->type) {
        case EVENT_STREAM_STOPPED:
            LOGD("Video stream stopped");
            return EVENT_RESULT_STOPPED_BY_EOS;
        case SDL_QUIT:
            LOGD("User requested to quit");
            return EVENT_RESULT_STOPPED_BY_USER;
        case EVENT_NEW_FRAME:
            if (!screen.has_frame) {
                screen.has_frame = true;
                // this is the very first frame, show the window
                screen_show_window(&screen);
            }
            if (!screen_update_frame(&screen, &video_buffer)) {
                return EVENT_RESULT_CONTINUE;
            }
            break;
        case SDL_WINDOWEVENT:
            screen_handle_window_event(&screen, &event->window);
            break;
        case SDL_TEXTINPUT:
            if (!control) {
                break;
            }
            input_manager_process_text_input(&input_manager, &event->text);
            break;
        case SDL_KEYDOWN:
        case SDL_KEYUP:
            // some key events do not interact with the device, so process the
            // event even if control is disabled
            input_manager_process_key(&input_manager, &event->key, control);
            break;
...

结合代码和SDL文档,可以发现SDL_KEYDOWNSDK_KEYUP事件会被传递到input_manager_process_key()函数,这两个事件只针对按键,不针对输入法;而SDL_TEXTINPUT事件则会被传递到input_manager_process_text_input()函数,这个事件处理的是输入法确认输入后所发送的文本。

这里我们可以使用调试工具(或万能的printf)了解上面『测试』这个文字的输入过程中,事件传递的流程。最终得到如下所示的事件顺序:

2020-06-17 17:16:49.831 scrcpy[57759:5396589] INFO: KEYINPUT # c
2020-06-17 17:16:49.919 scrcpy[57759:5396589] INFO: KEYINPUT # c抬起
2020-06-17 17:16:50.731 scrcpy[57759:5396589] INFO: KEYINPUT # e
2020-06-17 17:16:50.840 scrcpy[57759:5396589] INFO: KEYINPUT # e抬起
2020-06-17 17:16:51.341 scrcpy[57759:5396589] INFO: KEYINPUT # s
2020-06-17 17:16:51.440 scrcpy[57759:5396589] INFO: KEYINPUT # s抬起
2020-06-17 17:16:51.657 scrcpy[57759:5396589] INFO: KEYINPUT # h
2020-06-17 17:16:51.719 scrcpy[57759:5396589] INFO: KEYINPUT # h抬起
2020-06-17 17:16:51.933 scrcpy[57759:5396589] INFO: KEYINPUT # i
2020-06-17 17:16:52.041 scrcpy[57759:5396589] INFO: KEYINPUT # i抬起
2020-06-17 17:16:52.408 scrcpy[57759:5396589] INFO: KEYINPUT # 空格
2020-06-17 17:16:52.408 scrcpy[57759:5396589] INFO: TEXTINPUT # 测试
2020-06-17 17:16:52.519 scrcpy[57759:5396589] INFO: KEYINPUT # 空格抬起

可以发现,除了倒数第二行的TEXTINPUT事件,其他均为KEYDOWNKEYUP事件。那么问题来了,我们如何屏蔽掉这些多余的事件呢?

继续阅读input_manager_process_key()函数和input_manager_process_text_input()函数,我们会发现它们都对『某种特殊情况』做了判断,并会抛弃掉特殊情况下的一些输入:

// input_manager_process_key()
...
struct control_msg msg;
// convert_input_key()返回true才会真正插入字符,可是为什么会存在返回false的情况?
if (convert_input_key(event, &msg, im->prefer_text)) {
    if (!controller_push_msg(controller, &msg)) {
        LOGW("Could not request 'inject keycode'");
    }
}
...

// input_manager_process_text_input()
...
void
input_manager_process_text_input(struct input_manager *im,
                                 const SDL_TextInputEvent *event) {
    // 为什么要在prefer_text为假的时候提前返回呢?
    if (!im->prefer_text) {
        char c = event->text[0];
        if (isalpha(c) || c == ' ') {
            assert(event->text[1] == '\0');
            // letters and space are handled as raw key event
            return;
        }
    }
...

是的!这两处可疑的地方都与prefer_text这个参数有关,其值为真或为假会对scrcpy处理按键输入和文本输入的行为进行截然相反的控制:

  1. prefer_text为假:

    • TEXTINPUT事件所得到的字符长度如果为1,直接抛弃事件
    • KEYDOWNKEYUP不受影响
  2. prefer_text为真:

    • TEXTINPUT事件不受影响
    • KEYDOWNKEYUP事件对应的字符如果是字母或空格,直接抛弃事件

需要补充说明的是,根据SDL官方文档和我们的实际测试,在输入英文(不使用输入法)的时候,按下一个键会触发三个事件:

  1. KEYDOWN事件
  2. TEXTINPUT事件
  3. KEYUP事件

也就是说如果没有prefer_text参数的控制,按下一个键我们将会得到两个键。继续翻阅代码,我们会发现prefer_text参数的默认值为false,即满足上文第一种情况。

结合我们上面输入中文的测试,可以发现KEYDOWNKEYUP事件在prefer_text默认为false情况下是会被直接输入到Android端的;而如果我们想屏蔽这两个事件,就必须保证prefer_text为真。

怎么样,读到这里,相信读者们已经发现一些端倪了吧?

0x03 问题解决

继续阅读源码,会发现prefer_text参数来源于启动时传入的--prefer-text选项,文档中这样描述这个选项:

Text injection preference
There are two kinds of events generated when typing text:

key events, signaling that a key is pressed or released;
text events, signaling that a text has been entered.
By default, letters are injected using key events, so that the keyboard behaves as expected in games (typically for WASD keys).

But this may cause issues. If you encounter such a problem, you can avoid it by:

scrcpy --prefer-text

(but this will break keyboard behavior in games)

关于这个参数的解释写得非常抽象,仅描述了这一参数的行为和特殊情况下后果,并没有介绍这一参数的一般用途,难怪笔者一开始并未发现。

这里我们使用上文提到的分支d613b10efcdf0d1cf76e30871e136ba0ff444e6e进行重新构建,并在启动时携带--prefer-text参数:

此时问题不再出现!

0x04 后续

鉴于scrcpy这部分文档过于简略,且不容易被注意到,笔者向作者提交了一个Issue(#1516)说明这一情况,希望引起作者重视,完善相关文档(或者考虑默认启用这一选项?)。如果作者有意,笔者也希望能够为其撰写中文文档,方便其他有同样需求的用户快速上手,毕竟在国内互联网生态下,太多软件只有手机版本,有此需求的中国用户数不胜数,而能够读懂源码,并能在机缘巧合之下读到本文的人确实少之又少。

也许当读者读到本文的时候,这一软件版本已经高于1.14,相关问题也早已得以解决,但笔者依旧希望通过这篇文章,为读者提供一种解决问题的思路,即:

多读源码!

查看原文

赞 0 收藏 0 评论 4

路人甲的世界 发布了文章 · 6月3日

使用简单的快捷键,让MacOS在文件选择对话框中显示隐藏文件

MacOS对用户文件和系统文件做了较为严格的区分,尽管Macintosh HD磁盘可以类比Unix下的根目录,但我们却无法看到熟悉的Unix目录结构,这其实是因为MacOS的文件系统将系统目录设置为了隐藏目录;同理,对于点号开头的文件,我们也无法在Finder中看到。对于独立的Finder窗口,我们可以在菜单栏中选择显示->显示系统文件,但在选择文件、打开文件或保存文件的对话框中,由于没有对应的菜单栏,我们该如何启用这一选项呢?
本文原载于未命名小站,由作者本人同步至SegmentFault,转载请注明原作者博客地址或本链接,谢谢!

0x01 发现问题

由于笔者最近需要使用Julia语言开发一些项目,但发现VSCode的Julia扩展存在诸多问题,于是决定使用IntelliJ IDEA作为Julia开发环境。但笔者在配置Julia环境的时候,遇到了这样的问题:IntelliJ IDEA需要在文件选择对话框中配置Julia可执行文件路径,但这一路径在/usr/local/bin/julia,无法直接访问到,如图所示:

图1. Julia executable部分不允许直接输入路径,需要在弹出的对话框中选择文件

图2. 文件选择对话框无法选择被隐藏的/usr目录。

0x02 分析问题

使用MacOS比较多的读者应该非常清楚如何在Finder中显示隐藏文件:分别点击菜单栏中的显示->显示系统文件即可在Finder中找到我们所需的文件:

图3. 打开『显示系统文件』选项

图4. 打开『显示系统文件』选项后,就可以访问系统目录,并找到我们想要的文件了

但由于文件选择对话框并没有菜单栏,我们不能使用类似的方法实现这一需求,那么有没有变通方法呢?答案是肯定的。

0x03 解决问题

细心的读者在图3的截图中应该能发现,菜单栏中很多功能都有对应的快捷键,而『显示系统文件』的快捷键就是⇧⌘.,即Shift + Command + .,那么我们在对话框中按下这一组合键能否起效呢?

我们尝试在IntelliJ IDEA的文件对话框中键入这一快捷键,随着对话框刷新,之前没能显示的系统文件立刻显示了出来!

图5. 文件选择对话框中键入快捷键后,之前隐藏的系统文件也能显示出来了

这时候,我们再选择Julia的可执行文件,完成IntelliJ IDEA下Julia扩展的基础设置,接下来就可以开开心心的在IDEA中开发Julia程序!

0x04 类比与扩展

其实除了使用『显示隐藏文件』这一方法,我们还可以通过Finder的另一个快捷键⇧⌘G来实现快速跳转。

这个快捷键存在于Finder菜单栏的前往->前往文件夹...中,选择后会弹出一个小的对话框,要求用户直接输入路径而不是选择文件,而这一方法在文件选择对话框中依旧有效:

图6. 键入快捷键后弹出的目录跳转对话框

图7. 输入我们想要的目录,就算是隐藏目录也能轻松到达!

0x05 后记

MacOS的快捷键系统非常庞大,几乎所有的常用操作都有其对应的快捷键。但让人费解的是,它在注重用户体验的另一面,却在用复杂的快捷键阻碍用户更轻松地使用MacOS。

在笔者看来,Finder中的选项理应在全局生效,就算不生效,至少也应该提供一个在文件选择对话框中访问菜单栏的快速入口。

但不论如何,使用简单的类推思维,我们最终还是解决了这一问题。如果真正想玩转MacOS,这样的思维必不可少,希望这篇文章能给读者一些启发,让大家能在遇到同类问题的时候类比思考,进一步发掘MacOS下更多『不可言说』的使用技巧。

查看原文

赞 0 收藏 0 评论 0

路人甲的世界 赞了文章 · 5月26日

思否开源项目推介丨WP Editor.md:WordPress 下的 Markdown 编辑器插件

WP Editor.md

开源项目名称:WP Editor.md
开源项目简介:WordPress 下的 Markdown 编辑器插件
开源项目类型:个人开源项目
项目创建时间:2017 年 1 月
GitHub 数据:518 Star,76 Fork GitHub
地址:https://github.com/LuRenJiasW...

项目负责人自荐:

@路人甲的世界:WP Editor.md 首个版本发布于 2017 年,是 WordPress 下少有的 Markdown 编辑器和公式编辑器插件,核心部分基于已经停止更新的 Editor.md(本插件 Fork 后继续独立开发),支持 Markdown 编辑与预览,此外还支持 LaTeX 公式撰写、代码高亮、思维导图绘制、外置图床、流程图绘制等多种功能。

本插件主要使用 PHP 开发(因为是 WordPress 插件),Markdown 和图像渲染部分由 JavaScript 完成(未来打算迁移到 TypeScript),拥有非常不错的性能。

在这三年来我与另一位作者 JaxsonWang(目前已离开项目组)的努力经营下,本插件已经收获近 3700 位月活用户,共获得了超过 80000 的总安装量,托管在 GitHub 的源码库也拥有了 500+ 的 Stars 数。鉴于国内 WordPress 生态并不大,能取得如此成绩我已经感到非常高兴。

由于目前我在学业和事业(目前正在实习)方面均有一定压力,开发速度可能不如其他开源项目迅速,通常两个月左右发布一次新版本。但我愿意经常写文章介绍本插件的功能、特色与技巧等内容,也愿意在社区中活跃地与用户沟通,帮助用户解决问题。

思否推荐语:

借助一款好用的插件,可以让博主们更专心创作,而不是寻找折中方案来解决问题。

WordPress 在 5.0 版本的时候更新了 Gutenberg 编辑器,支持 Markdown,Gutenberg 虽然含着金钥匙出生,但是对 Markdown 的友好程度远不如我们今天开源项目 WP Editor.md。WP Editor.md 对原生 Markdown 语法进行了非常友好的支持,同时也增添了相当多的功能:LaTeX 公式撰写、代码高亮、思维导图绘制、外置图床、流程图绘制。

在和作者的交流过程中,作者提到 SegmentFault 对他有着非常特别的意义,“在我 2017 年开始自学软件开发的时候,SegmentFault 的问答功能在我并不顺利的自学路上给与了我非常多帮助。”

现在,作者在“自学成才”后因希望通过自己的开源项目让更多想搭建个人网站的用户了解到 WordPress、爱上 WordPress,并能使用 WP Editor.md 获得极佳的 Markdown 编辑体验,为开发者带来更多的便利。


clipboard.png

该项目已入选「SFOSSP - 思否开源项目支持计划」,我们希望借助社区的资源对开源项目进行相关的宣传推广,并作为一个长期项目助力开源事业的发展,与广大开发者共建开源新生态。

有意向的开源项目负责人或团队成员,可通过邮箱提供相应的信息(开源项目地址、项目介绍、团队介绍、联系方式等),以便提升交流的效率。

联系邮箱:pr@segmentfault.com

clipboard.png

查看原文

赞 10 收藏 1 评论 0

路人甲的世界 赞了文章 · 5月19日

SegmentFault 思否开源项目支持计划启动,为你的开源项目助力!

SegmentFault 思否开源项目支持计划

很高兴可以看到,开源正迎来最好的发展时期。

20 年前的开源项目,基本上是由个人开发者主导的。但随着开源精神的发展以及开源文化的普及,越来越多的企业与科技公司参与到了开源生态的建设当中。

但归根结底,开源更多的是一种社会化活动,很多的项目创意也是来自个人或者小型团队。个人有了兴趣,有了想法,并进而动手形成项目,而后建立社区,或者成立公司继续推进。这种模式将会持续存在,也是技术创新的一种健康和合理途径。所以游离于大公司外的开源项目和植根于大公司内的开源项目,会长期共存。

但根据社区部分开源作者的反馈,大部分的优质个人开源项目很难进行有效的传播推广。


为助力优质开源项目成长, SegmentFault 思否社区作为服务于开发者的技术社区,正式推出「SFOSSP - 思否开源项目支持计划」,我们希望借助社区的资源对开源项目进行相关的宣传推广,并作为一个长期项目助力开源事业的发展,与广大开发者共建开源新生态。

一、思否提供哪些永久的免费支持资源?

  1. 为开源项目在社区创建技术标签 ,如「Flutter」「TDengine」等;
  2. 思否编辑团队协助进行开源项目报道的文案整理优化;
  3. 思否资讯板块配合开源项目的相关发布报道;

二、思否可参与报道的内容类型有哪些?

  1. 开源项目介绍
  2. 开源项目重大版本发布
  3. 开源项目重要的里程碑事件
  4. 开源项目重要的技术突破
  5. 更多的报道需求,请与思否编辑团队进行深度沟通交流

三、对于开源项目有哪些要求?

  1. 个人项目、团队项目、商业开源项目都可以申请参与;
  2. 开源项目需要有明确的应用场景以及较高的项目质量;
  3. 开源项目的负责人要对提供的报道内容真实性负责。

有意向的开源项目负责人或团队成员,可通过邮箱提供相应的信息(开源项目地址、项目介绍、团队介绍、联系方式等),以便提升交流的效率。

  • 联系邮箱:pr@segmentfault.com
  • 思否开源项目申请表:申请表

clipboard.png

查看原文

赞 36 收藏 5 评论 19

路人甲的世界 发布了文章 · 5月9日

十个PM2中冷门但实用的功能

PM2发布于2013年,是使用JavaScript开发,主要用于Node.js业务持久化的进程管理器。相对于Systemd、Supervisord等通用进程管理器,PM2对JavaScript的业务更为友好,且使用更为简单,有着丰富的可扩展性,对非JavaScript业务的管理同样出色。可惜的是许多PM2用户对PM2的了解并不多,大部分用户都只掌握了基础的进程管理,其实PM2的能力绝不止于此,充分使用PM2能够让业务开发和维护的效率大大提升。本文就来列举这样十个PM2中冷门但实用的功能,希望能够帮助读者对PM2有新的认识。
本文原载于未命名小站,由作者本人同步至SegmentFault,转载请注明原作者博客地址或本链接,谢谢!
提前说明:本文使用PM2的命令行模式讲解,但以下内容对于API调用同样有效,命令行和API之间的转换规律可以阅读官方文档

0x01 自动保存

通常我们希望PM2本身开机自启,需要执行pm2 startup让其注册到操作系统的服务管理工具中,如果我们还希望PM2中的进程能随着PM2启动而启动,就需要每次在新增或删除进程后执行pm2 save,但如果你是一个像笔者一样记性不好的人,很可能会忘记执行这一步,导致PM2重新启动后,一个业务都没启动。那么这『多余』的一步有没有办法能自动执行呢?答案是有的:

pm2 set pm2:autodump true

在Shell中输入这一行命令,我们就开启了PM2的自动保存功能,这样子我们对进程的变更将会被即时保存到~/.pm2/dump.pm2中,无需手动执行pm2 save

这里我们使用到了pm2 set这个命令,其实这个命令执行的是对~/.pm2/module_conf.json的修改。这个文件是PM2下各模块的通用配置文件,在安装其他PM2模块(如反向代理、负载均衡服务器等)同样也有可能接触到这个文件。但对于PM2本身来说,目前可供我们使用的配置项只有autodumpregistrydocker这三个,且没有集中的文档对其进行描述,感兴趣的读者可以阅读这三个配置项在源码中的实现,此处不再赘述:

0x02 输出日志到文件

部分业务可能为了省事将日志直接输出到stdout和stderr,在Shell中直接运行时我们可以使用如Linux和MacOS的重定向符>来将stdout输出到文件,再使用2>&1将stderr输出到stdout。但假设这样一个『省事』的业务上了生产环境,我们需要使用PM2来运行之,应该怎样做才能看到日志呢?PM2同样为我们提供了日志重定向的功能:

pm2 start --log [fille] ...

是的,只需要在启动进程时指定--log参数,并提供日志文件的路径(是否存在这个文件都没有关系),就可以将stdout和stderr输出到我们指定的日志文件了。接下来我们就可以使用如tail一类的工具来对日志进行跟踪,或者你也可以使用PM2自带的日志显示功能:

pm2 logs [id]

这里的id是你在执行pm2 ps时候所看到的进程id。

0x03 设置内存限制

也许你需要在PM2中运行一个内存管理比较差劲的程序,但又不希望这个程序在发生内存泄漏后消耗掉所有资源,影响其他进程,这时候PM2的内存限制功能就可以派上用场了:

pm2 start --max-memory-restart=1024M ...

这里的单位可以为K(iB)、M(iB)和G(iB),使用该参数启动进程后,PM2就会在进程内存使用率超过限制时强制重启进程,对于一些存在内存泄漏问题但不便于解决(或没必要解决)的业务非常实用。

0x04 查看某个进程的信息

通常我们可以使用pm2 ps来查看当前正在运行的所有进程,但这一命令只显示了最基础的信息,如环境变量、运行入口、运行参数等信息并没有在列表中显示出来。那么我们应该如何查看这些信息呢?PM2提供了这样的方法:

pm2 show [id]

PM2会输出关于这个进程的所有信息,如下图所示:

0x05 使用总览面板监控所有进程

上一节我们提到了使用pm2 show来查看某个进程的详细信息,但在生产环境下我们更多时候需要监控所有进程,包括CPU、内存使用、日志输出等信息,PM2同样提供了如下的命令来帮助我们监控所有进程:

pm2 monit

PM2会启动一个面板,如下图所示:

该面板可以分为四部分:进程列表(左上角)、当前进程日志(右上角)、当前进程性能信息(左下角)、当前进程基础信息(右下角)。我们可以使用键盘左右方向键来切换面板,使用上下方向键在面板中滚动,对所有进程进行监控。

实际上PM2 Plus还额外提供了资源占用历史、内存/CPU详细分析(Profiling)等高级功能,但由于该功能需要付费使用,此处不再展开说明,如果愿意付费使用的读者可以查阅官方文档:PM2 Plus documentation

0x06 使用SourceMap获取错误位置

刚刚我们谈了那么多『不规范』的业务(如日志输出到stdout、内存泄漏等),接下来我们举一个『规范』的例子,也就是使用Webpack(或其他构建工具)对JavaScript代码进行压缩后的线上业务。

如果这些业务在线上出现了错误,但由于代码被压缩,只能显示错误出现在第一行(本来就只有一行),我们要怎样才能在日志中看到更详细的信息呢?

PM2考虑到了这一点,并提供了自动加载SourceMap的功能:

pm2 start --source-map-support ...

假设你加载的js文件是index.js,在开启SourceMap支持后,PM2会自动寻找同目录下的index.js.map,并在出现错误时加载之,在日志中输出更易读的错误日志。下面是一个例子:

// error.js
setInterval(function() {
    triggerError();
}, 1000);

function triggerError() {
    throw new Error("Some error...");
}

这里笔者使用Webpack生成了对应的error.min.jserror.min.js.map文件:

webpack ./error.js -o error.min.js --devtool source-map --output-source-map-filename error.min.js.map

然后使用pm2加载error.min.js(不开启SourceMap):

官网的文档存在错漏,关闭SourceMap支持应该是--disable-source-map-support而非--disable-source-map,笔者提交了相关修订:pull/185
pm2 start ./error.min.js --disable-source-map-support

可以看到错误信息只显示出现在第一行(但显然问题并不是出在第一行)。

接下来我们开启SourceMap支持,再运行一遍:

pm2 start ./error.min.js --source-map-support

此时就能从日志中看到正常的调用栈信息了,帮助我们更高效的跟踪问题的来源。

0x07 业务更新时自动重启进程

在业务开发与测试过程中,我们经常会遇到文件更新后需要重启业务的情况。对于本地环境我们可以使用如Webpack Dev Server等工具监听文件变化,然后在文件发生改变后重新运行服务器。而PM2同样提供了类似的功能帮助我们实现这一需求:

pm2 start --watch

这样只要当前目录下有任意文件发生改变,PM2都会尝试重启进程。

在使用这一参数的时候,有几个需要注意的地方:

  1. 请在程序所在的目录执行启动命令,否则将会监视的不是程序所在目录,而是你执行目录当前所在的目录。
  2. 开启--watch参数后,就算你手动停止进程(不删除),进程也会在文件发生改变后自动启动,解决该问题的方法是在停止进程的时候加入如下参数:
pm2 stop [id] --watch
  1. 如果我们需要忽略一些目录的变化(如临时文件)或只监听某些目录的变化,就需要使用PM2的API:PM2 Ecosystem,然后撰写如下配置文件到ecosystem.config.js(摘自官方文档):
module.exports = {
  apps: [{
    script: "app.js",
    watch: ["server", "client"],
    // Delay between restart
    watch_delay: 1000,
    ignore_watch : ["node_modules", "client/img"],
    watch_options: {
      "followSymlinks": false
    }
  }]
}

0x08 更聪明的失败重启策略

相信很多使用过PM2或Docker的读者都遇到业务出现运行时错误后不断自动重启的问题。但很多时候运行时错误并非来自于业务本身,比如数据库服务器中断、连接数过多、甚至是上文提到的--watch参数过于灵敏(很多IDE或编辑器支持自动保存,可能保存的版本尚未开发完成,存在语法错误)。那么有没有诸如延时重启、无缝重启等功能呢?PM2提供了大量的相关选项:

1. 固定延时

pm2 start --restart-delay=2000 ...

这里的2000单位为毫秒,即在需要重启的时候等待两秒钟。

2. 灵活延时

更多时候我们需要的是不断延长的重启时间,比如Filezilla连接FTP客户端失败后的重试时间会随着重试次数增多不断延长。PM2也提供了这样的功能:

pm2 start --exp-backoff-restart-delay=1000

此处的1000单位也是毫秒,PM2会在多次重启失败后以设定的时间为初始值,使用指数移动平均算法不断延长重试时间,最高为15000毫秒(即15秒),并在进程成功启动30秒后重置重试时间到到初始值。该算法的具体实现可以参考PM2的相关源码:

3. 零延时高可用

重启总是需要耗时的,如果我们希望业务在重启的时候不中断,就像Kubernetes的滚动更新一样,那应该怎么做呢?PM2的集群模式可以帮助我们实现这一需求。

需要注意的是,这里我们所指的『集群』并非是Kubernetes这样逻辑独立的服务器集群,而是Node.js原生支持的Cluster组件。如果对Cluster组件不了解的读者可以阅读这篇Node.js官方文档:Cluster | Node.js v14.2.0 Documentation

简而言之,Cluster组件就类似于PHP中的FPM或是Nginx中的Worker,为单线程的JavaScript运行时增加了能在多CPU上并行接收请求的能力,即运行多个实例作为子进程,并由一个父进程负责请求的调度。

但就算你不懂Cluster组件或是不想为现有业务加入Cluster支持,也没有关系,因为PM2为你实现了它,这样我们无需对现有源码进行任何修改,也能充分利用Cluster组件的功能来实现高可用,任意一个进程的停止,不会对整个业务造成影响。这就是本节笔者要提到的『零延时高可用』。

这里笔者以一个非常简单的Web服务器为例:

var http = require("http");

http.createServer(function (request, response) {
   response.writeHead(200, {'Content-Type': 'text/plain'});
   response.end('Hello World\n');
}).listen(8081);

然后我们使用如下命令启动包含四个进程的Cluster(因为笔者的电脑刚好是四个核心):

pm2 start ./server.js -i 4

这里的4就是进程数,也可以设置为max以匹配当前环境最大核心数。

接下来我们就能在pm2 ps的结果中看到如下四个子进程:

需要注意,Cluster模式下重启业务需要使用reload,而且不能使用进程ID(因为我们需要重启的是一组进程而非一个),如下所示:

pm2 reload server

这里的server是上面pm2 ps结果中进程组共有的name。这样就既能充分利用服务器性能,又能实现业务的高可用,而我们所需要耗费的只是额外添加一个-i参数。

4. 关闭失败重启功能

有时候我们还会使用pm2来进行一些尽管耗时,但不需要一直在后台运行的业务,例如爬虫。PM2默认会在进程退出后重新启动,但也提供了参数帮助我们关闭此功能:

pm2 start --no-autorestart ...

使用这个参数后,在业务退出时,状态会直接变为stop,而不会自动重启。

0x09 一条命令操作一组业务

上一节我们提到了可以使用Cluster批量生成进程并对其进行管理,但Cluster只是生成了一批一模一样的进程。一个常规的业务(尤其是微服务当道的现在)可能会由多个进程组成。这里笔者假设有一个业务叫做吃乎,包含API(api.js)、服务端渲染(ssr.js)、数据库(db.js)、监控(monitor.js)四个组件,如何对它们进行批量管理(比如重启)呢?

细心的读者可能在上面pm2 ps的输出结果中看到了namespace的字段,默认为default,其实这就是本节要说的关键内容:命名空间。我们可以使用命名空间对同一类业务进行归类,然后按命名空间来对业务进行批量管理:

pm2 start api.js --namespace chihu
pm2 start ssr.js --namespace chihu
pm2 start db.js --namespace chihu
pm2 start monitor.js --namespace chihu

这时候我们可以看到,这四个进程的namespace均为chihu。如果我们需要停止这四个业务,就不需要逐一停止,只需要执行一条命令:

pm2 stop chihu

这样就能一次停止四个进程了。其他操作同样类似,只要在对应命令的--help面板中能看到namespace的参数就可以,如下所示:

P.S. 这里的吃乎只是笔者胡诌的一个业务,写完之后顺手搜索了一下,没想到真有一个网站叫做吃乎:吃乎 - 发现身边的美食。如有雷同,纯属巧合哈😂。

0x10 PM2的内置HTTP服务器

最后笔者想介绍一个极为实用但极少有人提及的功能:HTTP服务器。之前和很多同学探讨大前端项目前后端分离的时候,发现他们大多都使用Nginx、Apache甚至Tomcat来托管前端的静态页面,然后使用PM2来托管后端API。但为了一个简单的前端页面专门撰写一堆配置文件,实在是太浪费时间了,PM2可能也发现了这一点,于是贴心的内置了HTTP服务器:

pm2 serve [path] [port]

是的,就这么简单,一条命令就能启动一个HTTP服务器。由于这个HTTP服务器使用的是Node.js实现,因此性能同样非常优异,在大部分情况下足够使用。如果是负载非常大的业务,一般也不会考虑使用PM2,而会使用更具扩展性的Kubernetes。


以上是笔者所分享的十个PM2中冷门但实用的功能。其实这十个功能在阅读官方文档和源码的过程中都可以了解到,而在学习过程中总结、掌握这些小技巧对于实际使用过程中的效率提升有着非常大的帮助。

限于篇幅,一些更为高级的功能(如Load&Dump、PM2 Plus)等并未在本文中提及。如果读者们想了解更多相关内容,不要犹豫,马上去阅读文档和相关源码吧!相信大家在阅读完毕后,一定会有比本文更多的收获,静心阅读永远是最高效、最深刻、最细致的学习方式。

查看原文

赞 2 收藏 2 评论 0

路人甲的世界 赞了回答 · 2019-05-24

解决请教一个JavaScript浮点数的问题

日经问题,请自行搜索:舍入误差

解决方案无非是两个:

  1. 对精度要求不高的时候,可以直接采用定点运算,即对确定的小数位数直接进行四舍五入。

  2. 对精度要求很高时,采用高精度(大数)运算,这一点网上资料很多,请自行查阅。

关注 4 回答 2

路人甲的世界 赞了文章 · 2019-05-19

前端每日实战:166# 视频演示如何用 CSS 创作一个 Safari LOGO

图片描述

效果预览

按下右侧的“点击预览”按钮可以在当前页面预览,点击链接可以全屏预览。

https://codepen.io/comehope/pen/rgmPLR

可交互视频

此视频是可以交互的,你可以随时暂停视频,编辑视频中的代码。

请用 chrome, safari, edge 打开观看。

https://scrimba.com/p/pEgDAM/c2LBPPtg

源代码下载

每日前端实战系列的全部源代码请从 github 下载:

https://github.com/comehope/front-end-daily-challenges

代码解读

容器基本属性

Safari 浏览器的 LOGO 是一个指南针的形状,它的主要元素有 2 个,一个是围绕在表盘周围的刻度线,一个是中间的指针。所以我们定义 dom 结构如下,其中 .marks 代表刻度线,.pointer 代表指针。.marks 中有 4 个 <span> 元素,它们代表刻度线,实际的刻度线有几十条,这里只定义 4 条,目的是便于书写样式,等样式写好后,接下来会用 JavaScript 批量生成刻度线:

<figure class="safari">
    <div class="marks">
        <span></span>
        <span></span>
        <span></span>
        <span></span>
    </div>
    <div class="pointer"></div>
</figure>

让作品显示在页面正中,页面背景为黑色:

body {
    margin: 0;
    height: 100vh;
    display: flex;
    align-items: center;
    justify-content: center;
    background-color: black;
}

LOGO 容器是一个白色的圆角正方形。作品将使用 em 作为长度单位,如果想修改 LOGO 的尺寸,只要修改这里的 font-size 属性就可以了。用 flex 布局令其中的子元素 .marks.pointer 都居中显示:

.safari {
    font-size: 10px;
    width: 15em;
    height: 15em;
    background-color: snow;
    border-radius: 25%;
    padding: 1em;
    display: flex;
    align-items: center;
    justify-content: center;
}

绘制刻度线

先绘制出刻度线所在的表盘,用线性渐变填充上蓝色渐变色:

.marks {
    width: inherit;
    height: inherit;
    background-image: linear-gradient(
        hsl(191, 98%, 55%),
        hsl(220, 88%, 53%)
    );
    border-radius: 50%;
}

再绘制出刻度线。围绕着一个圆周绘图的技巧是先令一组元素逐个旋转较小的角度(用 rotate() 函数实现),再让这些元素向旋转的方向移动(用 translate() 函数实现)。这里用变量 --rotate-deg 存储旋转的角度:

.marks {
    display: flex;
    align-items: center;
    justify-content: center;
}

.marks span {
    position: absolute;
    width: 0.1em;
    height: 0.9em;
    background-color: snow;
    transform: rotate(var(--rotate-deg)) translateY(6em);
}

.marks span:nth-child(1) {--rotate-deg: 0deg;}
.marks span:nth-child(2) {--rotate-deg: 90deg;}
.marks span:nth-child(3) {--rotate-deg: 180deg;}
.marks span:nth-child(4) {--rotate-deg: 270deg;}

现在可以看到 4 条刻度线分别定位到表盘的上、下、左、右的边缘位置了。

用 Javascript 批量生成刻度线

因为刻度线有很多条,为了减少代码量,我们用 JavaScript 来批量创建刻度线。

在此之前,先删除掉 html 中的声明 <span> 元素的 4 行代码:

<figure class="safari">
    <div class="marks">
        <!-- <span></span>
        <span></span>
        <span></span>
        <span></span> -->
    </div>
    <div class="pointer"></div>
</figure>

再删除 css 中设置刻度线角度的代码:

/* .marks span:nth-child(1) {--rotate-deg: 0deg;}
.marks span:nth-child(2) {--rotate-deg: 90deg;}
.marks span:nth-child(3) {--rotate-deg: 180deg;}
.marks span:nth-child(4) {--rotate-deg: 270deg;} */

然后用 js 来批量创建 60 条刻度线:

const MARKS_COUNT = 60

Array(MARKS_COUNT).fill('').forEach((x, i) => {
    let span = document.createElement('span')
    span.style.setProperty('--rotate-deg', i * 360 / MARKS_COUNT + 'deg')
    document.querySelector('.marks').appendChild(span)
})

这里稍复杂的是表达式 i * 360 / MARKS_COUNT + 'deg',其中 360 / MARKS_COUNT 是把一个圆周的 360 度分成若干份(也就是刻度线数量那么多的份数)之后每一份的角度,再用每一份的下标值 i 去乘它,就得到每条刻度线应旋转的角度了。

接下来设置刻度线的细节,令刻度线长短交错。代表刻度线长度的变量是 --h,长线长 0.9em,短线长 0.5em,为了让刻度线对齐,再用变量 --y 存储偏移量,令长线偏移 6em,短线偏移 6.2em。同时修改 height 属性和 translateY() 函数,让它们引用这 2 个变量的值。因为刻度线长短交错,所以用 :nth-child(odd):nth-child(even) 来设置 2 组不同的参数值:

.marks span {
    height: var(--h);
    transform: rotate(var(--rotate-deg)) translateY(var(--y));
}

.marks span:nth-child(odd) {--h: 0.9em; --y: 6em;}
.marks span:nth-child(even) {--h: 0.5em; --y: 6.2em;}

至此,刻度线绘制完成。

绘制指针

指针是由 2 个三角形组成的,对于这种成对的元素,通常都用伪元素绘制。先确定一下指针的尺寸,用 flex 令它的子元素(也就是 2 个伪元素)纵向排列:

.pointer {
    position: absolute;
    width: 1.4em;
    height: 12em;
    display: flex;
    flex-direction: column;
}

绘制三角形的技巧是令容器的尺寸为 00 高,然后用 3 条边框构成三角形,要是看不懂这段代码的话,动手试试就明白了。这里也定义了一个变量 --c,用于存储 2 个三角形的颜色,分别是红色和白色:

.pointer::before,
.pointer::after {
    content: '';
    border-bottom: 6em solid var(--c);
    border-left: 0.7em solid transparent;
    border-right: 0.7em solid transparent;
}

.pointer::after {
    transform: rotate(180deg);
}

.pointer::before {--c: crimson;}
.pointer::after {--c: snow;}

到这里,指针绘制完成,整个 LOGO 的形状也已经完成了。

最后,加一点动画效果,让指针像指南针那样转动。原理很简单,就是让指针在 30 度到 50 度之间来回摆动:

.pointer {
    transform: rotate(30deg);
    animation: rotate 1s ease-in-out infinite alternate;
}

@keyframes rotate {
    to {
        transform: rotate(50deg);
    }
}

大功告成!

查看原文

赞 15 收藏 9 评论 5

路人甲的世界 赞了文章 · 2019-05-19

Android屏幕适配方案分析

为什么要屏幕适配

Android开发过程中我们常用的尺寸单位有px、dp,还有一种sp一般是用于字体的大小。但是由于px是像素单位,比如我们通常说的手机分辨例如1920*1080都是px的单位。现在Android屏幕分辨率碎片化720x1280、1080x1920、2280x1080,这就造成例如187px会在各个分辨率的机型上都是显示一样大小的,那肯定不是我们想要的效果,所以用px单位我们是难以达到适配效果的,那么为什么用dp可以呢?

使用px单位从左到右依次为 480 800、1080 1920、1440 * 2560

使用dp单位从左到右依次为 480 800、1080 1920、1440 * 2560

屏幕总宽度依次为 320dp、415dp、411dp

那么什么是dp?

dp指的是设备独立像素,以dp为尺寸单位的控件,在不同分辨率和尺寸的手机上代表了不同的真实像素,比如在分辨率较低的手机中,可能1dp=1px,而在分辨率较高的手机中,可能1dp=2px,这样的话,一个187dp高度的控件,在不同的手机中就能表现出差不多的大小了。

dp如何计算成px

android中的dp在渲染前会将dp转为px,计算公式:

  • px = density * dp;
  • density = dpi / 160;
  • px = dp * (dpi / 160);

而dpi是根据屏幕真实的分辨率和尺寸来计算的,每个设备都可能不一样的。

由于density不是固定不变的,所以每个分辨率不同的设备他们的density都肯定不相等,这样就会造成每个设备的宽/高对应的总dp都是不同的,假设480 800分辨率的density是1.51080 1920分辨率的density是2.61440 * 2560分辨率的density是3.5。那么它们对应的宽度总dp = (宽度px) / density,分别为320dp、415dp、411dp。可以看出单位为dp的时候三个设备之间的差距就不是很大了,但是这样肯定还是不能满足我们对屏幕适配的要求的。下面来看看Android常见的三种比较成熟的屏幕适配方案,并分析这几种方案的优劣。

屏幕适配方案

1.1 宽高限定符适配

设定一个基准的分辨率,也就是设计图对应的分辨率,其他分辨率都根据这个基准分辨率来计算,在不同的尺寸文件夹内部,根据该尺寸编写对应的dimens文件。

比如我们的设计图 375 * 667为基准分辨率

  • 宽度为375,将任何分辨率的宽度整分为375份,取值为x1-x375
  • 高度为667,将任何分辨率的高度整分为667份,取值为y1-y667

那么对于1080*1920的分辨率的dimens文件来说,

  • x1=(1080/375)*1=2.88px
  • x2=(1080/375)*2=5.76px
  • y1=(1920/667)*1=2.87px
  • y2=(1920/667)*2=5.75px

当代码里面引用高度为y_187,在APP运行时会根据当前设备分辨率去找对应xml文件中对应的高度,我们就可以按照设计稿上的尺寸填写相对应的dimens引用了,这样基本解决了我们的适配问题,而且极大的提升了我们UI开发的效率。

验证方案

简单通过计算验证下这种方案是否能达到适配的效果,例如设计图上有一个宽187dp的View。

480 * 800

  • 设计图占宽比: 187dp / 375dp = 0.498
  • 实际在480 800占宽比 = 187 1.28px / 480 = 0.498

1080 * 1920

  • 设计图占宽比: 187dp / 375dp = 0.498
  • 实际在1080 1920占宽比 = 187 2.88px / 1080 = 0.498
  • 计算高同理

但是这个方案有一个致命的缺陷,那就是需要精准命中才能适配,比如1920x1080的手机就一定要找到1920x1080的限定符,否则就只能用统一的默认的dimens文件了。而使用默认的尺寸的话,UI就很可能变形,简单说,就是容错机制很差。

1.2 smallestWidth适配

smallestWidth适配,或者叫sw限定符适配。指的是Android会识别屏幕可用高度和宽度的最小尺寸的dp值(其实就是手机的宽度值),然后根据识别到的结果去资源文件中寻找对应限定符的文件夹下的资源文件。

这种机制和上文提到的宽高限定符适配原理上是一样的,都是系统通过特定的规则来选择对应的文件。

可以把 smallestWidth 限定符屏幕适配方案 当成这种方案的升级版,smallestWidth 限定符屏幕适配方案 只是把 dimens.xml 文件中的值从 px 换成了 dp,原理和使用方式都是没变的

├── src/main
│   ├── res
│   ├── ├──values
│   ├── ├──values-sw320dp
│   ├── ├──values-sw360dp
│   ├── ├──values-sw400dp
│   ├── ├──values-sw411dp
│   ├── ├──values-sw480dp
│   ├── ├──...
│   ├── ├──values-sw600dp
│   ├── ├──values-sw640dp

验证方案

1920 * 1080分辨率的手机,dpi为420,我们同样设置一个View为187dp宽

  • density = (dpi = 420) / 160 = 2.6
  • 屏幕总宽度dp = 1080 / density = 415
  • 找到文件夹values-sw410dp下的187dp = 204.45dp
  • 通过公式px = density * dp,计算出px = 531.57
  • 算出占屏幕宽度的比例,56.86 / 1080 = 0.492

1440 * 2560分辨率的手机,dpi为560,我们同样设置一个View为187dp宽

  • density = (dpi = 420) / 160 = 3.5
  • 屏幕总宽度dp = 1440 / density = 411
  • 找到文件夹values-sw410dp下的187dp = 204.45dp
  • 通过公式px = density * dp,计算出px = 715.57
  • 算出占屏幕宽度的比例,715.57 / 1440 = 0.496

因为识别的文件夹是values-sw410dp的文件夹,但是屏幕宽度为415dp和411dp,所以最后计算出的占比会有一点点误差,基本可以忽略不计,可以达到相对比较准确的适配效果

优点

  1. 非常稳定,极低概率出现意外
  2. 不会有任何性能的损耗
  3. 适配范围可自由控制,不会影响其他三方库
  4. 在插件的配合下,学习成本低

缺点

  1. 侵入性高,在所有地方都需要引用。
  2. 还是没有办法覆盖所有的机型分辨率,部分机型可能适配效果还是不佳
  3. 不能以高度为基准进行适配
  4. 生成很多文件,增大APP体积1~2M

1.3 今日头条适配方案

今日头条屏幕适配方案的核心原理在于,根据以下公式算出 density

默认px = density * dp,也就是屏幕总宽度dp = 屏幕宽度px / density,这个时候我们假设所有设备上的屏幕总宽度dp会等于我们设计图375dp,那么可以得出一个公式:

density = 屏幕宽度px / 设计图宽度(375dp)

然后我们通过系统api,将density赋值给系统,抛弃掉系统默认计算density的计算公式。

这样可以很巧妙的实现屏幕适配,而且侵入性极低,甚至可以忽略不计。

验证方案

1920 * 1080分辨率的手机,我们同样设置一个View为187dp宽,设计图宽度为375dp

  • density = (屏幕宽度px = 1080) / 375 = 2.88
  • View宽度 = density * 187dp = 538.56
  • 算出占屏幕宽度的比例,57.6 / 1080 = 0.498

1440 * 2560分辨率的手机,我们同样设置一个View为187dp宽,设计图宽度为375dp

  • density = (屏幕宽度px = 1440) / 375 =3.84
  • View宽度 = density * 187dp = 718.08
  • 算出占屏幕宽度的比例,718.08 / 1440 = 0.498

可以看出,这种方案是完全没有误差的,而且侵入性极低,只需要修改系统的density。虽然修改系统的density属性会产生一小部分影响,但是基本都是很好解决的。

优点

  1. 使用成本非常低,操作非常简单
  2. 侵入性非常低
  3. 可适配三方库的控件和系统的控件

缺点

  1. 会全局影响APP的控件大小,例如一些第三方库控件,他们设计的时候可能设计图尺寸并不是像我们一样是375dp,这样就会导致控件大小变形等一些问题。

参考文章

骚年你的屏幕适配方式该升级了!-SmallestWidth 限定符适配方案

Android 屏幕适配终结者

Android 目前最稳定和高效的UI适配方案

广而告之

本文发布于薄荷前端周刊,欢迎Watch & Star ★,转载请注明出处。

欢迎讨论,点个赞再走吧 。◕‿◕。 ~

查看原文

赞 27 收藏 20 评论 1

认证与成就

  • 获得 3 次点赞
  • 获得 12 枚徽章 获得 0 枚金徽章, 获得 2 枚银徽章, 获得 10 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

  • WP Editor.md

    WP Editor.md是一个漂亮又实用的在线Markdown文档编辑器。

注册于 2017-07-04
个人主页被 449 人浏览