• 插件发布
  • [分享] 关于模板插件自定义设置界面的其中一种做法

前言

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 版本,没有什么区别,看哪个用起来顺手罢了,代码提供在这里: 下载源码

希望你也能体验到通过写插件满足自己需求的乐趣

优秀,被你玩出花了🌸

4 个月 后

想问下 preload.js是咋用的呢 node小白快哭了 样例代码没有p'reload所以完全不知道咋用

7 个月 后

maoyiba

这是一个使用 webpack 打包的项目源码,webpack 类比 maven,需要使用 webpack 将源码重新打包,打包的结果是 preload.js,项目源码并不能直接运行

解压源码后,以utools-template-setting-ts为例,在项目目录依次执行命令行

npm install
npm run build

在生成的 dist 目录里可以看到重新打包后的结果,这才是最终的插件目录

    5 个月 后

    https://files.catbox.moe/uu4tte.png
    如图,我以前买过会员,现在版本升级以后,显示我不是会员,要重新开会员

    leqq00

    首先重启 uTools 或重新登录 uTools 账号尝试一下,最近 uTools 账号自动退出导致很多人都出了问题;然后检查一下网络情况,uTools 的网络有没有被拦截;最后如果还是有问题,麻烦再 @ 我一下,我会和 uTools 官方一起查一下付款信息有没有问题。

    lanyuanxiaoyao 大佬,我使用你的模板尝试开发。但是引入第三方包node-fetch 却不能正常使用,fetch打印出来显示是module,而正常情况下,应该是f函数。

    // noinspection JSXNamespaceValidation
    
    const Nano = require('nano-jsx')
    const fetch = require('node-fetch')
    const { Fragment, jsx } = require('nano-jsx')
    const { PureCss, CustomCss } = require('./css')
    
    const SettingUI = () => {
      let submit = async event => {
        utools.dbStorage.setItem("username", document.getElementById('username').value)
        utools.dbStorage.setItem("password", document.getElementById('password').value)
        utools.showNotification('保存成功')
      }
    
      return jsx`
    <${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 id="username" class="pure-input-1"  placeholder="请输入邮箱" />
            <label for="stacked-password">密码</label>
            <input id="password" class="pure-input-1" placeholder="请输入密码" />
            <div class="form-button-group">
              <button type="submit" class="pure-button pure-button-primary" onclick=${event => submit(event)}>保存</button>
              <button class="pure-button"  onclick=${event => utools.outPlugin()}>取消</button>
            </div>
        </fieldset>
      </form>
    </body>
    </${Fragment}>`
    }
    
    window.exports = {
      'setting': {
        args: {
          enter(action, callback) {
            utools.setExpendHeight(480)
            Nano.render(jsx`${SettingUI}`, document.documentElement)
            document.getElementById('username').value = utools.dbStorage.getItem("username")
            document.getElementById('password').value = utools.dbStorage.getItem("password")
          }
        },
        mode: 'none'
      },
      'index': {
        args: {
          enter(action, callback) {
            document.getElementById('setting')?.remove()
            callback([
              {
                title: '新建工单',
                description: '默认科室信息科',
                icon: 'logo.png'
              }
            ])
          },
          search: (action, searchWord, callbackSetList) => {
            // 获取一些数据
            // 执行 callbackSetList 显示出来
            callbackSetList([
              {
                title: '新建工单',
                description: '默认科室信息科',
                icon: 'logo.png',
                text: searchWord
              }
            ])
          },
          select: async (action, itemData, callbackSetList) => {
    
           console.log(fetch)                  //Module
           fetch("http://xxxx")               //报错fetch not a fun
    
          },
          placeholder: "新建工单"
        },
        mode: 'list'
      }
    }

      梧桐叶纷飞

      我简单试了一下类似的代码,我在 ts 的模板里没有问题,js 的模板里有问题,那就应该 node-fetch 这个库的模块类型的问题,我之前没有用过 node-fetch,也许时 webpack 打包的时候需要额外的设置

      无论如何,如果你只是想要使用 fetch 的话,可以直接拥 fetch,也不用额外的三方库,Electron 没有跨域问题,浏览器的 fetch 就行了

      lanyuanxiaoyao
      感谢大佬的耐心解答,我发现 有道翻译 这个插件,他是用另一种方案实现的 设置界面 和 主程序界面。
      可以明显看出 其设置界面与 这种方案不同。 大佬有思路吗,可以指点下其如何实现的设置界面。
      1

      leqq00

      你还有其他付费插件吗?他们正常吗?因为这个插件不涉及第三方服务,都是直连 uTools 服务的,此外,在发生这个事情之前有做过其他操作吗?比如杀毒软件有没有拦截 uTools 的相关接口?

      闽ICP备18007474号