普通视图

发现新文章,点击刷新页面。
昨天以前Seven's blog

给 eleventy(11ty) 添加 sitemap.xml 和 robots.txt

作者 Seven
2024年6月14日 10:09

配置过程

  1. 添加数据文件 _data/site.json,写入以下内容,定义站点信息和 sitemap 中的一些默认值:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    {
    "baseUrl": "https://xiaobot.osguider.com",
    "robots": "/robots.txt",
    "sitemap": {
    "path": "sitemap.xml",
    "changefreq": "daily",
    "priority": 0.5
    }
    }
  2. 添加 sitemap 模板文件,写入以下内容:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    ---
    permalink: "{{ site.sitemap.path }}"
    eleventyExcludeFromCollections: true
    ---
    <?xml version="1.0" encoding="UTF-8"?>

    <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
    {% for page in collections.all %}
    {% unless page.data.sitemap.ignore %}
    <url>
    <loc>{{ site.baseUrl }}{{ page.url | url }}</loc>
    <lastmod>{{ page.date | date: '%Y-%m-%dT%H:%M:%S.%LZ' }}</lastmod>
    <changefreq>{{ site.sitemap.changefreq }}</changefreq>
    <priority>{{ page.data.sitemap.priority | default: site.sitemap.priority | default: 0.5 }}</priority>
    </url>
    {% endunless %}
    {% endfor %}
    </urlset>
  3. 【可选】配置不同页面的 sitemap 表现:

    • 如果不希望某些页面在被包含在 sitemap 文件中,在页面元数据中添加 sitemap.ignore: true 即可;
    • 可以对不同的页面设置不同的 sitemap 优先级,在页面元数据中添加 sitemap.priority: 0.5,取值范围 0-1;
    • 对于分页数据,要设置 pagination.addAllPagesToCollections: true 才会在 sitemap.xml 文件中包含每一个分页页面。
  4. 添加模板文件 src/robots.txt,写入以下内容:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    ---
    eleventyComputed:
    permalink: "{{ site.robots }}"
    eleventyExcludeFromCollections: true
    ---
    Sitemap: {{ site.baseUrl }} {{ site.sitemap.path }}

    User-agent: *
    Disallow:

重新编译,over!

参考文档

  • How to create sitemap.xml
  • Sitemap xml
  • liquid
  • sitemap format

开源服务指南博客文章自动生成

作者 Seven
2024年4月21日 21:22
  1. GitHub Actions 可以添加运行参数。我只需要设置 filepath 和 content 两个参数,然后配合 shell 脚本就可以自动生成和提交博文到 GitHub 仓库,进而触发自动构建和发布。

  2. GitHub Actions 提供了 REST API 来触发前面的工作流,这样我就可以通过 HTTP 请求来自动生成和发布博文。

  3. 开源服务指南数据库现在是建立在 Notion 上的,Notion 也提供了 REST API 的交互方式。所以我只需要定时扫描 Notion 数据库,获取状态刚变更为 “已发布” 的博文,提取文章内容,通过第 2 步中提到的 REST API 来触发第 1 步中提到的 GitHub Actions 即可自动生成和发布博文。这里我使用了 Cloudflare Workers 实现。

  4. 怎么监测 Notion 数据库文章状态变动呢?想要监测状态“变动”,我们需要知道变动前的状态和变动后的状态,进而需要有数据库缓存变动前的状态,能做,但麻烦。所幸,pipedream 帮我们做好了这个事情。它能够监测 Notion 数据库变动,并且触发工作流执行。


所以,最后的工作流程就是:

  • Pipedream 监测开源服务指南 Notion 文章数据库变动,提取状态为“已完成”的文章,把文章 id 通过 HTTP 请求发送给 Cloudflare Workers;
  • Cloudflare Workers 根据文章 id 查询文章内容,把文章路径和文章内容作为参数,发送请求给 Github Actions;
  • Github Actions 把文章内容写入文章路径,提交文章源文件到 Github 仓库;
  • Github Actions 监听代码提交,持续集成和发版;

嗯,云服务挺好用。

置于为什么不直接用 Pipedream 提取参数触发 Github Actions 工作流,个人主观意愿影响比较多:Pipedream 代码编写体验略差,稳定性欠佳,所以在逐步往 Cloudflare Workers 迁移。这个回头细讲。


附录:

  • Github Actions 文档
  • Cloudflare Workers 文档
  • Pipedream 文档
  • 第 1 步中提到的 Github Actions 代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
name: Create Post

on:
workflow_dispatch:
inputs:
path:
description: 'File path'
required: true
content:
description: 'File content'
required: true

permissions:
contents: write

jobs:
create-file:
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Create Post
run: |
cat << EOF > ./content/post/${{ github.event.inputs.path }}
${{ github.event.inputs.content }}
EOF

- name: Commit and push changes
run: |
git config user.name "username"
git config user.email "email@osguider.com"
git add ./content/post/${{ github.event.inputs.path }}
git commit -m "add post: ${{ github.event.inputs.path }}"
git push
  • 第 2 步中提到的 HTTP 请求:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import axios from 'axios';

const filePath = 'daily/daily-01.md';
const fileContent = 'Hello World';

const owner = "osguider";
const repo = "blog";
const workflow_file = "create-post.yml";
const url = `https://api.github.com/repos/${owner}/${repo}/actions/workflows/${workflow_file}/dispatches`;
const headers = {
'Authorization': `Bearer ${process.env.GITHUB_REPO_PAT_BLOG}`,
'Accept': 'application/vnd.github.v3+json',
'Content-Type': 'application/json'
};
const data = {
'ref': 'main',
'inputs': {
path: filePath;
content: fileContent;
},
};

axios.post(url,
data,
{ headers: headers })
.then((response) => {
console.log('GitHub Action dispatched successfully!');
})
.catch((error) => {
// TODO 错误通知
console.log(`Failed to dispatch GitHub Action: ${error}`);
});

流程引擎技术调研

作者 Seven
2024年1月2日 23:28

概念

  • 工作流
  • BPMN
  • DMN (Decision Model & Notation)
  • CMMN (Case Management Model and Notation)

选型

后端

Activiti

  • 官网
  • Github Star 9.1k
  • Document | Activiti Core

activiti 由 Alfresco 软件开发,目前最高版本 activiti 7
activiti 有 5、6、7 几个主流版本。其中 5 和 6 的核心 Leader 是 Tijs Rademakers,后来由于内部分歧,Tijs Rademakers 在 2017 年离开团队,创建了 flowable。
现在 activiti 由 Salaboy 团队接管,5 和 6 两个版本已经暂停维护,activiti 7 仍然使用 activiti 6 的内核,并没有开发新的特性。只是在 activiti 上层封装了一些应用。

flowable

  • 官网
  • Github Star 6k
  • Document | 文档
    使用
用户和用户组 | 中文

参考文档:

前端

bpmn

参考文档

  • 常见的工作流方案对比
  • BPM、BPMN、BPMN2.0概念介绍
  • BPMN和DMN基本概念和使用案例

IC卡、ID卡、CPU卡、RFID 和 NFC 的区别与联系

作者 Seven
2024年1月2日 23:28

RFID 卡

是指非接触式类电子卡片/标签,包括有ID卡、IC卡和NFC卡以及其它等电子卡/标签。他们主要的区别在于工作频段。

NFC

NFC,全称是Near Field Communication,“近距离无线通信”,NFC本质信息双向交换。

NFC和RFID都是基于位置相近的两个物体之间的信号传输,NFC技术增加了点对点(P2P)通信功能,NFC设备彼此寻找对方并建立通信连接。P2P通信的双方设备是对等的,而RFID通信的双方设备是主从关系。

NFC 的工作频率是 13.56Mhz,所以只能读取和模拟 13.56Mhz 的 IC 卡。

ID 卡

全称身份识别卡(Identification Card),低频(频率有125Khz、250 Khz、375 Khz、500 Khz等)。
只能读卡,不可写入、不可存储、不可加密的感应卡,(卡面)含固定的编号,出厂时固化了ID。

  • 特点

    卡面上有 10 位或者 8 位数字(卡号)。

  • 安全性

    安全性较低,ID 卡不可存储,所以卡片持有者的权限、功能操作要完全依赖于网络系统。
    很容易复制

  • 应用

    门禁系统、企业工牌

IC 卡

全称集成电路卡(Integrated Circuit Card),也称智能卡(Smart Card)。
可读写数据、容量大、有加密功能、数据记录可靠、使用更方便,如一卡通系统、消费系统等。

  • 网络依赖性

    IC 卡可记录用户资料,可脱离网络使用

  • 安全性

    IC 卡的加密和反复读些的特性,使其安全性远大于 ID 卡

  • 应用

    门禁卡、地铁、校园一卡通。
    二代身份证属于 type B 射频 IC 卡

CPU 卡

如果不强调的话CPU卡也是IC卡的一种,是高级版的IC卡,CPU卡有信息处理的功能,优点是存储空间大、读取速度快、支持一卡多用功能等特点 。如果不强调无线的话,电话卡中SIM卡就是典型的CPU卡。

  • 安全性

    CPU卡内含有随机数发生器,硬件加密算法等,配合芯片上的OS系统,达到金融级的安全级别,防止重复卡、仿制卡、卡上数据非法修改

  • 应用

    金融、保险、交警、政府行业等多个领域,小额支付行业
    SIM 卡

参考资料

  • IC卡、ID卡、CPU卡、RFID和NFC的区别
  • ID / IC 卡基本原理介绍与门禁卡 DIY

2023 年终总结

作者 Seven
2024年1月1日 00:00

前言

每一年都会发生很多很多的事情,能引发我去觉悟的,却寥寥无几。

2023,我称之为自己的开悟元年

