前置条件

在开始接入之前,请确保已从管理员处获取以下两个凭证,并明确各自的使用范围:

凭证格式使用位置用途
AppKey UUID ✓ 前端 加载结构编辑器,绑定到授权域名
ApiKey UUID ✗ 仅后端 调用结构检索 API,必须保密,不得出现在任何前端代码中
🔒 ApiKey 安全警告(必读):ApiKey 是后端服务凭证,一旦泄露到前端页面(JS 源码、网络请求等),任何人均可不限量调用检索接口,导致配额耗尽。严禁将 ApiKey 写入任何前端 JS 代码。检索请求必须通过你自己的服务端转发,由服务端持有并传递 ApiKey。
⚠ 域名绑定:AppKey 在颁发时绑定了授权域名,编辑器只能在对应域名下加载。跨域调用将返回 Error call Host 错误。如需增加域名,请联系管理员。

引入 JS 文件

SDK 由服务端统一托管,直接从 API 部署域名 引入,无需 npm 安装。

⚠ 跨域要求(必读):sdk.js 内部会向 APIURL 发起 XHR 请求加载编辑器资源。
因此 sdk.js 的引入地址CJSDraw_SDK.Cfg.APIURL 必须指向同一个域名(即 API 服务的部署域名)。
如果两者域名不一致,或者 APIURL 与你自己页面的域名不同且服务端未配置 CORS,编辑器将无法正常加载。
html
<!-- 1. jQuery(SDK 依赖)——从 API 部署域名加载 -->
<script src="//mol.mkbio.com.cn/static/jquery.min.js"></script>

<!-- 2. 结构搜索 SDK——必须从 API 部署域名加载,不能从你自己的域名引入 -->
<script src="//mol.mkbio.com.cn/static/sdk.js"></script>

<!-- 3. 配置 APIURL——必须与上面 sdk.js 的域名完全一致 -->
<script>
    CJSDraw_SDK.Cfg.APIURL = "//mol.mkbio.com.cn";
</script>
💡 提示:使用 // 开头的协议无关 URL,SDK 会自动继承当前页面的 http / https 协议,无需手动区分。

初始化结构编辑器

在页面上准备一个容器元素,然后调用 CJSDraw_SDK.Init() 初始化编辑器。默认加载内置编辑器;如需使用 Ketcher,传入 version: "ketcher-sa"(参见编辑器选型)。

HTML 容器

html
<!-- 编辑器将渲染到此元素内 -->
<div id="jsdrawbox"></div>

初始化调用

javascript
var appkey = "your-appkey-uuid";

// Init(place, cfg) — 两个参数,callback 放在 cfg 内
CJSDraw_SDK.Init("jsdrawbox", {
    appkey:   appkey,
    skin:     "w8",     // 皮肤,固定值
    width:    "100%",   // 编辑器宽度
    height:   "510px",  // 编辑器高度
    callback: function() {
        // 编辑器加载完成后的回调
        console.log("编辑器已就绪");
    },
});

封装成函数(推荐)

Demo 页面中的封装写法,方便统一传入 AppKey:

javascript
function InitJsDraw(el, callback, cfg, appkey) {
    cfg = $.extend({
        appkey:   appkey || '',
        callback: function() { callback(); }
    }, cfg);
    CJSDraw_SDK.Init(el, cfg);
}

// 调用示例
InitJsDraw(
    "jsdrawbox",                         // 容器 id
    function() { console.log("就绪"); }, // 回调
    { skin: "w8", width: "100%", height: "510px" },
    "your-appkey-uuid"
);

获取编辑器中的结构

从编辑器取出用户绘制的分子结构,SDK 通过 CJSDraw_SDK.Do("getSmiles", ...) 异步获取 SMILES 字符串,结果在回调的 data.value 字段中。

javascript
CJSDraw_SDK.Do("getSmiles", {}, function(data) {
    var smiles = data.value;   // SMILES 字符串,在 data.value 字段
    console.log("当前结构 SMILES:", smiles);

    if (!smiles) {
        alert("请先绘制结构");
        return;
    }
    // 继续后续操作,如检索、提交等
});
注意:如果编辑器为空(未绘制任何结构),data.value 为空字符串,需先判空再提交。

