三个有用的 Golang 辅助网站

5e54199359bbafe0ef692365a9bcffb6

在我数十年的编程生涯里,我大部分时间写的都是动态类型语言,所以我对于类型并不太感冒。不过,当我在写 Golang 的时候,确实遇到了一些过去写 PHP、 Node.js、前端不同的问题。

一个最令我困扰的点便是:对于 JSON 的处理,需要先定义结构体来做转换。对于一些简单的 JSON 结构,我还可以简单的完成定义。但如果是一些比较复杂的结构体,我就放弃了手动定义,而是选择借助一些辅助工具来完成

d2b5ca33bd970f64a6301fa75ae2eb22

比如我最常用的便是各种 JSON to Go 的网站,贴一个 JSON 进去,然后自动生成对应的 Golang 的 Struct,接下来我只需要复制右侧生成的 Struct 到我的代码里,就可以完成 JSON 的定义,使用 Golang 自带的 JSON 库解析即可。

截图的网站是 https://mholt.github.io/json-to-go/

另外一个可以实现 JSON 转换成 Go Struct 的网站是 https://transform.tools/json-to-go,它提供了更加全面的转换路径。不过有些时候选择太多也会让我有点困惑。

d2b5ca33bd970f64a6301fa75ae2eb22 1

另外一个令我困扰的也是类似的格式转换,不过是 YAML 的。思路相同,手写要配置的 YAML 文件,并生成对应的 Struct: https://zhwt.github.io/yaml-to-go/。在设计风格上,和上面的 JSON2Struct 类似,是基于上面的项目改造过来的。

d2b5ca33bd970f64a6301fa75ae2eb22 3

除此之外,如果你在定义 YAML 的时候, 对于部分数据如何定义不太明确,可以考虑使用 JSON2Yaml 的工具来实现将你需要的数据转换为 YAML 定义。

在 Go 当中嵌入父目录中的文件

5e54199359bbafe0ef692365a9bcffb6

