HBuilderX 插件开发指南(二):插件开发细节 及 代码演示

我们在 HBuilderX 插件开发指南(一) 中已经了解了 插件从开发到发布的细节,本期文章重点 将具体学习 插件开发需要注意的细节

项目结构

├── README.md  // 插件文档
├── changelog.md // 插件更新日志
├── extension.js // 插件入口文件
├── node_modules
├── package.json //  插件配置文件
└── src
    ├── html.js // 界面
    ├── main.js // 主要逻辑
    └── static // 依赖的静态资源

package.json (摘取 官方文档)

所有的插件在根目录都要有一个package.json文件,该文件继承npm规范,并扩展了部分字段,以下列出各个字段的含义:

字段名称类型是否必须描述
nameString插件名称
displayNameString用于展示在插件列表中的名称
versionString插件版本号,检查升级时会用到
enginesObject该Object的属性至少要包含HBuilderX,属性值为兼容的主版本号,如果HBuilderX的版本低于该版本,将会提示用户升级HBuilderX。例如:{“HBuilderX”:“^2.7.0”}。
descriptionString简短的插件描述,不要超过30个字
mainString插件代码入口文件,配置型插件可不填
activationEventsArray激活事件的列表,如为空,则表示该插件不会懒加载
contributesObject插件的配置扩展点
extensionDependenciesArray该插件依赖的其他插件id

创建插件项目,默认会有activationEvents 字段,如果你开发的 使用度很高, 个人建议不要懒加载机制,删除此字段即可。

package.json 示例

这是一个 文章(一) 中 用到的一个简单示例,在 编辑器右键菜单 注册了一个 demo新命令,点击此选项 将在窗口右下角弹窗一个消息通知框

// package.json

{
	"id": "ide-demo",
	"name": "ide-demo",
	"description": "针对HBuilderX 开发的demo",
	"displayName": "ide-demo",
	"version": "2.0.0",
	"publisher": "your name",
	"keywords":[
        "ide"
    ],
	"engines": {
		"HBuilderX": "^3.6.2"
	},
	"categories": [
		"Other"
	],
	"main": "./extension",
	"activationEvents": [
		"onCommand:extension.helloWorld"
	],
	"contributes": {
		"commands": [{
			"command": "extension.helloWorld",
			"title": "demo新命令"
		}],
		"menus": {
			"editor/context": [{
					"command": "extension.helloWorld",
					"group": "z_commands",
					"when": "editorTextFocus"
				},
				{
					"group": "z_commands"
				}
			]
		}
	},
	"extensionDependencies": [
		"plugin-manager"
	],
	"dependencies": {}
}

配置扩展点 contributes(重点)

commands 声明命令

commands 扩展点用于声明一个命令命令可以通过menus扩展点和菜单关联到一起

// 注意menus 第4个对象,当属性title和command都为空时,将被识别为分割线
{
	....
	
	"main": "./extension",
	"contributes": {
		"commands": [
			{
				"command": "extension.demo1",
				"title": "demo命令1"
			},
			{
				"command": "extension.demo2",
				"title": "demo命令2"
			},
			{
				"command": "extension.demo3",
				"title": "demo命令3"
			}
		],
		"menus": {
			"editor/context": [
				{
					"command": "extension.demo1",
					"group": "z_commands",
					"when": "editorTextFocus"
				},
				{
					"command": "extension.demo2",
					"group": "z_commands",
					"when": "editorTextFocus"
				},
				{
					"command": "extension.demo3",
					"group": "z_commands",
					"when": "editorTextFocus"
				},
				{
				 "group": "z_commands"
			    }
			]
		}
	},

 ....
}

效果演示
因为 menus 使用的是 editor/context(编辑器右键菜单),所以我们可以 在编辑器区域,右键单击

我们可以发现,菜单栏中 出现了demo命令1、demo命令2、demo命令3 选项

当属性titlecommand都为空时,将被识别为分割线 (demo命令3 下面的分割线)

