uml html doc,nodejs开发starUML插件总结

一、需求分析

starUML介绍

StarUML是一种创建UML类图,生成类图和其他类型的统一建模语言(UML)图表的工具

该工具使用nodejs+electron开发。目前官方插件功能支持导出html格式的文件,由首左侧导航菜单和右侧iframe嵌套页面组成,如下图:

2c4311d3a15b?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation

starUML导出的html页面

目前支持左侧多级菜单、右侧页面展示(包括标题 图片 表格等)

需要在starUML编辑界面中点击 文件 > 导出 > 导出html doc...功能,在指定目录下生成html文件包。

目前需要开发的是在starUML工具菜单中集成一个可以将UML图表导出为WORD格式的文件,尽可能还原原先的功能,包括:

左侧多级快捷导航菜单

将html页面转换为word格式页面,包括大小标题、描述、表格、图片

目的是为了更直观的展示内容,方便阅读,另外在WORD中可以进行编辑修改的功能。

二、可行性调研

生成word文件需要用到文件流读写,starUML平台使用nodejs混合桌面应用开发,所以选择nodejs来操作生成word,由于自己写word格式文件难度较大,所以需要选择一个第三方工具包处理生成word文件,nodejs生态圈还不是很大,工具较少,最早确定使用officegen工具进行word文件的生成

三、开发starUML插件

资料整理

插件开发模式

首先根据操作系统打开对应的目录

Mac OS X: ~/Library/Application Support/StarUML/extensions/user

Windows: C:\Users\AppData\Roaming\StarUML\extensions\user

Linux: ~/.config/StarUML/extensions/user

在此目录下创建你的插件文件夹

代码编写

进入到新创建的文件夹中,创建main.js文件

写入如下内容

/*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50, browser: true */

/*global $, define, app, C2S, md5 */

// 项目基于require.js的模块机制 包括以上全局模块可以使用

define(function (require, exports, module) {

"use strict";

// 引入一些全局提供的功能模块

var ExtensionUtils = app.getModule("utils/ExtensionUtils"),

NodeDomain = app.getModule("utils/NodeDomain"),

FileUtils = app.getModule("file/FileUtils"),

FileSystem = app.getModule("filesystem/FileSystem"),

Async = app.getModule("utils/Async"),

Repository = app.getModule("core/Repository"),

ProjectManager = app.getModule("engine/ProjectManager"),

Commands = app.getModule("command/Commands"),

CommandManager = app.getModule("command/CommandManager"),

MenuManager = app.getModule("menu/MenuManager"),

DiagramManager = app.getModule("diagrams/DiagramManager"),

Dialogs = app.getModule("dialogs/Dialogs"),

MetadataJson = app.getModule("metadata-json/MetadataJson");

// Officegen = app.getModule("officegen/Officegen");

// console.log(Officegen);

// 定义操作菜单路径

var CMD_FILE_EXPORT_WORD_DOCS = 'file.export.wordDocs';

// 注册自定义node模块

var hopeDomain = new NodeDomain("hope", ExtensionUtils.getModulePath(module, "node/HopeDomain"));

/**

* 写入自定义文件 html、txtd等

* @param filename 格式 - 文件路径/文件名.后缀名

* @param txt 文本内容

* @returns {RegExpExecArray}

* @private

*/

function _writeBinaryFile (filename, txt) {

console.log(filename);

console.log(txt);

return hopeDomain.exec("writeFile", filename, txt);

}

// 写入word文件

function writeDoc () {

hopeDomain.exec("writeDoc")

.done(function (res) {

// 返回执行结果

console.log('res', res);

}).fail(function (err) {

console.error("writeDoc-error", err);

});

}

// 注册软件顶部菜单栏

CommandManager.register("WORD Docs...", CMD_FILE_EXPORT_WORD_DOCS, writeDoc);

// 设置菜单 给 file->Export->下面新增一个`WORD Docs...`菜单

var menuItem = MenuManager.getMenuItem(Commands.FILE_EXPORT);

menuItem.addMenuDivider();

menuItem.addMenuItem(CMD_FILE_EXPORT_WORD_DOCS);

});

