标签归档:开发经验

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

自 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 的绘图

作为一个工程师,我难免会在自己的博客 / 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 当中实现拖拽上传

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

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

d2b5ca33bd970f64a6301fa75ae2eb22 16

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

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

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

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

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

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

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

d2b5ca33bd970f64a6301fa75ae2eb22 14

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

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

d2b5ca33bd970f64a6301fa75ae2eb22 15

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

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 中配置安全规则,来保护配置和数据文件

由于 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 优化路径显示

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 忘记密码了怎么办?

我的个人 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

使用 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 数据

数据分析是一个非常常见的需求,而在实际的落地场景当中, 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 是一个不错的选择。