☰
Jupyter Notebook 中运行 JavaScript 的三种方式与实战
2026/10/18 18:58:41 网站建设 项目流程

Jupyter Notebook 默认是 Python 的天下,这话没错,但总有那么几次,你需要在 notebook 里运行 JavaScript。我前阵子做数据可视化 demo 的时候就撞上了这个需求:数据用 Python 处理完了,但想要一个更自由的前端交互效果,plotly 和 bokeh 又都太模板化,干脆直接上 JS。折腾了一圈发现,在 Jupyter Notebook 里运行 JavaScript 其实有三条完全不同的路,分别适合不同场景。这篇文章我就把这三种方式全部拆开讲清楚,包括怎么装环境、怎么传参、会遇到哪些坑,以及我自己平时到底用哪种。适合对 Jupyter 有基础了解、又不想为了几行 JS 单独开前端项目的朋友。

1. 为什么非要在 Jupyter 里运行 JavaScript

1.1 谁会有这种需求

需求往往来自三类人。第一类是前端工程师,习惯写 JS,但被数据分析任务拖进了 Jupyter 环境,希望在同一个界面里用熟悉的语法做点计算、调个接口;第二类是数据工程师,他们用 Python 处理完数据后,想在前端做一些高度定制化的交互展示,比如自绘 canvas 图表、操作 DOM 元素、接入第三方前端 SDK;第三类是教学场景里的老师,希望学生在同一个 notebook 里同时体验 Python 和 JS,省得来回切换两套开发工具。

我自己的场景更直接:要给同事演示一批数据在浏览器里的落点效果。Python 侧虽然有现成绘图库,但那种自由拖拽、实时标记的交互效果,用 JS 写起来反而更顺手。我又不想为了一个演示去搭工程化前端项目,最后决定就在 Jupyter 里跑 JS。这件事听起来小,但做起来有讲究。

1.2 三条路线先画个蓝图

方案一:安装 JavaScript 内核(基于 Node.js),把整个 Jupyter Notebook 变成 JS 环境。每个 cell 都是 JS 代码,执行逻辑和跑 Node 脚本基本一致。

方案二:Jupyter 保持 Python 内核不变,在 cell 中用 IPython.display.Javascript 把 JS 代码推送到 notebook 前端浏览器执行。这种做法允许 Python 先算好数据,再交给 JS 去做展示,是三种方式里最灵活的组合。

方案三:直接在 Markdown cell 里写 HTML 和 script 标签,借助 Jupyter 渲染 raw HTML 的能力,让 JS 代码在页面渲染时执行。这个最轻量,但只能做纯前端展示,和 Python 几乎没有数据通道。

三个方案不是替代关系。我的判断是:方案一适合“整本 notebook 都是 JS”的人,方案二适合“Python 算数据、JS 做交互”的混合需求,方案三适合“纯前端小组件、不需要 Python 参与”的场景。下面我按难度从低到高逐个拆解。

2. 方案一:把 Notebook 变成 JS 环境(JavaScript 内核)

2.1 环境安装与内核注册

这个方案的本质是让 Node.js 成为 Jupyter 的执行后端,官方实现叫 IJavascript。安装分两步:先保证机器上有 Node.js,然后通过 npm 安装 IJavascript 并注册内核。

安装 IJavascript:

npm install -g ijavascript

注册内核:

ijsinstall

注册完成后,在终端里执行jupyter kernelspec list,如果看到 JavaScript 相关条目,就说明内核已经挂上了。如果没看到,可以先重启 Jupyter 或 JupyterLab 再检查,有时候是内核服务缓存的问题。

Node.js 的版本要注意,不要低于 12,太老的版本很多 ES6 语法和 npm 包都跑不起来。我建议 Windows 用户直接装官方安装包,macOS 用户用 Homebrew,Linux 用户优先用 nvm 做版本管理。比如我用 nvm 装长维护版:

nvm install 18 nvm use 18

2.2 基本用法和第一印象

