前端不用Electron也能写Win应用 动手实现一个SwitchHosts程序

说到前端写Windows应用程序,首先想到的一定是Electron,这次我们换一个小众点的工具aardioaardio是一个很轻量的工具,有着自己的语法,面向windows应用开发,刚好其中也有支持前端应用打包功能。

选型

aardio中涉及到HTML+CSS+JS的功能。有Chrome AppElectron,同时还有Sciter.jsHTMLayout两个UI引擎。
一下子就有四种选项,这里按自己摸索的简单总结一下,可能会不太准确:

Chrome App

默认打包出来的应用程序不携带浏览器,程序会启动调用本地的浏览器渲染(不限于官方的ChromeEdge以及国产主流Chromium内核的浏览器都行),如果都没有会自动安装微软的Edge(Chromium版)浏览器。
优势是生成体积比较小(但外部环境不稳定,产生的显示Bug可能性也更多),基础大小估计在800KB左右(经UPX压缩过),前端项目迁移起来不用过多修改就能跑起来。如果要指定环境,也可以在启动代码内指定浏览器路径。

Electron

可以构建Electron的应用,Electron本身就是做桌面程序的为什么还需要使用aardio
看了一下,除了可以利用aardio的一些内置的方便功能函数,同时生成的应用体积也是差异很大,软件自带的示例Electron项目生成文件大概在800KB左右(经UPX压缩过),作者可能使用了精简版的内核替换了原本的程序。如果只做Windows平台应用,这样的打包体积还是很吸引人的。

Sciter.js

这主要是面向桌面开发的一个UI渲染引擎,可以利用HTML+CSS写界面,将浏览器许多无用功能剔除,只留下UI引擎常用的功能,使得体积尽量小巧。
原本以为能将整个前端项目打包进去,但尝试了一下,好像不行。HTMLCSS是单独传入的,没办法像是前端那么自由的引用。使用逻辑更像是一个html界面就配置一个Sciter.js对象,而不是将html程序的所有逻辑放到里面去执行。
生成的应用体积也是比较小的,大概在2.8MB左右(经UPX压缩过)。看介绍,大部分常用的CSS支持,同时也有一些特殊的语法方便处理界面。

HTMLayout

HTMLayoutSciter.js的前身,程序体积更小,但支持的CSS功能也更加有限。同样也不太适用前端项目的打包。
对比了一下HTMLayoutSciter.js生成应用的体积,应用大概在1.5MB左右(经UPX压缩过),两者只差了1.3MB左右,但放弃的新语法可能要很多,所以这个大部分场景可以不必考虑了。

尝试一下

官方有推过一个修改版的hostsSwitchHelper,使用的就是Chrome App模式,既然是尝试,那就先选这个模式来试试。

Chrome App模式其实很小白,如果没有与系统交互的功能,那么几乎就是直接将前端项目编译,然后导入到开发工具中就能生成一个应用。

先来看看界面:
前端不用Electron也能写Win应用 动手实现一个SwitchHosts程序_第1张图片
选择Web界面栏目,然后选择第一项Chrome App,设置好目录就能创建出一个Chrome App例子了,其中自带已经能跑起来的代码。
代码量很少,不熟悉的可以直接在这个项目上做简单修改,就是一个自己的项目了。

做一个自己的程序

这是已完成的界面:

前端不用Electron也能写Win应用 动手实现一个SwitchHosts程序_第2张图片

前端部分

先从熟悉的开始吧,把前端部分先完成。

由于程序不大,于是不使用Vue或者React了,找了个原生漂亮的UI框架LuLu UI,配合TypeScript,打包工具也不要太重的WebPack,我们选用更加轻量的parcel

其实一开始是直接用LuLu UI+原生JS,简单的写几个页面,后来对界面细节不断调整(这块反复修改花了不少时间),以及操作逻辑的优化,总体逻辑相对复杂了起来,于是引入了打包工具,将CSS换成了LESSJavaScript换成了TypeScript,并且分出了目录结构出来。

打包其实只需要编译完成的前端文件,所以前端工程可以随意放在哪儿都行,最后只需要把编译出的文件放到aardio设置的目录下就可以了。

至于前端就很普通,常见的建立项目,补充样式与逻辑,加上parcel本身也很轻量易用,太细节的就不多说了,挑一些主要的说。

关闭翻译提示

如果不设置,网页默认是英文,打开后Chrome会弹出翻译提示,这个功能对于本地应用程序无意义。设置的话就是标注网页语言为中文,或是简单粗暴在head标签里加上这句,禁止使用翻译功能。

屏蔽影响使用的快捷键