自 Go 1.16 版本开始,Go 提供了将二进制文件打包进入到 Binary 文件当中的机制:`//go:embed。不过,我看到的示例大多数都是嵌入当前文件夹下的子文件夹的示例。并没有嵌入父一级文件夹的示例。于是,我便开始研究起来。

为什么会需要嵌入父目录中的文件?

这是因为不同的项目的构建规则不同。一些小型项目可能只有一个 main.go 或同级目录下几个其他的 go source 文件即可,但对于更大型的项目,合理的项目拆分是有助于帮助提升项目的可维护性的。

以我为例,我的目录结构如下:biz 目录下是我的核心逻辑,也是我日常写代码的地方;conf 则放置了各种配置,比如各种 API Key,ral 则是网络访问层,比如我比较喜欢用 gin 来做底层的网络访问层;script 放置了开发所需要的各种脚本文件;static 则放置了前端所需要的 JS / CSS 文件。在开发一些偏内容向的页面时,我会习惯使用 Server Side Render,所以我在 biz 目录下还有一个 template 目录。而主要的逻辑则放在 handler。

├── biz
│   ├── dal
│   ├── handler
│   ├── logic
│   ├── service
│   └── template
├── conf
├── ral
│   └── gin
├── script
└── static
    └── public
Code language: PHP (php)

在日常开发时,一个非常常见的操作是在 handler 里处理基本的逻辑,并将 template 中的模板渲染出来,并返回给用户,这个时候就需要在 handler 里使用上一级目录的文件了。

错误的尝试1:使用 .. 来引入

作为文件系统的常规配置,我自然知道可以通过 .. 来代表上一级目录,因此我试着使用如下的语法来引入文件

//go:embed ../template/*
Code language: JSON / JSON with Comments (json)

结果报了个错误 invalid pattern syntax。看得出来,是因为我的语法出现了问题。

通过搜索,我找到了这个 Github issue,才得知,由于避免跨模块的访问,go embed 阻止了 .. 语法的使用

Because embed.Files implements fs.FS, it cannot provide access to files with names beginning with .., so files in parent directories are also disallowed entirely, even when the parent directory named by .. does happen to be in the same module.

https://go.googlesource.com/proposal/+/master/design/draft-embed.md

错误的尝试2: 使用 go:generate 来执行复制

在上面的 issue 当中,我注意到还有另外一个方式,可以实现类似的效果。可以借助 go:generate来实现在执行 go generate 方法时,复制文件到本地,这样就可以实现在 Build 二进制文件之前,把文件复制到本地,即规避了跨模块的问题,也避免了将 template 文件放在本地,污染代码目录的问题。

//go:generate cp -r ../../assets ./local-asset-dir
//go:embed local-asset-dir
var assetFs embed.FS
Code language: JavaScript (javascript)

这个方法在运行的时候没有效果,不过在其他人可能是有效果的(我猜是因为我开发时很少直接用 go generate,都是直接 go run 了)。不过,这个方法也有个问题,因为是写在代码本身当中的,很有可能后续因为某些原因,这些代码被误删除或修改了,导致整个系统的运行不正常。而散落在各文件的描述也会使得修复十分复杂。

正确的做法

在看这个 issue 的最后,我注意到了正确的用法,在另外一个 issue 当中,开发者 tomasf 提到了,我们应该在 asset 目录下创建一个 embed.go 来完成 embedFs 的建立,并在其他文件直接引入这个文件,完成定义。

You can create a embed.go file in your assets directory and make the assets available as it’s own package to the rest of your program.

tomasf

这个说法一出,豁然开朗!尝试一下,果然有效。我在 template 目录下创建了一个 embed.go 文件,并添加了如下代码。

package template

import "embed"

//go:embed *
var TemplateFs embed.FS

并在另外一个文件当中使用template.TemplateFs.ReadFile("index.tmpl") 来完成模板文件的引用。这样既不违背 golang 的跨模块,也不会使得代码不可维护,非常好。

参考阅读

  • https://go.googlesource.com/proposal/+/master/design/draft-embed.md
  • https://github.com/golang/go/issues/46056
  • https://github.com/golang/go/issues/41191#issuecomment-686621090
  • https://blog.carlmjohnson.net/post/2016-11-27-how-to-use-go-generate/
  • https://blog.carlmjohnson.net/post/2021/how-to-use-go-embed/

在 Dokuwiki 当中接入 draw.io 的绘图

dfe5624d0afe5e8220b907e92766dd4b

作为一个工程师,我难免会在自己的博客 / Wiki 当中加入流程图、时序图之类的。因此,需要更加简单的绘图的方式。

过去往往是采用本地绘制好图片,然后再复制上传到 Wiki 当中。不过,随着 tldraw、draw.io 等一类在线绘图工具出现以后,大家开始习惯于在线绘制图片。

在使用飞书文档的时候,我觉得其中内嵌 Diagram.net 的服务体验很不错。

现在,在 Dokuwiki 当中你也可以实现类似的功能。安装 Dokuwiki 上的 drawio 插件,随后,在你们的编辑器当中就可以看到一个 Drawio 的图标,点击这个图片,就可以插入一个 Drawio 的图片引用

d2b5ca33bd970f64a6301fa75ae2eb22 17
Drawio 的图标

插入完成后,你可以在下方的输入框当中看到插入新的引用,此时你可以修改具体的文件名和对应的 namespace。

插入完成后,点击保存(也可以在预览中修改),保存成功后,你会看到一个 Start drawing by clicking here,点击这个图标,就可以在渲染出的新的 UI 当中绘制你想要的流程图。

d2b5ca33bd970f64a6301fa75ae2eb22 18

绘制完成后,点击右上角的保存,即可保存你绘制的流程图。

d2b5ca33bd970f64a6301fa75ae2eb22 19

此时,你的流程图就绘制好了,其他人看的时候也是你画好的流程图。

后续如果你需要编辑这个流程图,只需要在登录的状态下,点击绘制好的图片,就会自动进入到编辑的模式,允许你修改你的流程图。

以下是一个简短的视频介绍:

在 Dokuwiki 当中实现拖拽上传

dfe5624d0afe5e8220b907e92766dd4b

在 Dokuwiki 当中,如果你想要上传一个文件,需要先将文件通过媒体管理器上传到wiki当中,再将对应的媒体插入到对应的页面中,流程繁琐不说,还容易把文件上传到意料之外的地方。

不过,你可以通过安装第三方插件,来实现 Drag & Drop 拖拽上传。

d2b5ca33bd970f64a6301fa75ae2eb22 16

安装 Dokuwiki 的 Dropfiles 插件,你就可以开启在编辑 Dokuwiki 的同时,直接拖拽上传文件的体验。

同时,因为你上传时已经在文本编辑的状态,因此,你上传的文件也会自动按照 wiki 所在路径来上传,对于不支持分页管理的 dokuwiki 来说,是一个非常有用的 Feature。

此外,如果你安装了这个插件,我强烈你开启 insertFileLink 这个选项,开启后,你拖拽上传的文件链接将会自动插入到当前文件中,十分方便。

在 Dokuwiki 上实现反向链接的展示

dfe5624d0afe5e8220b907e92766dd4b

Wiki 的一般用法是正向的链接到某个特定的页面,即所谓的正向链接。

但反向链接可以帮我们更好的对信息进行汇总和分析 —— 比如我们可以知道哪些地方引用了当前的 wiki 页面,从而实现更好的组织不同的信息。

想要在 Dokuwiki 当中实现这样的效果,需要使用一个第三方插件 —— Backlinks

d2b5ca33bd970f64a6301fa75ae2eb22 14

安装上这个插件后,你只需要在想要展示反向链接的地方插入{{backlinks>.}}就可以展示当前页面的反向链接。

不过,如果你想要达成比较好的效果,可以选择像我一样,建设一个 Side bar ,并在 Side bar 当中展示具体的反向链接,这样在具体看效果的时候,非常的简单和明确。

d2b5ca33bd970f64a6301fa75ae2eb22 15

如何在 Dokuwiki 当中隐藏掉外部链接前的 Icon

dfe5624d0afe5e8220b907e92766dd4b

Dokuwiki 在链接外部网站时,默认会在链接前展示一个地球的小图标。但如果你和我一样,觉得这个小图标有点碍眼,则可以尝试通过添加 CSS 来隐藏前面的小地球。

d2b5ca33bd970f64a6301fa75ae2eb22 12

你只需要在 /conf 文件夹下创建一个 userstyle.css 的文件,并在其中添加对应的 CSS

.page a.urlextern,
.page a.interwiki,
.page a.windows,
.page a.mail,
.page a.media {
  padding-left: 0 !important;
  background: none !important;
}
Code language: CSS (css)

添加完后,执行 Shift + Control + R 来强制刷新 CSS,刷新完成后,就可以看到没有小地球的连接了。

d2b5ca33bd970f64a6301fa75ae2eb22 13

在 Dokuwiki 中配置安全规则,来保护配置和数据文件

dfe5624d0afe5e8220b907e92766dd4b

由于 dokuwiki 的文件路径默认会放在 data/conf/bin 等几个目录当中,如果你不对相应的文件进行保护,如果某个人是了解 dokuwiki 的情况下,它可以越过你的 dokuwiki ,直接读取 wiki 的内容。

而你没有进行保护的时候,Dokuwiki也会在配置当中展示你的配置不安全。

d2b5ca33bd970f64a6301fa75ae2eb22 11

想要开启 Nginx 的保护,你需要在 Nginx 的配置文件当中,添加如下代码。

 location ~ /(data|conf|bin|inc|vendor)/ {
      deny all;
 }
Code language: JavaScript (javascript)

添加完成后,执行如下命令来重启 Nginx,使命令生效。

nginx -t 
nginx -s reload

生效成功后,再次刷新,就可以看到提示已经消失了。同时如果你直接访问 data 目录下的数据文件,也会直接报错。

Dokuwiki 配置 Rewrite 优化路径显示

dfe5624d0afe5e8220b907e92766dd4b

Dokuwiki 在生成 URL 的时候,支持生成三种不同的 URL:

  • Rewrite 版: /wiki:welcome?do=admin&page=config
  • dokuwiki 控制版: /doku.php/start?do=admin&page=config
  • 默认版:/doku.php?id=start&do=admin&page=config

配置 Rewrite 可以让你的 wiki 的路径更加简单的和明确,屏蔽语言信息,因此,一般而言,都建议大家配置上对应的 rewrite 规则。

在 Nginx 你的 Host 配置下加入如下规则,来实现 Rewrite 的转发


    location / { try_files $uri $uri/ @dokuwiki; }
 
    location @dokuwiki {
        rewrite ^/_media/(.*) /lib/exe/fetch.php?media=$1 last;
        rewrite ^/_detail/(.*) /lib/exe/detail.php?media=$1 last;
        rewrite ^/_export/([^/]+)/(.*) /doku.php?do=export_$1&id=$2 last;
        rewrite ^/(.*) /doku.php?id=$1&$args last;
    }
Code language: JavaScript (javascript)

添加完成后可以执行如下命令来重启 Nginx

nginx -t 
nginx -s reload

再回到 Dokuwiki 后台的配置管理器,找到 userewrite 这一项配置,将其配置为使用.htaccess,并保存,即可将 dokuwiki 默认生成的 URL 变成一个更加干净的 URL。

Dokuwiki 忘记密码了怎么办?

dfe5624d0afe5e8220b907e92766dd4b

我的个人 Wiki 系统是 Dokuwiki 。相比于别的 Wiki ,Dokuwiki 轻量的同时,功能齐全,帮助我在 TiddlyWiki 和 MediaWiki 之间找到了一个平衡。

我在使用 Tiddly Wiki 的时候,会遇到忘记密码的情况,这种情况下,就需要对 Dokuwiki 进行密码重置的操作。

如果你和我一样,关闭了 Dokuwiki 的任意人可上传,则需要通过修改具体的 auth 文件来完成。

Dokuwiki 的用户授权信息放置在 conf/users.auth.php 文件当中,你需要在服务器上打开这个文件,并在其中添加如下信息

deleteme:$1$4fd0ad31$.cId7p1uxI4a.RcrH81On0:-:-:admin,user 
Code language: JavaScript (javascript)

添加完成后,你将会获得一个名为 deleteme,密码为admin 的用户,接下来你只需要使用这个用户登录到你的 Dokuwiki 当中,并在管理当中的「用户管理器」中修改之前的用户的账号密码。

d2b5ca33bd970f64a6301fa75ae2eb22 9

在用户管理器当中可以修改对应的用户的密码。

修改完成后,使用之前的用户登录,并删除之前的用户即可。

d2b5ca33bd970f64a6301fa75ae2eb22 10

使用 Sheetjs 将 JSON Array 转化为 Excel

white printing paper with numbers

使用 node-excel-stream 来按行处理 Excel 数据 中,我提到,如果你希望简单的完成 Excel 的读取和处理,那么 node-excel-stream 是个不错的选择。而反过来,如果你希望将 JSON Array 导出为 Excel,那么 Sheetjs 是个不错的选择。

注意

Sheetjs 和 exceljs 不同,区分了商业版和社区版。我们这里使用的是社区版 Sheetjs CE

用法

使用 Sheetjs 对数据进行导出时,你只需要调用 XLSX 方法当中的 json_to_sheet ,就可以将你的 JSON Array 变为一个 worksheet,接下来只需要将其放入一个新的 workbook 当中,并导出为文件,就可以完成 JS 数据导出为 Excel。

const XLSX = require("xlsx");

const data = [
  {
    ...
  },
  ...
]

const worksheet = XLSX.utils.json_to_sheet(data);
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, "sheetNameIsFirst");
XLSX.writeFile(workbook, "output.xlsx");
Code language: JavaScript (javascript)

使用 node-excel-stream 来按行处理 Excel 数据

white printing paper with numbers

数据分析是一个非常常见的需求,而在实际的落地场景当中, Python 是使用最多的。不过我因为写了很久的前端,其实对于Python已经生疏了。当我开始启动项目时,就会选择执行 npm init 来初始化一个项目。既然如此,就试着使用 Node.js 来做数据分析。

在 Node.js 当中操作 Excel ,最好的便是 Exceljs。不过 ExcelJs 封装了大量的函数,对于绝大多数的数据分析场景来说,可能并不适用(也不一定,只是我比较喜欢用代码来描述逻辑,Excel 更多是一个导入导出)。

当明确了我只是需要一个简单的导入导出后,那么 node-excel-stream 就进入了我的视野。

读取 Excel 内容

和 Exceljs 不同,node-excel-stream 的封装相对简单,就是一个 Reader 和 Writer ,提供的方法也十分简单:读取文件、定义格式,按行处理内容;

需要注意的是,node-excel-stream 只支持 xlsx ,而不支持 xls,所以如果你用的是旧版,则需要重新保存成 xlsx 来进行处理。

let dataStream = fs.createReadStream('data.xlsx');
let reader = new ExcelReader(dataStream, {
    sheets: [{
        name: 'Users',
        rows: {
            headerRow: 1,
            allowedHeaders: [{
                name: 'User Name',
                key: 'userName'
            }, {
                name: 'Value',
                key: 'value',
                type: Number
            }]
        }
    }]
})
console.log('starting parse');
reader.eachRow((rowData, rowNum, sheetSchema) => {
    console.log(rowData);
})
.then(() => {
    console.log('done parsing');
});
Code language: JavaScript (javascript)

写入 Excel 内容

写入时和读取时相比,稍微复杂一点,需要将所有的输入使用 Promise.all包裹起来

let writer = new ExcelWriter({
    sheets: [{
        name: 'Test Sheet',
        key: 'tests',
        headers: [{
            name: 'Test Name',
            key: 'name'
        }, {
            name: 'Test Coverage',
            key: 'testValue',
            default: 0
        }]
    }]
});
let dataPromises = inputs.map((input) => {
    // 'tests' is the key of the sheet. That is used
    // to add data to only the Test Sheet
    writer.addData('tests', input);
});
Promise.all(dataPromises)
.then(() => {
    return writer.save();
})
.then((stream) => {
    stream.pipe(fs.createWriteStream('data.xlsx'));
});
Code language: JavaScript (javascript)

总结

如果你需要将 Excel 导入到 Js 当中进行处理,那么 node-excel-stream 是一个不错的选择。

warehouse — 一个简单易用的 JSON 数据库

black and yellow printed paper

在 Hexo 的 Github 组织下,有一个不明显,但却很有用的仓库 —— warehouse。

warehouse 是一个 JSON 数据库,基于 JSON 实现了各种类似于 SQL 的查询,可以帮助我们基于一个 JSON 文件来进行查询。在 Hexo 的静态生成过程中,warehouse 帮了大忙。

用官方的话来说,warehouse 就是 A JSON database with Models, Schemas, and a flexible querying interface.

在实际使用上,warehouse 确实如他所说的那边方便(虽然某些方法没有,但依然不影响他的使用很方便)。

Example

比如,如下代码就定义了 一个 Post 模型和对应的表。并实现了在这个表中插入一个新的数据。

var Database = require('warehouse');
var db = new Database();

var Post = db.model('posts', {
  title: String,
  created: {type: Date, default: Date.now}
});

Post.insert({
  title: 'Hello world'
}).then(function(post){
  console.log(post);
});
Code language: JavaScript (javascript)

如果你需要将这些数据保存为一个单独的文件,只需要修改初始化的参数,并执行 save 方法,就可以将 JSON 导出到指定的文件中

var db = new Database({
  path: "./test.json", // 将数据存储在 test.json 当中
});
db.save();
Code language: JavaScript (javascript)

类似的,如果数据已经构建好了,也只需要执行 load 方法,就可以加载数据。

var db = new Database({
  path: "./test.json", // 将数据存储在 test.json 当中
});
db.load();
Code language: JavaScript (javascript)

场景

如果你希望在内存当中对于 JSON 有一个更好的操作方式,那么 warehouse 是个不错的选择,不需要另外单独安装数据库,就可以实现类似于数据库的查询方式,体验还是非常好的。

如果你想了解更多,可以查看

如何判断一个颜色是什么颜色?

red yellow and green paper

当你看到这个题目的时候,你可能会有点迷惑:“一个颜色是什么颜色”,这个问题好无厘头。但如果我换个用法 ,你可能就能明白 — 人类是如何辨别出一个颜色是红色而不是绿色?

回想小时候,大家应该都听说过 —— 三原色。不同的人可能记得不同,有的人记得是红绿蓝(色光三原色),也有人记得是红青黄(美术三原色)。结果不一样,但并不影响。两种三原色都告诉你了 —— 一个颜色是可以由另外三种颜色组合而成。

这意味着,每一个颜色都对应着三个坐标。他们是 RGB 也好,还是其他也好,都是通过三个颜色的色码来确认是哪个具体的颜色的。我们的显示器也是如此制造的。

但三原色有个问题 —— 变量太多。假设我们在每个维度分 3 个不同的结果,三原色可以拼出 27 种不同的组合。如果我们在每个维度分成 10 阶,这个结果就是 1000 个不同的维度。按照 RGB的 0 ~ 255 ,一共 256 阶,则一共可以形成 16,777,216 个组合。如果你想要构建出一组用于判断的规则,就十分的困难。

因此,换一个坐标系会是更好的办法 —— 试试 HSL 描述方法。

HSL 是将 RGB 三维坐标转换为色相、饱和度、亮度(英语:Hue, Saturation, Lightness)组成的新维度坐标。

d2b5ca33bd970f64a6301fa75ae2eb22 2

当我们变为新的坐标系后,我们定义颜色的坐标就从过去的三个维度,变为了只需要色相(Hue)这一个坐标就可以定位的坐标。毕竟,如果一个蓝色不饱和饱和,都不影响他是个蓝色;同样的,一个蓝色的亮度不高,但依然是蓝色。

通过从 RGB 坐标系转变为 HSL 坐标系,我们就将过去的 16,777,216 个组合简化为 360 阶。从过去的三维坐标系,变为了全新的一维坐标系。

d2b5ca33bd970f64a6301fa75ae2eb22 3

这样我们就可以十分简单的来细分我们的不同的颜色,更好的来编写规则。比如Hue 在 15 ~ 45 时为橙色;在45 ~ 75 时为黄色;在 75 ~105 时为绿色等等。

在我们写代码的时候也会更加简单,我们只需要计算出具体的 Hue 的值,就可以得出对应的颜色的名字。

RGB to HSL 算法

如下算法来自 Wikipedia

https://zh.wikipedia.org/wiki/HSL%E5%92%8CHSV%E8%89%B2%E5%BD%A9%E7%A9%BA%E9%97%B4

设 (rgb)分别是一个颜色的红、绿和蓝坐标,它们的值是在0到1之间的实数。设max等价于rgb中的最大者。设min等于这些值中的最小者。要找到在HSL空间中的 (hsl)值,这里的h ∈ [0, 360)是角度的色相角,而sl ∈ [0,1]是饱和度和亮度,计算为:14aa645507f35455bbc990e04ecc0907128c509a

fffb33944b2529ac9398bfb365968795a03b3ddc{\frac {1}{2}}\end{cases}}”>

l={\begin{matrix}{\frac  {1}{2}}\end{matrix}}(max+min)

h的值通常规范化到位于0到360°之间。而h = 0用于max = min的(定义为灰色)时候而不是留下h未定义。

RGB to HSL Sample Code

function rgbToHsl(r, g, b) {
  r /= 255, g /= 255, b /= 255;

  var max = Math.max(r, g, b),
    min = Math.min(r, g, b);
  var h, s, l = (max + min) / 2;

  if (max == min) {
    h = s = 0; // achromatic
  } else {
    var d = max - min;
    s = l > 0.5 ? d / (2 - max - min) : d / (max + min);

    switch (max) {
      case r:
        h = (g - b) / d + (g < b ? 6 : 0);
        break;
      case g:
        h = (b - r) / d + 2;
        break;
      case b:
        h = (r - g) / d + 4;
        break;
    }

    h /= 6;
  }

  return [h, s, l];
}
Code language: JavaScript (javascript)

延展阅读

https://www.december.com/html/spec/colorterms.html

使用小程序的 Canvas 2D 提取特定点的颜色

6ee6df690137fd06bc6166adb63caca1

在小程序当中,我们可以通过在 Canvas 画布上绑定 Tap 事件,来获取到用户点击行为,当我们获取到点击行为对应的坐标时,就可以读取到对应位置的颜色。并根据我们的需求,将颜色转换成我们想要的 RBG 色值。

这就是我们常见的各种取色软件的最常见的实现方式。当然,落实到实际开发过程中,大家或多或少会有所不同(涉及到调色。我们拍照的颜色和我们所看到的颜色并不完全一样。不同的相机调色会有所不同。类似的,我们需要加入相应的逆向算法排除对应的相机自身调教的影响)。

在小程序的场景中具体实现方式可参考如下代码:

页面布局

<view>
  <view><button bindtap="chooseImage">1. 选择图片</button></view>
  <view><canvas id="myCanvas" bindtap="onCanvasTap" type="2d" style="background-color:gray;width: 100%;margin-top: 100rpx;"></canvas></view>
</view>
Code language: HTML, XML (xml)

对应的 JS

和在 在小程序中使用的 Canvas 2D API 绘制本地图片 一样,你需要提取到 Canvas 的 Ctx ,用于执行操作。当你通过 bindTap 拿到你的点击点相对于 Canvas 的 左上角的坐标后,就可以使用 <a href="https://developer.mozilla.org/zh-CN/docs/Web/API/CanvasRenderingContext2D/getImageData">getImageData</a> 来提取具体范围的数据。因为我的目的是提取某个点的颜色,因此我的第三、第四参数都是 1 (取一个像素点的宽度和高度)。

提取出图片数据后,你可以在 data 当中提取一个 4 个元素的数组,按照顺序,分别是 RGBA 的四个值,你可以根据需要使用这个数据(比如放大展示在界面上)。

Page({
  data: {
    color: ''
  },
  onCanvasTap(e) {
    const query = wx.createSelectorQuery()
    query.select('#myCanvas')
      .fields({
        node: true,
        size: true
      })
      .exec((res) => {
        const canvas = res[0].node
        const ctx = canvas.getContext('2d')
        const data = ctx.getImageData(e.detail.x, e.detail.y, 1, 1).data;
        const [red,gree,blue,alpha] = data;
      })
  },
});
Code language: JavaScript (javascript)

WordPress 出现 RedisException: OOM command not allowed when used memory > maxmemory 的报错怎么处理?

a computer screen with a bunch of text on it

早晨起来,想登录博客,记录下自己的灵感,突然发现死活登录不上 WordPress 后台。

登录到 VPS 后台,发现没有出现我之前常出的问题 — 硬盘满了。于是再次回到网页端登录,仔细研究后发现,我的登录应该是成功的,但登录完成后,又重新跳转回来,根据这个情况,我猜测可能是登录态出现了问题。

于是尝试切换到 Safari 、Chrome 的无痕模式登录,依然没有解决问题。因此可以排除掉客户端的问题导致的。

找到问题

接下来就是查看服务端问题。登录到服务器上,找到 WordPress 的日志,查看最近的几条日志,突然在众多 Notice 当中,看到了一个 Exception:

RedisException: OOM command not allowed when used memory > 'maxmemory'
Code language: JavaScript (javascript)

看到这个报错,突然明白了问题在哪了。

我的 WordPress 使用 Redis 作为缓存,而我过去一直配置的缓存空间是 128M,看报错,显然是因为 Redis 使用的内存空间大于 128M,而我过去没有配置逐出机制(默认是 noeviction),导致直接 OOM 爆掉了。

而我的登录态也使用了 Redis,没办法在缓存当中塞入新的 Key,自然登录也就失败了。

解决问题

找到问题之后,下一步便是解决问题。

解决问题并不复杂,为 Redis 调整内存空间大小,并配置逐出机制,就可以解决这个问题。

maxmemory 221000000
maxmemory-policy allkeys-lru

将 Redis 逐出机制设置为 allkeys-lru ,并将内存设置为 200M 后,重启 Redis ,果然我的 WordPress 可以正常登录了。

参考

allkeys-lru 表示对移除最近使用最少的 (least recently used)Key。

更多算法可以参考 Key Evicution

使用 Proxy.py 自建 HTTP Proxy

black smartphone

如果你需要对一个集群进行开发,且开发过程中需要访问内网,一个比较常见的方式是建立虚拟专有网。但倘如你不愿建立虚拟专有网,那么一个比较简单的方式是建立一层 HTTP Proxy 来访问内网当中的数据,你只需要在某一个节点上设定 HTTP Proxy,并在你自己的电脑上配置 HTTP Proxy ,就可以完成使用对应的节点来访问具体的网页,解决 HTTP 访问的问题。

原理

HTTP Proxy Servr 的原理并不复杂,就是一个标准的正向代理。你只要能搭建这样的一个服务即可。

e6c9d24ely1h341f4087mj21t90g2755

实际操作逻辑

以下文章以 Debian GNU/Linux 10 为例

1. 在你的节点上安装 Python3 & pip 3

Python3 是运行 Proxy.py 的运行环境,而 pip3 则是安装 Proxy.py 的工具。执行如下命令来安装。

apt install python3 python3-pip

2. 安装 Proxy.py

这里我们不使用 Virtual Env 来安装,主要是方便后续加入 Systemd 来进行控制。如果你习惯使用 Supervisord 来控制的话,就可以使用 Virtual Env 来安装 Proxy.py。执行如下命令

pip3 install proxy.py
Code language: CSS (css)

3. 启动 Proxy 进行测试

在你的节点上执行如下命令来启动一个测试服务器。

proxy --hostname 0.0.0.0 
Code language: CSS (css)

如果你需要监听某一个特定的 IP, 则将 0.0.0.0 修改为你具体的 IP。

配置后,你可以看到如下的输出,则说明已经成功启动了 Proxy 服务器。

启动完成后,你就可以在本地测试访问如下命令来进行测试。

curl -x [服务器IP]:8899  http://httpbin.org/get
Code language: JavaScript (javascript)

当你看到如下输出时,确认返回值当中的 origin 是否是你的节点 IP,如果是你的节点 IP 则说明你的 HTTP Proxy 已经配置成功了。后续你就可以通过这个 HTTP Proxy 来进行访问了。

e6c9d24ely1h341n60ii2j21b40cuta8

测试完成后,你可以执行 Ctrl + C 来关闭当前的 Proxy Server。

4. 配置 Basic Auth Authentication

上面配置好了 HTTP Proxy Server,但一个问题是这个 Server 没有安全验证,随时会被别人所利用。所以我们需要配置一些基本的安全配置,来解决这个问题。

在你的 Proxy.py 当中加入 –basic-auth 选项,则可以实现加入基本的鉴权。其中,用户密码用英文冒号隔开。比如我需要创建一个用户名是 admin ,密码也是 admin 的proxy ,就可以执行如下命令。

proxy --hostname 0.0.0.0 --basic-auth admin:admin
Code language: CSS (css)

5. 配置开机自启动

现在 Auth 也有了,但是我们不能总是开着 Terminal 来运行我们的服务,所以一个好的办法是为其加入 Systemd ,这样我们就可以使用系统自带的能力来完成 Proxy 的自启动了。

执行 systemctl edit --force --full proxy.service 来创建一个新的 Systemd 服务,并在其中加入如下代码并保存。

[Unit]
Description=Proxy Service
Wants=network.target
After=network.target

[Service]
ExecStartPre=/bin/sleep 10
ExecStart=proxy --hostname 0.0.0.0 --basic-auth admin:admin
Restart=always

[Install]
WantedBy=multi-user.target
Code language: JavaScript (javascript)

保存完成后,执行 systemctl enable proxy.service 配置该服务器的开机自启动 & systemctl start proxy.service ,即可实现 Proxy 服务的开机自启动,并在当前启动该服务。

总结

有了 HTTP Proxy ,你能以一个更加简单的方式来访问内网当中的服务器。你只需要控制 Proxy 节点的可访问权限,就能很好的处理内网数据的安全问题。

记录一个令人血压飙升的前端项目

programming language

最近在看一个前端项目,里面出现了一些令人血压暴涨的骚操作。虽然可以用,但对我来说,确实是让我看到血压飙升。

1. 尽可能使用常规的指标

项目的诉求是实现页面的自适应化,在不同尺寸的屏幕上尽可能保持一致的显示。

为了实现这个诉求,我给他们的建议是使用 Bootstrap 之类的框架,在不同的屏幕上尽可能保持一致。使用 Bootstrap 之类的框架来完成。

最后研发团队选择采用了一个有用,但我看起来非常头疼的方法 —— 根据屏幕宽度计算 rem。通过这样的方式,实现了在不同的屏幕宽度下,都可以和设计稿的尺寸一比一。

但这样带来的问题是,rem 计算出的结果也是一个非常的大的值(当然也可以非常小),但依然不是一个常规的结果,需要你常常使用诸如 0.03 这样的数值来指定宽度和高度。

d2b5ca33bd970f64a6301fa75ae2eb22 38

我觉得,这样的写法并不是说不行,只是这样的写法其实会降低整个项目的可维护性。

2. 文件放置符合规范

其次,这个项目中还出现了一个问题是 —— 文件到处乱丢。在一些项目中,大家往往会有一些明确的文件路径的规范,比如样式文件应该放在哪里。在这个项目中,就出现了样式乱丢的情况。

d2b5ca33bd970f64a6301fa75ae2eb22 39
和 Component 放在一起
d2b5ca33bd970f64a6301fa75ae2eb22 40
符合规范统一放在 scss 文件中

这样的混乱的写法则会导致在实际开发的时候,如果想要调整样式,可能会出现调试失效,修改起来较为混乱的问题。

3. 一个项目中混用多个不同的尺寸单位

d2b5ca33bd970f64a6301fa75ae2eb22 41
另外一个文件采用 px 作为单位

刚刚有用 rem 的单位,另外就有一个文件采用的是 px 为单位,我非常担心在不同尺寸屏幕下出现的错位问题。

4. 放弃 Bootstrap 的样式类,转而改用手动撰写每一行样式

d2b5ca33bd970f64a6301fa75ae2eb22 42
其实完全可以用 position-absolute d-flex 等方法来覆盖

总结

这个项目从我目前的视角来看,依然还有很大的改进空间。且留下了不少的坑。在我看来,长期来看,这些代码将会留下不可维护的问题。希望他们后续慢慢修改和调整吧。唉。

在 WordPress 中实现同一个 CSS 类在不同页面展示不同的效果

programming language

在使用 WordPress 开发时,某些时候你会遇到在同一个类在不同页面的时候,希望有不同表现的诉求。在这种情况下,一个比较简单 or 比较 Tricky 的技巧是通过 Body 层的类选择器来实现。

在 WordPress 进行渲染时,会自动在页面的 <body> 上加入 postid-[id] 的类,对比本文就是 postid-5888

d2b5ca33bd970f64a6301fa75ae2eb22 29
比如本文的 class

类似的,如果是一个 Page,也会生成 page-id-[id] 的类。

d2b5ca33bd970f64a6301fa75ae2eb22 30
页面生成的类是 page-id-[id],和 post 规则不完全相同

因此,如果你需要某个页面的元素样式和其他页面元素样式不同,但页面结构保持一致,一个比较好的方式是通过这些页面类选择器来实现。以你要自定义 btn 样式为例。则可以这样实现:

// 标准页面的样式
.btn{
   // your styles
}

// 在文章 ID 为 5888 的文章上的样式
.postid-5888 .btn{
  // your styles
}
// 在页面 ID 为 84 的文章上的样式
.page-id-84 .btn{
  // your styles
}

Code language: JavaScript (javascript)

总结

借助上面的方式可以实现同一个类在不同的页面展示不同的样式。但我个人觉得,这样的实现方式可能对于 Debug 不太方便,尽可能少用。但如果你需要用的话,那一定要注意好组织代码,避免后续维护成本提升。

此外,除了通过修改 style.css 来实现,你可以考虑将 Page 单独的样式梳理在一个 page.css 中进行维护,从而降低维护难度和成本。
WordPress 当中也有一些插件来实现某个页面自定义的 CSS 能力,将这些样式放在插件中来实现也是一个不错的选择:Post/Page specific custom CSS

Reference