新建 Notebook 时,在 Kernel 菜单里选择 JavaScript (Node.js),之后每个 cell 就是一段 JS 代码。第一次用的时候,最需要适应的点是“输出”逻辑。比如:

const data = [1, 2, 3, 4, 5]; const doubled = data.map(x => x * 2); doubled

执行后,最后一个表达式的值会直接显示在输出区,这和 Python 内核很类似。如果你用console.log打印,也会在输出区显示,但格式上没有自动返回那么整洁。

这个内核真正有价值的地方在于,它支持 Node.js 生态。如果你想用某个 npm 包,在当前工作目录安装后,直接require即可,我举例:

npm install lodash
const _ = require('lodash'); const result = _.chunk(['a', 'b', 'c', 'd'], 2); result

我拿它跑过好几次定时抓取任务,把抓回来的数据整理打印在 notebook 里。比起单独维护一个 Node 脚本,这样做的好处是过程是分部展示的,抓数据、清洗、分析每个环节都看得见,别人也容易接手。

2.3 这个方案里我踩过的几个坑

第一个坑:require的模块搜索路径容易混乱。Jupyter 的当前工作目录往往和你npm install时所在的目录不一致。比如笔记本放在子文件里,而依赖装在根目录,require就会报Cannot find module。排查方式是先打印process.cwd(),确认实际工作目录,然后在代码头部显式设置:

process.env.NODE_PATH = process.cwd() + '/node_modules'; require('module').Module._initPaths();

第二个坑:最外层直接写await会报语法错误。JS 内核不像现代浏览器一样支持顶层 await,异步代码必须包在 async 函数里:

async function fetchStatus() { const response = await fetch('https://example.com'); return response.status; } fetchStatus();

第三个坑也是很多人忽略的:这里的 JS 是 Node.js 后端环境,没有document、window这些浏览器对象。你要是想操作 DOM 或者做前端渲染,在这个内核里会直接得到ReferenceError: document is not defined。所以方案一适合做后端逻辑、数据处理、接口调用,不适合做交互展示。这一条非常重要,我在第五节会对比得更清楚。

3. 方案二:Python 内核里直接调前端 JS(display 方式)

3.1 原理:display(Javascript) 是怎么工作的

方案二的核心是把 JS 代码当作一个带类型的输出对象,通过 Jupyter 的前端通道发送到浏览器并执行。代码并不在 Python 进程里运行,而是在你打开 notebook 的那个浏览器页面的 JavaScript 环境里运行。因此它可以访问 DOM、window、document 这些浏览器 API,真正意义上做到了“前端执行”。

代码示例:

from IPython.display import display, Javascript display(Javascript('alert("Hello from JS");'))

就这么一行,浏览器会立刻弹出一个 alert 对话框。这个细节说明了一个重要事实:我们直接写的是 JS 而非 Python,一切行为都是浏览器行为。

3.2 实战:用 JS 在 notebook 里画一个可交互的 canvas

我们用一个更贴近实际的例子。在 notebook 里生成一个 canvas,放一个按钮,点击按钮就在画布上随机位置画一个圆:

from IPython.display import display, Javascript js_code = """ const div = document.createElement('div'); div.innerHTML = `<canvas id="myCanvas" width="300" height="200" style="border:1px solid #ccc;"></canvas> <button id="drawBtn" style="margin-left:10px;">画圆</button>`; document.body.appendChild(div); const canvas = document.getElementById('myCanvas'); const ctx = canvas.getContext('2d'); document.getElementById('drawBtn').onclick = function() { ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.beginPath(); ctx.arc(Math.random() * 250 + 25, Math.random() * 150 + 25, 30, 0, 2 * Math.PI); ctx.fillStyle = `rgb(${Math.random() * 255 | 0}, ${Math.random() * 255 | 0}, ${Math.random() * 255 | 0})`; ctx.fill(); }; """ display(Javascript(js_code))