网页的F12Ctrl+Shift+iCtrl+u等,万一触碰到会影响应用程序体验,用JS代码将这些快捷键禁用。
由于KeyboardEvent.keyCode已经被废弃了,这里直接使用KeyboardEvent.key判断

  window.onkeydown = window.onkeyup = window.onkeypress = function (event: KeyboardEvent) {
    //禁止ctrl+u
    if (event.ctrlKey && event.key.toLowerCase() == 'u') return false;
    //禁止ctrl+s
    if (event.ctrlKey && event.key.toLowerCase() == 's') return false;
    //禁止ctrl+shift+i
    if (event.ctrlKey && event.shiftKey && event.key.toLowerCase() == 'i') return false;
    //禁止 F5
    if (event.key == 'F5') return false;
    //禁止 F12
    if (event.key == 'F12') return false;
  };
屏蔽右键

右键的菜单也有无关动东西,影响使用体验,重点还很难看。如果没有右键功能可以直接屏蔽,这里为了方便加上了右键功能,所以重画了右键菜单键,替换了原生的右键。

  window.oncontextmenu = function (event: MouseEvent) {
    event.preventDefault();
    show(event); // 里面包括打开自己的菜单层
    return false;
  };
  
  document.addEventListener('click', close); // 关闭右键菜单 close函数里就是将相应的层隐藏

show的逻辑很简单,获取当前鼠标位置,然后判断窗口是否会溢出,做相应的高度修改,最后将指定的菜单层显示。

/**
 * 显示右键菜单
 * @param {MouseEvent} event
 */
function show(event: MouseEvent) {
  if (!isNavItem(event)) return;
  // 获取当前鼠标位置
  let mouseX = event.clientX;
  let mouseY = event.clientY;
  // 判断边界值,防止菜单栏溢出可视窗口
  const winHeight = document.documentElement.clientHeight ?? document.body.clientHeight;
  // BUS.contextMenu是存储的前端菜单DOM
  if (mouseY > winHeight - BUS.contextMenu.offsetHeight) mouseY = winHeight - BUS.contextMenu.offsetHeight - 10;
  else mouseY = mouseY;

  BUS.contextMenu.style.left = `${mouseX}px`;
  BUS.contextMenu.style.top = `${mouseY}px`;
}

这里增加了一些细节判断,像是编辑器区域不弹出右键,在标签项右键与导航空白区右键显示不同菜单。

调试模式

由于禁用掉开发者工具在调试的时候很麻烦,通过注释代码屏蔽又容易忘记,parcel打包工具同样提供了环境变量。

process.env.NODE_ENV === 'production'
网页元素重渲染

由于没有使用现代框架,这部分起始逻辑很重。像是鼠标右键不同地方显示不同菜单的功能实现起来就会很麻烦,要不然代码就很冗余。于是引入了小巧快捷的前端模板引擎art-template,一部分操作交给引擎判断,通过添加不同的类名展示不同的状态。

比如鼠标右键点击在标签项上,如果当前的项没有添加url参数,则是本地的Hosts,渲染时会添加disabled类名,标识禁用此元素。disabled类的CSS核心是pointer-events: none;,这样即可让此元素不响应鼠标事件,无需再添加多余的js逻辑。

导航栏的当前选中的类名添加、添加新项等操作全部只是在统一的数据中做记录,然后依赖重新渲染一次性替换掉HTML,避免了写复杂冗长的DOM增删逻辑,也简化了代码耦合。

所以项目里单独做了一个数据管理的模块。从原本的简单nav渲染单文件逻辑开始,到后来由于UI逻辑越做越细致,这个里也从单文件变成了多文件,尽量把外部操作与纯数据操作隔离。

文本编辑器

如果使用textarea标签可以实现多行文本,但没办法提供注释文字的区分显示,也没办法提供行号、快捷键、搜索等扩展功能,所以我们需要一个文本编辑器。

经过筛选比较常见的代码编辑器有CodeMirroraceEditormonacoEditorVS Code的前身),三者中CodeMirror体积最小,我们需要的功能也不要太多,于是就选这个了。

使用方法网上很多,源码中注释也很清楚,使用方式就不多提了。

语法颜色

由于我们的Hosts实际只需要给 #号上色,编辑器有类似的语法,但也会将多余的关键字和结构显示出颜色,这时候我们可以自己修改一个。
简单的翻阅语法源码能发现在tokenBasereturn中的token有对很多信息做匹配,将没有的信息删除,减少判断逻辑,连带的可以删除几个相关函数代码。之后在底部CodeMirror.defineMIME中有自定义许多关键字,对我们也没用,都可以删除。然后记得改一个新名字,这样就实现了一个我们专门的语法,导入插件中就能使用了。

主题样式我们也可以自定义,同样是找一份看的不错的样式,然后把CSS代码复制过来,其中有许多状态的样式名称,根据相应名称修颜色即可。