在这里插入图片描述

menus 菜单(重点)

menus扩展点会关联一个命令到相应的菜单项里面,当菜单触发时将会执行对应的命令

menus节点下配置的对象内的key指的是要注册到哪个弹出菜单里面,详情参考

每个菜单内的group (重点)

每个弹出菜单内的group都不一样,下面列出所有的弹出菜单下可用的group,下边是图片,不可点击,具体文档查看这里~,配置参数都不相同,强烈建议亲自看文档

在这里插入图片描述
例如: 编辑器右键菜单 - editor/context~

  • copy
  • goto
  • copyPath
  • assist
  • z_commands

在这里插入图片描述

官方文档有详细的介绍,这里就不一一介绍了,我们来查看具体效果

代码

我们在编辑器-右键菜单、顶部菜单-文件、顶部菜单-编辑,3个地方分别添加了一个命令项

{
	"id": "ide-demo",
	"name": "ide-demo",
	"description": "针对HBuilderX 开发的demo",
	"displayName": "ide-demo",
	"version": "2.0.0",
	"publisher": "your name",
	"keywords":[
        "ide"
    ],
	"engines": {
		"HBuilderX": "^3.6.2"
	},
	"categories": [
		"Other"
	],
	"main": "./extension",
	"contributes": {
		"commands": [
			{
				"command": "extension.demo1",
				"title": "demo编辑器-右键菜单"
			},
			{
				"command": "extension.demo2",
				"title": "demo顶部菜单-文件"
			},
			{
				"command": "extension.demo3",
				"title": "demo顶部菜单-编辑"
			}
		],
		"menus": {
			"editor/context": [
				{
					"command": "extension.demo1",
					"group": "z_commands",
					"when": "editorTextFocus"
				},
				{
				 "group": "z_commands"
			 }
			],
			"menubar/file": [
				{
					"command": "extension.demo2",
					"group": "new",
					"when": "editorTextFocus"
				},
				{
				 "group": "new"
			 }
			],
			"menubar/edit": [
				{
					"command": "extension.demo3",
					"group": "undo",
					"when": "editorTextFocus"
				},
				{
				 "group": "undo"
			 }
			]
		}
	},
	"extensionDependencies": [
		"plugin-manager"
	],
	"dependencies": {
		"json-comments": "^0.2.1",
		"request": "^2.88.2"
	}
}

位置演示

demo编辑器-右键菜单

在这里插入图片描述
demo顶部菜单-文件

在这里插入图片描述
demo顶部菜单-编辑

在这里插入图片描述
到了这一步,我们关于命令配置,便算是完成了。

下面开始梳理插件代码逻辑相关的内容

插件代码说明

extension.js

extension.js是插件入口文件,插件入口文件必须有 activate 方法,该方法在插件激活的时候调用。

打印日志

插件打印日志功能一直很重要,在开发时需要调试打印,发布后插件各个节点进度打印,这里我们可以通过 console.log、console.errorhx.window.createOutputChannel 实现

老窗口:console.log、console.error
新窗口:hx.window.createOutputChannel (建议使用)

const hx = require('hbuilderx')

let wgtConsole = null
//该方法将在插件激活的时候调用
function activate (context) {
    let disposable = hx.commands.registerCommand('extension.demo1', param => {
		console.log('ide-demo插件---编辑器-右键菜单启动命令-----会在老窗口打印')
		
        wgtConsole = hx.window.createOutputChannel('ide-demo-console')
        wgtConsole.show()
        wgtConsole.appendLine('ide-demo插件---编辑器-右键菜单启动命令------会在新窗口打印')
			
        if (param === null) {
            wgtConsole.appendLine('请选中将要执行的项目!')
            return
        }

    })
    //订阅销毁钩子,插件禁用的时候,自动注销该command
    context.subscriptions.push(disposable)
}

//该方法将在插件禁用的时候调用(目前是在插件卸载的时候触发)
function deactivate () {

}