回填结构到编辑器

将已知的 Molfile 字符串渲染到编辑器中,通常用于"查看详情"或"再次编辑"场景。命令为 setMolfile,参数字段为 mol(Molfile 格式,非 SMILES):

javascript
// 取得当前编辑器的 Molfile(例如保存时使用)
CJSDraw_SDK.Do("getMolfile", {}, function(data) {
    var molfile = data.value;  // Molfile 字符串,在 data.value 字段
    console.log(molfile);
});

// 回填 Molfile 到编辑器(再次编辑场景)
CJSDraw_SDK.Do("setMolfile", { mol: molfileString }, function(data) {
    console.log("结构已回填");
});
提示:如果你只有 SMILES,可先通过后端将 SMILES 转换为 Molfile,再调用 setMolfile 回填。

编辑器配置项参考

参数类型必填默认值说明
appkeystring必填编辑器授权 AppKey(UUID)
versionstring可选默认编辑器编辑器版本:不传使用默认编辑器;传 "ketcher-sa" 切换为 Ketcher 编辑器(功能更丰富,支持反应式)
skinstring可选"w8"编辑器皮肤,仅默认编辑器有效,固定为 w8
widthstring可选"100%"编辑器宽度,支持 CSS 值如 "800px"
heightstring可选"510px"编辑器高度,建议不低于 400px
callbackfunction可选编辑器加载完成后的回调函数

编辑器选型

平台支持两款结构编辑器,通过 Init()version 参数切换,其余 API(getSmilessetMolfile 等)完全一致。

编辑器version 值特点适用场景
默认编辑器(JSDraw) 不传 / 省略 轻量、加载快,基础绘图功能完整 大多数化合物检索场景
Ketcher "ketcher-sa" 功能全面、界面现代,支持反应式(Reaction)、SMARTS、模板库等 需要复杂绘图、反应式搜索的场景

切换为 Ketcher 编辑器

javascript
// 使用 Ketcher 编辑器:仅需传入 version: "ketcher-sa"
CJSDraw_SDK.Init("jsdrawbox", {
    appkey:  "your-appkey-uuid",
    version: "ketcher-sa",   // ← 切换为 Ketcher
    width:   "100%",
    height:  "560px",         // Ketcher 建议高度 ≥ 500px
    callback: function() {
        console.log("Ketcher 已就绪");
    },
});
💡 切换编辑器不影响其他 API:无论使用哪款编辑器,CJSDraw_SDK.Do("getSmiles", ...)Do("getMolfile", ...)Do("setMolfile", ...) 的调用方式完全相同,无需修改业务代码。
注意:同一页面同时只能初始化一个编辑器实例。如需切换编辑器类型,请刷新页面重新初始化。

推荐接入架构

结构检索需要携带 ApiKey,为防止泄露,不能由前端直接调用检索接口。正确的做法是让前端从编辑器获取 SMILES 后,提交给你自己的后端接口,由后端持有 ApiKey 并转发给我们的检索服务。

🖥️
用户浏览器
渲染编辑器
获取 SMILES
⚙️
你的后端服务
持有 ApiKey
转发检索请求
🔬
结构检索服务
执行搜索
返回结果
前端只持有 AppKey(编辑器授权),ApiKey 仅存在于你的服务端配置中,不得出现在任何前端代码或网络请求里。

后端代理说明

你的后端接收前端传来的 SMILES 和检索参数后,以 ApiKey 为鉴权凭证,调用我们的结构检索 REST API,再将结果返回给前端。ApiKey 始终保存在服务端环境变量中,不会暴露给浏览器。

python 示例(后端,仅供参考)
import os, requests

# ApiKey 从环境变量读取,严禁硬编码或返回给前端
API_KEY = os.environ["CHEM_API_KEY"]

