xcLeigh头像
关注
鸿蒙平台 RKWard IDE 适配实战:基于 Electron 壳方案的纯 JavaScript R 语言统计分析开发封面图

鸿蒙平台 RKWard IDE 适配实战:基于 Electron 壳方案的纯 JavaScript R 语言统计分析开发

RKWard 是 KDE 桌面环境下经典的 R 语言集成开发环境,提供脚本编辑、控制台交互、统计计算、数据可视化和工作空间管理等功能。本文记录将 RKWard IDE 的核心能力基于 Electron 壳方案适配到鸿蒙 PC 平台的完整流程,实现一个纯前端 JavaScript R 执行引擎,无需安装原生 R 或联网,在离线环境下即可完成统计分析、Canvas 绘图和工作空间管理。

欢迎加入开源鸿蒙 PC 社区:https://harmonypc.csdn.net/

欢迎在 PC 社区平台申请新建项目:https://atomgit.com/OpenHarmonyPCDeveloper

AtomGit 仓库地址:https://atomgit.com/OpenHarmonyPCDeveloper/ohos_RKWard

一、技术架构分析

1.1 功能定位

RKWard IDE 的核心功能是对 R 语言代码进行编辑、执行和可视化。与传统方案依赖 Rscript 命令行或 WebR(WebAssembly 方式运行 R)不同,本方案将 R 执行引擎完全用 JavaScript 实现,涵盖随机数生成、统计计算、表达式解析和 Canvas 绘图四大模块,在浏览器渲染进程中直接完成所有计算和图表渲染。

1.2 目标架构(鸿蒙 Electron)

  • 技术栈:Electron + HTML/CSS/JavaScript + 鸿蒙 web_engine 模块
  • 核心逻辑:纯前端 JavaScript 实现 R 执行引擎(JSREngine),渲染进程直接完成统计计算和绘图
  • 无外部依赖:不安装 R 语言环境,不启动 Rscript 子进程,不加载 WebR WASM 文件
  • 离线可用:所有计算和绘图在本地完成,无需网络连接

1.3 架构设计

层级职责技术实现
主进程窗口管理 + 文件操作 IPCElectron BrowserWindow + fs
渲染进程UI 交互 + 脚本管理原生 DOM + JavaScript
执行引擎R 代码解析与执行xorshift32 PRNG + 表达式求值
绘图引擎Canvas 图表渲染散点图/直方图/箱线图/折线图
样式层深色主题Catppuccin Mocha 配色方案

1.4 鸿蒙平台适配要点

鸿蒙 Electron 适配层存在三大兼容约束,必须在开发中严格遵守:

  • 禁止使用原生 prompt/confirm/alert 对话框,调用会导致 SubWindow 崩溃
  • 禁止使用原生 select 元素,调用会触发 SubWindow 崩溃
  • 禁止使用 setWindowOpenHandler 和 will-navigate API,调用会导致页面纯白

1.5 JSREngine 支持的 R 函数清单

本引擎实现了 R 语言核心子集,覆盖统计计算、数据生成和绘图三大类别:

类别函数功能说明
随机数set.seed, rnorm可复现的正态分布随机数(xorshift32 + Box-Muller)
数据生成c, seq, rep向量拼接、等差序列、重复向量
统计函数mean, sd, median, min, max基本描述性统计量
分位与频数quantile, table, cut分位数计算、频数统计、区间分组
线性回归lm(最小二乘系数)斜率和截距计算
控制台输出cat, print格式化输出到控制台
绘图函数plot, hist, boxplot, lines, abline, legendCanvas 图表渲染
布局控制par(mfrow)多图网格布局

二、环境准备

2.1 开发环境要求

  • 操作系统:Windows 10/11
  • 开发工具:DevEco Studio(鸿蒙官方 IDE)
  • HarmonyOS SDK:API 21+(5.0.5+)
  • Node.js:v20+

2.2 项目结构

ohos_hap/
├── electron-apps/
│   └── RKWard/                     # RKWard IDE 应用源码
│       ├── main.js                 # Electron 主进程(窗口 + 文件 IPC)
│       ├── renderer.js             # 渲染进程(编辑器 + 控制台 + 绘图查看器)
│       ├── r-engine.js             # 纯 JS R 执行引擎(796 行)
│       ├── index.html              # IDE 布局(工具栏 + 侧栏 + 编辑器 + 底部面板)
│       └── styles/
│           └── rkward.css          # Catppuccin Mocha 深色主题(717 行)
├── web_engine/                     # 鸿蒙 web_engine 模块
│   └── src/main/resources/
│       └── resfile/resources/app/  # 部署目录
└── build-profile.json5             # 鸿蒙构建配置

三、核心适配流程

3.1 第一步:创建主进程

文件:electron-apps/RKWard/main.js

主进程负责窗口创建和文件操作 IPC。R 脚本执行完全在渲染进程中通过 JSREngine 完成,主进程不再调用 Rscript。

// RKWard - R 语言 IDE 主进程
var electron = require('electron');
var app = electron.app;
var BrowserWindow = electron.BrowserWindow;
var screen = electron.screen;
var ipcMain = electron.ipcMain;
var dialog = electron.dialog;
var fs = require('fs');

app.disableHardwareAcceleration();