这里我开启了高亮选中行,所以在当前行会有一个蓝色底色,由于我在编辑器文本框背景上又加了一个当前项的类型图标,会导致图标被覆盖,这时候只需要将div.CodeMirror-selected的颜色调成半通明模式即可,但由于我背景的透明度调的很低(背景透明度是0.06,而选中的透明度是0.8),所以不仔细看的话实际上还是有点像是被覆盖住。

快捷键

本身是做切换标签才自动保存的逻辑,但如果用户修改了文本内容,误以为是实时保存而直接关闭软件,这样就会导致修改丢失。所以需要加上一个当前文本修改过的标识,于是又在右上角加了一个保存按钮(一个不好看,顺便又加上了刷新与更新远程文件按钮)。

有了按钮,自然的会习惯用Ctrl+s去保存文件,好在CodeMirror是提供了自定义快捷键的。

extraKeys: {
  'Ctrl-S': eventTextareaSave, // 这里是自定义的保存逻辑
}
网络请求及代理

常见的应用场景是从网络上复制别人整理好的Hosts信息,所以为了方便增加了从网络获取Hosts功能。但这种一般需要借助代理功能才能获取到,搜索网上并没见到js能直接设置代理的办法。

这时候就应该将前端不易操作的功能放到aardio中,通过inet.http库可以直接发起网络请求,同时默认也会走系统代理设置,完全满足了目前的功能所需。

aardio部分

到这里就进入了我们不是熟悉的部分了。aardio语法很像js,但细节上跟js又有差距,所以写起来的时候很容易弄混,要注意一下。

这里说几个看目前代码需要理解的点:
import,是将对应的命名空间导入当前的命名空间,使用的时候可以直接使用。会按内核库标准库用户库(应用程序lib目录)查询引入。
var,申明变量不一定都要使用var关键字,如果没使用是默认挂在当前命名空间下的。如果使用了,则作用域是在当前代码块。同时申明变量也只有var关键字,不要顺手把ES6的关键字用上了。
global,这是默认的全局空间名字,简写是..在代码中经常看到,比如要操作io库,可以写成..io调用。
tableaardiotable是一种类型,类似js里的数组和对象,两种类型同时是table类型。写法上keyvalue连接是用=,末尾结束是用;,这个与js差异很大,不小心写错会导致报错,复查错觉无时往往容易因为惯性而忽略。
多返回值,程序是支持多返回值的,用,分隔,接收返回也是直接用,分隔写变量即可接收。
目录环境,路径如果是~开头的代表exe应用程序运行的当前目录,如果是在测试环境,则是aardio.exe主程序的目录。

其他不用写大段逻辑的话,基本也能看懂了。

创建chrome程序

main.aardio是程序主入口,在这个文件里写起始逻辑。

import chrome.app; // 导入所需的库
var theApp = chrome.app(); // 创建一个实例
theApp.start(url); // chrome打开指定的url

这样就能启动一个chrome应用了。

js与aardio交互
theApp.external = {
    key = function(){
        // 逻辑
    };
}

external里面传入相应的逻辑,前端通过aardio组件可以实现调用。

引入前端文件

前端不用Electron也能写Win应用 动手实现一个SwitchHosts程序_第3张图片

在左侧新建“虚拟目录”,右侧会出现设置项,“目录路径”修改成我们前端的路径,“内嵌资源”要选true,不然无法生成单文件的应用程序。

最后对着这个虚拟目录单击右键,选择“同步本地目录”,如果目录内有文件的话会全部显示出来。如果有修改重新生成前端文件的话,也要记得手动同步一次,目录是不会自动刷新的。

编译应用

这样我们的前端项目就能打包成win的应用程序了,按F7就能一键编译,稍微等一儿编译好的应用就生成了,是不是很简单?

Electron版尝试

aardioElectron做了简化,目前的项目可以直接复制到Electron工程的渲染进程中使用,只要启动代码做相应修改即可。但尝试下来,发现UI渲染十分卡顿,不知道是什么原因。目前功能暂时够用,这块等之后有空再了解吧。

结尾

总体使用下来,对于熟悉前端以及前端工具链的形式,aardio的界面和文本编辑器很不适应(VS Code上有人做了一个插件,说是可以直接在VS Code编译,配置了依然报错,而且没有格式化,没有智能提示,反不如原生IDE方便)。
文档也相对比较简单,需要有一定的耐心,很多地方需要自己从右侧的范例中查找例子学习,如果没有例子,则要从左下角的标准库里,看源代码和智能提示了解具体有什么功能和如何编写代码。

项目源码

具体细节有兴趣的话请查阅源码: HostsSwitch
https://github.com/Lnncoco/ho...

你可能感兴趣的:(前端)