def structure_search(smiles, searchtype, page=1, psize=12):
    resp = requests.post(
        "https://your-chem-service.com/api/search",
        headers={"Authorization": f"Bearer {API_KEY}"},
        json={
            "smiles":     smiles,
            "searchtype": searchtype,
            "page":       page,
            "psize":      psize,
        },
    )
    return resp.json()
具体的检索 REST API 接口签名(请求参数、响应格式、错误码)请参阅 API 文档

检索类型枚举

向后端传递 searchtype 参数以指定检索模式,由后端转发给检索服务:

1
子结构检索
查询分子是否包含绘制的结构片段(Substructure)
2
全结构检索
精确匹配完整分子结构(Exact Match)
4
相似度 ≥ 90%
Tanimoto 相似度不低于 0.90
5
相似度 ≥ 80%
Tanimoto 相似度不低于 0.80
6
相似度 ≥ 60%
Tanimoto 相似度不低于 0.60

渲染检索结果

你的后端将检索结果返回给前端后,前端根据数据渲染结果列表。建议后端直接在结果中携带预存的结构图 URL(imgUrl),避免前端批量实时调用 /api/mol2svg 渲染接口耗费服务器资源(详见结构渲染接口说明)。

后端建议返回的数据格式

json — 推荐响应格式
{
  "ok":       true,
  "nextpage": 2,          // 下一页页码,≤ 0 表示无更多结果
  "list": [
    {
      "cas":    "110-89-4",   // CAS 号
      "smile": "C1CCNCC1",   // SMILES
      "imgUrl":"/api/mol2svg?smile=C1CCNCC1", // 保存时预存的结构图 URL,直接展示
      "enname":"Piperidine"   // 英文名
    }
  ]
}

前端渲染示例

javascript
var nextpage = 1;

function renderResults(result, isFirstPage) {
    if (!result.ok) {
        $("#results").html("<p>检索失败,请稍后重试</p>");
        return;
    }
    if (!result.list || result.list.length === 0) {
        $("#results").html("<p>未检索到匹配化合物</p>");
        return;
    }

    var html = '';
    result.list.forEach(function(item) {
        // 使用后端预存的结构图 URL(推荐),避免批量实时渲染耗费服务器资源
        var imgUrl = item.imgUrl || ('/api/mol2svg?smile=' + encodeURIComponent(item.smile));
        html += '<div class="result-card">'
              + '  <img src="' + imgUrl + '" alt="结构式" loading="lazy">'
              + '  <p class="cas">' + item.cas + '</p>'
              + '  <p class="name">' + (item.enname || '—') + '</p>'
              + '</div>';
    });

    if (isFirstPage) {
        $("#results").html(html);
    } else {
        $("#results").append(html);  // 加载更多时追加
    }

    // 控制"加载更多"按钮
    nextpage = parseInt(result.nextpage);
    $("#load-more").toggle(nextpage > 0);
}

结构渲染为 SVG 图片

无需鉴权,直接拼接图片 URL 即可将 SMILES 渲染为结构式图片,可直接用于 <img> 标签:

html
<!-- GET /api/mol2svg?smile=SMILES -->
<img src="/api/mol2svg?smile=C1CCNCC1"
     alt="结构式"
     style="max-width:200px">
javascript
// 动态拼接,注意 SMILES 需 encodeURIComponent
var smiles  = "C1CCNCC1";
var imgUrl  = "/api/mol2svg?smile=" + encodeURIComponent(smiles);
$("#mol-img").attr("src", imgUrl);
💡 返回的是 SVG 格式,可缩放且不失真,适合在单条记录详情页等低频场景中按需使用。
⚠ 不建议在检索结果列表中批量实时调用:每次渲染都需服务端执行化学计算,并发请求过多会严重消耗服务器资源。
推荐做法:保存化合物时,预先将结构式静态图片地址存入记录;查询结果直接返回该地址,前端无需实时调用渲染接口。

完整页面示例