一些故事和感悟,与你共享。

见自己、见天地、见众生

认识了一位新朋友,毫无保留地跟我分享她的世界。音乐、美食、玩乐、世界观,等等等等。给我异常封闭的世界打开了一扇窗。

还有一位老朋友,劳心劳力带我四处游玩,没有一丁点怨言。见到了许多从未见过的风水,体验了许多从未体验过的人情。

读万卷书不如行万里路,行万里路不如阅人无数。

可能多亏了这些朋友们,才能让我愿意打开自己的心门,尝试着去接触这个世界。

当然,按照我现在的境况,距离“见自己、见天地、见众生”还相差甚远。但我相信自己终能解开枷锁,释放真我。

直面恐惧

克服恐惧最好的方法,是直面恐惧。

作为一名后端开发,我对前端一直有一种莫名其妙的恐惧。
究其根由,还是写得比较少。索性趁着元旦 3 天假期,通宵达旦写了自己挂念许久的静态页面 看见导航

旁观只觉其易,亲历便知其难。
所幸、攻坚克难的过程,才是我们真正的成长。

直面恐惧,给了我莫大的信心和勇气,迎接属于我的 2023。

主观能动性

从毕业到现在工作 5 年,我所在的部门几乎年年拆分重组。以往是被动的随波逐流,公司安排到哪儿就去哪儿,今年恰逢前领导的部门扩招,索性主动换了一次部门。
现领导待我恩重如山(这个比喻没有丝毫夸张成份),提高了我的薪资待遇,也给了我管理团队的机会。我对他个人能力十分敬佩,只是当时对于整个团队氛围有点悲观,终究是做了个不义之徒,落荒而逃。

想着有始有终,鼓起勇气找领导坦白了要离开的事情。
我说我今年特别没有成就感,忙来忙去一整年,到头回顾发现自己好像啥也没做,没有任何成长。
领导的视角和观点对我产生了很大的冲击:

  1. 一个人不是技术成长了才算成长,管理能力成长了也是成长。
  2. 在管理的过程中,不是只有自己成长了才算成长,你帮助团队成员成长了也是成长。
  3. 不是结果成功了才算成长,走了失败的路线,知道那条路是错的也是成长。
  4. 不要给自己、和自己做的事情设限。你觉得你只能做到这种程度,那你就只能做到这种程度;你觉得你做的事情只值 10 万,那它就不可能做成 100 万的样子。
  5. 想要做成一件事情,需要自己去主动推进。有人告诉你这条路不行,你报告给领导说这条路不行。别人可能根本就没试过,你也根本就没争取过。然后呢?这条路真的不行了。
  6. 如果不知道自己该往哪里走,就看看比自己优秀的人到底好在哪里,怎样才能补全这些差距。选择道路的时候,也要想好这条路可能会帮助你获得什么成长。
  7. 要敢于突破自我,破除界限,坚守底线。好的产品是大家一起做好的,不是说某一个环节做好就能做好。作为一个开发,也要时刻想一想怎么把产品做得更好。需求有问题就跟需求聊,产品方向有问题就跟产品或者项目经理聊。不要因为你只是一个开发,就只做开发的事情,就只是别人说啥做啥,随波逐流。

一方面解答了我个人成长的困境,一方面阐释了团队滞后的原因。可能别人会觉得这是鸡汤,但对于当时的我来讲,真的是醍醐灌顶。
事后跟同事调侃说,如果我能早一点找领导聊天,可能就不会申请换部门了。
但如果终究是如果,流程走完,板上钉钉,最后还是带着满腔愧疚,搬到了新部门。

信仰崩塌

后面大概想了一下我为什么会走:

  1. 一方面是觉得当时的团队没有产品思维,客户说啥就是啥,朝令夕改,忙来忙去一场空,一年下来不知道自己做了什么,缺乏成就感。
  2. 另一方面是领导想培养我做管理,但是我却在计较技术上的得失。目标没对齐,难免有落差。
  3. 新部门的领导是我的旧识,我对他最大的印象:他是公司里我知道的唯一一个会过滤客户需求的人,不合理/会变动/没效益的需求,几乎都不会透传到开发层面,也就是我们几乎不会做无效开发。

但很快,现实给了我当头一击。
新领导现在是中心经理,不再劳心需求的事情。这边的需求一样是透传客户想法、一样是频繁变动,甚至连需求文档都写不明白。
一开始我还想推动一下变革,改一改团队的问题。但后面愈发疲惫,只觉蚍蜉撼树,螳臂当车,难有成效。

突觉程序员和建筑工人竟如此相似:

  • 建筑工人按照工程师的蓝图一砖一瓦建造现实中的高楼大厦。
  • 程序员按照需求的描述一字一句构筑网络上的虚拟系统。

人人都觉得拆楼重建成本巨大,人人都觉得代码重写信手拈来。加加班就能搞定的事情,不是很简单吗?

我对技术一直都有一种信仰:我一直觉得技术改变世界,科技构建未来。

突然间,我开始怀疑人生:我是谁?我在哪儿?我活着是为了什么?日复一日通宵达旦写一些没人用的垃圾系统的意义在哪里?
突然间,我对技术的信仰崩塌了。只觉程序员不过是个提线木偶,在别人的操作下苟活罢了。

按理说生活中不该有这么多抱怨的,要么忍受、要么改革、要么离开。
至于为什么不跳槽,这个问题我也没想明白。

信念重塑,独立开发

我从高中开始衷爱 IT 的原因,是幻想着有朝一日能够让电脑替我工作。
但现实却非常有趣,更像是我是在为电脑工作。

既然工作中不太如意,索性就开始尝试把曾经的幻想落实下来。试一试能不能走通自己梦想中的独立开发和自由职业的道路。

全网漫游指南

缘分使然,我关注到了 FutureForceDAO全网漫游指南
当时觉得指南跟我的导航目的相似,都是想把美好的工具分享给更多人知道。既然如此,为什么不一起把这件事情做好呢?
当即填写了申请表格,看看能否为社区做出一点点贡献。
今年最大的收获,当属于此。

互利共赢才能合作长久

我是一个头脑异常简单的人,很少去深入思考权衡利弊。申请加入 FutureForceDAO 的原因纯粹是觉得可以跟一群有着共同目标的人一起把一件非常有价值的事情完成得更好。
在加入 FutureForceDAO 的 OnBoarding 会议上,创始人 Augustus 问了我一个问题:“你希望社区能给你带来什么?”。
当时听到这个问题我愣了一下:我居然可以索取回报吗?
(嗯,奴性确实是重了一点)

事后回想,一个社区如果想要稳定发展,势必要让成员在付出的同时获得回报。这些回报可以是能力成长、可以是归属感、可以是成就感。但是不能什么都没有,没有互利的合作是无法长久的。

这并不是一件很大的事情,但是足以让我认识到我的思维模式有问题,为以后的改变奠定了基础。

在生活工作中其实也有这样的问题:

  • 你越是软弱,别人就越觉得你好欺负。
  • 你越是越是加班,别人就越觉得你加班理所当然。
  • 你越是谦让,世界上留给你的机会就越少。

你并不欠任何人任何东西,自己的权益终究是要自己去争取,自己的人设最后还是要自己来打造。

运营的重要性

后来正式加入 FutureForceDAO,开始在日常事务中逐渐了解它,这再一次颠覆了我的认知。

按照我以往的逻辑,如果我想要开发一款产品,那必定是去构思、去设计、去实现,埋头苦干几个月,憋出一个大招。
最后呢?可能市场并不买单。劳心劳力的结果,可能是竹篮打水一场空。

FutureForceDAO 采用了完全不同的方式:从产生想法开始,就积极与用户紧密交流。然后不断地开发 MVP,不断地迭代升级。最终产品研发完成,已经有了非常多的和产品一起成长起来的对产品有着深厚情感的用户。这些用户,无疑才是最大的宝藏。

当你凝望深渊的时候,深渊也在凝望你

『全网漫游指南』的 slogan 是“定义未来的同时,未来也在塑造我们”。这不禁让我想起了一句谚语:“当你凝望深渊的时候,深渊也在凝望你”。
初闻不识言中义,再见已是局中人。

万事万物皆有其“反身性”:你在影响事物的同时,事物也在影响着你。

  • 当我们在躬身做事的时候,事情同时也在反哺我们经验。
  • 当我们深入研究某个垂直领域的时候,这个垂直领域可能也在限制你的横向拓展。
  • <>
  • 当我自以为在给『全网漫游指南』无私奉献的时候,『全网漫游指南』着实教会了我许多许多东西。
  • 我自以为编程技能还算可以,但程序员的清高可能恰巧限制了我靠运营去赚钱的能力。

我们应该享受这种影响,也应该畏惧这种影响。

Learning by Doing

Learning by Doing,是 FutureForceDAO 的口头禅。

在学习中实践,在实践中学习,相辅相成,彼此成就。

成长无非“觉、知、行”。认知提升了,行动没跟上,空想多于实践,就会造成欲求不满、极度内耗的情况。
在以往的生活中,我就是这样一个极度内耗的人。

好在遇到了 FutureForceDAO,让我在做事的过程中获益匪浅。

开源服务指南

眼睛看到的都是别人的故事,动手完成的才是自己的成长。

仿照着『全网漫游指南』,我开始学习借助 Notion 和 Pipedream 这些可能我之前可能看不起(因为数据库和代码其实更灵活)的 SaaS 产品,快速地构建属于自己的『开源服务指南』。
这并不是一件容易的事情,好在我们克服的每一个艰难,最终都会是自己的成长。