执行这个 cell,你会在 notebook 页面上直接看到按钮和画布,点击试一下就能随机画圆。这种画法最大的优势是:没有任何额外依赖,浏览器原生 canvas API 就能搞定。如果你对绘图库比较熟,也可以在 JS 里引入 ECharts、D3 等库的 CDN,效果自然更强。

3.3 Python 变量传给 JS 的几种姿势

这是方案二里大家问得最频繁的问题。很多人把 Python 算好的结果塞进 JS,发现要么显示 undefined,要么直接语法报错。核心思路是:让 Python 把数据结构序列化成 JS 能直接识别的字面量,然后拼进代码字符串。

最简单的做法是字符串格式化:

data = [100, 200, 150, 300] js_code = f""" const data = {data}; console.log(data); """ display(Javascript(js_code))

这里 Python 列表会被渲染成[100, 200, 150, 300],JS 直接就能用。但隐患也在这里:如果 data 是字符串类型,或者包含单引号、双引号,直接拼串会破坏 JS 语法。更稳妥的姿势是用json.dumps先做序列化:

import json data = {"name": "demo", "value": 3.14} js_code = "const data = " + json.dumps(data) + ";" display(Javascript(js_code))

json.dumps会把 Python 的None转成null,True转成true,字典变成对象字面量,生成的字符串是合法 JS 语法,安全系数高很多。

还有一种更绕但可靠的方式:用 HTML 标签把数据嵌进 notebook 页面,再由 JS 去读取。比如先把json.dumps的结果塞进一个隐藏的 script 标签,让数据变成全局变量:

from IPython.display import display, HTML, Javascript import json data = {"items": [1, 2, 3], "label": "示例"} payload = json.dumps(data) display(HTML(f""" <script> window.__data_from_python = {payload}; </script> """)) display(Javascript("console.log(window.__data_from_python.items);"))

这种方式的好处是数据不会出现在函数参数里,也没有语法转义问题。数据量小的时候用第一种、第二种都行,数据量大的时候建议让 JS 直接异步请求 API,而不是通过 Python 中转,因为大体积 JSON 塞进 notebook 输出会导致页面卡顿。

3.4 这种方式的使用边界

方案二最容易被忽略的是全局作用域问题。你通过 display(Javascript) 执行的代码,都跑在同一个浏览器页面的全局作用域里。这意味着不同 cell 之间用var定义的变量可以共享,但也有覆盖同名变量的风险。我在一个 notebook 中定义过window.currentData,后来另一个 cell 中又用var currentData覆盖了它,排查了很久才发现是变量名冲突。

另外,display(Javascript) 是一次性执行。如果需要重复调用某个函数,最好把它挂到window上,这样后续 cell 可以随时调用:

js_code = """ window.renderChart = function(config) { document.getElementById('chartArea').innerHTML = 'chart: ' + config.title; }; """ display(Javascript(js_code))

之后随便哪个 cell:

display(Javascript("window.renderChart({title: 'hello'});"))

这个模式很适合做“先定义、后调用”的流程。如果有较长的 JS 代码,我建议先在一个独立的.js文件里写好并测好,再用 Python 读取文件内容来执行:

with open('my_script.js', 'r', encoding='utf-8') as f: js_code = f.read() display(Javascript(js_code))

这样能利用编辑器或 IDE 的语法提示,调试效率高很多。别在 notebook cell 里写几百行 JS,报错定位会非常痛苦。

4. 方案三:Markdown 裸奔大法(HTML 混排)

4.1 快速上手:一个可点击的组件

把任意 cell 切换成 Markdown,然后直接写 HTML 和 script:

<div style="padding:20px;background:#f5f5f5;border-radius:8px;"> <h3>JS Demo</h3> <button id="demoBtn">点我</button> <p id="demoText">还没点击</p> </div> <script> document.getElementById('demoBtn').onclick = function() { document.getElementById('demoText').innerText = '当前时间:' + new Date().toLocaleString(); }; </script>