以下是一个符合安全架构的完整最小化示例:前端仅持有 AppKey,所有检索请求通过调用你自己的后端接口完成,ApiKey 不出现在任何前端代码中。

🔒 示例中的 /your-api/structure-search 是你自己服务端的代理接口,由你的后端持有 ApiKey 并转发给检索服务。请勿将 ApiKey 替换进此前端代码。
html — 完整示例(安全架构,无 ApiKey 在前端)
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>结构式检索</title>
  <script src="//mol.mkbio.com.cn/static/jquery.min.js"></script>
  <script src="//mol.mkbio.com.cn/static/sdk.js"></script>
  <script>CJSDraw_SDK.Cfg.APIURL = "//mol.mkbio.com.cn";</script>
  <style>
    body { font-family: sans-serif; max-width: 960px; margin: 0 auto; padding: 20px; }
    #results { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 20px; }
    .card { border: 1px solid #e2e8f0; border-radius: 8px; padding: 12px; width: 160px; text-align: center; }
    .card img { max-width: 100%; height: 120px; object-fit: contain; }
    .card p { font-size: 12px; color: #64748b; margin: 4px 0 0; }
  </style>
</head>
<body>

<!-- 编辑器容器 -->
<div id="jsdrawbox"></div>

<!-- 检索类型 + 搜索按钮 -->
<div style="margin-top:12px;">
  <label><input type="radio" name="stype" value="1" checked> 子结构</label>
  <label><input type="radio" name="stype" value="2"> 全结构</label>
  <label><input type="radio" name="stype" value="4"> 相似≥90%</label>
  <button id="btn-search" style="margin-left:12px;padding:6px 18px;">搜索</button>
</div>

<!-- 结果容器 -->
<div id="results"></div>
<div id="load-more" style="display:none;text-align:center;margin-top:16px;">
  <button id="btn-more">加载更多</button>
</div>

<script>
  // ⚠ 前端只持有 AppKey,ApiKey 由后端保管,禁止出现在此处
  var APP_KEY  = "your-appkey-uuid";
  var nextpage = 1;
  var curSmiles = "";
  var curType  = "1";

  // 初始化编辑器(Init 只接受两个参数,callback 在 cfg 内)
  CJSDraw_SDK.Init("jsdrawbox", {
    appkey:   APP_KEY,
    skin:     "w8",
    width:    "100%",
    height:   "480px",
    callback: function() { console.log("编辑器就绪"); },
  });

  // 新检索:先取 SMILES,再调自己的后端接口
  function newSearch() {
    CJSDraw_SDK.Do("getSmiles", {}, function(data) {
      if (!data.value) { alert("请先绘制结构"); return; }
      curSmiles = data.value;
      curType  = $("input[name='stype']:checked").val();
      nextpage = 1;
      $("#results").empty();
      fetchSearch();
    });
  }

  // 调用你自己的后端代理接口
  function fetchSearch() {
    fetch("/your-api/structure-search", {
      method:  "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        smiles:     curSmiles,
        searchtype: curType,
        page:       nextpage,
        psize:      12,
      }),
    })
    .then(r => r.json())
    .then(function(result) {
      if (!result.ok) { alert("检索失败"); return; }
      if (!result.list || !result.list.length) {
        $("#results").html("<p>未检索到匹配化合物</p>"); return;
      }
      result.list.forEach(function(item) {
        // 使用后端预存的结构图 URL,避免批量实时渲染
        var imgUrl = item.imgUrl || ('/api/mol2svg?smile=' + encodeURIComponent(item.smile));
        $("#results").append(
          '<div class="card">'
          + '<img src="' + imgUrl + '" loading="lazy" alt="结构式">'
          + '<p>' + item.cas + '</p>'
          + '<p>' + (item.enname || '—') + '</p>'
          + '</div>'
        );
      });
      nextpage = parseInt(result.nextpage);
      $("#load-more").toggle(nextpage > 0);
    });
  }

  $(function() {
    $("#btn-search").on("click", newSearch);
    $("#btn-more").on("click", fetchSearch);
  });
</script>
</body>
</html>