惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

S
Schneier on Security
有赞技术团队
有赞技术团队
T
The Blog of Author Tim Ferriss
F
Fortinet All Blogs
D
DataBreaches.Net
F
Full Disclosure
腾讯CDC
博客园 - 【当耐特】
MyScale Blog
MyScale Blog
Stack Overflow Blog
Stack Overflow Blog
小众软件
小众软件
Hugging Face - Blog
Hugging Face - Blog
Last Week in AI
Last Week in AI
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
爱范儿
爱范儿
The GitHub Blog
The GitHub Blog
Engineering at Meta
Engineering at Meta
大猫的无限游戏
大猫的无限游戏
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
S
SegmentFault 最新的问题
The Register - Security
The Register - Security
WordPress大学
WordPress大学
博客园 - 聂微东
雷峰网
雷峰网
J
Java Code Geeks
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
P
Privacy International News Feed
酷 壳 – CoolShell
酷 壳 – CoolShell
A
Arctic Wolf
Scott Helme
Scott Helme
C
Cyber Attacks, Cyber Crime and Cyber Security
T
Tor Project blog
博客园 - 三生石上(FineUI控件)
Know Your Adversary
Know Your Adversary
AWS News Blog
AWS News Blog
G
Google Developers Blog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
C
CERT Recently Published Vulnerability Notes
O
OpenAI News
Project Zero
Project Zero
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
Application and Cybersecurity Blog
Application and Cybersecurity Blog
云风的 BLOG
云风的 BLOG
N
News and Events Feed by Topic
MongoDB | Blog
MongoDB | Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Microsoft Security Blog
Microsoft Security Blog
Cisco Talos Blog
Cisco Talos Blog
P
Palo Alto Networks Blog
Schneier on Security
Schneier on Security

@Lenciel

马代(2) 马代(1) Thoughts On AI In Early 2026 (3) Thoughts On AI In Early 2026 (2) Thoughts On AI In Early 2026 (1) 江门 Nginx 挡爬虫 2025年终总结 What I know in 2025, 😳 技术债的心理模型
Valine to Waline
Lenciel · 2026-03-11 · via @Lenciel

目录

Why

五年前修改模板的时候我引入了 Valine 来提供评论功能。

主要是图方便:在 LeanCloud 上部署个免费服务,前端简单调调就行。

这五年里运行良好,唯一的担心就是这免费的服务别哪天没了。

果然,墨菲定律法力无边,前两天看到了 LeanCloud 的停服公告国内做云做基础设施的团队很多都是老朋友了,大家这些年真的是因为选择的不同,过着天差地别的日子啊…

经过一点儿调查,选择了 Waline 作为下一站,继续折腾。

How

Waline 的官方文档对我没啥大用Waline 之前也可以用 LeanCloud 做后台所以迁移数据的文档基本上是针对这种用法的,但好像 LeanCloud 停服的消息传开他们的文档也在更新中。 ,因为我这次不想再依赖谁家的后台(Waline 真是支持蛮多后台),甚至不想依赖docker compose 唤起的不透明的容器。

我得从头到尾独立部署它。

数据导出和导入

在 LeanCloud 后台选择「导入导出 > 限定 Class > Comment > 导出」,之后会收到邮件(我把 User 也导出了,因为 Waline 看起来也有一张 wl_Users 表)。

然后官网上有一个把这个导出的 json 转成 csv 的工具。似乎是想说,不管后面用什么样的数据库,都可以把这个 csv 导入进去。

其实会有两个问题。

一个是 Valine 的数据库 schema 和 Waline 最新的设计并不兼容(比如 sticky等字段根本就没有),如果直接导入原来的表结构和数据作为 wl_Comment ,运行时会报错。

另一个就是,如果选择先用 Waline 的 schema 建表,再导入数据,那么 json 先转成 csv:

  • 首先,显得有点儿多此一举;
  • 其次,把字段缺失的 csv 往任何数据库里面导都是个付费软件才会提供的功能;

