如何将手写识别Shiny应用模块化?排查模块实现问题
问题描述
我有一个基于Google Input Tools的Shiny应用,支持在Canvas上手写并转换为文本输入。现在想要把这个功能模块化,实现任意数量的画布实例。我已经掌握R端的模块化方法,但不知道怎么处理JavaScript中的命名空间问题(比如Shiny.setInputValue里的recognized_text需要加入命名空间)。模块化后出现两个问题:
- 只有第一个画布能正常书写
- 第一个模块的发送按钮无法把识别出的文本显示到文本框里
需要排查问题并给出正确的实现方案。
问题代码(模块化尝试版本)
模块UI与Server
library(shiny) # 模块UI handwritingModuleUI <- function(id) { ns <- NS(id) tagList( tags$script(src = "https://www.google.com/jsapi"), tags$script(src = "https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"), tags$script(HTML(' google.load("elements", "1", {packages: "transliteration"}); var transliterationControl; function onLoad() { var options = { sourceLanguage: google.elements.transliteration.LanguageCode.ENGLISH, destinationLanguage: [google.elements.transliteration.LanguageCode.URDU], shortcutKey: "ctrl+g", transliterationEnabled: true }; transliterationControl = new google.elements.transliteration.TransliterationControl(options); transliterationControl.makeTransliteratable(["handwriting_input"]); var canvas = document.getElementById("handwriting_canvas"); var ctx = canvas.getContext("2d"); var isDrawing = false; var lastX = 0; var lastY = 0; canvas.addEventListener("mousedown", startDrawing); canvas.addEventListener("mousemove", draw); canvas.addEventListener("mouseup", stopDrawing); canvas.addEventListener("mouseout", stopDrawing); function startDrawing(e) { isDrawing = true; [lastX, lastY] = [e.offsetX, e.offsetY]; } function draw(e) { if (!isDrawing) return; ctx.beginPath(); ctx.moveTo(lastX, lastY); ctx.lineTo(e.offsetX, e.offsetY); ctx.strokeStyle = "black"; ctx.lineWidth = 2; ctx.stroke(); [lastX, lastY] = [e.offsetX, e.offsetY]; } function stopDrawing() { isDrawing = false; } $("#recognize_btn").click(function() { var canvasData = canvas.toDataURL("image/png"); var currentText = $("#handwriting_input").val(); var recognizedText = currentText + "手写内容"; Shiny.setInputValue("recognized_text", recognizedText); }); $("#clear_btn").click(function() { ctx.clearRect(0, 0, canvas.width, canvas.height); }); } google.setOnLoadCallback(onLoad); ')), div( id = ns("handwriting_container"), canvas(id = ns("handwriting_canvas"), width = "300", height = "200", style = "border:1px solid black;"), actionButton(ns("clear_btn"), "清空画布"), actionButton(ns("recognize_btn"), "识别并发送"), textInput(ns("handwriting_input"), "输入框") ) ) } # 模块Server handwritingModuleServer <- function(id) { moduleServer(id, function(input, output, session) { observeEvent(input$recognized_text, { updateTextInput(session, "handwriting_input", value = input$recognized_text) }) }) } # 主应用 ui <- fluidPage( handwritingModuleUI("module1"), br(), handwritingModuleUI("module2") ) server <- function(input, output, session) { handwritingModuleServer("module1") handwritingModuleServer("module2") } shinyApp(ui, server)
问题原因分析
- 重复加载JS资源:模块UI中重复引入Google Input Tools和jQuery,多次加载会导致全局变量冲突。
- 元素ID未命名空间化:JS中使用固定ID(如
handwriting_canvas),但模块生成的实际ID是带命名空间的(如module1-handwriting_canvas),导致JS无法找到后续模块的元素。 - 输入值无命名空间:
Shiny.setInputValue("recognized_text", ...)直接写入全局输入,模块Server无法区分不同实例的输入。 - 全局变量覆盖:
transliterationControl是全局变量,多个模块会互相覆盖,导致只有最后一个模块的转写控件生效。
正确模块化实现方案
完整代码
library(shiny) # 模块UI handwritingModuleUI <- function(id) { ns <- NS(id) tagList( div( id = ns("handwriting_container"), canvas(id = ns("handwriting_canvas"), width = "300", height = "200", style = "border:1px solid black;"), actionButton(ns("clear_btn"), "清空画布"), actionButton(ns("recognize_btn"), "识别并发送"), textInput(ns("handwriting_input"), "输入框") ), # 传递命名空间给JS初始化函数 tags$script(sprintf('initHandwritingModule("%s");', ns(""))) ) } # 模块Server handwritingModuleServer <- function(id) { moduleServer(id, function(input, output, session) { # 监听当前模块命名空间下的recognized_text输入 observeEvent(input$recognized_text, { updateTextInput(session, "handwriting_input", value = input$recognized_text) }) }) } # 主应用UI ui <- fluidPage( # 统一加载JS资源,避免重复加载 tags$script(src = "https://www.google.com/jsapi"), tags$script(src = "https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"), # 封装通用JS逻辑,支持命名空间 tags$script(HTML(' google.load("elements", "1", {packages: "transliteration"}); // 存储每个模块的转写控件实例,避免全局冲突 var transliterationControls = {}; function initHandwritingModule(namespace) { // 拼接命名空间前缀 var nsPrefix = namespace ? namespace + "-" : ""; // 初始化转写控件 var inputId = nsPrefix + "handwriting_input"; var options = { sourceLanguage: google.elements.transliteration.LanguageCode.ENGLISH, destinationLanguage: [google.elements.transliteration.LanguageCode.URDU], shortcutKey: "ctrl+g", transliterationEnabled: true }; transliterationControls[namespace] = new google.elements.transliteration.TransliterationControl(options); transliterationControls[namespace].makeTransliteratable([inputId]); // 获取当前模块的画布元素 var canvas = document.getElementById(nsPrefix + "handwriting_canvas"); var ctx = canvas.getContext("2d"); var isDrawing = false; var lastX = 0; var lastY = 0; // 绑定画布手写事件 canvas.addEventListener("mousedown", startDrawing); canvas.addEventListener("mousemove", draw); canvas.addEventListener("mouseup", stopDrawing); canvas.addEventListener("mouseout", stopDrawing); function startDrawing(e) { isDrawing = true; [lastX, lastY] = [e.offsetX, e.offsetY]; } function draw(e) { if (!isDrawing) return; ctx.beginPath(); ctx.moveTo(lastX, lastY); ctx.lineTo(e.offsetX, e.offsetY); ctx.strokeStyle = "black"; ctx.lineWidth = 2; ctx.stroke(); [lastX, lastY] = [e.offsetX, e.offsetY]; } function stopDrawing() { isDrawing = false; } // 绑定识别按钮事件,发送带命名空间的输入值 $("#" + nsPrefix + "recognize_btn").click(function() { var currentText = $("#" + inputId).val(); var recognizedText = currentText + "手写内容"; // 给输入值添加命名空间 Shiny.setInputValue(namespace + "recognized_text", recognizedText); }); // 绑定清空画布按钮事件 $("#" + nsPrefix + "clear_btn").click(function() { ctx.clearRect(0, 0, canvas.width, canvas.height); }); } google.setOnLoadCallback(function() { // 页面加载完成后自动初始化已渲染的模块 }); ')), # 加载两个模块实例 handwritingModuleUI("module1"), br(), handwritingModuleUI("module2") ) # 主应用Server server <- function(input, output, session) { handwritingModuleServer("module1") handwritingModuleServer("module2") } shinyApp(ui, server)
关键改进说明
- 统一JS资源加载:将Google Input Tools和jQuery移到主UI,仅加载一次,避免冲突。
- JS逻辑封装为可复用函数:
initHandwritingModule接收命名空间参数,为每个模块独立初始化控件和事件。 - 元素ID命名空间化:通过
nsPrefix拼接带命名空间的元素ID,确保JS能精准定位对应模块的元素。 - 输入值命名空间化:
Shiny.setInputValue(namespace + "recognized_text", ...)确保每个模块的识别结果发送到独立的命名空间输入,模块Server自动映射对应输入。 - 控件实例隔离:用
transliterationControls对象存储每个模块的转写控件,避免全局变量覆盖。
内容的提问来源于stack exchange,提问作者umair durrani
相关产品推荐
相关产品推荐

