Jupyter Notebook运行JavaScript完全指南:魔法命令、require加载与前后端通信

发布时间:2026/10/18 5:05:32
Jupyter Notebook运行JavaScript完全指南:魔法命令、require加载与前后端通信
还在为Jupyter Notebook只能跑Python而纠结其实它原生支持在浏览器前端执行JavaScript通过几个魔法命令就能直接在单元格里跑JS代码还能和Python内核双向通信。这篇文章会从基础用法讲起手把手拆解怎么用%%javascript、怎么通过require加载外部JS库再到怎么自定义扩展实现前后端数据互通。很多人在这一步踩过坑比如输出不显示、函数名冲突、require异步加载失败我都会逐一说明排查方法。文末还会分享一些我实际用过的组合玩法比如用JS做数据可视化、动态修改Notebook界面希望能给你一些参考。1. 项目整体设计与核心思路拆解1.1 在Jupyter Notebook中运行JavaScript的几种方式Jupyter Notebook本身是浏览器里的一个交互式环境服务端跑内核通常是Python/IPython前端渲染成一个HTML页面。这个架构决定了它天然支持在浏览器端执行JavaScript只是很多人没注意到这一点。实际开发中在Notebook里运行JavaScript通常有四种方式第一使用%%javascript魔法命令。这是IPython内置的直接在单元格里写JS代码代码会被发送到浏览器前端在页面上下文中执行。优点是简单直接适合快速测试交互功能缺点是执行完的结果不会自动显示在输出区需要手动操作DOM才能看到效果。第二使用%%js或%%javascript配合element参数。这种方式其实是%%javascript魔法命令的变体在JS代码中可以通过element变量拿到当前单元格对应的DOM容器然后往里面注入HTML内容。这样就能把JS执行结果可视化地展示在单元格下方相当于打通了“执行代码”和“显示输出”的链路。第三使用%%html魔法命令嵌入自定义HTML和JavaScript脚本。这种方式适合做富交互组件比如表单、按钮、图表容器JS代码通过script标签嵌入在渲染时由浏览器自动执行。实际操作中我经常用它来搭一个可交互的控制面板。第四通过自定义Jupyter扩展Custom JavaScript加载外部库。在Notebook的自定义JS文件通常是custom.js里配置requirejs路径就能在单元格中通过require异步加载JS库比如D3.js、Plotly.js、MathJax等等。这种方式虽然配置稍繁琐但能极大地扩展Notebook的前端能力。1.2 为什么选JavaScript而不是一直用Python这个问题我做过很多次分享核心原因是Jupyter Notebook的前端本质上是一个Web页面页面上的一切视觉交互、DOM操作、动画、图表渲染都依赖JavaScript。Python在内核里负责计算和数据处理但要把结果“画”在浏览器页面上最终还是要由前端JS来执行。举例来说你想在Notebook里嵌入一个可拖拽的滑块、一个动态更新的图表或者一个需要实时响应用户操作的测验控件用Python往输出区渲染HTML是“一次性”的无法与页面上的元素持续交互。但用JavaScript直接操作DOM就能实现真正的动态效果。另外JavaScript生态里有一批优秀的浏览器端库比如D3.js做数据可视化、Three.js做3D渲染、CodeMirror做代码编辑器这些库天生运行在浏览器里如果非要绕到Python内核去调用性能和体验都会大打折扣。直接用JS在前端运行能利用浏览器本身的能力减少不必要的通信开销。还有一个很实际的场景Jupyter Notebook的界面本身包括菜单、按钮、快捷键就是HTMLJS构建的。如果你想改造界面比如添加一个自定义工具栏按钮或者自动展开所有输出这必须得用JavaScript去操作。Python端是碰不到页面DOM的。1.3 适用人群与典型场景这个项目最适合以下几类人一是在Notebook里做数据分析但觉得静态图表不够用的人。用JS可以把静态图表改造成可缩放、可筛选、可联动交互的视图分析体验会提升一个档次。二是想开发内部工具或教学演示课件的人。在Notebook里嵌入可交互的HTML/JS组件通过代码和可视化结合的方式来讲解算法学生可以直接在页面上操作参数实时看到输出变化。三是对Jupyter扩展开发感兴趣的人。理解了JavaScript在Notebook中的加载和执行机制后再去写自定义扩展、部署小工具就顺畅很多因为所有前端逻辑都是JS在支撑。当然也要说明这个方案的目标是“前端交互”不是拿Jupyter Notebook去替代完整的Web开发环境。涉及复杂的前端工程化打包、路由、状态管理建议还是用常规的前端框架。Notebook里的JS更适合轻量级、即写即用、随算随看的场景。2. 核心细节解析与实操要点2.1 %%javascript魔法命令的用法与底层逻辑先看最基础的%%javascript魔法命令。在单元格顶部输入这一行下面的代码就会以JavaScript模式发送到浏览器前端执行。%%javascript console.log(Hello from JavaScript!);执行这个单元格时你会看到控制台打印出Hello from JavaScript!。但注意Notebook单元格的输出区域不会显示这个结果因为console.log的输出走的是浏览器开发者工具的控制台而不是Notebook的输出缓冲区。那如果需要把结果显示在单元格下方呢这就要用到element对象了。在%%javascript魔法命令中IPython会自动传入一个element参数它代表当前单元格绑定的DOM元素。你可以直接往这个元素里注入HTML或文本%%javascript var div element.get(0); div.innerHTML h3这是JS执行的结果/h3;执行后你会看到单元格下方出现一行带标题的HTML内容。这里有一个很关键的细节element是jQuery对象所以要用element.get(0)取出原生的DOM节点或者直接element.html(...)设置内容。有一点容易踩坑%%javascript里的JavaScript是在页面全局作用域执行的你在一个单元格里定义的变量或函数会在Notebook的整个生命周期内保留。这意味着你在A单元格定义了window.myVar 42B单元格的JS代码里可以直接访问。好处是跨单元格共享状态坏处是如果不小心覆盖了已有全局变量可能会引发奇怪的问题。2.2 通过%%html嵌入JS组件的完整要点除了%%javascript更灵活的方式是用%%html魔法命令直接在单元格里写HTML和JS浏览器会完整渲染这个HTML片段。%%html div idcontainer p点击按钮查看效果/p /div script document.getElementById(container).onclick function() { alert(Hello from HTMLJS!); }; /script这个方式更大的价值在于可以配合CSS做样式控制组合出可交互的面板。我有段时间做算法演示就是在Notebook里用%%html搭了一个控制台有滑块、输入框、按钮点击按钮调用已定义好的JS函数实时更新实验结果。实际操作中嵌入的HTML内容会被放入当前单元格的输出区域。如果要对组件做样式定制可以把CSS写在style标签里Jupyter页面的全局样式不会和你的局部样式冲突太多但注意不要滥用全局选择器最好给容器设置独立ID或Class避免样式穿透。这里有个细节%%html里嵌的script默认会在HTML渲染完成后同步执行。如果你需要等待外部资源加载完毕再执行JS逻辑就要用window.onload或者document.addEventListener(DOMContentLoaded, ...)来监听页面事件。但要注意Notebook页面本身已经加载完了这个事件可能不会再次触发所以更稳妥的做法是直接在script里写代码或者使用setTimeout做延迟。2.3 前端与后端的通信基础Jupyter Notebook提供了Jupyter.notebook.kernel接口JavaScript可以通过这个接口向Python内核发送代码或者执行一些内核操作。最基本的用法如下%%javascript var kernel Jupyter.notebook.kernel; kernel.execute(print(这条消息来自JavaScript));执行后会触发Python内核执行print(这条消息来自JavaScript)结果会输出到对应单元格的输出区。这个能力很实用比如你在前端做了参数调整需要后端重新计算就可以通过kernel.execute把参数传回去。如果要传递更复杂的对象可以把对象序列化成JSON字符串再用Python的json.loads解析%%javascript var data {name: 测试, value: 42}; var jsonStr JSON.stringify(data); kernel.execute(import json; data json.loads( jsonStr ));不过这种字符串拼接方式比较脆弱如果字符串里包含引号或反斜杠很容易出错。更稳妥的做法是在Python端定义一个接收参数的函数然后通过kernel.execute调用它%%javascript kernel.execute(receive_data(name测试, value42));这里需要先保证Python端定义了这个函数。我在实操中一般会先跑一个单元格定义好接收函数再让前端来调用。需要提醒的是kernel.execute的调用是异步的。你连续调用多次kernel.execute执行结果不一定按调用顺序返回。如果后续操作依赖前一个结果需要在回调中处理或者用Promise封装。2.4 通过require加载外部JS库的机制JupyterNotebook页面上默认启用了requirejs这是一个AMD模块加载器可以动态加载JavaScript库。在Notebook环境中可以直接用require函数加载外部库%%javascript require([https://cdn.jsdelivr.net/npm/chart.js4.4.0/dist/chart.umd.min.js], function(Chart) { var canvas document.createElement(canvas); element.append(canvas); new Chart(canvas.getContext(2d), { type: bar, data: { labels: [A, B, C], datasets: [{ data: [1, 2, 3] }] } }); });require的第一个参数是模块路径数组第二个参数是加载完成后的回调函数。库加载完成后会作为回调函数的参数传入。这里需要注意require是异步的所以必须把依赖该库的代码都放进回调里执行不能在外面直接使用。另一种方式是使用define定义模块。如果你加载的库本身是AMD模块比如jQuery、D3.jsrequire会自动检测并正确解析如果库不是AMD模块比如一些老的全局脚本库require依然可以加载但可能取不到返回值此时需要访问全局对象上被加载的变量。我在实际项目中经常用一个本地镜像服务器来存放JS库文件这样加载速度会明显提升也避免了在演示现场因外网问题导致加载失败的情况。如果你在离线环境使用Notebook这个方案尤其值得采用。3. 实操过程与核心环节实现3.1 环境准备与基础验证在动手之前先确认你的环境满足基本要求安装了Jupyter Notebook或JupyterLab内核是IPython默认的Python 3内核浏览器使用Chrome、Firefox或Edge均可打开Notebook新建一个单元格先验证最基础的%%javascript是否可用%%javascript console.log(测试JavaScript环境);打开浏览器开发者工具F12切到Console面板。如果能看到测试JavaScript环境说明JS环境正常可以继续后续操作。接着验证element对象%%javascript element.append(p stylecolor:greenJS执行成功/p);执行后单元格下方出现绿色文字说明DOM渲染链路正常。3.2 案例一通过HTML控制面板实现参数调节我经常用这一个套路做交互演示用%%html搭一个带滑动条的面板前端JS把滑块值传给Python内核Python计算后再把结果返回前端展示。先运行一个Python单元格定义接收和计算函数def compute_value(x): y x ** 2 2 * x 1 print(f输入 {x}计算结果 {y}) return y再用%%html写前端面板%%html style #panel-container { font-family: sans-serif; padding: 12px; border: 1px solid #ccc; border-radius: 6px; width: 280px; } /style div idpanel-container label参数值: span idvalue-display5/span/label input typerange idparam-slider min0 max10 value5 stylewidth: 100%; button idrun-btn运行计算/button p idresult-text等待计算.../p /div script var slider document.getElementById(param-slider); var valueDisplay document.getElementById(value-display); var resultText document.getElementById(result-text); var runBtn document.getElementById(run-btn); slider.addEventListener(input, function() { valueDisplay.textContent slider.value; }); runBtn.addEventListener(click, function() { var value slider.value; var kernel Jupyter.notebook.kernel; var command compute_value( value ); kernel.execute(command); resultText.textContent 已提交参数 value 请查看内核输出; }); /script执行这个单元格后你会看到一个小面板。拖动滑块点击按钮再看下方Python单元格输出的结果。这里有几个操作要点变量Jupyter是Notebook全局对象我在代码里直接用Jupyter.notebook.kernel获取内核引用。如果你在使用JupyterLab或者其他前端环境接口名称可能不同需要注意适配。前端事件绑定要放在script标签内并且确保DOM已经渲染完成。在%%html输出中脚本执行时容器元素已经存在所以不需要额外监听load事件。用kernel.execute执行Python代码时命令字符串里要注意引号转义。我这里是数字所以直接拼接没有风险如果传递字符串建议用JSON.stringify封装。3.3 案例二用JavaScript绘制动态图表接着演示一个偏可视化的场景用Chart.js画动态更新的图表。先通过%%javascript配合require加载Chart.js然后生成一个可以随数据变化的柱状图。%%javascript require([https://cdn.jsdelivr.net/npm/chart.js4.4.0/dist/chart.umd.min.js], function(Chart) { // 创建容器 var container $(div stylewidth:600px;height:400px;/div); element.append(container); var canvas document.createElement(canvas); container.append(canvas); var chart new Chart(canvas.getContext(2d), { type: bar, data: { labels: [一月, 二月, 三月], datasets: [{ label: 销售额, data: [120, 190, 150], backgroundColor: [#ff6384, #36a2eb, #ffce56] }] }, options: { responsive: true, maintainAspectRatio: false } }); // 模拟动态更新 var counter 0; var intervalId setInterval(function() { counter; chart.data.datasets[0].data [ Math.floor(Math.random() * 200) 50, Math.floor(Math.random() * 200) 50, Math.floor(Math.random() * 200) 50 ]; chart.update(); if (counter 10) { clearInterval(intervalId); } }, 1000); });执行后你会看到一个动态变化的柱状图——每秒钟数据随机更新一次柱子高度变化。这里有几个值得注意的细节用$(div.../div)创建DOM元素是因为element本身是jQuery对象jQuery的$函数在Notebook环境中通常是全局可用的用起来利落。Chart.js实例化时需要确保canvas已经在页面文档流中。先创建容器追加到element再在容器里创建canvas这样实例化时能正确获取尺寸。我在setInterval里10次后清除定时器防止页面持续运行消耗资源。如果你在实验时忘记清除切换页面或关闭Notebook前可能一直有JS在循环这是个容易忽略的小坑。Chart.js CDN地址要选对。v4版本的UMD包名是chart.umd.min.js如果你引用了错误路径require回调里的Chart会是undefined后续调用会直接报错。遇到这种情况先检查浏览器Network面板看脚本是否加载成功。3.4 案例三直接操作Notebook界面DOM说到实际操作层面的乐趣用JS修改Notebook界面非常好玩。比如自动展开所有代码单元格的输出或者为标题添加动态样式。一个比较有用的脚本全文深色模式切换。%%javascript if (document.body.classList.contains(dark-mode)) { document.body.classList.remove(dark-mode); } else { document.body.classList.add(dark-mode); }这个脚本本身没有效果因为Notebook的CSS里并没有定义dark-mode类。你需要先定义一个样式%%html style .dark-mode { background-color: #1e1e1e !important; color: #d4d4d4 !important; } .dark-mode .CodeMirror { background-color: #252526 !important; } /style实际执行时先跑样式代码再跑切换脚本页面主体背景和代码编辑器颜色会发生变化。这个美化应用虽小却能帮你理解一件事Notebook界面本身是DOM结构你可以通过JS任意操作、修改样式和内容。另一个常用的界面操作是获取当前所有单元格的文本内容汇总统计。%%javascript var cells Jupyter.notebook.get_cells(); var pythonCount 0; var markdownCount 0; var jsCount 0; cells.forEach(function(cell) { if (cell.cell_type code) { var code cell.get_text(); if (code.indexOf(%%javascript) 0) { jsCount; } else { pythonCount; } } else if (cell.cell_type markdown) { markdownCount; } }); alert(代码单元格: pythonCount 个, Markdown单元格: markdownCount 个, JS单元格: jsCount 个);这个脚本展示了一个核心能力在Notebook中不仅可以用JS操作页面还可以读取Notebook的文档结构实现对Notebook自身的元信息分析。这为写一些自动化工具比如统计单元格数量、检查未执行代码、批量导出内容打开了入口。3.5 自定义Jupyter扩展的搭建示例如果想跳出单元格的限制给整个Notebook加一个自定义工具栏按钮可以尝试写一个扩展。扩展的核心机制是在Jupyter前端启动时加载一个自定义JS文件在里面注册UI元素和事件。先找到Jupyter的配置目录在custom目录下创建custom.js。具体路径根据你的Jupyter版本稍有不同常见的是~/.jupyter/custom/custom.js在custom.js里写define([ base/js/namespace, base/js/events ], function(Jupyter, events) { function add_toolbar_button() { if (document.querySelector(#my-custom-button)) { return; } var button document.createElement(button); button.id my-custom-button; button.textContent 我的按钮; button.style.marginLeft 8px; button.addEventListener(click, function() { require([base/js/namespace], function(Jupyter) { var cells Jupyter.notebook.get_cells(); var executed 0; cells.forEach(function(cell) { if (cell.cell_type code cell.get_text().indexOf(%%javascript) ! 0) { cell.execute(); executed; } }); console.log(已执行 executed 个代码单元格); }); }); var toolbar document.querySelector(#maintoolbar-container); if (toolbar) { toolbar.appendChild(button); } } events.on(app_initialized.NotebookApp, function() { setTimeout(add_toolbar_button, 500); }); });这个扩展的作用是在工具栏添加一个“我的按钮”点击后逐一执行所有Python代码单元格。这里用到了app_initialized.NotebookApp事件确保Notebook应用初始化完成后再添加按钮。搭好之后重启Notebook服务再进入页面工具栏就会出现新按钮。需要补充的是Jupyter扩展开发用的是AMD模块和RequireJS和单元格里直接用%%javascript不太一样。写扩展时必须有define包装依赖模块通过回调参数注入。如果你对模块化开发不熟可以先从简单功能练起比如先加一个静态按钮再加上事件逻辑。如果你不想动配置文件还有一个轻量级替代方案用require在单元格中动态加载和注册扩展逻辑。不过这种方式只在当前页面有效刷新后需要重新执行。但作为实验和学习来说已经足够。3.6 通过comm实现真正的前后端双向通信前面用kernel.execute本质上是前端向内核发命令属于单向通道。如果内核需要主动向浏览器推送消息比如长时间计算完成后的通知、实时进度更新就需要用到comm通道。基础知识comm是Jupyter内核与前端之间的一对一通信通道可以双向发送消息。在Python端可以用ipykernel.comm.Comm创建通道在JS端通过Jupyter.notebook.kernel.comm_manager注册和管理通道。先看Python端一个简单的示例from ipykernel.comm import Comm def send_notification(message): comm Comm(target_namenotify, data{message: message}) comm.send({type: notification, content: message})实际使用时更常见的模式是预先注册comm目标然后等前端发起连接from ipykernel.comm import Comm from IPython import get_ipython def comm_target(comm, open_msg): # comm是通道对象open_msg是前端传来的打开消息 comm.on_msg(_handle_comm_msg) comm.send({type: ack, content: 连接成功}) get_ipython().kernel.comm_manager.register_target(notify, comm_target)前端JS侧建立连接%%javascript var kernel Jupyter.notebook.kernel; var comm kernel.comm_manager.new_comm(notify); comm.on_msg(function(msg) { var data msg.content.data; if (data.type notification) { alert(收到通知: data.content); } }); comm.open();这段代码将前端与Python端的comm目标建立连接。Python端执行comm.send时前端on_msg回调就会触发。这个通信方式的核心价值在于Python端可以主动向浏览器端推送数据而不需要前端轮询。这在做长时间训练进度展示、实时日志流、异步任务状态更新时非常有用。我尝试过一个实时的数据流监控面板Python端模拟生成实时数据通过comm推送前端用Chart.js实时绘制曲线完全不需要刷新页面效果相当的顺滑。唯一的遗憾是需要处理时机问题——comm连接建立之后Python端才能发送数据如果两端不同步就可能会丢消息。4. 常见问题与排查技巧实录4.1 执行JS后单元格没有输出这是最常见的状况。%%javascript默认不会向Notebook的输出区写入内容console.log也只在浏览器控制台可见。如果你看不到输出不是代码错了而是输出机制就是这样设计的。解决方法是如果需要显示输出用element对象追加内容或者用%%html。如果你只是想调试打开浏览器开发者工具看Console面板。另外检查一下你的输出区域是否被折叠了有些Notebook版本默认把输出折叠需要手动展开。4.2 require加载外部库时回调函数没有执行我在实际开发中遇到最多的问题就是require加载失败。原因通常是这几个一是路径写错。比如把chart.umd.min.js写成了Chart.min.js在CDN上有时候能访问到但库内部可能依赖其他模块导致初始化失败。解决方法是先在浏览器地址栏直接访问这个CDN路径看是否能正常返回文件内容。二是库不是AMD模块。如果库本身不支持AMD也就是没有define调用require加载后可能出现undefined is not a function错误。解决办法是在require回调里直接使用全局变量比如window.Chart或者用shim方式配置依赖。三是网络问题。CDN加载受网络环境影响某个区域访问某个CDN节点可能特别慢甚至超时。我在演示前都会先在Notebook里跑一次小测试确认CDN可访问再正式开始。如果不行就把库文件下载到本地用相对路径加载。4.3 JavaScript变量与函数跨单元格冲突在%%javascript中定义的全局变量会保留在页面级的window对象上。如果你在多个单元格里定义同名变量比如各个实验都用data这个变量后执行的单元格会覆盖前一个的值冲突就来了。换句话说如果你的多个单元格都声明了var data ...它们其实是同一个全局变量。一个单元格里的操作会影响另一个单元格的后续执行逻辑。更规范的做法是尽量用let或const在局部作用域中声明变量不要污染window对象。如果必须共享状态建议用一个命名空间对象来存放%%javascript window.myNamespace window.myNamespace || {}; window.myNamespace.counter 0; window.myNamespace.counter;4.4 执行kernel.execute时Python报错前端通过kernel.execute传给Python内核的代码如果包含特殊字符或引号很可能会在字符串拼接时出错。例如%%javascript kernel.execute(print(hello world));表面上看起来没问题的代码如果外层引号与内层引号冲突Python端会直接报SyntaxError。我把之前踩过的坑列成了一张速查表场景Python端命令拼接方式说明传数字kernel.execute(compute( num ))数字可直接拼接传字符串kernel.execute(print( str.replace(/\\/g, \\\\).replace(//g, \\) ))需处理反斜杠和引号转义传JSONkernel.execute(import json; datajson.loads( JSON.stringify(obj).replace(//g, \\) ))JSON中单引号需转义传对象引用kernel.execute(obj.method( JSON.stringify(arg) ))用JSON.stringify自动序列化最稳妥的方式还是在你本地定义的Python函数中接收字符串参数而不是直接在命令里拼字符串。4.5 自定义扩展不生效如果按上面写的custom.js添加按钮但没看到效果优先排查这几个点先是路径是否正确。不同操作系统、不同Jupyter版本custom目录的位置可能不同。你可以执行jupyter --config-dir查看配置目录然后在配置目录下创建custom/custom.js。再是语法错误。custom.js用的是AMD规范如果define的依赖或回调里写法有误会导致整个文件加载失败。在浏览器控制台看是否有报错信息。最后浏览器缓存是一个重要因素。修改custom.js后浏览器可能还在用旧的缓存。建议在Jupyter页面按CtrlShiftR强制刷新或者重启浏览器。5. 个人实操总结与扩展玩法5.1 从踩坑中积累的操作习惯经过不少项目的磨砺我逐渐形成一套稳定可行的操作习惯分享出来供你参考。在Notebook里写JS我一般会先用%%html做一个小的测试容器把核心交互逻辑跑通然后再决定是否迁移到custom.js做全局扩展。这样能隔离问题避免全局变量污染。当需要加载外部库时我更倾向于下载到本地而不是直接用CDN。把JS库放在Notebook工作目录的lib文件夹下然后用相对路径加载。这样做的核心好处是演示现场不受网络影响加载速度也明显更快。在写跨前后端通信时我会在Python端设置一个统一的入口函数所有参数先序列化为JSON字符串在函数内部用json.loads解析。这样做的好处是逻辑清晰避免在命令行里处理复杂的引号转义问题。5.2 组合玩法举例把前面几个技术点组合起来可以做出一些很有意思的应用场景。一个比较典型的例子是给Notebook做一个“富文本仪表盘”。先用%%html搭一个Dashboard布局引入Chart.js和D3.js再通过kernel.execute或者comm把Python计算的结果传给前端前端实时刷新图表形成一个完整的数据监控闭环。另一个思路是用JS实现Notebook的自动化操作。遍历所有单元格批量清理输出批量重命名或者自动检查哪些单元格还没执行过。这类工具我之前就是通过custom.js加一个按钮实现的。还有一个方向是做交互式教学。把讲解PPT化的HTML组件嵌入Notebook学生通过拖拽、点击等方式探索参数直接看到结果联动。这种用Notebook做富交互课件的方式比静态网页更有互动感也更贴近代码。5.3 后续可以继续深入的方向如果这篇内容对你有启发后续可以去研究几个相关的方向。一是JupyterLab扩展开发。JupyterLab是新一代界面它用PhosphorJS构建扩展机制和Notebook不完全一样但核心思维方式相通。学会在Notebook里写扩展再迁移到JupyterLab会平滑很多。二是ipywidgets的高级用法。这个库封装了前端widget模型允许用纯Python写出可交互控件不需要直接写JS。但理解它的底层运行时其实就是前端JS组件再去调试定制会更加顺手。三是前端可视化库的深度集成。除了Chart.jsD3.js、ECharts的Jupyter集成方式也各具特色利用通用的require机制实现按需加载是统一它们路径的通用方案。我在最开始接触这个领域的时候最深的体会是Jupyter Notebook远不止一个Python运行环境它同时是一个浏览器应用而浏览器应用的前端能力恰恰是很多数据科学工作流中比较容易被忽视的维度。搞懂了前端怎么运行、前后端怎么通信能够解锁不少实用的可能性。也算我完整探索这个方向的一条主线。