软件启动时候会自动加载自定义插件目录下的main.js文件

由于上面用到了自定义的nodejs模块 需要在插件目录中再创建一个node目录

目录下新建HopeDomain.js文件

2c4311d3a15b?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation

插件目录结构

// HopeDomain.js

/*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4,

maxerr: 50, node: true */

/*global */

(function () {

"use strict";

// 引入nodejs fs文件操作模块

var fs = require("fs");

// 引入html转word插件

var HtmlDocx = require('html-docx-js');

// 引入nodejs office文件生成库

var officegen = require('officegen')

// html片段

var html = '

标题1

'

/**

* 写入文件方法

* @param filename

* @param txt

* @returns {*}

*/

function writeFile (filename, txt) {

return fs.writeFileSync(filename, txt, { encoding: 'utf8' });

}

/**

* 写入word文件方法

* @returns {*}

*/

function writeDoc () {

// 使用html片段生成word文件流 并写入到文件中保存

var docx = HtmlDocx.asBlob(html, { orientation: 'landscape', margins: { top: 720 } });

return writeFile('/Users/feifei/Desktop/html-docs/hope-' + new Date().getTime() + '.docx', docx)

// officegen生成word文件方法 没有实现成功

// var docx = officegen('docx');

// var header = docx.getHeader().createP({ align: ('center') });

// header.addText('hoperun', { font_size: 8, font_face: 'SimSun' });

// header.addHorizontalLine();

// var pObj1 = docx.createP();

// pObj1.addLineBreak();

// pObj1.addLineBreak();

// pObj1.options.align = 'center';

// pObj1.addText('目录', {

// font_size: 20

// });

// var out = fs.createWriteStream('/Users/feifei/Desktop/html-docs/hope-' + new Date().getTime() + '.docx'); // 创建文件

// out.on('error', function (err) {

// });

// docx.generate(out, {

// 'finalize': function (written) {

// if (written === 0) {

// console.log('恭喜处理成功!');

// }

// },

// 'error': function (err) {

// }

// });

}

// 自定义node模块初始化方法

function init (domainManager) {

// 如果没有hope模块则创建自己的hope模块

if (!domainManager.hasDomain("hope")) {

domainManager.registerDomain("hope", { major: 0, minor: 1 });

}

// 注册nodejs与插件异步通信回调的方法

domainManager.registerCommand(

"hope", // domain name

"writeFile", // command name

writeFile, // command handler function

false, // this command is synchronous in Node

"Returns the total or free memory on the user's system in bytes",

[

{

name: "filename", // parameters

type: "string",

description: "file name"

},

{

name: "txt", // parameters

type: "string",

description: "txt data"

}

],

[

{

name: "result", // return values

type: "string",

description: "result"

}

]

);

domainManager.registerCommand(

"hope", // domain name

"writeDoc", // command name

writeDoc, // command handler function

false, // this command is synchronous in Node

"Returns the total or free memory on the user's system in bytes",

[],

[]

);

}

exports.init = init;

}());

开发插件目前只实现了用html转word文档的插件html-docx-js将html文档转为word文件,无法定制化,word内容根据html格式生成且样式不太美观

之前选择的officegen插件在这里不能使用 可能因为软件nodejs版本较低不支持

最终没有达到理想的效果,放弃了这种方式

最后总结一下在开发插件中遇到的一些问题

工具支持插件开发的dev模式,可以刷新插件,重新加载,但是经常刷新了还是看不到修改代码后的效果,最后发现需要重启软件才有效果,因此走了不少弯路,模式比较麻烦

nodejs模块是异步执行回调的 插件中代码可以调试 开发nodejs模块调试比较困难 因此无法看到officegen运行出错的信息