function createWindow() {
  var display = screen.getPrimaryDisplay();
  var width = display.workAreaSize.width;
  var height = display.workAreaSize.height;
  mainWindow = new BrowserWindow({
    width: Math.floor(width * 0.9),
    height: Math.floor(height * 0.85),
    webPreferences: {
      nodeIntegration: true,
      contextIsolation: false
    }
  });
  mainWindow.loadFile('index.html');
}

app.whenReady().then(createWindow);
app.on('window-all-closed', function() { app.quit(); });

文件操作 IPC 保留了 dialog:openFile、dialog:saveFile、fs:readFile、fs:writeFile 四个 handler,供渲染进程进行脚本文件的打开和保存。

// 打开文件对话框
ipcMain.handle('dialog:openFile', async function() {
  var result = await dialog.showOpenDialog(mainWindow, {
    properties: ['openFile'],
    filters: [{ name: 'R Scripts', extensions: ['R', 'r'] }]
  });
  if (result.canceled) return { ok: false };
  return { ok: true, path: result.filePaths[0] };
});

// 读取文件内容
ipcMain.handle('fs:readFile', async function(event, filePath) {
  try {
    var content = fs.readFileSync(filePath, 'utf-8');
    return { ok: true, content: content };
  } catch(e) {
    return { ok: false, error: e.message };
  }
});

3.2 第二步:设计 IDE 四区域布局

文件:electron-apps/RKWard/index.html

界面采用经典 IDE 四区域布局:左侧边栏(脚本列表 + 工作空间变量)、中间编辑器、底部面板(控制台/输出/绘图/帮助四标签)、顶部工具栏。

<!-- 工具栏 -->
<div id="toolbar">
  <div class="toolbar-left">
    <span class="app-logo">R</span>
    <span class="app-title">RKWard</span>
    <button id="btnNew" class="tb-btn" title="新建脚本">📄 新建</button>
    <button id="btnOpen" class="tb-btn" title="打开文件">📂 打开</button>
    <button id="btnSave" class="tb-btn" title="保存文件">💾 保存</button>
  </div>
  <div class="toolbar-right">
    <button id="btnRun" class="tb-btn primary" title="运行整个脚本">▶ 运行</button>
    <button id="btnRunSel" class="tb-btn primary" title="运行选中代码">▶▶ 选中运行</button>
    <button id="btnPlot" class="tb-btn" title="执行绘图代码">📊 绘图</button>
    <button id="btnClear" class="tb-btn" title="清空控制台">🗑️ 清空</button>
  </div>
</div>

<!-- 主体区域 -->
<div id="mainArea">
  <!-- 侧栏:脚本标签 + 工作空间变量 -->
  <div id="sidebar">
    <div class="sidebar-section">
      <div class="sidebar-header"><span>📝 脚本</span></div>
      <div id="scriptTabs" class="sidebar-list"></div>
      <button id="btnAddScript" class="sidebar-add">+ 新建脚本</button>
    </div>
    <div class="sidebar-section flex-grow">
      <div class="sidebar-header">
        <span>🔬 工作空间</span>
        <button id="btnRefreshVars" class="mini-btn">🔄</button>
      </div>
      <div id="varList" class="sidebar-list"></div>
    </div>
  </div>

  <!-- 侧栏拖拽条 -->
  <div id="sidebarResize" class="resize-handle-v"></div>

  <!-- 右侧内容区 -->
  <div id="content">
    <!-- 编辑器 -->
    <div id="editorArea">
      <div id="editorHeader">
        <span id="fileLabel" class="file-label">未命名脚本.R</span>
      </div>
      <div id="editorBody">
        <div id="lineNumbers" class="line-numbers">1</div>
        <textarea id="codeEditor" class="code-editor" spellcheck="false"
          placeholder="# 在此输入 R 代码"></textarea>
      </div>
    </div>

    <!-- 编辑器/控制台拖拽条 -->
    <div id="panelResize" class="resize-handle-h"></div>

    <!-- 底部面板(4 个标签) -->
    <div id="bottomPanel">
      <div id="bottomTabs">
        <button class="bottom-tab active" data-tab="console">控制台</button>
        <button class="bottom-tab" data-tab="output">输出</button>
        <button class="bottom-tab" data-tab="plots">绘图</button>
        <button class="bottom-tab" data-tab="help">帮助</button>
      </div>
      <!-- 控制台 -->
      <div id="tabConsole" class="tab-content active">
        <div id="consoleOutput" class="console-output"></div>
        <div id="consoleInputArea">
          <span class="console-prompt">&gt;</span>
          <input id="consoleInput" class="console-input" type="text">
        </div>
      </div>
      <!-- 绘图 -->
      <div id="tabPlots" class="tab-content">
        <div id="plotContainer" class="plot-container">
          <div class="plot-placeholder">
            <div style="font-size:36px">📊</div>
            <div>点击工具栏「绘图」按钮执行 R 绘图代码</div>
          </div>
        </div>
      </div>
      <!-- 帮助 -->
      <div id="tabHelp" class="tab-content">
        <div id="helpInputArea">
          <input id="helpInput" class="help-input" type="text" placeholder="输入 R 函数名查询帮助">
          <button id="btnHelpSearch" class="tb-btn mini">查询</button>
        </div>
        <pre id="helpContent" class="help-content"></pre>
      </div>
    </div>
  </div>