module.exports = {
    activate,
    deactivate
}


效果

老窗口
在这里插入图片描述

新窗口

在这里插入图片描述

识别文件,并弹窗显示

开发插件的 初衷(功能) 各不相同,我们这里以 读取manifest.json文件(识别功能),填写一些信息(表单功能),提交给后端(api交互) 为例

HBuilderX API

HBuilderX 插件教程

// extension.js
const hx = require('hbuilderx')
const showView = require('./src/main.js')
var fs = require('fs')

let wgtConsole = null
//该方法将在插件激活的时候调用
function activate (context) {
    let disposable = hx.commands.registerCommand('extension.demo1', param => {
        console.log('ide-demo插件---编辑器-右键菜单启动命令-----会在老窗口打印')
		
        wgtConsole = hx.window.createOutputChannel('ide-demo-console')
        wgtConsole.show()
        wgtConsole.appendLine('ide-demo插件---编辑器-右键菜单启动命令------会在新窗口打印')
			
        if (param === null) {
            wgtConsole.appendLine('请选中将要执行的项目!')
            return
        }
				
				
        let workspaceFolder = (param.document && param.document.workspaceFolder) || param.workspaceFolder
        const { uri,appid } = workspaceFolder
        wgtConsole.appendLine(`本次检测appid:${appid}`)
        wgtConsole.appendLine(`本次检测路径:${uri.fsPath}/manifest.json`)
        	
        // console.log(workspaceFolder,'------workspaceFolder')
        
        fs.readFile(uri.fsPath + '/manifest.json', function (err,bytesRead) {
          
        
        	if (err) {
        						wgtConsole.appendLine(`本次检测没有检测到manifest.json!请检验此路径是否正确${uri.fsPath}`)
        					 
        	} else {
        		 wgtConsole.appendLine('识别到manifest.json,正在打开发布窗口!')
						 
        		showView(workspaceFolder,bytesRead,wgtConsole)
        	}
        })
    })
    //订阅销毁钩子,插件禁用的时候,自动注销该command
    context.subscriptions.push(disposable)
}

//该方法将在插件禁用的时候调用(目前是在插件卸载的时候触发)
function deactivate () {

}

module.exports = {
    activate,
    deactivate
}

// main.js

const fs = require('fs')
const path = require('path')
const hx = require('hbuilderx')
const JSONC = require('json-comments')

const Html = require('./html.js')

var request = require('request')

let wgtConsole = null

let webviewDialog = null

/**
 * @description 打开视图
 * @param {Object} projectInfo 项目管理器选中的项目信息
 */
const showView = async (param = {}, bytesRead, wgtCon) => {

    wgtConsole = wgtCon
    var file = bytesRead.toString('base64')
    file = 'data:application/json;base64,' + file

    let info = {}

    // 创建webviewDialog, 并设置对话框基本属性,包括标题、按钮等
    webviewDialog = hx.window.createWebViewDialog({
        modal: true,
        title: 'ide-demo插件',
        description: '本插件功能包含:文件读取、权限授权、表单上传等功能;主要以演示为主,逻辑交互不会太严谨,大家后期根据诉求进行完善即可!',
        dialogButtons: ['提交', '关闭'],
        size: {
            width: 700,
            height: 600
        }
    }, { enableScripts: true })

    // 获取项目信息,并读取manifest.json
    let projectData = getProjectInfo(param)

    // 用于渲染对话框主要内容
    let webview = webviewDialog.webView

    webview.html = Html(projectData)

    webview.onDidReceiveMessage(msg => {
        let action = msg.command
        switch (action) {
        case 'closed':
            // 关闭对话框
            webviewDialog.close()
            break
        case 'submitApp':
            let data = msg.data

            // 设置对话框指定按钮状态
            webviewDialog.setButtonStatus('提交', ['loading', 'disable'])
            submitApp({
                ...data,
                file
            }, info)
            break
        default:
            break
        }
    })

    // 显示对话框,返回显示成功或者失败的信息,主要包含内置浏览器相关状态。
    let promi = webviewDialog.show()

    // info = await loginFn()

    promi.then(function(data) {
        console.log(data,'---------首次打开插件')
    })
}