参考一些现有的插件,原生node部分开发难度比较大,还有starUML图表数据解析转化比较复杂

四、开发nodejs html转word工具

由于开发starUML插件不是很理想,所以选择了另外一种方式,在工具自带的转换html格式的文档之后再进行操作,将html文件转换为word文件,不需要处理图表数据,从现成的html文件中取数据

准备工作

html文档目录&文件分析

按照文档结构生成word文档目录

图表svg格式图片处理嵌入

html页面转换为word格式

所用到的第三方库简介

cheerio jquery核心功能的一个快速灵活而又简洁的实现,主要是为了用在服务器端需要对DOM进行操作的地方,一般用在nodejs爬虫功能的开发中。

officegen 模块可以为Microsoft Office 2007及更高版本生成Office Open XML文件。此模块不依赖于任何框架,您不需要安装Microsoft Office,因此您可以将它用于任何类型的JavaScript应用程序。输出也是流而不是文件,不依赖于任何输出工具。此模块应适用于支持Node.js 0.10或更高版本的任何环境,包括Linux,OSX和Windows。

此模块生成Excel(.xlsx),PowerPoint(.pptx)和Word(.docx)文档。 Officegen还支持带有嵌入数据的PowerPoint本机图表对象。

svg2png 一个可以将svg文件转为png格式图片的小工具 使用 pn 模块中提供的文件操作功能

pn全称node-pn,由于早期版本nodejs不支持promise,pn将nodejs常用api的方法全部转化为promise实现,方便进行异步操作。(目前node开发环境支持es6语法,svg2png插件基于这个库实现的)

图片处理

由于部分html页面中包含图片资源,处理模板时候,图片处理流程不好控制,所以单独先将图片文件夹中的svg文件转换为png格式的图片

/**

* 处理图片文件夹的方法

* @param dirname 文件夹名称

* @param url

* @returns {Promise}

*/

function parseImg (dirname) {

return new Promise((resolve, reject) => {

//根据文件路径读取文件,返回文件列表

fs.readdir(dirname, function (err, files) {

let svgArr = []; // 图片数组

if (err) {

console.log('\n未发现图片目录'.red);

resolve();

} else {

//遍历读取到的文件列表

files.forEach(function (filename, index) {

svgArr.push(filename);

let fileUrl = path.join(dirname, filename);

if (filename.split('.')[ 1 ] === 'svg') {

fsP.readFile(fileUrl)

.then(svg2png)

.then(buffer => {

fsP.writeFile(fileUrl.replace('svg', 'png'), buffer).then(() => {

resolve(svgArr)

})

})

.catch(e => reject(e));

}

});

}

});

})

}

模板处理

首先创建word文件流,在文件流中添加内容,类似富文本编辑器,先获取所有html内容集合,根据标题创建目录和链接,跟下面每个页面对应上,可以从目录点击跳转对应页面,当页word内容填充完毕创建文件流,由officegen插件生成最终的word文件,其中获取所有文件(fileDisplay),目录生成(parseLink),单个html内容解析(parseHtml)使用单独方法处理,下面单独讲解。

/**

* 开始模板处理流程

* @param path 当前html文件夹路径

* @returns {Promise}

*/