从刚开始决定要做『开源服务指南』,到真正写下第一篇文章,花了将近 3 个月的时间。
从我开始动手搭建『开源服务指南』,到第一篇文章发布,花了 1 周的时间。
没错,我又花费了两个多月的时间去构思、去梳理、去空想。我以为我设计了一个很完美的工作流程,但真正实践的时候,才发现事实与构想,所差甚远。

好在后面及时调整,让自己只专注当下最重要的一件事情。不断迭代,不断优化。
每完成一个优化点,对我来说都有着巨大的成就感。这些成就感,又不断地给我提供了继续前行的动力。
个人成长理论中把这个过程称为“最小正循环”:给自己最小的阻碍,让自己最轻松、最快速地克服阻碍,体会成长的快感。小步快走,飞速成长。
这就是 MVP 和迭代的魅力。

当然,我也会焦虑。因为是 MVP,必定与自己的终极目标相差甚远。我也五次三番地想要抛弃 Notion 和 Pipedream,通过自己写代码一步到位。
我总是会头脑发热、不记成本地去追逐自己的热情,这并不是一件好的事情。
好在这时候我意识到了投入产出比的问题,我给自己定了一个目标:公众号粉丝 3000 之前,不再考虑写代码的事情。焦虑随之消失。

截至现在,『开源服务指南』历时 8 个月,已经:

  • 更新了168 篇文章
  • 有 3141 位 公众号 粉丝
  • CSDN 访问 7万+,入围 博客之星 2023
  • 掘金 优质作者周榜第 8
  • 当然,也终于有了官网 开源服务指南

你看,空想全是困难,实践皆是成绩。『开源服务指南』夺走了我几乎所有业余时间,也给了我极大的成长。


至于『开源服务指南』是什么,以一言概之:“用中文推荐优质开源项目,让开发者更容易找到趁手的开源工具”
如果你对开源服务指南感兴趣,欢迎参阅这篇文章 《开源服务指南使用手册(还没写呢,回头再点)》

副业

人总是贪心的,我也想在其他方向获得突破。花了 1960 元加入 生财有术,花了 9000 多元入局了『IDO 老徐』的八年合伙人。
我也总是懒惰的,买了等于会了的魔咒依然在生效,今年并没有很好地学习和实践。

亦仁说副业最重要的事情是“躬身入局、把手弄脏”。
老徐也总是提醒我们“去执行,出结果”。
我却还是放不下程序员的假清高,没能放下姿态、迈出脚步去赚钱。

消费总有冲动的成分,但我并不后悔。影响很多时候是潜移默化的,先埋下一颗种子,它自会在合适的时候生根发芽。

后记

相信我的明年会比今年更好,祝愿所有的读者都能找到自己的心安。

作者 Seven
2023年11月11日 08:45

“终有一天,我要抛却一切枷锁。踏上旅程,环游世界。”

“健康永远是第一要务,是时候好好规划一下了。”

“等我调研完所有的时间管理理念并且总结出一套适合自己的方法论再去执行。”

“我想开个自媒体帐号分享心得,倒逼自己成长。但是…..等想好名字再开始吧。”

“完美主义不可取,得先完成再谈优化呀。这个毛病得改改。”

……

嗯,你说的对。然后呢?

不了了之。


“等”,是一个在我的生活中高频出现的字眼。

总想着等时机成熟之后再去行动,可是不行动又怎会促使时机成熟呢?

在漫长的等待中丧失热情,失去动力。然后、再等待下一个等待的目标,再等待下一次失去动力。

不满足现状,寄希望于将来。碌碌终日,郁郁寡欢。


成长大概 觉、知、行 三字。

行动是进步的拦路虎,也是突破的登云梯。

不妨就从今天开始吧,迈步踏上行程,再辨路在何方。

计划、执行、复盘、迭代,周而复始,终至终途。

记一次 Notion 油猴脚本的编写经历

作者 Seven
2023年10月9日 22:46

起因

最近借助 Notion 作为数据库搭建了《开源服务指南》的工作流,Notion 是真的好用,无奈复制出来的 Markdown 文本中图片部分格式不对(不知道是不是我使用方法有问题),需要手动修正。一回生二回熟三回咱可不就烦了嘛,所以想着写个油猴插件解决 把 Notion Page 内容复制为标准 Markdown 文本 这个问题。

折腾

获取页面 HTML,然后 HTML 转 Markdown

先查了一下资料,在 GreasyFork 搜索 notionmarkdown 关键字,找到了这么个脚本:复制为Markdown格式

使用类似的思路,先获取目标 DOM 的 HTML 代码,然后 HTML 转 Markdown。但是理想很丰满,显示很骨感。
实际上 Notion Page 内容并不是标准的语义化的 HTML 标签,而是使用了大量的 div 和自定义样式。所以转换效果并不理想,会有很多样式丢失。
比如无序列表变成了普通文本,代码块也会变成普通文本等。这个问题也不是不能修复,只是要把对应的 HTML DOM 替换成标准格式,略微繁琐,遂放弃。

以下是相关代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
// ==UserScript==
// @name Notion-Copy-Then-Fix-Markdown-v1
// @name:zh-CN Notion 复制 Markdown 格式修复 v1
// @namespace http://tampermonkey.net/
// @version 0.1
// @description 修复 Notion 复制内容后 Markdown 格式问题
// @author Seven
// @match *://www.notion.so/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=notion.so
// @require https://unpkg.com/turndown/dist/turndown.js
// @grant none
// ==/UserScript==

