前言
uTools 官方的模板插件对于简单查询插件的开发是非常友好的,但是模板插件虽然免除了写 UI 上的繁琐步骤,但是在设置插件参数的时候又显得捉襟见肘,非常难受,一些简单的文本参数还可以变通一下,但是复杂的参数便无能为力,于是简单整理了一个还算简单的模板插件自定义设置界面的做法,抛砖引玉。
模板插件的本质
模板插件也是 uTools 插件,众所周知,普通的 uTools 插件其实是一个单页 web 应用,运行在 Electron 提供的"浏览器"里,模板插件也不例外,在模板插件的开发者工具中可以看到,模板插件也是一个完整的 web 应用,模板插件只是在 uTools 里把模板代码的相关参数传进这个通用的 web 插件里面。
既然模板插件也是一个 web,那么留给我们的可操作空间就大了很多,其中一个念头就是: 把模板插件本身的 web 代码给扔了,直接换上我们自己的代码。
替换
其实很多年前我们就是这么写前端代码的,在 JavaScript 里面,如果我们要改某个页面元素的内容,会用document.getElementById('id').innerHTML='<div>xxx</div>'
这样的语句来操作,也就是说,我们在任何时候都可以直接修改页面元素,直接替换成我们自己想要的. 后来在有了现代化的 web 框架之后,这种方式就被抛弃了,但实际上,如果你认真看 vue 或 react 的文档,就会发现,其实这些框架也是用同样的做法实现自己的功能,只不过在被 webpack 等开发工具包装之后,隐藏了这些细节,或者放在不被人注意到的地方。
// Vue
Vue.createApp({
render() {
return <h1>Hello, world!</h1>
}
}).mount('#counter');
// React
ReactDOM.render(
<h1>Hello, world!</h1>,
document.getElementById('root')
);
所以,我们通过这种方式来达到自定义设置界面的功能。
编写 UI
虽然用innerHTML
可以改变任意元素的内容,但是如果全程使用这个特性来操作的话,那难受程度简直难以想象,有没有省时省力的方式在 JavaScript 里面写 UI 界面呢,答案是JSX
,这是一个在 JavaScript 书写 HTML 内容的方案,react 里面用得非常广泛,但 JSX 并不是 react 特有的,实际上 vue 也支持这种方式。
工具
尽管 vue 和 react 的功能和生态都很强大,但是一个小小的模板插件,我们只是想画个简单的 UI,根本不需要用牛刀. 这里推荐一个简单的库来实现: nano-jsx,这是一个号称只有「1kb」大小的 JSX 解析库,并且提供了 TypeScript 和 JavaScript 两种环境下写模板 HTML 的方法。
编码
通过实际的代码来说明上述的内容,这里演示的是 TypeScript 项目代码,JavaScript 项目也是同样的,只要把类型忽略掉就是了。
这是代码的目录结构
utools-template-setting-ts
├── webpack.config.js
├── tsconfig.json
├── public
│ ├── logo.png
│ └── plugin.json
├── src
│ ├── css.ts
│ └── index.tsx
└── package.json
其中index.tsx
是插件的入口,里面定义了所有东西,其中的内容如下,需要解释的部分有一些注释:
import Nano, {Component, Fragment} from 'nano-jsx'
import {CustomCss, PureCss} from './css'
class Setting extends Component {
submit() {
alert('submit')
}
render() {
return (
<Fragment>
<head>
<title>Setting</title>
<style>{PureCss}</style>
<style>{CustomCss}</style>
// 这一行必须要有(在下面会详说)
<script src="index.js"/>
</head>
<body>
// 这一行也必须要有(在下面会详说)
<div id="root"/>
<form id="setting" class="pure-form pure-form-stacked">
<fieldset>
<legend>用户设置</legend>
<label for="stacked-email">邮箱</label>
<input
class="pure-input-1"
type="email"
placeholder="请输入邮箱"
/>
<label for="stacked-password">密码</label>
<input
class="pure-input-1"
type="password"
placeholder="请输入密码"
/>
<div class="form-button-group">
<button
type="submit"
class="pure-button pure-button-primary"
onclick={() => this.submit()}
>
保存
</button>
<button class="pure-button">取消</button>
</div>
</fieldset>
</form>
</body>
</Fragment>
)
}
}
// @ts-ignore
window.exports = {
'setting': {
args: {
enter(action, callback) {
utools.setExpendHeight(480)
Nano.render(<Setting/>, document.documentElement)
},
},
mode: 'none',
},
'index': {
args: {
enter(action, callback) {
document.getElementById('setting')?.remove()
callback([
{
title: 'Title',
description: 'Description',
icon: 'logo.png',
},
])
},
},
mode: 'list',
},
}
UI
如果只有干瘪的原生界面,那就远远称不上好看,因此在上述例子里引入了一个纯 CSS 的 UI 库PureCSS,用来美化界面,为什么选择纯 CSS 的库,主要是考虑到模板插件本来就是逻辑简单,体积小巧,再引入一些庞大的 UI 库,有点累赘,所以使用纯 CSS 库,引入 CSS 资源的方式,我采用直接硬编码到代码里面,因为这个使用 JSX 实现的页面并不在 webpack 的管理下,所以 CSS 文件没有办法被自动加载到页面里,所以使用硬编码引入会简单很多。
成果
最后完成的效果如下:
- 模板列表界面
这是模板插件默认的列表界面
- 设置界面
问题和兼容
上面代码中的 JSX 里面有两行代码写着必须要有,分别是 head 里的<script src="index.js"/>
和 body 里的<div id="root"/>
,这是为什么呢?
前言里提到,这个方案的原理是将页面代码替换掉,而我们又知道模板代码的本质是一个自定义插件,那么新的问题来了,将页面代码替换之后,原来模板插件的页面代码去哪了呢? 答案就是不见了,也因此带来了一个 bug: 以上述插件为例,在去掉上面那两行不能去掉的代码后,按Setting -> Esc -> Index
的顺序操作,会发现,Index 页面并不能正常得显示,显示的内容还是 Setting,尽管插件高度是 Index 的高度。
要说明这个 bug,首先要明白的是,模板插件里面的每个 Feature,都是同一个插件的不同入口,但本质上,不同的 Feature 打开的是同一个插件页面,只是根据 Feature 的不同显示了不同的内容,并没有任何页面上的切换,所以当我们在 Setting 里将页面替换之后,原来模板插件的页面和处理逻辑都被抛掉了,自然无法处理其他 Feature 进入时需要的内容展示。
解决方法也很简单,不要破坏原有的模板插件的结构和引入,在 mode 设置为 none 的情况下,模板插件不会显示内容,自然也不影响 Setting 页面的展示,在其他 Feature 的入口,将 Setting 页面隐藏起来,就不会影响到正常模板插件页面的显示了. 而上面的两行「必要代码」,其实就是原本模板插件的页面结构和引入,保留在我们自己定义的设置页面里,就可以让自定义的页面代码和模板插件的页面代码共存了。
结束
这就是在模板插件里面实现自定义设置界面的简单方式,具体的应用可以在插件市场搜索「书签与历史记录」插件体验(给个好评😛). 方案的核心是在 JavaScript 里直接替换掉原有的模板插件页面,达到自定义的效果,并通过 JSX 简化在 JavaScript 里写 HTML 的复杂程度,最终达到不错的效果,足以应付一般的使用场景。
文中提到的示例代码分为 JavaScript 版本和 TypeScript 版本,没有什么区别,看哪个用起来顺手罢了,代码提供在这里: 下载源码
希望你也能体验到通过写插件满足自己需求的乐趣