async function start (path) {

// 指定生成文件为word格式

const docx = officegen('docx');

// 获取到所有html文件内容的集合

const htmlMap = await fileDisplay(path + '/contents');

// 创建页眉

const header = docx.getHeader().createP({ align: ('center') });

header.addText('hoperun', { font_size: 8, font_face: 'SimSun' });

header.addHorizontalLine();

// 创建目录页面

let pObj1 = docx.createP();

pObj1.addLineBreak(); // 换行

pObj1.addLineBreak();

pObj1.options.align = 'center';

pObj1.addText('目录', {

font_size: 20

});

pObj1.addLineBreak();

pObj1.addLineBreak();

// 创建目录每一行的标题

htmlMap.map((item, index) => {

let pObj = docx.createP();

parseLink(pObj, item, index)

})

docx.putPageBreak();

// 处理单个html页面的模板

htmlMap.map((item, index) => {

parseHtml(docx, item, index, process.cwd()); // process.cwd() 当前nodejs命令执行的文件夹路径

})

const out = fs.createWriteStream(path + '/out.docx'); // 创建文件

out.on('error', (err) => {

console.log(err);

});

// 生成word文件

docx.generate(out, {

// 完成

'finalize': function (written) {

if (written === 0) {

console.log('');

}

},

'error': function (err) {

console.log(err);

}

});

}

1、获取所有html文件内容 - fileDisplay 方法实现

从导航栏html文件中,解析所有的超链接,得到有序的html页面名称,根据页面名称读取对应html文件,将页面内容,文件名,文件类型集合返回。

//文件遍历方法

function fileDisplay (filePath) {

return new Promise((resolve, reject) => {

// 菜单文件路径

let menuUrl = path.join(filePath, 'navigation.html');

// 读取菜单html内容

let menu = fs.readFileSync(menuUrl, 'utf-8');

// 解析html

const $menu = cheerio.load(menu);

// 拿到所有的超链接

const files = $menu('#navigation-tree a');

if(!files.length) {

console.log('\n处理失败,未发现html文件,请检查导出目录文件是否齐全');

}

let contArr = [];

//根据文件路径读取文件,返回文件列

files.each(function (index, item) {

let filename = $menu(this).attr("href"); // 文件名

let type = $menu(this).siblings("span").attr('class'); // 文件类型

//获取当前文件的绝对路径

let filedir = path.join(filePath, filename);

// 读取文件内容

let content = fs.readFileSync(filedir, 'utf-8');

const $ = cheerio.load(content);

contArr.push({

html: $('body').html(),

title: $menu(this).text(),

type: type ? type.replace('node-icon ', '') : 'tit'

});

resolve(contArr)

});

})

}

2、目录生成 - parseLink 方法实现

由于标题没有层级关系,所以加上字符缩进还有符号美化一下,另外给每个标题加上超链接标识,方便跳转至对应页面

// 根据类型添加不同符号、层级缩进空格 美化菜单

const textArr = {

'tit': ' ★ ',

'_icon-UMLModel': ' ▸ ',

'_icon-UMLComponentDiagram': ' ☆ ',

'_icon-UMLComponent': ' + ',

'_icon-UMLOperation': ' ● ',

'_icon-UMLDependency': ' ↑ ',

'_icon-UMLUseCaseDiagram': ' ☆ ',

'_icon-UMLUseCase': ' + ',

'_icon-UMLAttribute': ' ● ',

'_icon-UMLInteraction': ' + ',

'_icon-UMLSequenceDiagram': ' ☆ ',

'_icon-UMLLifeline': ' ● ',

'_icon-UMLMessage': ' ● ',

'_icon-UMLAssociation': ' + ',

'_icon-UMLAssociationEnd': ' ● ',

'_icon-UMLInclude': ' ● ',

'_icon-UMLActor': ' + '

}

// 格式化标题内容

function getTitleText (item) {

return textArr[ item.type ] + item.title

}

/**

* 解析菜单

* @param p 文本容器

* @param item 单个页面对象

* @param index 索引

*/

function parseLink (p, item, index) {

p.addText(getTitleText(item), {

color: item.type === 'tit' || item.type === '_icon-UMLModel' ? '#333' : '#050cff', // 文本颜色

font_face: 'Arial', // 字体

font_size: item.type === 'tit' ? 15 : 12, // 字号

hyperlink: 'hoperun' + index // 锚点跳转标识

});

}

3、html内容解析 - parseHtml 方法实现

解析html部分比较复杂,要分析html的dom结构,使用jquery选择器取到对应数据,包括标题,描述,表格,图片,锚点的添加。