所以还不如直接写个脚本把 json 转成相应数据库的 sql 文件。

比如我选择 sqlite,数据导入其实就下面几步。

1.下载 Waline 最新版的 schema,然后创建数据库:

$ sqlite3 /Users/lenciel/Downloads/waline.sqlite < /Users/lenciel/Downloads/waline.sqlite.sql

2.编写一个简单的脚本转换 json 到 sql(核心逻辑就是映射有的,放过没有的,对 Users 可以如法炮制):

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
读取 Comment.0.json(每行一个 JSON 对象),生成 INSERT SQL 插入到 wl_Comment 表。
只映射 JSON 中存在且 wl_Comment 表也有的字段,其余字段留空/使用默认值。
"""

import json
import sys
import os

# wl_Comment 表的所有字段(不含 id,因为是 AUTOINCREMENT)
WL_COMMENT_COLUMNS = [
    "user_id", "comment", "insertedAt", "ip", "link",
    "mail", "nick", "rid", "pid", "sticky",
    "status", "like", "ua", "url", "createdAt", "updatedAt"
]

# JSON key → wl_Comment 字段的直接映射(key 名一致的)
DIRECT_MAP = {
    "comment": "comment",
    "ip": "ip",
    "link": "link",
    "mail": "mail",
    "nick": "nick",
    "ua": "ua",
    "url": "url",
    "createdAt": "createdAt",
    "updatedAt": "updatedAt",
}

# 需要特殊处理的字段
# insertedAt 在 JSON 里是 {"__type": "Date", "iso": "..."}
# pid/rid 在 JSON 里是 objectId 字符串,但 wl_Comment 里是 INTEGER
# 这里先把 pid/rid 当字符串存(因为原始数据就是 objectId 字符串)

def escape_sql(value):
    """转义 SQL 字符串中的单引号"""
    if value is None:
        return "NULL"
    s = str(value)
    s = s.replace("'", "''")
    return f"'{s}'"


def extract_value(record, col):
    """从 JSON record 中提取对应 wl_Comment 字段的值"""

    if col == "insertedAt":
        v = record.get("insertedAt")
        if isinstance(v, dict):
            return v.get("iso")
        return v  # 如果不是 dict,直接用

    if col == "status":
        # wl_Comment.status 是 NOT NULL,JSON 里没有 status 字段,默认 approved
        return record.get("status", "approved")

    if col in DIRECT_MAP:
        json_key = DIRECT_MAP[col]
        return record.get(json_key)

    # pid, rid: JSON 里是 objectId 字符串,暂时直接存
    if col in ("pid", "rid"):
        return record.get(col)

    # user_id, sticky, like: JSON 里没有,返回 None
    return None


def record_to_insert_sql(record, table_name="wl_Comment"):
    """将一条 JSON 记录转为 INSERT SQL"""
    cols_with_values = []

    for col in WL_COMMENT_COLUMNS:
        val = extract_value(record, col)
        if val is not None:
            cols_with_values.append((col, val))

    if not cols_with_values:
        return None

    col_names = ", ".join([f'"{c}"' for c, _ in cols_with_values])
    col_values = ", ".join([escape_sql(v) for _, v in cols_with_values])

    return f'INSERT INTO "{table_name}" ({col_names}) VALUES ({col_values});'


def main():
    json_file = sys.argv[1] if len(sys.argv) > 1 else "Comment.0.json"
    output_file = sys.argv[2] if len(sys.argv) > 2 else "import_comments.sql"

    if not os.path.exists(json_file):
        print(f"错误: 文件 {json_file} 不存在")
        return 1

    records = []
    with open(json_file, "r", encoding="utf-8") as f:
        for line_num, line in enumerate(f, 1):
            line = line.strip()
            if not line:
                continue
            try:
                record = json.loads(line)
                records.append(record)
            except json.JSONDecodeError as e:
                print(f"警告: 第 {line_num} 行 JSON 解析失败: {e}")

    print(f"读取到 {len(records)} 条记录")

    sql_statements = []
    for record in records:
        sql = record_to_insert_sql(record)
        if sql:
            sql_statements.append(sql)

    with open(output_file, "w", encoding="utf-8") as f:
        f.write("BEGIN TRANSACTION;\n")
        for sql in sql_statements:
            f.write(sql + "\n")
        f.write("COMMIT;\n")

    print(f"生成 {len(sql_statements)} 条 INSERT 语句,输出到 {output_file}")
    return 0


if __name__ == "__main__":
    exit(main())

3.把生成的 sql 文件再导入之前创建的数据库:

$ sqlite3 /Users/lenciel/Downloads/waline.sqlite < /Users/lenciel/Downloads/import_comments.sql

部署服务端

看起来 Waline 还不支持 node 24,所以我用 nvm 安装了 node 18 给它( npm audit fix 爽到爆炸)。

$ mkdir waline && cd waline
$ nvm install 18

安装可以按照官方文档来,反正就是一行命令(但运行的时候不太可能直接用文档里那个 node 命令,这个后面会说)。但关于环境变量,特别是究竟有哪些环境变量,以及哪些配置在前端哪些在后端,很让我迷糊了一会儿。所以我推荐引入 dotenv,并且仔细看看相关文档

$ npm install dotenv
$ vi .env

这里面最重要的当然是关于数据库和域名的配置,但还有一些没有写在文档里但使用起来大概也需要的东西,就主要靠江湖历练。比如虽然说了 GRAVATAR_STR 是基于 nunjucks 语法,但不熟悉的朋友大概看了也不会知道究竟怎么去换 retro 或者其他风格的头像。

部署完成之后,最好通过反向代理给它一个独立的子域名。

前端部署

在相应的地方加上类似于下面的片段即可完成前端的初始化(需要注意的是 serverURL 之外, elpath 还得根据自己的 Blog 系统来指定,比如 Jekyll 的 path 就是 {{ page.url }}):

<link rel="stylesheet" href="https://unpkg.com/@waline/client@v3/dist/waline.css" />
<link rel="stylesheet" href="https://unpkg.com/@waline/client@v3/dist/waline-meta.css" />

<script type="module">
  import { init } from 'https://unpkg.com/@waline/client@v3/dist/waline.js';
  const locale = {
    placeholder: '欢迎留言~',
  };
  init({
    el: '#vcomments',
    serverURL: 'https://comments.lenciel.com',
    path: '{{ page.url }}',
    highlight: true,
    locale,
  });
</script>

长期运行

上传本地生成的数据库文件之后,运行下面的命令验证安装没有问题:

$ node node_modules/@waline/vercel/vanilla.js

然后就可以写一个简单的启动脚本,交给 pm2 来管理。

module.exports = {
  apps: [{
    name: "waline",        // 进程名
    script: "app.js",      // 启动脚本
    cwd: "/工作目录/waline",   // 工作目录
    interpreter: "../.nvm/versions/node/v18.20.8/bin/node",  // 替换为你的 Node 路径
    exec_mode: "fork",
    instances: 1,
    autorestart: true,
    watch: false,
    max_memory_restart: "100M",  // 内存超限重启
    // 日志配置
    log_date_format: "YYYY-MM-DD HH:mm:ss",
    error_file: "/工作目录/waline/logs/error.log",
    out_file: "/工作目录/waline/logs/out.log",
    merge_logs: true,
    // 禁用 dotenv 提示(实在是多到刷屏)
    env: {
      DOTENV_SILENCE: "1",
      NODE_NO_WARNINGS: "1"
    }
  }]
};

比如每次更改了环境变量,就可以通过 pm2 来重启进程并观察日志:

$ pm2 restart waline
$ pm2 logs waline --lines 100

完成部署之后,我怀着从租房到买房的心情,愉快地调整了一些样式,因为看起来可以很多年都不需要为评论搬家了。