(function() {
'use strict';

const urlProtocol = document.location.protocol;
const urlOrigin = document.location.origin;
const urlPath = (document.location.pathname.substring(0, document.location.pathname.lastIndexOf('/'))) + '/';

function initCopyButton() {
let copyButton = document.createElement('div');

copyButton.style.position = "fixed";
copyButton.style.width = "44px";
copyButton.style.height = "22px";
copyButton.style.lineHeight = "22px";
copyButton.style.top = "14%";
copyButton.style.right = "1%";
copyButton.style.background = "#0084ff";
copyButton.style.fontSize = "14px";
copyButton.style.color = "#fff";
copyButton.style.textAlign = "center";
copyButton.style.borderRadius = "6px";
copyButton.style.zIndex = 10000;
copyButton.style.cursor = "pointer";
copyButton.style.opacity = 0.6;
copyButton.innerHTML = "Copy Content";

copyButton.addEventListener('click', copyPageContent);
console.log('initCopyButton');
document.body.prepend(copyButton);
}

function getContentElement() {
let pageContent = document.querySelector('main.notion-frame .notion-page-content');
if (!pageContent) {
return;
}
return pageContent.innerHTML;
}

function html2Markdown(htmlText) {
if (!htmlText) {
return;
}

htmlText = htmlText
.replace(/<figure[\s\S]+?<\/figure>/gi, processFigure)
.replace(/<img[^>]+>/gi, processImg)
.replace(/(<a.+?href=")(.*?")(.*?<\/a>)/gi, parseHref)
;
// 引入 turndown.js 库
let turndownService = new TurndownService();
let markdownText = turndownService.turndown(htmlText);
markdownText = markdownText.replace(/<img.+?>/g, "");
return markdownText;
}

function processFigure(str) {
str = str.replace(/<noscript>[\s\S]*<\/noscript>/, '');
let img = str.match(/<img[^>]+?>/);
if (img) {
return img[0];
}
return str;
}

function processImg(imgStr) {
let src = (imgStr.match(/\ssrc=(["'])(.*?)\1/) || [])[2];
if (!src) {
return '';
}
let original = (imgStr.match(/\sdata-original=(["'])(.*?)\1/) || [])[2];
if (original) {
src = original;
}
if (src.toLowerCase().indexOf('http') === 0) {
return '<img src="'+src+'" />';
} else if (src.indexOf('//') === 0) {
src = urlProtocol + src;
} else if (src.indexOf('/') === 0) {
src = urlOrigin + src;
} else {
src = urlPath + src;
}
return '<img src="'+src+'" />';
}

function parseHref(match, head, link, tail){
if (link.substr(0, 4) === 'http') {
return head + link.replace(/#.*/,"") + tail;
}
var path = document.location.pathname.split('/');
path.pop();
if (link[0] === '#' || link.substr(0, 10) === 'javascript' || link === '"') { // "#" "javascript:" ""
return head + '#"' + tail;
} else if (link[0] === '.' && link[1] === '/'){ // "./xxx"
return head + document.location.origin + path.join('/') + link.substring(1) + tail;
} else if (link[0] === '.' && link[1] === '.' && link[2] === '/') { // ../xxx
var p2Arr = link.split('../'),
tmpRes = [p2Arr.pop()];
path.pop();
while(p2Arr.length){
var t = p2Arr.pop();
if (t === ''){
tmpRes.unshift(path.pop());
}
}
return head + document.location.origin + tmpRes.join('/') + tail;
} else if (link.match(/^\/\/.*/)) { // //xxx.com
return head + document.location.protocol + link + tail;
} else if (link.match(/^\/.*/)) { // /abc
return head + document.location.origin + link + tail;
} else { // "abc/xxx"
return head + document.location.origin + path.join("/") + '/' + link + tail;
}
}

function copyToClipboard1(text) {
const input = document.createElement('textarea');
input.style.position = 'fixed';
input.style.opacity = 0;
input.value = text;
document.body.appendChild(input);
input.select();
const res = document.execCommand('copy');
document.body.removeChild(input);
return res;
}

function copyToClipboard(text) {
navigator.clipboard.writeText(text)
.then(() => {
console.log('文本已成功复制到剪贴板');
})
.catch((err) => {
console.error('复制操作失败', err);
});
}

function copyPageContent() {
const innerHtmlOfPageContent = getContentElement();
let markdownContent = html2Markdown(innerHtmlOfPageContent);
markdownContent = fixMarkdownContent(markdownContent);
console.log(markdownContent);
// 将转换后的 Markdown 文本写入剪贴板
navigator.clipboard.writeText(markdownContent)
.then(() => {
showMessage('复制成功');
})
.catch((err) => {
showMessage('复制失败');
});

const copyResult = copyToClipboard(markdownContent);

const message = copyResult ? '复制成功' : '复制失败';
// 添加提示信息(可选)
}

function fixMarkdownContent(markdown) {
if (!markdown) {
return;
}

const regex = new RegExp(`!\\[\\]\\(${urlOrigin}\\/image\\/(http.*?)\\?.*?\\)`, 'g');
return markdown.replaceAll(regex, (match, group1) => {
const processedText = decodeURIComponent(group1);
return `![picture](${processedText})`;
});
}

// 添加提示信息的函数(可选)
function showMessage(message) {
const toast = document.createElement('div');
toast.style.position = 'fixed';
toast.style.bottom = '20px';
toast.style.left = '50%';
toast.style.transform = 'translateX(-50%)';
toast.style.padding = '10px 20px';
toast.style.background = 'rgba(0, 0, 0, 0.8)';
toast.style.color = 'white';
toast.style.borderRadius = '5px';
toast.style.zIndex = '9999';
toast.innerText = message;
document.body.appendChild(toast);
setTimeout(function() {
toast.remove();
}, 3000);
}

function init() {
initCopyButton();
}

init();
})();

借用 clipboard.js 执行复制功能

无意间发现了一个叫作 clipboard.js 的工具,看起来很方便。试了一下,只能复制 DOM 的 innerText,并不能复制 DOM 元素(或者说触发 Notion 本身的复制方法),作罢。

以下是代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
// ==UserScript==
// @name New Userscript
// @namespace http://tampermonkey.net/
// @version 0.1
// @description try to take over the world!
// @author You
// @match https://www.notion.so/futureforcedao/No-42-a696f6e800174eeeac124f56cf2949da
// @icon https://www.google.com/s2/favicons?sz=64&domain=notion.so
// @grant none
// @require https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.11/clipboard.min.js
// ==/UserScript==

(function() {
'use strict';

init();

/**
* 初始化动作
*/
function init() {
waitFor('#notion-app .notion-page-content').then(([notionContentElement]) => {
initCopyButton();
});
}

/**
* 初始化复制按钮
*/
function initCopyButton() {
let copyButton = document.createElement('div');

copyButton.id = 'copyButton';
copyButton.style.position = "fixed";
copyButton.style.width = "88px";
copyButton.style.height = "22px";
copyButton.style.lineHeight = "22px";
copyButton.style.top = "14%";
copyButton.style.right = "1%";
copyButton.style.background = "#0084ff";
copyButton.style.fontSize = "14px";
copyButton.style.color = "#fff";
copyButton.style.textAlign = "center";
copyButton.style.borderRadius = "6px";
copyButton.style.zIndex = 10000;
copyButton.style.cursor = "pointer";
copyButton.style.opacity = 0.6;
copyButton.innerHTML = "Copy Content";
// copyButton['data-clipboard-target']='#notion-app .notion-page-content';

console.log('initCopyButton');
document.body.prepend(copyButton);


var clipboard = new ClipboardJS('#copyButton', {
target: function(trigger) {
return document.querySelector('#notion-app .notion-page-content');
}
});

clipboard.on('success', function(e) {
console.info('Action:', e.action);
console.info('Text:', e.text);
console.info('Trigger:', e.trigger);

e.clearSelection();
});

clipboard.on('error', function(e) {
console.error('Action:', e.action);
console.error('Trigger:', e.trigger);
});
}


/**
* 等待指定 DOM 元素加载完成之后再执行方法
*/
function waitFor(...selectors) {
return new Promise(resolve => {
const delay = 500;
const f = () => {
const elements = selectors.map(selector => document.querySelector(selector));
if (elements.every(element => element != null)) {
resolve(elements);
} else {
setTimeout(f, delay);
}
}
f();
});
}
})();

直接调用系统复制功能,然后读取剪切板内容并进行替换

后面我发现这个复制为 Markdown 的功能应该是 Notion 自己实现的,并不是粘贴时编辑器把富文本变成了 Markdown。既然如此,为啥不直接调用 Notion 自己的复制功能,然后我们读取剪切板再做内容修正嘞?

说干就干,搞完之后还是有一个很奇怪的小问题:同一个页面第一次复制内容不对,但是从第二次开始就对了。
各种调测,最后发现好像(嗯,只是好像我也没深究)Notion 是在我执行复制操作之后花了一定时间才完成 Markdown 的转换工作,遂不得已加了一个延时操作,问题解决。

以下是代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
// ==UserScript==
// @name Notion-Copy-Then-Fix-Markdown
// @name:zh-CN Notion 复制 Markdown 格式修复
// @namespace http://tampermonkey.net/
// @version 0.1
// @description 修复 Notion 复制内容后 Markdown 格式问题。感谢 [复制 Notion 页面正文内容](https://greasyfork.org/zh-CN/scripts/398563-copy-notion-page-content/code) 为本脚本提供了诸多参考。
// @author Seven
// @match *://www.notion.so/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=notion.so
// @grant GM_setClipboard
// ==/UserScript==

(function() {
'use strict';

init();

/**
* 初始化动作
*/
function init() {
waitFor('#notion-app .notion-page-content').then(([notionContentElement]) => {
initCopyButton();
});
}

/**
* 初始化复制按钮
*/
function initCopyButton() {
let copyButton = document.createElement('div');

copyButton.style.position = "fixed";
copyButton.style.width = "88px";
copyButton.style.height = "22px";
copyButton.style.lineHeight = "22px";
copyButton.style.top = "14%";
copyButton.style.right = "1%";
copyButton.style.background = "#0084ff";
copyButton.style.fontSize = "14px";
copyButton.style.color = "#fff";
copyButton.style.textAlign = "center";
copyButton.style.borderRadius = "6px";
copyButton.style.zIndex = 10000;
copyButton.style.cursor = "pointer";
copyButton.style.opacity = 0.6;
copyButton.innerHTML = "Copy Content";

copyButton.addEventListener('click', copyPageContentAsync);
console.log('initCopyButton');
document.body.prepend(copyButton);
}

/**
* 复制 Notion Page 内容
*/
function copyPageContentSync() {
copyElementSync('#notion-app .notion-page-content');
// copyElementChildren('#notion-app .notion-page-content');

navigator.clipboard.readText()
.then(clipboardContent => {
console.log('clipboardContent', clipboardContent);
const markdownContent = fixMarkdownFormat(clipboardContent);
console.log('markdown', markdownContent);

GM_setClipboard(markdownContent);
showMessage('复制成功');
});
}
async function copyPageContentAsync() {
await copyElementAsync('#notion-app .notion-page-content');
// copyElementChildren('#notion-app .notion-page-content');

const clipboardContent = await readClipboard();
if (!clipboardContent) {
showMessage('复制失败');
return;
}

console.log('clipboardContent', clipboardContent);
const markdownContent = fixMarkdownFormat(clipboardContent);
console.log('markdown', markdownContent);

GM_setClipboard(markdownContent);
showMessage('复制成功');
}

/**
* 修正 markdown 格式
*/
function fixMarkdownFormat(markdown) {
if (!markdown) {
return;
}

// 给没有 Caption 的图片添加 ALT 文字
return markdown.replaceAll(/\!(http.*\.\w+)/g, (match, group1) => {
const processedText = decodeURIComponent(group1);
console.log('regex', processedText);
return `![picture](${processedText})`;
});

// TODO 给有 Caption 的图片去除多余文字
}

/**
* 读取系统剪切板内容
*/
async function readClipboard() {
try {
const clipText = await navigator.clipboard.readText();
return clipText;
} catch (error) {
console.error('Failed to read clipboard:', error);
}
}

function copyElementChildren(selector) {
const dom = document.querySelector(selector);
const range = document.createRange();
range.setStart(dom, 0);
range.setEnd(dom, dom.childNodes.length);

const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);

document.execCommand('copy');
// selection.removeAllRanges();
}

/**
* 复制 DOM 元素(在 DOM 元素上执行复制操作)
*/
async function copyElementAsync(selector) {
const pageContent = document.querySelector(selector);
// pageContent.focus();
let range = document.createRange();
range.selectNodeContents(pageContent);

let selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
pageContent.focus();
await sleep(500);

document.execCommand('copy');
selection.removeAllRanges();
}
function copyElementSync(selector) {
const pageContent = document.querySelector(selector);
// pageContent.focus();
let range = document.createRange();
range.selectNodeContents(pageContent);

let selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
pageContent.focus();

document.execCommand('copy');
// selection.removeAllRanges();
}

/**
* 在页面显示提示信息
*/
function showMessage(message) {
const toast = document.createElement('div');
toast.style.position = 'fixed';
toast.style.bottom = '20px';
toast.style.left = '50%';
toast.style.transform = 'translateX(-50%)';
toast.style.padding = '10px 20px';
toast.style.background = 'rgba(0, 0, 0, 0.8)';
toast.style.color = 'white';
toast.style.borderRadius = '5px';
toast.style.zIndex = '9999';
toast.innerText = message;
document.body.appendChild(toast);
setTimeout(function() {
toast.remove();
}, 3000);
}

/**
* 等待指定 DOM 元素加载完成之后再执行方法
*/
function waitFor(...selectors) {
return new Promise(resolve => {
const delay = 500;
const f = () => {
const elements = selectors.map(selector => document.querySelector(selector));
if (elements.every(element => element != null)) {
resolve(elements);
} else {
setTimeout(f, delay);
}
}
f();
});
}

/**
* 延迟执行
**/
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
})();

直接追加一个 EventListener 做格式修正

前面的代码,就算加了延时还是会有一个小 bug:一键复制的时候偶尔会少复制最后一个 Notion Block 内容。
更新:出现这个问题的原因不明,但是只要在 Notion 文章最后加入一个空行,就能避免这个问题

最后还是硬着头皮看了看 Notion 的代码,发现 Notion 的复制事件是放在 window 上的(怪不得我之前在 document 上面找不到),既然找到了 Notion 自己的复制事件就好办了。最简单的方法就是在 window 上面追加一个 copy 事件,做额外的 Markdown 格式修正工作。

以下是代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// ==UserScript==
// @name copy-notion-page-content-as-markdown
// @name:en Copy Notion Page Content AS Markdown
// @name:zh-CN 复制 Notion Page 内容为标准 Markdown 文本
// @description 复制 Notion Page 内容为标准 Markdown 文本。
// @description:zh-CN 复制 Notion Page 内容为标准 Markdown 文本。
// @description:en Copy Notion Page Content AS Markdown.
// @namespace https://github.com/Seven-Steven/tampermonkey-scripts/tree/main/copy-notion-page-content-as-markdown
// @version 0.2
// @license MIT
// @author Seven
// @match *://www.notion.so/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=notion.so
// ==/UserScript==

(function () {
'use strict';

init();

/**
* 初始化动作
*/
function init() {
injectActions();
}

function injectActions() {
window.addEventListener('copy', fixNotionMarkdownInClipboard);
}

/**
* 修正剪切板中 markdown 的格式
*/
function fixNotionMarkdownInClipboard() {
navigator.clipboard.readText().then(text => {
const markdown = fixMarkdownFormat(text);
navigator.clipboard.writeText(markdown).then(() => {
}, () => {
console.log('failed.');
})
})
}

/**
* 修正 markdown 格式
*/
function fixMarkdownFormat(markdown) {
if (!markdown) {
return;
}

// 给没有 Caption 的图片添加默认 ALT 文字
markdown = markdown.replaceAll(/\!(http.*\.\w+)/g, (match, group1) => {
const processedText = decodeURIComponent(group1);
console.log('regex', processedText);
return `![picture](${processedText})`;
});
// 给有 Caption 的图片去除多余文字
const captionRegex = /(\!\[(?<title>.+?)\]\(.*?\)\s*)\k<title>\s*/g;
return markdown.replaceAll(captionRegex, '$1');
}
})();

为了追求简单,这里移除了一键复制操作,后面有空再补。

完美方案

页面分析

  • 对于 view / database 等页面:Notion Page 可能会显示在 Center Peek / Side Peek 中。需要动态监听最近的祖先元素子节点变动,动态装载/卸载插件。

    页面结构如下:

    • div#notion-app
      • div.notion-app-inner
        • div.notion-cursor-listener 到这里是一定会有,再往下是 Peek,不一定会有
          • div.notion-peek-renderer
            • div.layout.layout-side-peek
              • div.notion-page-content
  • 对于正常的 Notion Page 页面,只需要装载插件就好,不需要卸载。

结构如下:

  • div#notion-app
    • div.notion-app-inner
      • div.notion-cursor-listener
        • main.notion-frame
          • div.notion-page-content

插件装载时机

  • 开局就找 .notion-page-content
    • 如果找得到,装载插件,结束
    • 如果找不到,不做任何事情,结束。
  • 同步查找 #notion-app main.notion-frame .notion-page-content
    • 如果找得到,代表当前页面是正常的 Notion Page 页面,装载插件
    • 如果找不到,代表当前页面是 view / database 页面, 给 #notion-app 添加 observe,监听其子节点变动,根据子节点变动情况动态装载 / 卸载插件。结束。

插件行为

  • 先给 window 追加一个类型为 copyEventListener,事件触发时,读取剪切板内容并修正 Markdown 格式;
  • 往页面注入一个“复制”按钮,用户点击按钮时,自动选中 Notion 页面内容并触发 copy 事件;

代码

Github

后记

技术不到位,就只能各种摸爬滚打。
按理说如果底子够硬的话是能找到 Notion 的转换方法的,直接调用就行了。

多看文档,了解一下浏览器有什么 API,每个 API 怎么调用、有什么能力,往往能起到事半功倍的效果。

脚本已发布至 Greasy Fork 可以自行下载使用。

参考文档

  • Event | Events | DOM-Level-3-Events | JavaScript 事件顺序 JS 中的事件接口、事件类型、以及事件执行顺序。
  • EventTarget
  • MutationObserver
    • MutationRecord
      • MutationRecord/addedNodes
      • MutationRecord/removedNodes
  • ClipboardAPI
  • ClipboardEvent
  • copy_event
  • 创建和触发事件
  • 复制 Notion 页面正文内容 这里有个等待页面 DOM 元素加载完成之后执行操作的方法非常棒,学到就是赚到。
  • 复制为Markdown格式 借助 turndown.js 实现 HTMLMarkdown,开眼。
  • 选择(Selection)和范围(Range) 概念讲解,写得很详细,但我没看完。
  • 防抖与节流 防抖与节流

蒲公英

作者 Seven
2023年3月28日 10:58

我最近突然对自己有了相对来说更加清醒的认知。

在互联网的世界里 我把自己比作蒲公英。

乘风而行,能比更多人更早地见到更多更新鲜的事物。
同时也是随风漂泊,居无定所。风过之后,尽是虚无。

能乘风但不能御风,观万物但未能留痕。
当下之路,或许是择一地生根发芽,向阳处恣意生长。

月刊-202208

作者 Seven
2023年2月26日 13:02

拓认知

  • 学习,没有捷径

开发资源

  • CompVis/stable-diffusion

    根据文字描述生成精美图片。

  • megaease/easegress

    全功能型的流量调度和编排系统。官网 | 文档

  • alibaba/DataX

    支持超多数据库的离线数据同步工具。

  • gerrit

    code review 工具

  • 3D 白模生成 blender + 插件 domlysz/BlenderGIS

  • 前端学习资料

    • 入门:https://www.freecodecamp.org
    • 补充:https://scrimba.com/learn/frontend
    • JavaScript: https://zh.javascript.info
    • MDN: https://developer.mozilla.org/en-US
    • Vue: https://vuejs.org
    • React: https://reactjs.org or https://beta.reactjs.org
    • ES6 Standard: https://www.ecma-international.org/publications-and-standards/standards/ecma-262/
  • CSS 库

    • 50projects50days
      50 个 web 小 demo,用来学习再好不过了
    • 30-seconds/30-seconds-of-css
      一些 css 代码片段,可以拿来学习练手
    • Tailwind CSS
      一个功能类优先的 CSS 框架
    • animate.css
      css 动效库
    • normalize.css
      让你的 HTML 默认样式保持跨浏览器的一致性
    • bulma
      基于 Flexbox 的现代化响应式 css 框架
    • IanLunn/Hover
      给你的网站添加 hover 动效
    • tobiasahlin/SpinKit
      有趣的页面加载动效
  • 商城系统

    • wei-it/weiit-saas
      weiit-saas是一款Java开源项目,属于weiit团队自研产品,意在通过技术封装,让企业无需代码开发,帮助企业一键生成小程序、公众号,让企业拥有独立品牌的自营商城。
    • fuint会员营销系统
      一款实体店铺会员管理和营销系统。
    • 开店星开源小程序商城
    • Lilishop B2B2C商城系统
    • 免费开源的B2B2C商城系统

看世界

  • 10 个你没见过的 GitHub 的高效开源神器,YYDS!
  • 盘点 7 个神级笔记开源应用!
  • craiyon 根据文本描述生成图片

免费申请 JetBrains 全家桶开源授权

作者 Seven
2022年8月7日 23:29

JetBrains 开源授权

JetBrains 针对部分群体推出了一系列特惠计划,其中包括 开源开发许可证。通过开源开发许可证,我们可以免费使用 JetBrains 旗下几乎所有开发工具。
本文讲述了如何申请 JetBrains开源开发许可证

申请条件

jetbrains-open-source-license-condition

总结一下:

  • 条件:

    • 项目符合 开源 定义
    • 开源项目正在积极开发且最近三个月内有更新
    • 申请人是这个开源项目的项目所有者或主要贡献者,提交申请时所填写的邮箱要和项目贡献人的邮箱以及 Github Profile 邮箱保持一致(申请期间 Github Profile 页面的邮箱不能隐藏,工作人员需要通过这个邮箱复核申请者的身份)
    • 不提供开源项目的付费版本,也不提供与该开源项目相关的任何付费服务(比如付费支持、咨询等)
    • 开源项目未获得商业公司或组织(NGO、教育、研究或政府组织)的资助
    • 不为项目的核心开发者支付工资
    • 鼓励开源项目提供社区标准文档,比如在 README 文件中提供 项目描述、行为准则贡献指南
    • 暂时对项目质量没有要求
  • 限制

    • 许可证仅提供给项目负责人和核心项目提交者
    • 许可证有效期为一年,如果一年后这个项目仍符合申请条件,可以继续申请
    • 免费许可证只能用于开发非商业开源项目
    • 不能与任何第三方共享许可证

申请过程

打开 JetBrains 开源开发许可证申请页面 Request for Open Source Development License 并填写申请资料:

intellij-open-source-license-request-new-customer

有以下几点作特别说明:

  1. Do we Know you?
    申请新的授权或者延续之前已经申请过的授权,如实填写即可;
  2. Tell us about your project
    • Project website: 如果项目有独立主页,填写项目主页地址;如无,填写同 Repository URL 即可;
    • License URL: 如有,填写项目开源协议地址;如无,在 Github 项目主页点击 “Add file” -> “Create new file”,文件名写 “LICENSE”,页面右侧会自动出现 LICENSE 选择提示,选择并添加适合自己的开源协议即可;
    • No. of required licenses: 表示要申请的开源开发许可证数量,数量需小于或者等于项目活跃核心开发者数量;
  3. Tell us about youself
    • Email address: 这里所填写的邮箱要和项目贡献人的邮箱以及 Github Profile 邮箱保持一致(申请期间 Github Profile 页面的邮箱不能隐藏,工作人员需要通过这个邮箱复核申请者的身份)

资料填写完成后提交申请,会跳转到如下页面:

intellij-open-source-license-request-confirmation

同时,我们会收到一封邮件,告诉我们 JetBrains 已经收到了刚刚提交的申请,团队大概会在一周内审查我们的开源项目。

intellij-license-request-notice

耐心等待几天,JetBrains 会在审查完成之后发送另一封邮件。告知我们申请已通过审批并指引我们领取 License,或者告知你拒绝审批的原因

intellij-license-request-result

如果审批通过,可以点击邮件中的 “Take me to my license(s)” 链接,将 Licenses 绑定到对应的帐号。


至此,我们已经免费拿到了 JetBrains 的开源开发许可证,许可证有效期一年,届时可视官方政策继续申请。

If you find that JetBrains software has been useful for your project, please consider mentioning JetBrains support on your project’s homepage. Feel free to use the JetBrains logo and a link to our website such as https://jb.gg/OpenSourceSupport.

参考文档

  • 免费申请和使用IntelliJ IDEA商业版License指南

使用 dockerfile-maven-plugin 构建 docker 镜像

作者 Seven
2022年7月16日 18:00

简介

本文介绍了 com.spotify:dockerfile-maven-plugin 的简单使用示例。
最终达成的目标是把 docker 镜像构建集成在 maven 打包过程中,可以使用 maven 命令构建 docker 镜像。

功能相似的插件有三个:

  • com.spotify:dockerfile-maven-plugin
    本文使用的插件,已经停止更新,但功能依旧稳定。
  • com.spotify:docker-maven-plugin
    本文所用插件的同胞兄弟,官方不推荐使用,已停止更新。
  • io.fabric8io:docker-maven-plugin
    支持在 pom.xml 中配置 Dockerfile 的各项内容,也支持自定义 Dockerfile,支持操作容器,功能强大,仍在更新。

使用

以 spring-boot-web 项目为例:

  1. 创建一个 spring-boot-web 项目并确保项目正常;

  2. 依据项目需要在合适的位置定制一个 Dockerfile,这里把 Dockerfile 放在了 src/main/docker 目录下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    FROM openjdk:8-jdk-alpine
    # 设置时区
    ENV TZ Asia/Shanghai
    RUN apk --no-cache add tzdata && cp /usr/share/zoneinfo/${TZ} /etc/localtime \
    && echo ${TZ} > /etc/timezone \
    && apk del tzdata

    VOLUME /tmp
    # 定义变量
    ARG JAR_FILE
    ADD target/${JAR_FILE} app.jar
    ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]
    # 声明服务以 tcp 协议运行在 8080 端口
    EXPOSE 8080/tcp
  1. project 标签下添加插件配置:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    <build>
    <plugins>
    <!--dockerfile-maven-plugin 配置-->
    <plugin>
    <groupId>com.spotify</groupId>
    <artifactId>dockerfile-maven-plugin</artifactId>
    <version>1.4.13</version>
    <executions>
    <execution>
    <id>build</id>
    <!--绑定 maven 阶段-->
    <!--install 表示运行 mvn install 时会自动执行 docker 镜像构建-->
    <!--install 也可以改成 package,运行 mvn package 时会自动执行 docker 镜像构建-->
    <phase>install</phase>
    <goals>
    <!--执行插件的目标-->
    <goal>build</goal>
    </goals>
    </execution>
    </executions>
    <configuration>
    <!--指定 docker 镜像仓库-->
    <repository>cn/diqigan/${project.artifactId}</repository>
    <!--指定 docker 镜像标签-->
    <tag>${project.version}</tag>
    <!--指定 Dockerfile 路径-->
    <dockerfile>src/main/docker/Dockerfile</dockerfile>
    <!--设置 Dockerfile 中变量-->
    <buildArgs>
    <!--对应 Dockerfile 中的 JAR_FILE 变量-->
    <JAR_FILE>${project.build.finalName}.jar</JAR_FILE>
    </buildArgs>
    </configuration>
    </plugin>
    <!--dockerfile-maven-plugin 配置-->
    </plugins>
    </build>
  2. 构建镜像

镜像构建有两种方式:

  • 绑定 maven 阶段自动构建:

    以文中所示配置,执行 mvn install 指令会自动构建 docker 镜像。

  • 显式运行构建指令 mvn package dockerfile:buld 构建 docker 镜像。

可以在 maven 控制台日志中看到 docker 镜像已经构建成功。

dockerfile-maven-plugin-build-log

  1. 跳过镜像构建

如果前面在 pom.xml 插件配置中绑定了在 maven 阶段自动构建 docker 镜像,但是某次打包过程中又不希望自动构建 docker 镜像,可以通过 dockerfile.skip 参数跳过 docker 镜像构建。

1
mvn clean install -Ddockerfile.skip

后记

  1. 个人不推荐在 pom.xml 中配置 Dockerfile 的各项内容,有种配置侵入的感觉,建议另写一个 Dockerfile 文件;
  2. 持续集成过程中完全可以使用 docker 相关指令构建镜像,maven 插件有点画蛇添足的意思;
  3. 本文相关代码可在 Seven-Steven/spring-examples 查看;

参考文档

  • 官方使用文档
  • 官方使用示例

Linux + picgo 图床自动化实践

作者 Seven
2022年6月15日 21:52

前言

因为 PicGo 在 Linux 下的 GUI 体验并不是很好,索性自己通过 Shell 脚本和快捷键的方式实现了较好的用户体验,记录下操作流程,希望能帮到各位。

环境

笔者测试的相关软件环境如下:

  • Manjaro Linux: 5.15.46-1-MANJARO (64位)
  • Node.js: v18.3.0
  • picgo: 1.5.0-alpha.0
  • [可选] picgo-plugin-web-uploader: 1.1.1
  • picgo-plugin-autocopy: 1.0.5
  • xclip: 0.13
  • notify-send: 0.7.12
  • [可选] espeak: eSpeak text-to-speech: 1.48.03 04.Mar.14

安装 picgo

  1. 安装 Node.js

    1
    yay -S nodejs
  2. 安装 picgo

    1
    2
    3
    4
    5
    npm install picgo -g

    # or

    yarn global add picgo

安装 picgo 插件

可以借助 picgo 插件实现一些特殊功能。

  • picgo-plugin-autocopy

    图片上传完毕后自动拷贝链接到剪切板。

    1
    picgo install autocopy
  • [可选] picgo-plugin-web-uploader

    用来实现自定义 web 图床,如果不需要自定义 web 图床,可以忽略这个插件。

    1
    picgo install web-uploader

    picgo 的配置文件默认存储在 ~/.picgo/config.json,picgo-plugin-web-uploader 的配置可以参考 官方文档

    这里是一份 美日小站 的自定义图床配置:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    "picBed": {
    "current": "web-uploader",
    "web-uploader": {
    "customBody": null,
    "customHeader": null,
    "url": "https://www.ladydaily.com/tools/upload/${token}",
    "paramName": "file",
    "jsonPath": "data.o_url"
    }
    }

    插件配置完成之后,可以使用 picgo upload 命令验证配置是否正确。

  • [可选] picgo-plugin-s3

    用来支持 s3 协议的图片上传,如果不需要使用 s3 协议,可以忽略这个插件。

    picgo 的配置文件默认存储在 ~/.picgo/config.json,picgo-plugin-s3 的配置可以参考 官方文档

    这里是一份 美日小站 的自定义 s3 图床配置:

    1
    2
    3
    4
    5
    6
    7
    8
    "aws-s3": {
    "accessKeyID": ${accessKeyId},
    "secretAccessKey": ${secretAccessKey},
    "bucketName": ${bucketName},
    "uploadPath": "{year}/{md5}.{extName}",
    "endpoint": "s3.ladydaily.com",
    "urlPrefix": "https://${bucketName}.s3.ladydaily.com/"
    }

编写 shell 脚本

  • 借助 xclip 实现了剪切板相关操作,manjaro 下安装 xclip 的命令为 sudo pacman -Sy xclip
  • [可选] 借助 espeak 实现了语音提示功能,manjaro 下安装 xclip 的命令为 yay -S espeak。如果不需要语音提示,删除脚本中的相关代码即可。

我这里把脚本写在了 ~/.script/app/picog-upload.sh 文件中,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/bin/zsh
source $HOME/.zshrc
picgo_upload_result=`picgo upload`

if [ $? -ne 0 ]; then
# 系统提示图片上传失败并显示失败原因
notify-send -u normal -t 5000 -a picgo 'picture upload failed' $picgo_upload_result
# 语音提示图片上传失败
# espeak 'picture upload failed'
exit
else
# 系统提示图片上传成功
notify-send -u normal -t 5000 -a picgo 'picture puload succees' `xclip -o -selection clipboard`
# 删除 xclip 自动添加的换行符
xclip -o -selection clipboard | sed '/^[[:blank:]]*$/d' | xclip -selection clipboard -r
# 语音提示图片上传成功
# espeak 'picture upload succeed'
fi

给脚本添加可执行权限:

1
chmod +x ~/.script/app/picgo-upload.sh

现在,可以使用命令 ~/.script/app/picgo-upload.sh 测试脚本是否正常。

配置快捷键

  1. 打开 manjaro 系统设置,选择 “工作区”-“快捷键”-“自定义快捷键”-点击鼠标右键-选择“新建组”-创建一个 “custom” 分组:

    manjaro-system-settings-keymap-new-group

  2. 鼠标右键 “custom” 分组,选择“新建”-“全局快捷键”-“命令/URL:”,创建一个名为 “picgo-upload”的快捷键:

    manjaro-system-settings-keymap-new-global-keymap

  3. 点击“触发器”,设置自己喜欢的快捷键,我这里设置的是 Ctrl+Alt+U

    manjaro-system-settings-keymap-new-global-keymap-set-keyboard

  4. 点击 “操作”,在 “命令/URL:”输入框中填写脚本路径 ~/.script/app/picgo-upload.sh
    勾选 “custom” 分组和 “picgo-upload” 快捷键,点击“应用”使配置生效:

    manjaro-system-settings-keymap-new-global-keymap-set-command

到这里,快捷键就配置完成了,拷贝一张图片,然后使用按下 Ctrl+ Alt+U 测试快捷键,不出意外的话,图片会上传成功并且在右上角弹窗提示:

manjaro-system-notify-picture-upload-success

同时,图片的 url 会自动复制到你的剪切板中。

使用 git 备份和恢复 dotfiles

作者 Seven
2022年3月12日 11:12

前言

Unix 用户的配置文件一般存储在以 . 开头的文件中,这些文件被统称为 “dotfiles”。

本文讲述了一种极其优雅的通过 git 备份和恢复 dotfiles 的方法。

备份

  1. 初始化 git 仓库

    1
    2
    3
    4
    5
    6
    # 初始化 git 仓库
    git init --bare $HOME/.dotfiles
    # 指定 git 仓库和工作树路径并创建指令别名,简化操作
    cp -a .bashrc{,.bak} && echo "alias dotfiles='git --git-dir=$HOME/.dotfiles/ --work-tree=$HOME'" >> .bashrc && source .bashrc
    # git status 不显示未跟踪的文件
    dotfiles config status.showUntrackedFiles no
  2. 创建远程仓库,比如 git@github.com/seven/dotfiles

  3. 添加文件

    1
    2
    3
    4
    dotfiles add .zshrc
    dotfiles commit -m "add .zshrc"
    dotfiles remote add origin ${git_repo}
    dotfiles push

恢复

1
2
3
4
5
6
7
8
9
10
# 把 dotfiles 克隆到本地临时目录
git clone --separate-git-dir=$HOME/.dotfiles ${git_repo} $HOME/dotfiles-tmp
# 用临时目录中的文件覆盖本地文件
cp $HOME/dotfiles-tmp/.* $HOME
# 删除临时目录
rm -r $HOME/dotfiles-tmp/
# 指定 git 仓库和工作树路径并创建指令别名,简化操作
cp -a .bashrc{,.bak} && echo "alias dotfiles='git --git-dir=$HOME/.dotfiles/ --work-tree=$HOME'" >> .bashrc && source .bashrc
# git status 不显示未跟踪的文件
dotfiles config status.showUntrackedFiles no

git 参数讲解

  • git init --bare ${path}

    Create a bare repository. If GIT_DIR environment is not set, it is set to the current working directory.

    ${path} 目录创建一个空的 git 仓库。

  • git --git-dir=${git_dir}

    Set the path to the repository (“.git” directory). This can also be controlled by setting the GIT_DIR environment variable. It can be an
    absolute path or relative path to current working directory.

    Specifying the location of the “.git” directory using this option (or GIT_DIR environment variable) turns off the repository discovery that
    tries to find a directory with “.git” subdirectory (which is how the repository and the top-level of the working tree are discovered), and
    tells Git that you are at the top level of the working tree. If you are not at the top-level directory of the working tree, you should tell
    Git where the top-level of the working tree is, with the –work-tree=${git_dir} option (or GIT_WORK_TREE environment variable)

    If you just want to run git as if it was started in ${git_dir} then use git -C ${git_dir}.

    指定 git 仓库的路径。

  • git --work-tree=${path}

    Set the path to the working tree. It can be an absolute path or a path relative to the current working directory. This can also be
    controlled by setting the GIT_WORK_TREE environment variable and the core.worktree configuration variable (see core.worktree in git-
    config(1) for a more detailed discussion).

    指定工作树的路径。

  • git clone --separate-git-dir=${git_dir}

    Instead of placing the cloned repository where it is supposed to be, place the cloned repository at the specified directory, then make a filesystem-agnostic Git symbolic link to there. The result is Git repository can be separated from working tree.

    把 git 仓库克隆到指定的目录 ${git_dir} 下,然后做一个与文件系统无关的 git 符号链接到该目录。从而使 git 仓库和工作树分离。

这里需要搞懂 git 的两个概念: “git 仓库” 和 “工作树”。
假设我们使用 git clone git@github.com/seven/dotfiles 把远程仓库克隆到了本地的 dotfiles 目录。dotfiles 目录就是“工作树”的路径,dotfiles/.git 目录就是 git 仓库的路径。

举一反三

  • 可以为不同的系统或者不同的电脑创建不同的分支
  • 可以通过灵活指定 git 仓库和工作树路径,达到备份其他文件的目的

参考文档

  • Hacker News
  • The best way to store your dotfiles: A bare Git repository
  • Dotfiles - ArchWiki
  • 用 Chezmoi 管理配置文件

通过代理连接 ssh 服务器

作者 Seven
2021年6月8日 06:33

前言

碍于各种复杂的网络环境,有些情况下我们并不能直接访问 SSH 服务器。

这时,网络代理和 SSH 隧道就成了救命稻草。

本文记录了通过网络代理和 SSH 隧道连接远程服务器的几种方法,希望对你有所帮助。

稻草堆

使用 SSH 隧道连接服务器

常用于跳板机场景。

1
ssh ${ssh-user}@${ssh-host} -o ProxyCommand="ssh ${jump-host-user}@${jump-host} -p ${jump-host-port} -W %h:%p"

SSH + ncat 使用 HTTP(s) 代理连接服务器

  1. 安装 ncat

    1
    sudo apt install ncat
  2. 通过代理连接远程服务器

    1
    ssh ${ssh-user}@${ssh-host} -o ProxyCommand="ncat --proxy ${proxy-ip}:${proxy-port} --proxy-type http --proxy-auth ${proxy-account}:${proxy-password} %h %p"

SSH + ncat 使用 Socks 代理连接服务器

  1. 安装 ncat

    1
    sudo apt install ncat
  2. 通过代理连接远程服务器

    1
    ssh ${ssh-user}@${ssh-host} -o ProxyCommand="ncat --proxy ${proxy-ip}:${proxy-port} --proxy-type socks5 --proxy-auth ${proxy-account}:${proxy-password} %h %p"

SSH + corkscrew 使用 HTTP(s) 代理连接服务器

  1. 安装 corkscrew

    1
    sudo apt install corkscrew
  2. 通过代理连接远程服务器

    1
    ssh ${ssh-user}@${ssh-host} -o "ProxyCommand corkscrew ${proxy-ip} ${proxy-port} %h %p [${auth-file-path}]"
    • 上述命令中 [] 包裹的部分代表可选参数。

    • 如果 HTTP 代理设有权限校验,需要将 HTTP 代理的账号密码以 account:password 的格式写入到文件中,然后在上述指令中指定 [${auth-file-path}] 即可。

SSH + nc 使用无身份校验 Socks 代理连接服务器

nc 工具的优点是多数服务器自带,无需额外安装;缺点是不支持有身份校验的 socks 代理。

1
ssh ${user}@${host} -o ProxyCommand="nc -X 5 -x ${proxy-ip}:${proxy-port} %h %p"

持久化配置

我们可以通过编辑 ~/.ssh/config 文件把 SSH 连接配置持久化,配置内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
Host ${ssh-host-alias}
HostName ${ssh-host-ip}
Port ${ssh-port}
User ${ssh-user}
ProxyCommand ${proxy-command}
Host *
ServerAliveCountMax 3
ServerAliveInterval 30
ExitOnForwardFailure yes
Compression yes
GSSAPIAuthentication no
ControlMaster auto
ControlPersist 4h

配置完成之后,就可以通过 ssh ${ssh-host-alias} 命令连接服务器啦。

后记

上述命令都可以通过灵活多变的指令来实现不同的效果,还请各位看官自行尝试,此处不再赘述。

SSH 功能非常强大,包括但不限于连接远程服务器、架设 socks 代理、文件传输、远程执行命令等等等等,建议深挖。

SSH config 也非常强大,适当的配置可以帮助我们很好得进行多服务器管理(甩 XShell 几条街)。

参考文献

  • 让你的SSH通过HTTP代理或者SOCKS5代理 — 神田长雨

Java 清空控制台输出

作者 Seven
2021年4月14日 22:02

前言

本文通过两种方法讲述了 Java 如何清空控制台输出,达到类似 Linux 中 top 命令的效果。

两种方法均在 Linux 环境中测试通过,Windows 环境请自行测试,理论可行。

下列代码需要在控制台执行才会有清除控制台效果,在 IDE 中执行无效。

使用 ASCI 控制码清空控制台

通过输出 ASCI 控制码清空控制台,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* ConsoleClear
*
* @author seven
* @date 2021-04-14 21:49
*/
public class ConsoleClear {
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
System.out.println(i);
}
// 清空控制台并把光标停留在起始位置
System.out.print("\033[H\033[2J");
// 效果同上
// System.out.print("\033[0;0H\033[2J");
System.out.fulsh();
}
}

另外,ASCI 控制码还可以实现前景色、背景色、下划线、消隐、闪烁效果的设置以及光标控制,具体方法请阅读参考文档。

通过 ProcessBuilder 清空控制台

ProcessBuilder 本质上是调用了系统命令来达到清空控制台的效果,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import java.io.IOException;

/**
* ConsoleClear
*
* @author seven
* @date 2021-04-14 21:49
*/
public class ConsoleClear {
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
System.out.println(i);
}
// 清空控制台
try {
final String os = System.getProperty("os.name");
// 根据不同环境执行不同命令
if (os.contains("Windows")) {
// 新建一个 ProcessBuilder,Windows 下要执行的命令是 cmd.exe,参数是 /c 和 cls
new ProcessBuilder("cmd", "/c", "cls")
//将 ProcessBuilder 对象的输出管道和 Java 的进程进行关联,这个函数的返回值也是一个 ProcessBuilder
.inheritIO()
//开始执行 ProcessBuilder 中的命令
.start()
//等待 ProcessBuilder 中的清屏命令执行完毕
//如果不等待则会出现清屏代码后面的输出被清掉的情况
.waitFor();
} else {
// 新建一个 ProcessBuilder,Linux 下要执行的命令是 clear
new ProcessBuilder("clear")
//将 ProcessBuilder 对象的输出管道和 Java 的进程进行关联,这个函数的返回值也是一个 ProcessBuilder
.inheritIO()
//开始执行 ProcessBuilder 中的命令
.start()
//等待 ProcessBuilder 中的清屏命令执行完毕
//如果不等待则会出现清屏代码后面的输出被清掉的情况
.waitFor();
}
} catch (final IOException | InterruptedException e) {
System.out.println("Clear Console Failed.");
}
}
}

可以将 ProcessBuilder 部分代码封装一下,这里为了便于理解,直接写了两次。

不存在的方法三

网上还盛传另一种方法,核心代码是:

1
Runtime.getRuntime().exec("cls");

我自己亲测无效,各位看官感兴趣的话还请自行踩坑 / 避坑。

参考文档

  • 终端/控制台设置颜色字体、光标定位和清屏 — 印林泉
  • java 控制台输出 颜色代码 — 夜半听风吟
  • java 在Cmd命令行实现清屏 — 蓝蓝223

Hexo + Typora + PicGo-Core 写作三件套

作者 Seven
2020年12月20日 16:54

前言

使用 Hexo 写博客已经很久了,各方面体验还算顺滑,唯一感觉不是那么爽的地方在于图床的使用。

如果在写作的过程中需要插入图片,我需要先中断写作,打开图床 web 页面,上传图片,复制图片链接,再回到编辑器粘贴图片链接。整个过程极其繁琐,令人苦不堪言。直到我遇到了 PicGo-Core

PicGo-Core 可以和 Typora 无缝连接,在几乎无感的情况下完成图片的上传与 markdown 格式的转换,极大得提高了写作的流畅程度。

本文将结合我的自身实践,讲述 Hexo + Typora + PicGo-Core 的联合配置过程,希望能够对你有所帮助。

文章适合已经把玩过 Hexo 或者对 Node 有所了解的用户理解,如果您没有接触过对应知识,还请自行补充。

文中 “Hexo + Typora 联动” 章节与 “Typora + PicGo-Core 联动” 章节并无关联,可以只阅读自己感兴趣的部分。

环境说明

本文讲述的所有操作均在以下环境中完美运行,其他环境大同小异,烦请看官自行钻研。

  • 操作系统:Ubuntu 20.04
  • node:v12.16.3
  • yarn:1.22.5
  • hexo: ^5.0.0
  • Typora:version 0.9.98(beta) for Linux
  • PicGo-Core: 1.4.12

Hexo + Typora 联动

默认情况下 hexo new ${post} 命令只会创建一个名为 ${post}.md 的 markdown 文件,我们需要自行使用编辑器打开这个文件,然后才可以进行编辑写作。Hexo 提供的 Scripts 能力可以合并这两个步骤,在创建文件之后自行调用编辑器打开文件供我们编辑。

操作步骤非常简单,只需要在 Hexo 博客根目录的 “scripts” 文件夹下新建一个 editArticle.js 文件并写入如下内容即可:

1
2
3
4
5
// 新建文档之后用 typora 打开文件
var spawn = require('child_process').spawn;
hexo.on('new', function(data){
spawn('${path-to-editor}', [data.path]);
});

其中:

  • ${path-to-editor} 代表编辑器可执行文件的位置,可以根据实际情况更改,比如我这里写的是 /usr/bin/typora

Typora + PicGo-Core 联动

  1. 安装 Typora

    直接在 Typora 官网下载安装即可。

  2. 安装 PicGo-Core

    Typora 支持一键安装 PicGo-Core 和一键打开 PicGo-Core 的配置文件,位置如图:

    Typora一键安装PicGo-Core

    如果你喜欢手动安装 | 参考文档,执行以下命令即可:

    1
    yarn global add picgo
  3. 配置 PicGo-Core | 参考文档

    PicGo-Core 官方支持 sm.ms,七牛,腾讯云 COS,阿里云 OSS,Github 图床,又拍云,imgur 等图床。选择自己喜欢的图床并按照文档进行配置即可。

    如果你使用的图床不在 PicGo-Core 支持列表中,也可以使用插件 picgo-plugin-web-uploader 来实现自定义图床。

    1. 安装插件 | 参考文档

      1
      picgo install web-uploader
    2. 配置插件 | 参考文档

      编辑 PicGo-Core 的配置文件,写入以下内容:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      {
      "picBed": {
      "current": "web-uploader",
      "web-uploader": {
      "customBody": null,
      "customHeader": null,
      "url": "https://www.ladydaily.com/tools/upload/${token}",
      "paramName": "file",
      "jsonPath": "data.o_url"
      }
      },
      "picgoPlugins": {
      "picgo-plugin-web-uploader": true
      }
      }

      我这里使用的是 多吉图床${token} 表示多吉图床的 token,其他图床大同小异,自行配置即可。

  4. 设置 Typora

    在 Typora 中依次点击 “文件” - “偏好设置” - “图像”:

    • 在 “插入图片时” 设置中选择 “上传图片”,并勾选 “对本地位置的图片应用上述规则”,“对网络位置的图片应用上述规则”,“允许根据 YAML 设置自动上传图片”,“插入时自动转义图片 URL” 等选项;
    • 在 “上传服务” 设置中选择 “PicGo-Core (command line)” ;

    设置Typora图片上传选项

    然后点击 “验证图片上传选项” 按钮,出现 “验证成功” 提示即可。

    验证Typora图片上传功能

后记

至此,写作三件套的联动配置已经全部完成。如果您有更好的想法或者实践,欢迎与我交流,万分感谢!

One More Thing

如果你也在从事微信公众号写作,相信你会喜欢这个可以帮你排版的工具:MarkDown Nice

参考文档

  • PicGo-Core 官方文档
  • Typora配合上传插件picgo自动上传图床chevereto — umr

月刊-202009

作者 Seven
2020年9月1日 21:19

优秀文章

  • 不行

    得不到就是得不到,别说自己不想要。
    不学就是不学,别说自己没时间。

  • 别给年轻人提建议

    每个人最终都将成为自己。

  • 如何理解所谓的压力与红利

    不要稀里糊涂的随波逐流,红利到来的时候,要知道红利是从哪里来的,怎么来的,这样才能更好的把握机会,争取实现自我突破和提升。挤压到来的时候,要知道挤压是怎么来的,这样才能未雨绸缪,更好的规避,更好的让自己处于相对安全和健康的位置。

    学会真正的理解因果,理解逻辑,理解世界。

    多读一点经济学的书,多理解相关经济学的逻辑,多关注政策面的新闻,更好的认识这个世界,认识自己所处的环境,不要简单归因,不要只看到表面,你会自己找到答案和真正的因果。

    如果你不知道如何把握红利,不知道如何规避压力,唯一能做的,是不断提升自己的职场竞争力,提升自己的信用,口碑和影响力。永远不要认为,做对一次选择,就可以安稳一生。

  • 快速学习秘诀:费曼学习法

    确定目标、模拟教学、反复理解、复习简化。

  • 如果高效学习有什么秘诀的话,那就都在这里了:)

    IT 领域学习的一些心得与建议,可作参考。

  • 鸡毛蒜皮的事儿

    我们是不是经常在“鸡毛蒜皮的小事”上面浪费了很多时间呢?

  • 千万并发,阿里淘宝的 14 次架构演进之路!

    文章以淘宝为例,介绍从一百个并发到千万级并发情况下服务端的架构的演进过程,同时列举出每个演进阶段会遇到的相关技术,可以让我们对架构的演进有一个整体的认知。

资源推荐

  • iptables 系列教程

    这是我我见过的最好的 iptables 教程。
    行文诙谐、通俗易懂、深入浅出、不失全面。

  • Github上 10 个开源免费且优秀的后台控制面板

    一些优秀的后台前端解决方案。

  • FinalShell

    一款小众但相对优雅的跨平台命令行终端工具,集成了 shell 和 ftp,有些许瑕疵,但让人眼前一亮。

  • 面包多

    面包多帮助创作者轻松获得收入。

看世界

  • 9 块 9 包邮,商家真的赚钱吗?

    看一下 9 块 9 包邮的商业逻辑。

  • 我的一年独立开发经历

    这是最好的时代: 人人都可以低成本的创造
    这是最坏的时代: 人人都可以低成本的创造

  • 让赚钱思路更加开阔的 7 个小建议

    即时正反馈、利他、不设限、高质信息管道、商业思维小闭环、异常值思维、组合思维。

  • 为什么你越攒钱越穷?

    从投资的角度看负债。

  • 年度种草!我的2019桌面工具清单!

  • 不上班后,我成了月入2万的“废人”

    “自立门户”并非工作自由。

❌
❌