/**

* 解析html文件内容

* @param docx doc对象

* @param item 页面对象

* @param index 索引

* @param doc_url html文档目录

*/

function parseHtml (docx, item, index, doc_url) {

// 解析页面html内容

const $ = cheerio.load(item.html);

// 创建标题

let pObj = docx.createP();

// 锚点开始

pObj.startBookmark('hoperun' + index);

pObj.options.align = 'center';

pObj.options.pStyleDef = 'Heading1';

let title = $('h1').eq(0).text();

// 处理内容不恰当的标题

if (title === '(unnamed)' || title === '/') {

title = $('section').eq(1).find('a').last().text().replace('/:', '');

}

pObj.addLineBreak();

pObj.addText(title, { font_size: 24, font_face: '', bold: true, underline: true });

pObj.addLineBreak();

pObj.addLineBreak();

// 创建描述

let pObj1 = docx.createP();

pObj1.options.align = 'left';

pObj1.addText('Description', { font_size: 24, font_face: '', bold: true, underline: true });

$('h3').each(function (index, item) {

if ($(this).text() === 'Description') {

let pObjd = docx.createListOfDots();

if ($(this).next().find('li').length > 0) {

$(this).next().find('li').each(function () {

pObjd.addText($(this).contents().filter(function (index, content) {

return content.nodeType === 3;

}).text());

})

} else {

pObjd.addText('none');

}

}

})

// 创建图片

if ($('img').length >= 1) {

let p = docx.createP();

p.addText($('img').length === 1 ? 'Diagram' : 'Diagrams', {

font_size: 24,

font_face: '',

bold: true,

underline: true

});

// 替换图片地址

$('img').each(function (item, index) {

p.addLineBreak();

let url = $(this).attr('src').replace('svg', 'png').replace('../', './')

p.addImage(path.resolve(doc_url, url));

p.addLineBreak();

})

}

// 创建表格

if ($('table').length > 0) {

let table = [];

let trs = $('table').eq(0).find('tr');

trs.each(function (i) {

let tds = [];

if (i === 0) {

$(this).children('th').each(function (j) {

tds.push({

val: $(this).text(),

opts: {

cellColWidth: 4261,

b: true,

sz: '48',

shd: {

fill: "7F7F7F",

themeFill: "text1",

"themeFillTint": "80"

},

fontFamily: "Avenir Book"

}

})

})

} else {

$(this).children('td').each(function (j) {

tds.push($(this).text());

})

}

if (tds) {

table.push(tds);

}

})

var tableStyle = {

tableColWidth: 4261,

tableSize: 24,

tableColor: "ada",

tableAlign: "left",

tableFontFamily: "Comic Sans MS",

borders: true

}

if (table[ 0 ].length > 1) {

let pObj = docx.createP();

pObj.addLineBreak();

pObj.addText('Properties', { font_size: 24, font_face: '', bold: true, underline: true });

pObj.addLineBreak();

docx.createTable(table, tableStyle);

}

}

// 锚点结束

pObj.endBookmark();

// 换页

docx.putPageBreak();

}

流程整理

将图片处理跟模板解析集成到一起

/**

* 功能入口函数

* @param path 当前命令执行的目录(html-docs文件夹目录)

* @returns {Promise}

*/

async function office (path) {

// 首先处理图片

let res = await parseImg(path + '/diagrams');

// 在图片处理完成之后再处理html模板,影响解析html时候图片资源的嵌入

setTimeout(() => {

start(path);

}, 3000)

}

五、开发nodejs命令行工具

开发过程中是将nodejs代码直接写在html-docs文件夹中的,不方便使用,需要将功能跟html文件分离,所以选择开发一个简单的nodejs全局命令行工具,只需在需要转换的目录下执行一个命令就可以处理好。

准备工作

需要用到的几个插件介绍