// 授权
function loginFn() {
    wgtConsole.appendLine('正在获取权限信息,请务必点击授权!!')

    return new Promise(resolve => {
        webviewDialog.setButtonStatus('授权中...', ['loading', 'disable'])

        let prom = hx.authorize.login({
            // 插件授权 ID   https://open.dcloud.net.cn/
            client_id: 'nIDmsBQxkI',
            scopes: ['basic', 'email', 'phone'],
            description: '获取用户信息和手机号;注意点击拒绝需关闭窗口后,才能重新授权!!'
        })

        prom.then(async param => {
            let str = ''
            // 授权没通过
            if (param['error']) {
                let obj = {
                    '0':'无错误',
                    '1':'当前没有登录用户',
                    '2':'用户取消了授权(直接关闭窗口操作)',
                    '3':'插件已废弃',
                    '4':'插件状态异常',
                    '5':'用户拒绝授权(用户点击拒绝),需关闭窗口后,才能重新授权',
                    '1002':'服务器参数错误',
                    '2001':'应用信息不存在',
                    '3004':'超时',
                    '3203':'404'
                }
								
                str = `授权异常:${obj[param['error']] || param['error']}`
             
                wgtConsole.appendLine(str)
                webviewDialog.displayError(str)
                webviewDialog.setButtonStatus('提交', [])
                return
            }

            let obj = await getToken(param['code'])

            if (obj.err) {
                str = `获取token异常:${obj.err}`
                wgtConsole.appendLine(str)
                webviewDialog.displayError(str)
                webviewDialog.setButtonStatus('提交', [])
                return
            }

            let info = await getUserInfo(obj.access_token)

            if (info.err) {
                str = `获取用户信息异常:${info.err}`
                wgtConsole.appendLine(str)
                webviewDialog.displayError(str)
                webviewDialog.setButtonStatus('提交', [])
                return
            }

            webviewDialog.setButtonStatus('提交', [])

            resolve(info)
        })
    })
}

/**
 * @description 提交
 * @param {Object} appInfo
 * @param {Object} webviewDialog
 * @param {Object} manifest  
 */
async function submitApp(appInfo, manifest) {

    if (!manifest.data) {
        manifest = await loginFn()
        if (!manifest.data) return
    }

    let {
        appName,
        version,
        description
    } = appInfo

    if (!version || !appName) {

        webviewDialog.setButtonStatus('提交', [])

        // 在对话框副标题下方显示红色错误信息,错误信息会由动态抖动效果
        let emsg = '所有信息必填,不能为空'
        wgtConsole.appendLine(emsg)
        webviewDialog.displayError(emsg)

        return
    }

    console.log(manifest, '--------manifest')

    let data = {
        // acl: 'private',
        ...appInfo,
        publisher: manifest.data.phone,
        publisherName: manifest.data.nickname,
        description: description || manifest.data.nickname + '在 ' + getNowDate() + '  提交上传',
        timeStamp: new Date().getTime().toString().substr(0, 10)
    }

    uploadFn(data)
}

/**
 *  提交信息
 * @param {*} form
 */
async function uploadFn(form) {
    wgtConsole.appendLine('调用我司上传接口:https://xxx.xxx.com/api/form')

    request.post('https://xxx.xxx.com/api/form', {
        body: form,
        headers: {
            // 'content-type': 'multipart/form-data; ',
            'content-type': 'appliction/json'
        },
        json: true
    },
    (error, response, res) => {
        if (error) {
            webviewDialog.displayError('接口' + error)
            webviewDialog.setButtonStatus('提交', [])
        } else if (response.statusCode === 200) {
            wgtConsole.appendLine('上传成功')
            webviewDialog.close()
            // hx.window.showInformationMessage('提交成功', ['关闭'])

        } else {
            let { error } = JSON.parse((res))
            wgtConsole.appendLine(error)
            webviewDialog.displayError(error)
            webviewDialog.close()
            webviewDialog.setButtonStatus('提交', [])
        }

    })

}

