clickgo-native 2.0.1 → 2.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -42,6 +42,8 @@ native.launcher(new Boot());
42
42
 
43
43
  ## Build a ClickGo Application as a Native Package
44
44
 
45
+ The application project does not need to install `electron` directly when using ClickGo Compiler to run and package it. The compiler supplies the Electron runtime. Native main-process code can use `native.getAppVersion()` to read the application's `package.json` version and `native.isPackaged()` to distinguish a packaged application from development runs. These APIs do not expose Electron to the HTML page.
46
+
45
47
  Building a desktop application consists of two steps: compile the ClickGo application into a `.cga` file, then package the Native project with Electron.
46
48
 
47
49
  Install the compiler globally first:
package/dist/index.d.ts CHANGED
@@ -91,6 +91,16 @@ export declare function showMainForm(path: string, opt?: {
91
91
  /** --- 应用启动后、网页加载成功前显示的底色 --- */
92
92
  'background'?: string;
93
93
  }): void;
94
+ /**
95
+ * --- 获取当前桌面应用的版本,来自应用 package.json ---
96
+ * @returns 应用版本
97
+ */
98
+ export declare function getAppVersion(): string;
99
+ /**
100
+ * --- 判断当前应用是否为已打包运行 ---
101
+ * @returns 是否已打包
102
+ */
103
+ export declare function isPackaged(): boolean;
94
104
  /** --- 用户调用运行 boot 类 --- */
95
105
  export declare function launcher(boot: AbstractBoot): void;
96
106
  /**
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import * as electron from 'electron';
2
2
  import * as nodePath from 'path';
3
+ import { pathToFileURL } from 'node:url';
3
4
  import * as lFs from './lib/fs.js';
4
5
  import * as lTool from './lib/tool.js';
5
6
  // npm publish --tag dev --access public
@@ -12,6 +13,10 @@ let isNoFormQuit = true;
12
13
  let form;
13
14
  /** --- 当前设定的通讯 token --- */
14
15
  let token = '';
16
+ /** --- 主窗体唯一允许使用 Native 通讯的页面地址(不含 hash) --- */
17
+ let mainPage = '';
18
+ /** --- 主框架正在更换文档时暂停 Native 通讯 --- */
19
+ let mainNavigating = false;
15
20
  /** --- 当前系统平台 --- */
16
21
  const platform = process.platform;
17
22
  // const platform: NodeJS.Platform = 'darwin';
@@ -42,6 +47,8 @@ const methods = {
42
47
  }
43
48
  form.setSize(width, height);
44
49
  form.center();
50
+ // --- 首个应用窗体设置尺寸后再允许缩放,启动屏保持固定大小 ---
51
+ form.resizable = true;
45
52
  }
46
53
  },
47
54
  // --- 设置窗体最大化、最小化、还原(从最大化还原) ---
@@ -471,18 +478,7 @@ export function showMainForm(path, opt = {}) {
471
478
  // --- 有主窗体了就不能创建了 ---
472
479
  return;
473
480
  }