执行该 Markdown cell 后,Jupyter 会把它当作 HTML 片段渲染在笔记本中,script 标签里的代码也会随渲染执行。从效果上看,这和方案二很接近,但没有 Python 参与,纯粹是前端展示。

4.2 有哪些坑和限制

这个方案最大的价值是简单,但也带来了三个限制。

第一,JupyterLab 对 Markdown 里 script 的执行策略比经典 Notebook 严格。我在 JupyterLab 中试过用fetch请求外部接口,有些环境会被安全策略拦截,而经典 Notebook 反而能跑。所以如果你在 JupyterLab 里发现脚本不生效,切回经典 Notebook 试试是一个有效的排除手段。

第二,脚本执行时机不可控。notebook 打开时,Markdown cell 会自动渲染。如果脚本在 DOM 结构准备好之前执行,按钮的事件绑定会失败,点击没有反应。我遇到这种情况时,通常用setTimeout或window.onload延迟:

<script> window.onload = function() { document.getElementById('demoBtn').onclick = function() { document.getElementById('demoText').innerText = '延迟绑定成功'; }; }; </script>

第三,它基本无法与 Python 交互。如果你希望 Python 算完数据后更新这个组件,方案三帮不上忙,需要回到方案二。

我实际使用方案三的场景多是文档类展示,比如在教程里嵌一个计算器小工具,让读者自己点按钮看效果,不依赖后端数据。这种场景方案三非常清爽。

5. 三种方式对比与选型建议

5.1 关键维度对比

对比项方案一:JS 内核方案二:display(Javascript)方案三:Markdown+HTML
是否需要换内核需要不需要不需要
代码运行环境Node.js 后端浏览器前端浏览器前端
能否访问浏览器 DOM不能能能
能否和 Python 变量交互不能直接可以(拼接/JSON)不能直接
是否依赖 npm 包需要不需要不需要
适合场景整本都是 Node 脚本Python 算数据 + JS 做展示纯前端小组件展示
数据量支持较大,Node 内存小到中,大数据会卡小,静态展示为主

5.2 我的选型习惯

我绝大多数时候选方案二。原因很简单:数据分析和清洗还是 Python 最顺手,方案二可以让我在一个 cell 里算好结果,紧接着用 JS 做定制化可视化,这是最舒服的协作方式。方案一更适合团队里有人已经习惯了 Node,想要一个带笔记功能的 Node 环境来测模块、调接口。方案三则给我做无交互类组件时使用。

有一点要特别提醒:不要在一个 notebook 里同时混用方案一和方案二。如果既有 Python 又有 Node 内核,两个会话是完全隔离的,变量不能互访。我在一个项目里曾想当然地以为能直接调用另一个内核的变量,结果花了不少时间排查才发现它们根本不在同一个进程里。所以,选定一种主要内核,然后围绕它组织代码,否则维护成本会直线上升。

选型后的落地建议有三条:

一是使用方案二时,把需要跨 cell 复用的函数挂到window上,避免使用裸全局变量。这个习惯能帮你避开很多命名冲突问题。

二是不要在一个 cell 里塞大量 JS。可以将逻辑拆成几个小 cell,先定义后调用,这样报错时能快速定位。

三是涉及外部库时,优先用浏览器加载 CDN 的方式,而不是把库源码塞进 notebook。比如:

display(Javascript(""" if (!window.echarts) { const script = document.createElement('script'); script.src = 'https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js'; document.head.appendChild(script); } """))

这样执行完还能用window.echarts访问库的资源,比手动粘贴源码干净太多。

6. 常见问题与排查实录

6.1 内核装不上 / Jupyter 不识别

现象:ijsinstall没报错,但启动 Jupyter 后新建 notebook 看不到 JavaScript 内核。

排查步骤如下:

  1. 确认 Node 版本:node -v,低于 12 就先升级。
  2. 执行jupyter kernelspec list,看有没有 JavaScript 条目。
  3. 如果没有,多半是 kernelspec 被装到了一个不归当前 Jupyter 搜索的目录。这种情况常见于系统存在多个 Python 环境,比如 conda 环境和系统 Python 混用。解决办法是找到 kernelspec 文件所在目录,手动注册到当前环境对应的目录。