</div>

<!-- 状态栏 -->
<div id="statusBar">
  <span id="statusLeft" class="status-left">就绪</span>
  <span id="statusRight" class="status-right">R: 检测中...</span>
</div>

<!-- R 执行引擎(纯 JavaScript) -->
<script src="r-engine.js"></script>
<script src="renderer.js"></script>

关键设计:r-engine.js 必须在 renderer.js 之前加载,确保 JSREngine 对象在渲染进程初始化时可用。

3.3 第三步:实现 JSREngine 纯 JS R 执行引擎

文件:electron-apps/RKWard/r-engine.js

这是整个项目的核心,约 800 行代码实现了 R 语言的解析和执行。引擎分为五大模块:

模块一:随机数生成器(xorshift32 + Box-Muller)

R 语言的 set.seed 和 rnorm 是统计分析的基础。引擎使用 xorshift32 算法生成均匀分布随机数,通过 Box-Muller 变换转换为正态分布。

var _seed = 1;
var _spareNormal = null;

function xorshift32() {
  _seed ^= _seed << 13; _seed = _seed | 0;
  _seed ^= _seed >>> 17; _seed = _seed | 0;
  _seed ^= _seed << 5;  _seed = _seed | 0;
  return (_seed >>> 0) / 4294967296;
}

function boxMuller(mean, sd) {
  if (_spareNormal !== null) {
    var v = _spareNormal;
    _spareNormal = null;
    return mean + sd * v;
  }
  var u1, u2;
  do { u1 = xorshift32(); } while (u1 === 0);
  u2 = xorshift32();
  var mag = Math.sqrt(-2.0 * Math.log(u1));
  _spareNormal = mag * Math.sin(2.0 * Math.PI * u2);
  return mean + sd * mag * Math.cos(2.0 * Math.PI * u2);
}

xorshift32 是 32 位异或移位算法,周期为 2^32 - 1,计算速度快且统计特性良好。Box-Muller 变换将两个均匀分布随机数转换为一个正态分布随机数,同时缓存第二个值供下次调用使用,避免浪费计算。

模块二:统计函数
function arrMean(a) {
  return a.reduce(function(s,v){return s+v;},0) / a.length;
}

function arrSd(a) {
  var m = arrMean(a);
  var ss = a.reduce(function(s,v){return s+(v-m)*(v-m);},0);
  return Math.sqrt(ss / (a.length - 1));
}

function arrMedian(a) {
  var s = a.slice().sort(function(a,b){return a-b;});
  var n = s.length;
  return n%2 ? s[(n-1)/2] : (s[n/2-1]+s[n/2])/2;
}

function arrQuantile(a, p) {
  var s = a.slice().sort(function(a,b){return a-b;});
  var n = s.length;
  var idx = p*(n-1), lo = Math.floor(idx), hi = Math.ceil(idx), f = idx-lo;
  return lo===hi ? s[lo] : s[lo]*(1-f) + s[hi]*f;
}

标准差使用 n-1 自由度的样本标准差,与 R 的 sd() 函数一致。分位数使用线性插值法,与 R 的 quantile() 默认 type=7 算法一致。

模块三:表达式求值

R 代码的表达式求值需要处理函数调用、命名参数、嵌套括号等复杂情况。引擎使用括号深度追踪的 findEqAtDepth0 算法来区分赋值运算符和命名参数中的等号,同时追踪双引号和单引号字符串,避免将字符串内的等号误判为命名参数。

// 查找 depth=0 处且不在字符串内的第一个 = 号位置
function findEqAtDepth0(s) {
  var d = 0, inSQ = false, inDQ = false;
  for (var i = 0; i < s.length; i++) {
    var ch = s[i];
    if (inDQ) { if (ch === '"' && s[i-1] !== '\\') inDQ = false; }
    else if (inSQ) { if (ch === "'" && s[i-1] !== '\\') inSQ = false; }
    else if (ch === '"') inDQ = true;
    else if (ch === "'") inSQ = true;
    else if (ch === '(' || ch === '[') d++;
    else if (ch === ')' || ch === ']') d--;
    else if (ch === '=' && d === 0 && i > 0 && s[i-1] !== '!' && s[i-1] !== '<' && s[i-1] !== '>') return i;
  }
  return -1;
}

参数名转换函数 paramName 将 R 风格的点号命名转为 JavaScript 驼峰命名,配合 parseNamedArgs 实现完整的命名参数解析:

// 将 R 风格参数名转为 camelCase (length.out → lengthOut)
function paramName(name) {
  return name.trim().replace(/\.(\w)/g, function(m, c) { return c.toUpperCase(); });
}