/**
 * @description 获取项目信息,并读取manifest.json
 * @param {Object} projectInfo
 */
function getProjectInfo(projectInfo) {
    let data = {}

    data.projectName = projectInfo.name
    data.projectType = projectInfo.nature
    data.projectPath = projectInfo.uri.fsPath

    let manifestFilePath = path.join(data.projectPath, 'manifest.json')
    if (!fs.existsSync(manifestFilePath)) {
        manifestFilePath = path.join(data.projectPath, 'src/manifest.json')
    }

    if (['App', 'UniApp_Vue', 'Wap2App'].includes(data.projectType) && fs.existsSync(manifestFilePath)) {
        let manifest = fs.readFileSync(manifestFilePath, 'utf-8')
        try {
            let {
                name,
                description,
                versionName,
                appid,
                versionCode
            } = JSONC.parse(manifest)
            data.name = name
            data.description = description
            data.versionName = versionName
            data.versionCode = versionCode
            data.appid = appid
        } catch (e) {}
    }
    return data
}

function getToken(code = '') {
    return new Promise(resolve => {
        wgtConsole.appendLine('授权成功,调用登录接口 https://account.dcloud.net.cn/dcloudOauthv2/accessToken!')

        request({
            url: 'https://account.dcloud.net.cn/dcloudOauthv2/accessToken',
            method: 'POST',
            json: true,
            headers: { 'content-type': 'application/json' },
            body: {
                code,
                client_id: '插件client_id   文章一有申请流程',
                app_secret: '插件app_secret 文章一有申请流程'
            }
        }, (err, rep, body) => {
            if (err) {
                resolve({ err: 'post 获取token 错误 err : ' + err })

            } else if (body.ret === 0) {

                resolve(body.data)
            } else {
                resolve({ err: '错误:' + body.ret + '_' + body.desc })
            }
        })

    })
}

function getUserInfo(access_token = '') {
    return new Promise(resolve => {
        wgtConsole.appendLine('登录成功,调用信息接口 https://account.dcloud.net.cn/dcloudOauthv2/userInfo')

        request({
            url: 'https://account.dcloud.net.cn/dcloudOauthv2/userInfo',
            method: 'POST',
            json: true,
            headers: { 'content-type': 'application/json' },
            body: { access_token }
        }, (err, rep, body) => {
            console.log(err, body, '-------err, rep, body')
            if (err) {
                resolve({ err: 'post 获取信息 错误 err : ' + err })
            } else if (body.ret === 0) {
                resolve(body)
            } else {
                resolve({ err: '错误:' + body.ret + '_' + body.desc })
            }
        })

    })
}

// 格式化日对象
function getNowDate() {
    var date = new Date()
    var sign2 = ':'
    var year = date.getFullYear() // 年
    var month = date.getMonth() + 1 // 月
    var day = date.getDate() // 日
    var hour = date.getHours() // 时
    var minutes = date.getMinutes() // 分
    var seconds = date.getSeconds() //秒
    var weekArr = ['星期一', '星期二', '星期三', '星期四', '星期五', '星期六', '星期天']
    var week = weekArr[date.getDay()]
    // 给一位数的数据前面加 “0”
    if (month >= 1 && month <= 9) {
        month = '0' + month
    }
    if (day >= 0 && day <= 9) {
        day = '0' + day
    }
    if (hour >= 0 && hour <= 9) {
        hour = '0' + hour
    }
    if (minutes >= 0 && minutes <= 9) {
        minutes = '0' + minutes
    }
    if (seconds >= 0 && seconds <= 9) {
        seconds = '0' + seconds
    }

    return year + '-' + month + '-' + day + ' ' + hour + sign2 + minutes + sign2 + seconds
}