commander node.js命令行界面的完整解决方案,受Ruby Commander启发。

inquirer 一个用户与命令行交互的工具,比如npm init,脚手架工具生成项目,项目没有用到,开发命令行工具必备小工具。

colors colors.js 是一个用于 node.js 终端 console.log 的颜色库 美化命令行。

single-line-log nodejs命令行的小工具,可以在同一行输出不用点的内容,可以做文本动态显示,进度条。

目录分析

2c4311d3a15b?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation

脚手架工具目录

bin 目录中放可执行命令文件

lib 目录中放插件包 比如html转word的工具就放到这里作为一个插件

index.js文件为入口 只需将lib目录导出

module.exports=require('./lib')

package.json 文件为项目元数据信息,作为命令行工具需要增加bin字段,如图上hoperun为全局可执行命令的名称,对应的值为bin下面对应的执行文件

bin/office.js

#!/usr/bin/env node

const program = require('commander');

const office = require('../lib/html-doc/office')

const inquirer = require('inquirer');

// 初始化配置选择项

const initQuestions = [ {

type: 'list',

name: 'plattype',

message: '请选择平台类型?',

choices: [

'pass',

'sass',

'iaas'

]

},

{

type: 'list',

name: 'vmCounts',

message: '请选择您包含的虚拟机数量?',

choices: [ '100', '200', '500', '1000' ]

}

];

// 登录命令输入项

const loginQuestions = [ {

type: 'input',

name: 'username',

message: '请输入用户名',

},

{

type: 'password',

name: 'password',

message: '请输入用户密码'

}

];

// 定义版本和参数选项

program

.version('v' + require('../package.json').version, '-v, --version')

.description('nodejs 命令行工具')

.option('-s, --star', 'starUMl生成word文档功能')

.option('-g, --generate', '生成xxx')

.option('-l, --login', '登录');

// 必须在.parse()之前,因为node的emit()是即时的

program.on('--help', function () {

console.log(' Examples:');

console.log('');

console.log(' this is an example');

console.log('');

});

program.parse(process.argv);

// 如果输入的命令是star话执行office方法,将当前命令执行的目录地址传入工具进行处理

if (program.star) {

office(process.cwd())

}

if (program.generate) {

inquirer.prompt(initQuestions).then(result => {

console.log("您选择的平台类型信息如下:");

console.log(JSON.stringify(result));

})

console.log('generate something')

}

if (program.login) {

inquirer.prompt(loginQuestions).then(result => {

console.log("您登陆的账户信息如下:");

console.log(JSON.stringify(result));

})

}

// if (program.args.length === 0) {

// program.help()

// }

六、项目优化

由于项目没有用户界面,只能在命令行操作,等待处理图片,解析html,生成word时候添加一些log日志,提示信息的比较好,所以选择了一些命令行优化工具,比如同行打印,文本颜色。

在office转换工具中,增加了一些错误处理,逻辑判断,保证在工具运行前,运行中都能将交互内容进行输出,让工具使用中更加人性化。

简单几个示例

目录检测

let checkDir = fs.existsSync(path + '/contents');

let checkNav = fs.existsSync(path + '/contents/navigation.html')

if (!checkDir || !checkNav) {

console.log((`\n当前目录${path}\n请在starUML生成的html目录下执行该命令`).red);

return

}

处理用时

// 生成word文件

docx.generate(out, {

// 完成

'finalize': function (written) {

if (written === 0) {

console.log('');

console.log('');

console.log('-----------------------------------------');

console.log('恭喜处理成功!');

console.log(('用时:' + ((new Date().getTime() - start_time) / 1000).toString() + 's').green);

console.log('-----------------------------------------');

console.log('');

console.log('');

}

},

'error': function (err) {

console.log(err);

}

});

同行打印,颜色提示

console.log('\n开始生成word文件......'.green);

slog(`正在处理第${index + 1}个文件...`);

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值