function parseNamedArgs(s) {
  var args = splitArgs(s);
  var result = {_pos:[]};
  args.forEach(function(a) {
    var eq = findEqAtDepth0(a);
    if (eq > 0 && !a.substring(0,eq).match(/["']/)) {
      result[paramName(a.substring(0,eq))] = evalArg(a.substring(eq+1).trim());
    } else {
      result._pos.push(evalArg(a));
    }
  });
  return result;
}
模块四:多行续行合并

R 代码中函数调用经常跨多行书写,引擎在解析前将续行合并为完整语句:

function joinLines(code) {
  var raw = code.split('\n');
  var result = [];
  var cur = '';
  var depth = 0;
  for (var i = 0; i < raw.length; i++) {
    var line = raw[i];
    var trimmed = line.trim();
    if (!trimmed || trimmed[0] === '#') {
      if (depth === 0) { if (cur) { result.push(cur); cur = ''; } continue; }
    }
    if (cur) { cur += ' ' + trimmed; } else { cur = trimmed; }
    // 计算未闭合括号数
    depth = 0;
    for (var j = 0; j < cur.length; j++) {
      if (cur[j] === '(' || cur[j] === '[') depth++;
      else if (cur[j] === ')' || cur[j] === ']') depth--;
    }
    if (depth <= 0) { result.push(cur); cur = ''; depth = 0; }
  }
  if (cur) result.push(cur);
  return result;
}

该函数追踪括号深度,当遇到未闭合的左括号时,将后续行拼接到当前语句,直到括号完全闭合。这确保了 hist(y, main = “标题”, col = “lightblue”, breaks = 15) 这类跨行调用被正确合并为单行。

模块五:公共 API

引擎对外暴露 6 个 API 方法,核心设计是 execute 和 plot 在每次调用时清空 _vars,确保工作空间不会跨脚本累积变量:

window.JSREngine = {
  execute: function(code) {
    _vars = {};
    _output = [];
    var lines = joinLines(code);
    try {
      for (var li = 0; li < lines.length; li++) {
        try {
          execLine(lines[li]);
        } catch(lineErr) {
          return {ok:false, error:'L' + (li+1) + ': ' + lineErr.message, stderr:''};
        }
      }
      return {ok:true, stdout:_output.join('\n'), stderr:''};
    } catch(e) {
      return {ok:false, error:e.message};
    }
  },
  plot: function(code, w, h) {
    _vars = {};
    _output = [];
    try {
      var lines = joinLines(code);
      for (var li = 0; li < lines.length; li++) {
        try { execLine(lines[li]); } catch(le) {
          return {ok:false, error:'plot L' + (li+1) + ': ' + le.message};
        }
      }
      var dataUrl = renderPlot(lines.join('\n'), w, h);
      return {ok:true, dataUrl:dataUrl};
    } catch(e) {
      return {ok:false, error:e.message};
    }
  },
  getVariables: function() {
    var vars = [];
    Object.keys(_vars).forEach(function(k) {
      var v = _vars[k];
      if (Array.isArray(v)) {
        vars.push({name:k, type:'numeric ['+v.length+']', value:v.slice(0,5).map(formatVal).join(' ')+' ...'});
      } else if (typeof v === 'number') {
        vars.push({name:k, type:'numeric', value:formatVal(v)});
      } else {
        vars.push({name:k, type:typeof v, value:''+v});
      }
    });
    return vars;
  },
  reset: function() {
    _vars = {};
    _seed = 1;
    _spareNormal = null;
  },
  ready: true
};

每次运行脚本都是独立的干净环境。reset() 额外清除随机数种子和 Box-Muller 缓存值,用于完全重置引擎状态。

3.4 第四步:实现 Canvas 绘图引擎

r-engine.js 内置了 4 种 Canvas 图表绘制函数,支持 par(mfrow) 多图网格布局。以散点图和直方图为例:

function plotScatter(ctx, w, h, x, y, opts) {
  var pad = {l:50,r:20,t:30,b:30};
  var xMin=Math.min.apply(null,x), xMax=Math.max.apply(null,x);
  var yMin=Math.min.apply(null,y), yMax=Math.max.apply(null,y);
  var xR=[xMin-(xMax-xMin)*0.05, xMax+(xMax-xMin)*0.05];
  var yR=[yMin-(yMax-yMin)*0.05, yMax+(yMax-yMin)*0.05];
  drawAxes(ctx, w, h, pad, xR, yR);
  if (opts.main) { ctx.fillStyle='#cdd6f4'; ctx.font='bold 13px sans-serif'; ctx.textAlign='center'; ctx.fillText(opts.main, w/2, 18); }
  var pw=w-pad.l-pad.r, ph=h-pad.t-pad.b;
  ctx.fillStyle = opts.col || '#89b4fa';
  for (var i=0;i<x.length;i++) {
    var px = pad.l + (x[i]-xR[0])/(xR[1]-xR[0])*pw;
    var py = h-pad.b - (y[i]-yR[0])/(yR[1]-yR[0])*ph;
    ctx.beginPath(); ctx.arc(px,py,3,0,Math.PI*2); ctx.fill();
  }
  // regression line
  if (opts.regression) {
    var lm = lmCoef(x, y);
    ctx.strokeStyle = '#f38ba8'; ctx.lineWidth = 2;
    ctx.beginPath();
    ctx.moveTo(pad.l, h-pad.b-(lm.slope*xR[0]+lm.intercept-yR[0])/(yR[1]-yR[0])*ph);
    ctx.lineTo(w-pad.r, h-pad.b-(lm.slope*xR[1]+lm.intercept-yR[0])/(yR[1]-yR[0])*ph);
    ctx.stroke();
  }
}

function plotHist(ctx, w, h, data, opts) {
  var pad = {l:50,r:20,t:30,b:30};
  var mn=Math.min.apply(null,data), mx=Math.max.apply(null,data);
  var nBins = opts.breaks || 15;
  var binW = (mx-mn)/nBins;
  var bins = new Array(nBins).fill(0);
  data.forEach(function(v) {
    var idx = Math.min(Math.floor((v-mn)/binW), nBins-1);
    bins[idx]++;
  });
  var maxC = Math.max.apply(null, bins);
  drawAxes(ctx, w, h, pad, [mn, mx], [0, maxC*1.1]);
  if (opts.main) { ctx.fillStyle='#cdd6f4'; ctx.font='bold 13px sans-serif'; ctx.textAlign='center'; ctx.fillText(opts.main, w/2, 18); }
  var pw=w-pad.l-pad.r, ph=h-pad.t-pad.b;
  ctx.fillStyle = opts.col || '#89b4fa';
  for (var i=0;i<nBins;i++) {
    var bx = pad.l + i/nBins*pw;
    var bw = pw/nBins - 1;
    var bh = bins[i]/maxC*ph*0.9;
    ctx.fillRect(bx, h-pad.b-bh, bw, bh);
  }
}

箱线图支持分组公式语法,折线图支持多数据集叠加和图例渲染。

多图布局通过 renderMultiPlot 函数实现,检测 par(mfrow = c(rows, cols)) 设置后,将代码按绘图函数拆分为独立块,每块重新执行代码并捕获变量状态,然后在子 Canvas 上渲染对应图表:

function renderMultiPlot(code, rows, cols, w, h) {
  var cv = createCanvas(w, h);
  var ctx = cv.ctx;
  var cellW = w/cols, cellH = h/rows;

  // 分段解析代码块
  var blocks = [], cur = [];
  code.split('\n').forEach(function(l) {
    l = l.trim();
    if (!l || l[0]==='#' || l.match(/^(par|set\.seed)\(/)) return;
    if (l.match(/^(plot|hist|boxplot)\(/)) {
      if (cur.length) blocks.push(cur);
      cur = [l];
    } else {
      cur.push(l);
    }
  });
  if (cur.length) blocks.push(cur);

  blocks.forEach(function(block, idx) {
    if (idx >= rows*cols) return;
    var r = Math.floor(idx/cols), c = idx%cols;
    ctx.save();
    ctx.translate(c*cellW, r*cellH);
    ctx.beginPath();
    ctx.rect(0, 0, cellW, cellH);
    ctx.clip();

    // 执行代码块(变量累积模式,不重置 PRNG)
    var subOutput = [];
    var origOutput = _output;
    _output = subOutput;
    block.forEach(execLine);
    var localVars = Object.assign({}, _vars);
    _output = origOutput;

    // 在子 canvas 上绘图
    var subCanvas = createCanvas(cellW, cellH);
    var subCtx = subCanvas.ctx;
    var histCall = block.find(function(l){return l.match(/^hist\(/);});
    var boxCall = block.find(function(l){return l.match(/^boxplot\(/);});

    if (histCall) {
      var hm = histCall.match(/^hist\(([\s\S]+)\)$/);
      if (hm) {
        var ha = parseNamedArgs(hm[1]);
        var data = ha._pos[0];
        if (Array.isArray(data)) plotHist(subCtx, cellW, cellH, data, {main:ha.main,breaks:ha.breaks||15,col:ha.col});
      }
    }
    if (boxCall) {
      var bm = boxCall.match(/^boxplot\(([\s\S]+)\)$/);
      if (bm) {
        var ba = parseNamedArgs(bm[1]);
        var bGroups, bValues;
        if (ba._pos[0] && typeof ba._pos[0] === 'string') {
          var fmm = ba._pos[0].match(/^(\w+)\s*~\s*(\w+)$/);
          if (fmm) { bValues = localVars[fmm[1]]; bGroups = localVars[fmm[2]]; }
        }
        if (!bGroups) { bGroups = localVars.groups; bValues = localVars.values; }
        if (bGroups && bValues) {
          plotBox(subCtx, cellW, cellH, bGroups, bValues, {main:ba.main});
        }
      }
    }

    ctx.drawImage(subCanvas.canvas, 0, 0);
    ctx.restore();
  });

  return cv.canvas.toDataURL('image/png');
}

箱线图支持 R 的公式语法 values ~ groups,在 renderMultiPlot 中通过正则从字符串参数中提取变量名,实现分组数据的自动解析。

3.5 第五步:实现渲染进程 UI 逻辑

文件:electron-apps/RKWard/renderer.js

渲染进程约 840 行代码,实现 IDE 的全部交互功能。核心是通过 JSREngine API 完成 R 代码执行:

function runScript() {
  if (!rEngineReady) {
    notify('R 未就绪', 'R 执行引擎尚未加载', 'error');
    return;
  }
  saveCurrentScriptState();
  var code = el('codeEditor').value;
  if (!code.trim()) {
    notify('空脚本', '编辑器中没有代码', 'error');
    return;
  }

  el('statusLeft').textContent = '执行中...';
  disableRunButtons(true);
  switchBottomTab('console');
  appendConsole('> [运行脚本]', 'info');

  var t0 = Date.now();
  var result = window.JSREngine.execute(code);
  var ms = Date.now() - t0;
  if (result.ok) {
    if (result.stdout) appendConsole(result.stdout, 'output');
    if (result.stderr) appendConsole(result.stderr, 'error');
    appendConsole('--- 执行完成 (' + ms + 'ms) ---', 'info');
    el('statusLeft').textContent = '执行完成 (' + ms + 'ms)';
  } else {
    appendConsole('错误: ' + result.error, 'error');
    el('statusLeft').textContent = '执行失败';
  }
  refreshVariables();
  disableRunButtons(false);
}

function runPlot() {
  if (!rEngineReady) {
    notify('R 未就绪', 'R 执行引擎尚未加载', 'error');
    return;
  }
  saveCurrentScriptState();
  var code = el('codeEditor').value;
  if (!code.trim()) {
    notify('空脚本', '编辑器中没有绘图代码', 'error');
    return;
  }

  el('statusLeft').textContent = '生成绘图...';
  disableRunButtons(true);
  switchBottomTab('plots');

  var result = window.JSREngine.plot(code, 800, 600);
  if (result.ok && result.dataUrl) {
    plotZoom = 1;
    plotFitMode = true;
    renderPlotView(result.dataUrl);
    el('statusLeft').textContent = '绘图完成';
  } else {
    el('plotContainer').innerHTML = '<div class="plot-error">' +
      '<div class="plot-error-icon">⚠️</div>' +
      '<div class="plot-error-msg">绘图失败: ' + escapeHtml(result.error || '未知错误') + '</div></div>';
    el('statusLeft').textContent = '绘图失败';
  }
  disableRunButtons(false);
}

变量面板通过 getVariables() 读取当前工作空间状态,不重新执行代码,避免 PRNG 状态被意外推进:

function refreshVariables() {
  var container = el('varList');
  if (!rEngineReady) {
    container.innerHTML = '<div class="var-empty">R 引擎未就绪</div>';
    return;
  }
  // 不传 code — 变量已在 execute() 中设置,仅读取当前状态
  var vars = window.JSREngine.getVariables();
  if (vars.length === 0) {
    container.innerHTML = '<div class="var-empty">工作空间为空</div>';
    return;
  }
  container.innerHTML = vars.map(function(v) {
    var typeClass = v.type.indexOf('[') >= 0 ? 'var-type arr' : 'var-type';
    var icon = v.type.indexOf('[') >= 0 ? '📊' : (typeof v.value === 'string' && isNaN(Number(v.value)) ? '📝' : '🔢');
    return '<div class="var-item">' +
      '<div class="var-row">' +
        '<span class="var-icon">' + icon + '</span>' +
        '<span class="var-name">' + escapeHtml(v.name) + '</span>' +
        '<span class="' + typeClass + '">' + escapeHtml(v.type) + '</span>' +
      '</div>' +
      (v.value ? '<div class="var-value">' + escapeHtml(v.value) + '</div>' : '') +
      '</div>';
  }).join('');
}

绘图查看器支持 Fit/Zoom 双模式:适应窗口模式自动计算容器可用空间按比例缩放,缩放模式基于图片原始尺寸乘以缩放因子设置像素尺寸,支持滚动查看细节。

function applyPlotZoom() {
  var img = el('plotImg');
  if (plotFitMode) {
    var wrap = el('plotCanvasWrap');
    var availW = wrap.clientWidth - 32;
    var availH = wrap.clientHeight - 32;
    var fitW = Math.min(availW, plotNaturalW);
    var fitH = fitW * plotNaturalH / plotNaturalW;
    if (fitH > availH) { fitH = availH; fitW = fitH * plotNaturalW / plotNaturalH; }
    img.style.width = Math.round(fitW) + 'px';
    img.style.height = Math.round(fitH) + 'px';
  } else {
    var dw = Math.round(plotNaturalW * plotZoom);
    var dh = Math.round(plotNaturalH * plotZoom);
    img.style.width = dw + 'px';
    img.style.height = dh + 'px';
  }
}

通知系统使用自定义 div 替代 alert/confirm,避免触发鸿蒙 SubWindow 崩溃:

function notify(title, msg, type) {
  var old = el('notification');
  if (old) old.remove();
  var div = document.createElement('div');
  div.id = 'notification';
  div.className = 'rkw-notification' + (type === 'error' ? ' error' : '');
  div.innerHTML = '<div class="notify-title">' + escapeHtml(title) + '</div>' +
    '<div class="notify-msg">' + escapeHtml(msg) + '</div>';
  document.body.appendChild(div);
  setTimeout(function() { if (div.parentNode) div.remove(); }, 4000);
}

3.6 第六步:深色主题样式

文件:electron-apps/RKWard/styles/rkward.css

采用 Catppuccin Mocha 配色方案,约 720 行 CSS 代码:

:root {
  --base: #1e1e2e;
  --mantle: #181825;
  --crust: #11111b;
  --surface0: #313244;
  --surface1: #45475a;
  --text: #cdd6f4;
  --subtext0: #a6adc8;
  --blue: #89b4fa;
  --green: #a6e3a1;
  --red: #f38ba8;
  --yellow: #f9e2af;
  --mauve: #cba6f7;
  --overlay0: #6c7086;
}

布局关键约束:编辑器最小高度 150px,底部面板最小高度 100px,拖拽条 5px 可见分隔,绘图区域深色背景 #0d0d14 突出图表内容。

四、文件同步部署

每次修改 electron-apps/RKWard/ 下的代码后,需要同步到鸿蒙 web_engine 部署目录:

# 清空部署目录
Remove-Item "web_engine\src\main\resources\resfile\resources\app\*" -Recurse -Force

# 复制最新文件
Copy-Item "electron-apps\RKWard\*" `
  -Destination "web_engine\src\main\resources\resfile\resources\app\" `
  -Recurse -Force

注意:每次修改代码后都必须同步,否则构建的 HAP 包不会包含最新代码。

部署路径:web_engine/src/main/resources/resfile/resources/app/

五、预置示例脚本

IDE 内置 4 个预置示例脚本,覆盖统计计算、多类型绘图、线性回归和分布比较:

5.1 统计示例.R

基础统计量计算:样本量、均值、标准差、中位数、分位数、频数分布表。仅产生 1 个变量 x。

# R 统计分析示例
# 生成正态分布数据
set.seed(42)
x <- rnorm(100, mean = 50, sd = 10)

# 基本统计量
cat("样本量:", length(x), "\n")
cat("均值:", mean(x), "\n")
cat("标准差:", sd(x), "\n")
cat("中位数:", median(x), "\n")
cat("最小值:", min(x), "\n")
cat("最大值:", max(x), "\n")

# 分位数
cat("\n分位数:\n")
print(quantile(x))

# 频率分布
cat("\n频率分布表:\n")
print(table(cut(x, breaks = seq(20, 80, by = 10))))

5.2 绘图示例.R

2x2 多图布局:散点图加回归线、直方图、箱线图、折线图加图例。产生 6 个变量。

# R 绘图示例
# 多种图表类型展示
set.seed(123)

# 设置 2x2 布局
par(mfrow = c(2, 2))

# 散点图
x <- rnorm(50)
y <- 2 * x + rnorm(50, sd = 0.5)
plot(x, y, main = "散点图", xlab = "X", ylab = "Y",
     col = "steelblue", pch = 19)
abline(lm(y ~ x), col = "red", lwd = 2)

# 直方图(复用 x)
x <- rnorm(200, mean = 100, sd = 15)
hist(x, main = "直方图", xlab = "值",
     col = "lightblue", border = "white", breaks = 20)

# 箱线图
groups <- rep(c("A", "B", "C"), each = 30)
values <- c(rnorm(30, 50, 8), rnorm(30, 65, 10), rnorm(30, 55, 12))
boxplot(values ~ groups, main = "箱线图",
        col = c("#89b4fa", "#a6e3a1", "#f38ba8"))

# 折线图
t <- seq(0, 4 * pi, length.out = 100)
plot(t, sin(t), type = "l", main = "正弦波",
     xlab = "t", ylab = "sin(t)", col = "purple", lwd = 2)
lines(t, cos(t), col = "orange", lwd = 2)
legend("topright", c("sin", "cos"),
       col = c("purple", "orange"), lwd = 2)

5.3 回归分析.R

手动计算最小二乘回归系数,绘制散点图加回归线。产生 5 个变量。

# 线性回归分析示例
# 生成含噪声的线性数据
set.seed(99)
x <- rnorm(30, mean = 5, sd = 2)
y <- 2.3 * x + 10 + rnorm(30, sd = 1.5)

# 回归统计
cat("=== 线性回归分析 ===", "\n")
cat("样本量:", length(x), "\n")
cat("X 均值:", mean(x), "\n")
cat("Y 均值:", mean(y), "\n")

# 计算回归系数
slope <- (mean(x * y) - mean(x) * mean(y)) / (mean(x * x) - mean(x) * mean(x))
intercept <- mean(y) - slope * mean(x)
cat("斜率:", slope, "\n")
cat("截距:", intercept, "\n")

# 预测值
pred <- intercept + slope * x
cat("\n前 5 组预测值:\n")
print(pred)

# 散点图 + 回归线
plot(x, y, main = "线性回归分析",
     col = "steelblue", pch = 19)
abline(lm(y ~ x), col = "red", lwd = 2)

5.4 分布比较.R

两组正态分布的统计对比,1x2 布局直方图加箱线图。仅产生 3 个变量。

# 正态分布比较示例
set.seed(77)

# 生成两组不同参数的正态分布
x <- rnorm(100, mean = 50, sd = 5)
y <- rnorm(100, mean = 60, sd = 10)

# 分组变量
groups <- rep(c("A", "B"), each = 100)
y <- c(x, y)

# 统计对比
cat("=== 分布统计对比 ===", "\n")
cat("\n--- A 组 ---\n")
cat("均值:", mean(x), "\n")
cat("标准差:", sd(x), "\n")
cat("中位数:", median(x), "\n")
cat("\n--- B 组 ---\n")
cat("均值:", mean(y), "\n")
cat("标准差:", sd(y), "\n")
cat("中位数:", median(y), "\n")

# 1x2 布局对比
par(mfrow = c(1, 2))

# 直方图对比
hist(y, main = "分布直方图",
     col = "lightblue", breaks = 15)

# 箱线图对比
boxplot(y ~ groups, main = "分布箱线图",
        col = c("#89b4fa", "#a6e3a1"))

六、常见问题与解决方案

Q1:工作空间变量累积怎么办?

问题现象:连续运行多个脚本后,工作空间变量面板显示大量无关变量

根本原因:execute() 和 plot() 不清理 _vars 对象,每次运行后变量全部保留

解决方案:在 execute() 和 plot() 开头执行 _vars = {} 清空工作空间,每次脚本运行都是独立环境。同时提供 reset() API 用于完全重置(包括随机数种子和 Box-Muller 缓存)。

execute: function(code) {
  _vars = {};
  _output = [];
  var lines = joinLines(code);
  try {
    for (var li = 0; li < lines.length; li++) {
      try {
        execLine(lines[li]);
      } catch(lineErr) {
        return {ok:false, error:'L' + (li+1) + ': ' + lineErr.message, stderr:''};
      }
    }
    return {ok:true, stdout:_output.join('\n'), stderr:''};
  } catch(e) {
    return {ok:false, error:e.message};
  }
}

Q2:多行函数调用解析失败怎么办?

问题现象:hist(y, main = “标题”, 换行 col = “blue”) 这类跨行调用导致图表不渲染

根本原因:代码按行分割后,续行被当作独立语句处理,函数调用括号不匹配导致正则匹配失败

解决方案:在 execute() 和 plot() 入口调用 joinLines() 函数,根据括号深度将续行合并为完整语句。追踪左括号和右括号的差值,当差值大于零时将后续行拼接到当前语句。

// 修复前:两行独立处理
// Line 1: hist(y, main = "分布直方图",    → 括号未闭合
// Line 2: col = "lightblue", breaks = 15) → 被误识别为赋值

// 修复后:合并为一行
// hist(y, main = "分布直方图", col = "lightblue", breaks = 15)

Q3:绘图图片溢出或无法滚动怎么办?

问题现象:放大图片后无法滚动查看细节,图片溢出容器

根本原因:CSS max-width: 100% 限制图片宽度,transform: scale() 不改变布局盒模型,overflow: auto 无法出现滚动条

解决方案:移除 max-width 和 transform,改用显式 width/height 设置像素尺寸。Fit 模式计算容器可用空间按比例缩放,Zoom 模式使用 naturalWidth × zoom 设置实际像素尺寸。

.plot-canvas-wrap {
  flex: 1; overflow: auto;
  display: flex; align-items: center; justify-content: center;
  padding: 16px; background: #0d0d14;
}
.plot-canvas-wrap img {
  display: block; object-fit: contain;
}

Q4:鸿蒙平台对话框崩溃怎么办?

问题现象:点击按钮后应用闪退或白屏

根本原因:使用了原生 prompt/confirm/alert 对话框或原生 select 元素,鸿蒙 Electron 适配层的 ArkWeb 内核对这些原生 UI 组件支持不完整

解决方案:所有对话框和下拉选择均使用自定义 div 实现。本工具的通知系统使用 rkw-notification 类名的 div 元素,4 秒后自动消失。

原生组件替代方案实现方式
alert/confirm自定义 notification divdiv + CSS 动画
prompt自定义 input 对话框div + input 元素
select不使用脚本标签切换代替

七、总结

本文记录了将 RKWard IDE 适配到鸿蒙 PC 平台的完整流程。核心技术要点总结如下:

技术点方案
R 执行引擎纯 JavaScript 实现 R 语言核心子集(xorshift32 + Box-Muller)
随机数生成xorshift32 PRNG + Box-Muller 正态分布变换,set.seed 可复现
表达式求值findEqAtDepth0 双引号+单引号追踪,区分赋值与命名参数
多行续行合并joinLines 根据括号深度自动合并跨行函数调用
Canvas 绘图散点图/直方图/箱线图/折线图 + par(mfrow) 多图网格布局
多图布局renderMultiPlot 块执行 + localVars 快照 + 公式解析
绘图查看Fit/Zoom 双模式,适应窗口缩放 + 手动缩放滚动
工作空间隔离execute/plot 清空 _vars,避免跨脚本变量累积
鸿蒙三防约束禁止 alert/confirm/prompt/select,全部使用自定义 div
文件同步部署PowerShell 一键复制到 web_engine resfile 资源目录

关键成果:

  1. 纯 JS R 执行引擎:用 JavaScript 实现 R 语言核心子集(随机数、统计计算、表达式求值),无需安装 R 或联网,完全离线可用
  2. Canvas 绘图引擎:支持散点图、直方图、箱线图、折线图 4 种图表类型,支持 par(mfrow) 多图网格布局
  3. 4 个预置示例:统计示例、绘图示例、回归分析、分布比较,覆盖主要使用场景
  4. 32 项功能测试:全部通过,覆盖统计、变量、帮助、绘图、控制台、算术、清理、回归、分布 9 大类别

整个适配过程严格遵守鸿蒙三防策略,未使用任何原生对话框、select 元素或窗口拦截 API。全部代码约 2756 行(r-engine.js 796 行 + renderer.js 837 行 + rkward.css 717 行 + index.html 130 行 + main.js 276 行),即可在鸿蒙 PC 平台上获得完整的 R 语言统计分析体验。

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/weixin_43151418/article/details/165006661

文章来源转载

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--