module.exports = showView

// html.js
const path = require('path')

const vueFile = path.join(path.resolve(__dirname), 'static', 'vue.min.js')
const utils = path.join(path.resolve(__dirname), 'static', 'utils.js')

const bootstrapCssFile = path.join(path.resolve(__dirname), 'static', 'bootstrap.min.css')
const customCssFile = path.join(path.resolve(__dirname), 'static', 'custom.css')

function Html (projectData) {
    projectData = JSON.stringify(projectData)
		
   

    return `
    <!DOCTYPE html>
    <html lang="en">
        <head>
            <meta charset="UTF-8">
            <link rel="stylesheet" href="${bootstrapCssFile}">
            <link rel="stylesheet" href="${customCssFile}">
            <script src="${vueFile}"></script>
            <script src="${utils}"></script>
        </head>
        <body>
            <div id="app" v-cloak>
                <form>
                    <div class="form-group row m-0 mt-3">
                        <label for="repo-type" class="col-sm-2 col-form-label">应用包名</label>
                        <div class="col-sm-10" >
                            <input type="text" class="form-control outline-none" v-model="appInfo.appid" placeholder="应用包名" disabled />
                        </div>
                    </div>
                    <div class="form-group row m-0 mt-3">
                        <label for="repo-type" class="col-sm-2 col-form-label">应用名称</label>
                        <div class="col-sm-10">
                            <input type="text" class="form-control outline-none" v-model="appInfo.name" placeholder="应用名称" disabled/>
                        </div>
                    </div>
                    <div class="form-group row m-0 mt-3">
                        <label for="git-url" class="col-sm-2 col-form-label">应用版本</label>
                        <div class="col-sm-10">
                           <input type="text" class="form-control outline-none" v-model="appInfo.versionName" placeholder="请输入版本号,例如:1.0.0" disabled/>
                        </div>
                    </div>
                    
                    <div class="form-group row m-0 mt-3">
                        <label for="repo-type" class="col-sm-2 col-form-label">应用描述</label>
                        <div class="col-sm-10">
                            <textarea type="text" v-model="appInfo.description" placeholder="请输入应用描述"></textarea>
                        </div>
                    </div>
										
                </form>
            </div>
            <script>
                Vue.directive('focus', {
                    inserted: function(el) {
                        el.focus()
                    }
                });
                var app = new Vue({
                    el: '#app',
                    data: {
                        appInfo: {
                            name: "",
                            appid: "",
                            versionName: "",
                            description: ""
                        }
                    },
                    async created() {
                        this.appInfo = ${projectData}
                    },
                    mounted() {
                        this.$nextTick(() => {
                            window.addEventListener('hbuilderxReady', () => {
                                this.btnClick();
                            })
                        });
                    },
                    methods: {
											btnClick() {
												
												hbuilderx.onDidReceiveMessage((msg)=>{
													if(msg.type == 'DialogButtonEvent'){
														let button = msg.button;
														if(button == '关闭'){
																hbuilderx.postMessage({
																		command: 'closed'
																});
														} else {
															hbuilderx.postMessage({
																	command: 'submitApp',
																	data: this.appInfo
															});
														}
													};
												});
											}
                    }
                });
            </script>
        </body>
    </html>
`
}

module.exports = Html

弹窗页面效果

在这里插入图片描述

授权弹窗效果

授权登录对接文档~
在这里插入图片描述

需要注意的地方

api接口

需要使用 request 与 json-comments

// package.json
...
"dependencies": {
	"json-comments": "^0.2.1",
	"request": "^2.88.2"
}

...


// main.js
let request = require('request')

request({
    url: 'https://account.dcloud.net.cn/dcloudOauthv2/accessToken',
     method: 'POST',
     json: true,
     headers: { 'content-type': 'application/json' },
     body: {
         code,
         client_id: '插件client_id',
         app_secret: '插件app_secret'
     }
 }, (err, rep, body) => {
    
 })


  • 10
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值