使 Bookmark Dashboard 可在线共享

发布: (2025年12月17日 GMT+8 18:40)
8 min read
原文: Dev.to

Source: Dev.to

介绍

最近,我经常需要与团队成员共享书签——代码仓库、文档链接、Figma 上的 UI 设计等。于是灵感来了:

如果我能把这些书签组织成一个仪表盘,并生成一个公共网页一次性分享所有内容会怎样?

如果能够实现,我就可以从一个或多个书签集合中准备一个仪表盘,然后直接生成相应的网页。无需手动复制粘贴书签,只需把 URL 发给同事,他们就能看到一个整洁的静态仪表盘,展示所有资源。如此便利!

带着这个想法,我立刻开始动手。由于我之前已经开发了 Bookmark Dashboard(一个浏览器扩展),主要任务就是添加一个 “生成共享链接” 按钮。点击后会:

  1. 保存当前仪表盘的布局。
  2. 生成一个唯一的 URL,打开后即可呈现当时仪表盘的完整复制。

仪表盘共享界面

更新 UI 是比较容易的工作;真正的挑战在于点击按钮时后端的处理。经过一番考虑,我选择了最简洁、最快速的实现方式。

下面,我将详细说明如何使用 Next.jsMongoDB 实现后端功能。

实现方法

只要传入的布局相同,仪表盘就会显示相同的内容。因此,共享功能归结为两个步骤:

  1. 保存 布局,在生成共享链接时记录。
  2. 恢复 布局,在打开链接时重新加载。

后端实现相对直接:

  • 创建一个 API 来接收仪表盘布局并将其存入数据库。
  • 创建一个页面,在访问时读取已存储的布局并渲染。
    该页面的 URL 即为共享链接。

仪表盘链接 API

我的后端服务使用 Next.js App Router 构建。要定义一个 API 端点,只需遵循基于文件夹的路由约定。

因为我希望 API 路径为 /dashboard/link,所以创建了如下文件夹结构,并在其中放置了 route.js

src/app/dashboard
└── link
    └── route.js

route.js

import { NextResponse } from 'next/server';
// database util
import connectMongo from '@/db/mongo';
// database model for dashboard layout data
import LinkData from '@/db/entity/link_data';

export async function PUT(req) {
  // TODO: read current user's account (e.g., from auth token)
  const account = /* ... */;

  // Read the dashboard layout from the request body
  const { content } = await req.json();

  // Connect to MongoDB and upsert the layout
  await connectMongo();
  const item = await LinkData.findOneAndUpdate(
    { account },
    { content },
    { upsert: true, new: true }
  );

  // Return the generated document ID
  return NextResponse.json(
    { re: item._id },
    { status: 200 }
  );
}

PUT 函数会自动处理指向 /dashboard/linkPUT 请求。连接到 MongoDB 后,它会将布局(content)保存到对应用户的账户下。前端(“Generate” 按钮的点击处理函数)可以调用此 API,返回的 _id 用于构建分享链接。

仪表盘页面

要通过其 _id 引用仪表盘,分享链接应为 /dashboard/link/[id]。我在 app/dashboard/link/[id] 下创建了一个 page.js

src/app/dashboard
└── link
    ├── [id]
    │   ├── Dashboard.js
    │   └── page.js
    └── route.js

page.js

// database util
import connectMongo from '@/db/mongo';
// database model for dashboard layout data
import LinkData from '@/db/entity/link_data';
// the component that actually renders the dashboard
import Dashboard from './Dashboard';

async function loadData(id) {
  await connectMongo();
  if (!id) return null;
  return await LinkData.findById(id).exec();
}

export default async function DashboardLinkPage({ params }) {
  const { id } = params;
  const data = await loadData(id);

  let layout = undefined;
  if (data?.content) {
    try {
      const parsed = JSON.parse(data.content);
      layout = parsed?.layout;
    } catch (e) {
      console.error('Failed to parse layout JSON', e);
    }
  }

  // This is a server‑side component (no "use client" directive)
  return <Dashboard layout={layout} />;
}

该页面从路由参数中提取 id,从 MongoDB 获取存储的布局,解析后将其传递给 Dashboard 组件(使用 React‑Grid‑Layout 构建,详见之前的文章)。

/dashboard/link/[id] 中的 [id] 替换为 API 返回的实际 _id 值。该 URL 即为可发送给他人的分享链接。对方打开后,将看到一个与生成链接时完全相同的静态仪表盘。

关于 MongoDB 连接的说明

route.jspage.js 都使用相同的 connectMongo 辅助函数,以在并发请求和热重载期间复用单个连接:

import mongoose from 'mongoose';

// Reuse the connection across requests (module re‑imports)
let cached = global.mongoose;
if (!cached) {
  cached = global.mongoose = { conn: null, promise: null };
}

async function connectMongo() {
  if (cached.conn) {
    return cached.conn;
  }

  if (!cached.promise) {
    const uri = process.env.MONGODB_URI;
    const opts = {
      useNewUrlParser: true,
      useUnifiedTopology: true,
    };
    cached.promise = mongoose.connect(uri, opts).then((mongoose) => mongoose);
  }

  cached.conn = await cached.promise;
  return cached.conn;
}

export default connectMongo;

这种模式确保 MongoDB 客户端在每个服务器实例中仅实例化一次,从而提升性能并防止连接耗尽错误。

结论

只需几行后端代码,Bookmark Dashboard 现在就支持生成可分享的静态仪表盘页面。工作流程如下:

  1. 点击 Generate Sharing Link → UI 调用 PUT /dashboard/link 接口。
  2. API 存储布局并返回一个 _id
  3. 构建 URL /dashboard/link/ 并分享它。
  4. 任何访问该 URL 的人都会看到完全相同的仪表盘布局,且在服务器端渲染。
if (!cached.promise) {
  cached.promise = mongoose
    .connect(MONGO_URI, {
      serverApi: { version: '1', strict: true, deprecationErrors: true },
    })
    .then((mongoose) => {
      return mongoose;
    });
}

cached.conn = await cached.promise;
await mongoose.connection.db.admin().command({ ping: 1 });
console.log('Successfully connected to db!');
} catch (e) {
  cached.promise = null;
  throw e;
}

return cached.conn;
}

全屏控制

  • 进入全屏模式
  • 退出全屏模式

由于 route.jspage.js 都在服务器端运行,它们可以共享相同的连接逻辑并受益于连接复用。

这种直接在服务器端页面查询数据库的模式是 Next.js 的突出特性之一。它实现了服务器端数据获取和渲染,这与 React 或 Vue 等客户端框架有显著区别。

完成总结

后端已经全部就绪。以下是整个流程的工作方式:

  1. 生成仪表盘分享链接 – 前端(在 “Generate” 按钮的点击处理函数中)调用 /dashboard/link 接口。
  2. 保存布局 – 接口存储布局并返回一个 _id
  3. 构建分享链接 – 使用该 _id 构造链接 /dashboard/link/_id
  4. 打开链接 – 页面从路径中提取 _id,获取已保存的布局,并渲染仪表盘内容。

示例

Dashboard generation preview

打开分享链接

Shared dashboard view

此可分享的仪表盘功能已在最新版本的 Bookmark Dashboard 中上线。它已经在我与他人共享资源时为我节省了大量时间。该功能免费,希望能帮助更多人快速、轻松地分享他们的收藏。

感谢阅读!

Back to Blog

相关文章

阅读更多 »