474
- // --- 成功运行一个 task 后再次添加 init ---
475
- methods['cg-init'] = {
476
- 'once': true,
477
- handler: function (t) {
478
- // --- t 是网页传来的 token ---
479
- if (!t || !form) {
480
- return;
481
- }
482
- form.resizable = true;
483
- token = t;
484
- },
485
- };
481
+ resetMainSession();
486
482
  const frm = createForm(path, {
487
483
  'width': opt.width,
488
484
  'height': opt.height,
@@ -495,6 +491,40 @@ export function showMainForm(path, opt = {}) {
495
491
  frm.webContents.openDevTools();
496
492
  }
497
493
  }
494
+ /**
495
+ * --- 更换主页面文档时撤销旧 token,重新允许一次合法初始化 ---
496
+ * @returns 无
497
+ */
498
+ function resetMainSession() {
499
+ token = '';
500
+ methods['cg-init'] = {
501
+ 'once': true,
502
+ handler: function (t) {
503
+ // --- t 是网页传来的 token ---
504
+ if ((typeof t !== 'string') || !t || !form || token) {
505
+ return;
506
+ }
507
+ if (hasFrame) {
508
+ form.resizable = true;
509
+ }
510
+ token = t;
511
+ },
512
+ };
513
+ }
514
+ /**
515
+ * --- 获取当前桌面应用的版本,来自应用 package.json ---
516
+ * @returns 应用版本
517
+ */
518
+ export function getAppVersion() {
519
+ return electron.app.getVersion();
520
+ }
521
+ /**
522
+ * --- 判断当前应用是否为已打包运行 ---
523
+ * @returns 是否已打包
524
+ */
525
+ export function isPackaged() {
526
+ return electron.app.isPackaged;
527
+ }
498
528
  /** --- 用户调用运行 boot 类 --- */
499
529
  export function launcher(boot) {
500
530
  (async function () {
@@ -511,24 +541,44 @@ export function launcher(boot) {
511
541
  electron.Menu.setApplicationMenu(null);
512
542
  // --- 实际用来监听网页传输过来的数据 ---
513
543
  electron.ipcMain.handle('pre', function (e, name, ...param) {
514
- if (!methods[name]) {
544
+ if (!form || form.isDestroyed() || mainNavigating ||
545
+ (e.sender !== form.webContents) ||
546
+ !e.senderFrame || (e.senderFrame !== form.webContents.mainFrame) ||
547
+ !mainPage || (getPageUrl(e.senderFrame.url) !== mainPage) ||
548
+ (typeof name !== 'string') || !Object.hasOwn(methods, name)) {
515
549
  return;
516
550
  }
517
- const r = methods[name].handler(...param);
518
- if (methods[name].once) {
551
+ // --- 无效初始化不能消耗一次性监听 ---
552
+ if ((name === 'cg-init') && ((typeof param[0] !== 'string') || !param[0] || token)) {
553
+ return;
554
+ }
555
+ const method = methods[name];
556
+ if (method.once) {
519
557
  delete methods[name];
520
558
  }
521
- return r;
559
+ return method.handler(...param);
522
560
  });
561
+ /**
562
+ * --- 精确匹配页面地址,允许同一页面改变 hash ---
563
+ * @param value 页面地址
564
+ * @returns 不含 hash 的地址,无效地址返回空字符串
565
+ */
566
+ function getPageUrl(value) {
567
+ try {
568
+ const url = new URL(value);
569
+ url.hash = '';
570
+ return url.href;
571
+ }
572
+ catch {
573
+ return '';
574
+ }
575
+ }
523
576
  /**
524
577
  * --- 验证 token 是否正确 ---
525
578
  * @param t 要验证的 token
526
579
  */
527
580
  export function verifyToken(t) {
528
- if (t !== token) {
529
- return false;
530
- }
531
- return true;
581
+ return (typeof t === 'string') && (token !== '') && (t === token);
532
582
  }
533
583
  /**
534
584
  * --- 内部调用用来创建实体窗体的函数 ---
@@ -554,6 +604,52 @@ function createForm(p, opt = {}) {
554
604
  'transparent': opt.transparent,
555
605
  };
556
606
  form = new electron.BrowserWindow(op);
607
+ // --- 页面地址由本地主进程确定,不接受网页修改 ---
608
+ const lio = p.indexOf('?');
609
+ const pageUrl = (p.startsWith('https://') || p.startsWith('http://')) ?
610
+ new URL(p) : pathToFileURL(lio === -1 ? p : p.slice(0, lio));
611
+ if ((pageUrl.protocol === 'file:') && (lio !== -1)) {
612
+ pageUrl.search = p.slice(lio + 1);
613
+ }
614
+ mainPage = getPageUrl(pageUrl.href);
615
+ mainNavigating = true;
616
+ /** --- 主动拦截初始页面重定向产生的加载失败无需再次抛出 --- */
617
+ let redirectBlocked = false;
618
+ form.webContents.on('will-navigate', (event) => {
619
+ if (getPageUrl(event.url) !== mainPage) {
620
+ event.preventDefault();
621
+ }
622
+ });
623
+ form.webContents.on('will-redirect', (event) => {
624
+ if (event.isMainFrame && (getPageUrl(event.url) !== mainPage)) {
625
+ redirectBlocked = true;
626
+ event.preventDefault();
627
+ mainNavigating = false;
628
+ }
629
+ });
630
+ form.webContents.setWindowOpenHandler(() => ({ 'action': 'deny' }));
631
+ form.webContents.on('did-start-navigation', (event) => {
632
+ if (event.isMainFrame && !event.isSameDocument && (getPageUrl(event.url) === mainPage)) {
633
+ mainNavigating = true;
634
+ resetMainSession();
635
+ }
636
+ });
637
+ form.webContents.on('did-navigate', () => {
638
+ mainNavigating = false;
639
+ if (form && (getPageUrl(form.webContents.getURL()) !== mainPage)) {
640
+ token = '';
641
+ delete methods['cg-init'];
642
+ }
643
+ });
644
+ form.webContents.on('did-fail-load', (_event, _code, _description, _url, isMainFrame) => {
645
+ if (isMainFrame) {
646
+ mainNavigating = false;
647
+ }
648
+ });
649
+ form.webContents.on('render-process-gone', () => {
650
+ token = '';
651
+ mainNavigating = true;
652
+ });
557
653
  form.webContents.userAgent = 'electron/' + electron.app.getVersion() + ' ' + platform + '/' + process.arch + ' frame/' + (hasFrame ? '1' : '0') + ' chrome/' + process.versions.chrome;
558
654
  form.once('ready-to-show', function () {
559
655
  if (!form) {
@@ -570,6 +666,9 @@ function createForm(p, opt = {}) {
570
666
  if (p.startsWith('https://') || p.startsWith('http://')) {
571
667
  // --- 加载网页 ---
572
668
  form.loadURL(p).catch(function (e) {
669
+ if (redirectBlocked) {
670
+ return;
671
+ }
573
672
  throw e;
574
673
  });
575
674
  }
@@ -586,8 +685,12 @@ function createForm(p, opt = {}) {
586
685
  throw e;
587
686
  });
588
687
  }
589
- form.on('close', function () {
688
+ form.on('closed', function () {
590
689
  form = undefined;
690
+ token = '';
691
+ mainPage = '';
692
+ mainNavigating = false;
693
+ delete methods['cg-init'];
591
694
  });
592
695
  // --- 最大化事件 ---
593
696
  form.on('maximize', function () {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clickgo-native",
3
- "version": "2.0.1",
3
+ "version": "2.0.3",
4
4
  "description": "The software developed with ClickGo will run in Windows, Mac OS, Linux.",
5
5
  "type": "module",
6
6
  "keywords": [