还有一次我在 Windows 上遇到EPERM权限错误,用管理员身份重新执行安装就好了。这类问题基本是环境路径和权限问题,和代码本身无关。

6.2 JS 代码不执行 / 黑屏无反应

方案二里最常见的原因就是 JS 字符串拼接出错。比如引号没闭合、括号不匹配,浏览器直接解析失败。建议你打开浏览器开发者工具,看 Console 面板有没有红色报错。如果是Uncaught SyntaxError,检查一下传给 display 的字符串里是不是混进了 Python 的None或True,这些会被原样输出成非 JS 标准语法。

另一个容易踩的点是:你写了一个很大的 JS 字符串,像几兆的数据,浏览器可能直接卡住。这时候把数据量缩小,或者改为异步接口请求,能立刻看到改观。

如果使用的是 JupyterLab,且脚本不执行,尝试切到经典 Notebook。我在 JupyterLab 升级到比较高版本后,遇到过多次脚本被安全策略拦截的情况,经典 Notebook 里能正常跑,这种兼容性问题只能靠切换环境来绕。

6.3 变量作用域与状态丢失

方案二里变量在浏览器前端,刷新页面后变量全部清空。每次重新打开 notebook,之前定义的window函数和变量都必须重新执行一遍。所以如果一个 notebook 的结构是先定义后使用,一定要按顺序执行所有 cell,不要跳着执行。

方案一里变量在 Node 进程里持久保存,cell 之间的var、let、const会保留状态,这对长时间运行的任务比较友好。

另外,方案二如果业务代码里用了let或const定义全局变量,这些变量有块级作用域,每个 cell 执行时有可能无法跨 cell 访问。如果你发现明明赋值了,但另一个 cell 里拿不到,改成var或者挂载到window即可。

6.4 中文乱码与编码问题

这个问题大多数出现在读取 JS 文件环节。Windows 上如果open()没指定 utf-8,默认编码可能是 gbk,读出来的中文注释或字符串会变乱码。正确写法:

with open('my_script.js', 'r', encoding='utf-8') as f: js_code = f.read()

传参时如果是中文字符串,尽量先用json.dumps包裹一层,不要直接拼进 JS,否则很容易被转义成\uXXXX或出现引号错误。

6.5 快速调试的几个实用习惯

我现在的调试流程是这样的:先用 Python 打印出 JS 代码字符串,确认拼接后的代码长什么样。然后复制到浏览器 Console 里手动执行,这样能看到详细的报错堆栈。定位问题后再放回 notebook。

如果是方案二里的 JS 有逻辑错误但没报错,通常可以在代码里写console.log输出关键变量,再到浏览器开发者工具的 Console 面板查看文章日志。还有一种快捷方式,如果你只是想验证一个 JS 片段是否可行,直接在浏览器地址栏按 F12,打开 Console 粘贴执行,效率会比在 notebook 里来回试更快。

6.6 一个小技巧:在 notebook 里直接用 fetch 调接口

最后分享一个我非常喜欢用的技巧。方案二里浏览器环境自带fetch,所以你可以用 JS 直接请求后端 API,拿到 JSON 数据后渲染到 notebook 页面。举个例子:

display(Javascript(""" fetch('https://api.example.com/data') .then(res => res.json()) .then(data => { const pre = document.createElement('pre'); pre.textContent = JSON.stringify(data, null, 2); document.body.appendChild(pre); }); """))

这个方式让不依赖 Python 的数据拉取和展示变得特别轻。我做原型看板时,经常在一个 Jupyter notebook 里同时完成 API 调试、数据格式分析和前端效果预览,全程不用启动任何额外服务。工具没有绝对的好坏,能在一个地方把活干完,对我来说就是好工具。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询