report-designer icon indicating copy to clipboard operation
report-designer copied to clipboard

源码解读

Open xinglie opened this issue 5 years ago • 8 comments

准备工作

源码采用typescript开发,所以最好安装vscode编辑器。传送门:https://code.visualstudio.com/ 编译、打包采用nodejs,所以需要电脑上安装nodejs环境。传送门:https://nodejs.org/

微软window环境下安装、运行nodejs或安装npm上的包,如果有问题请先自行通过搜索引擎找找相关方案

report-designer使用gulp作为构建工具,使用anywhere作为web服务器。所以需要先全局安装这2个基础的工具。

npm i gulp -g
npm i anywhere -g

其中anywhere非必选,只要有web server能通过http的形式访问report-desinger文件夹下的html文件即可

npm无法使用时

当npm安装不上包或因网络问题速度很慢时,可采用淘宝提供的国内境像,使用cnpm来安装

传送门:https://developer.aliyun.com/mirror/NPM

如果cnpm安装有问题,请自行搜索解决如何安装和使用cnpm。

当安装好cnpm后,下面所有跟npm相关的命令均可换成cnpm 来执行。

目录结构

magix-composer

report-desinger使用magix作为底层框架,该工具是为了打包magix的view而引入

这是一个打包工具,了解即可,更多信息可参考:https://github.com/thx/magix-composer

源码中并不包含node_modules目录,需要通过命令行进入magix-compoer,然后在该目录下运行

npm i

安装依赖

report-designer-server

这是用于report-designer打印页面在服务端转换图片和pdf的服务端处理程序

源码中并不包含node_modules目录,需要通过命令行进入report-designer-server,然后在该目录下运行

npm i

安装依赖

仅在打印页面需要使用服务端转换图片、pdf及打印时需要启用该服务,平常开发如果不需要,则不需要启动该服务接口,具体使用详情可参考report-designer-server目录下的readme.md

report-desinger

这是整个设计器代码存放的目录

源码中并不包含node_modules目录,需要通过命令行进入report-designer,然后在该目录下运行

npm i

安装依赖

开发

需要打开2个命令行窗口,在report-designer目录下分别执行

npm run watch

以及

npm run server

来启动调试服务。

npm run server 背后是通过anywhere启动http服务的

anywhere会打开http://localhost:8888/,默认页面是index.html,该页面是最终发布的入口页面,所以需要手动改为http://localhost:8888/index-debug.html进入开发页面。

提示

修改tmpl下的代码,编译工具会实时编译到src目录,生成浏览器可执行的代码。

在开发模式下,可以时不时的关注下npm run watch这个命令行窗口的实时编译输出。magix-composer不但可以编译转换代码,同时也会对项目的整体代码质量进行检测,包括不限于:模板书写检测、样式使用检测、文件不存在提示、html中标签如何安全使用检测等一系列的代码校验、安全保护等。可自行根据提示做改进,该工具的每一个提示都是非常有用的,最好消除magix-composer所有提示后再进行最后的打包。

首次及后续代码修改编译时需要一定的时间,尤其是首次,需要在编译结束后再使用浏览器打开,否则浏览器会报文件找不到的错误

发布

运行

npm run dist

即会通过gulp运行dist任务,把代码最终打包到report-desinger/dist/目录下。把相应的index.htmlprint.html入口文件及dist下的文件部署到最终服务器即完成了发布。

整体流程

启动npm run watch任务(该任务用来实时编译tmpl目录下的代码到src目录下,生成可供浏览器加载执行的代码) -> 启动npm run server服务(该任务用来通过localhost:8888查看页面) -> 修改tmpl目录下的代码并保存 -> 刷新//localhost:8888/index-debug.html查看页面展示 -> 修改完成后结束watch任务,运行npm run dist进行打包 -> 通过//localhost:8888/index.html确认打包结果 -> 确定无问题后,结束server服务,发布index.html、print.html和dist下打包好的js代码即可

注意事项

执行了npm run dist将无法通过index-debug.html等带有-debug后缀的页面访问,设计目的是防止本应通过index.html查看打包后的效果,却不小心通过index-debug.html看的是开发版本,导致可能存在隐患。

重要事项!!!

请务必阅读report-designer目录下的README.md文件!会影响最终打包的代码!

其它事项

  1. 开发阶段代码未降级!!需要使用较新的浏览器进行开发调试。设计器的源码保持与时俱进,所以很多语法都是新规范的写法
  2. 打包后,即gulp dist之后的代码进行了降级,以适应大部分的国内浏览器,默认降级到es2019,可以在配置文件中进行降级配置。

xinglie avatar Nov 08 '20 00:11 xinglie

Magix.View

提供区块化的方案,方便对页面进行无限拆分和自由组装功能。传送门:https://thx.github.io/magix/ 可以用reactvue中的组件概念来理解magix view

生命周期

ctor方法

仅在对象实例化时调用一次,可在该方法内绑定事件及在销毁时做一些清理工作

该方法很少用,它的特点是在继承时,子类无法覆盖也无法阻止父类的ctor方法被调用,请参考下面的init方法

init方法

仅在对象实例化时调用一次,可在该方法内绑定事件及在销毁时做一些清理工作

在有继承时,子类可以覆盖父类的init方法,覆盖后父类的init方法将不再被执行,除非子类显式调用。

assign方法

接收外部(父view)传递进来的数据,在对像生命周期之间,该方法可能会被调用很多次。 在该方法内部处理接收到的外部数据,并做业务处理,不能在该方法内部使用异步,异步请放在render方法中

render方法

渲染view,可在该方法内部使用任何你想要的方式,或同步或异步获取等,组织好数据后渲染界面

数据和更新

set方法

magix采用jshtml分离的方式进行开发,因此如果js中的数据需要传递到html上展示使用,则需要使用该方法把数据传递进去,这样在html中才能使用,否则html中将不能使用js中的数据。

set方法仅仅把数据放进去,并不会更新界面,更新界面还需要再调用digest方法进行更新

get方法

获取通过set方法放入的数据

digest方法

如果数据有变化的话,则以异步的方式更新界面。

import Magix from 'magix';
export default Magix.View.extend({
    tmpl: '@:./index.html',
    render() {
        this.set({
            key: 'value'
        });
        this.digest();
        //以上代码等价于
        this.digest({
            key: 'value'
        });
    }
});

事件

destroy

通常用于在ctorinit方法中,监听自身销毁时,做一些清理工作,如

import Magix from 'magix';
export default Magix.View.extend({
    tmpl: '@:./index.html',
    init() {
        let msg = () => alert('you clicked');
        document.body.addEventListener('click', msg);
        this.on('destroy', () => {
            document.body.removeEventListener('click', msg);
        });
    },
    render() {
        this.set({
            key: 'value'
        });
        this.digest();
        //以上代码等价于
        this.digest({
            key: 'value'
        });
    }
});

dom事件

支持所有的dom事件和自定义事件(magix使用捕获阶段绑定事件,因此支持如

scroll或 error 事件),比如以click为例 html中
<button mx-click="test({p1:'a',p2:'b'})">test</button>

如果需要传递参数,则如上,仅支持以对象形式的参数传递。

js中

import Magix from 'magix';
export default Magix.View.extend({
    tmpl: '@:./index.html',
    'test<click>'(e) {
        let { params } = e;
        console.log(params.p1, params.p2);
    }
});

其中e是原始事件对象,在该对象上扩展了params用于存放html中传递的参数,扩展了eventTarget用于存放触发事件的dom对象 事件在html中写的mx-click和执行的方法名称test,在js中需要相应的存在test<click>函数,否则会不执行。

关于设计

不管是使用构建工具gulp或是框架magix,所有的过程和步骤通常一次调用只完成一件事件。这比起webpack或其它双向绑定框架看上去要稍微多写点代码,但是每一步都是有预期的、明确的目标,后续出了bug也方便定位和修复。

这也是和团队一起,经过近十年无数个项目得到的开发经验。用目标单纯的工具或库,拒绝大包大揽。可能一键开发上手更快,但是出现问题就是一团麻。

模板语法

请阅读打包工具仓库里的说明:模板语法

xinglie avatar Nov 08 '20 00:11 xinglie

设计器

数据

Magix.State是一个用于存放全局数据和带pubsub的一个对象,一共5个方法

  1. set用于存放全局数据
  2. get用于读取存放的数据
  3. on用于监听事件
  4. off用于取消事件的监听
  5. fire用于触发一个事件

desinger文件夹,包含整个设计器的基础功能,如标尺,历史记录、工具栏、剪切板等。 同时在该文件夹下,elements.tsselection.tssnap.tsclipboard.tsgeneric.ts为重点核心文件。这些文件对存放在Magix.State中的数据进行操作或修改,同时再用事件的方式派发出去。

除此之外的其它如elements元素目录或panels面板目录,这些目录下的文件则多以监听State中事件的变化,然后再取出自己需要的数据进行界面渲染。

这种方式是插件化的基础,把设计器基础功能与插件化的扩展功能分别开来,方便自由拆装。

代码阅读

一次性看懂是不现实的,先对整体的流程有一个概念,比如如何开发,如何打包。然后再逐层深入,看开发命令是怎么启动的,打包是怎么回事,我要修改的话大概会改在哪里,心里先有一个总览。

然后找到入口,跟随入口及关键函数,不需要过多的细致阅读,先总体过一遍,知道哪些区块或界面是在哪个地方被调用的。

designer目录是需要花时间看的。 elements可设计元素,先从简单的看起,比如基础的线条,矩形,圆等。看懂后可以看图片或文本元素,它们比基础的元素多了数据绑定,然后再看单元格元素,它的数据绑定就比单个元素的要复杂些。再就需要看容器元素,这个因为支持嵌套,所以也就更复杂些。最后再看数据表格,因为该元素即包含容器的功能,也包含普通元素的功能。至此元素的学习就结束了。

其它目录都是辅助性的,并不复杂,前期可以跳过不看。

阅读时可以先找到相应功能的入口,打上断点,边看边调试代码,这样效率会快一些。

查看State上绑定的事件和数据

可以在调试工具console面板中,输入以下代码

seajs.require('magix').State

查看State上绑定的事件

输入以下代码

seajs.require('magix').State.get()

查看State中存放的数据

当然,也可以使用设计器自带的资源面板,进行查看,如下图 新版已经统一放在调试面板里,以下功能请在调试面板中查看 image

xinglie avatar Nov 08 '20 01:11 xinglie

Magix.State存放的数据及事件

数据

tmpl/desinger/index.tsinit方法中,一次性把设计器中需要的数据全部在这里进行了初始化,大约在280行左右,可自行查看 除设计器需要的数据外,还有零星的其它一些数据,这里不再一一说明。

事件

设计器通过Magix.State向外派发事件,供其它面板或元素监听。以下是派发的核心事件列表

事件名 解释
@:{event#magix.busy} magix框架内部繁忙
@:{event#example.change} 变更设计区示例
@:{event#toolbox.drag.element.move} 顶部元素列表某个元素正在被拖动中
@:{event#drag.element.move} 正在拖着某个元素移动
@:{event#toolbox.add.element} 顶部元素列表通过单击添加元素
@:{event#toolbox.drag.element.drop} 顶部元素列表某个元素在拖动过程中被释放,即松开鼠标
@:{event#drag.element.stop} 某个被拖动的元素停止拖动,即松开鼠标
@:{event#stage.save.content} 保存设计区中的数据
@:{event#history.shift} 历史记录正在被撤销或重做
@:{event#history.status.change} 历史记录的状态发生变化,即有新的历史记录进来
@:{event#stage.elements.change} 编辑区中的元素发生变化,比如新增或z轴调整
@:{event#stage.page.change} 编辑区相关的属性发生变化
@:{event#stage.select.elements.change} 编辑区中选中的元素发生改变,即从a选到了b
@:{event#stage.select.element.props.change} 编辑区选中元素的属性发生了改变,比如宽或高被修改
@:{event#stage.size.change} 编辑区大小发生了改变
@:{event#stage.scale.change} 缩放发生了变化

未在清单上的事件,通常是做体验优化的。

完整事件列表及说明可查阅:report-designer/types/app.d.ts中的EventsOfDesigner对象

xinglie avatar Nov 08 '20 08:11 xinglie

元素分类

目前设计器共提供了5大类元素,相应的在tmpl/elements/目录下就存在着这5大类的基类文件。

2020-02-05 合并了镂空、容器和普通元素,目前只有3大类 2021-09-25 元素支持子目录分类,svg和流程图分别以子目录存在,3大类的基类文件分别在各自的目录里。

普通元素

elements/normal.ts

普通元素指在设计阶段,元素本身不响应鼠标或键盘事件,全部交与设计器处理,元素在这个过程中仅展示自己

svg元素

elements/svg/designer.ts

svg元素多以点来表示整个图形,这与普通元素通过left,top以及width,height来表示图形不同,因此针对svg元素抽象了svg/designer.ts这个基类。

同时在svg中多了路径、虚圆等设计时所需要的界面提示等功能。

流程图元素

elements/flow/designer.ts

流程图元素是svg元素与普通元素的结合,以svg中的图形为基础,结合普通元素支持按快捷键Space在元素自身上显示输入框的功能。

容器元素

hod.ts 已合并下线

容器元素允许其它元素放置在它的内部,因此它的整个功能均与其它元素不同,比如你无法直接在容器元素上按下拖动。所以它在左上角会有自己的icon,此icon表示了整个容器元素,可通过该icon进行拖动、右键等功能。

而容器元素的内容区域,则主要用来处理其它元素放入、移动、对齐、删除等操作。容器元素内容区域有自己的右键菜单。

镂空元素

hollow.ts 已合并下线

如富文本,把元素内部做为完整的编辑区交给第三方,设计器会处理键盘、鼠标、右键等事件不与中间镂空区域冲突。

所以镂空元素与容器元素类似,它也会在左上角显示自己的icon来表示整个元素,而元素的内容区域则交给相应的元素来实现。

镂空元素的编辑区域全部属于自己,设计器不会在该区域处理键盘、鼠标、右键等事件。

xinglie avatar Nov 09 '20 09:11 xinglie

designer文件解读

@:{get.props}

元素添加到设计器区时,设计器本身并不关心添加进来的是什么元素,有哪些属性。所以当元素添加到设计区中时,需要元素本身向设计器提供自己的默认属性都有哪些。

该方法用于返回某个元素被添加到设计区中时,默认或初始化的数据是什么样的,后续在设计器中所有的修改都是针对这份数据来的。

所有的元素返回的数据中,必须包含x,y,width,height这4个基础数据。

props

主要向属性面板提供如何修改@:{get.props}返回的数据。

例如: 某个元素,自身都有哪些属性出现在属性面板中,这些属性通过何种方式来修改。 比如x,y坐标信息,使用数字输入框来修改,比如颜色使用颜色组件来修改。

props提供了这样一份渲染和修改清单,供属性面板使用。

其它数据是向设计器提供缩放时、改变位置时、设计区渲染时,缩放哪些属性,都移动哪些点以及是否在设计区支持旋转、调整尺寸等信息。

xinglie avatar Nov 09 '20 09:11 xinglie

界面细节

  1. 如何处理元素的拖动响应?
  2. 贴边滚动都有哪些细节需要处理?
  3. 如何设置全局鼠标样式?
  4. 元素从设计区向上拖有什么问题?
  5. 如何支持其它单位?
  6. 拉框选择的细节有哪些?
  7. 如何处理或合并同类型的历史记录?
  8. 拖动时按了快捷键如何解决?
  9. 元素是如何缩放的?
  10. 旋转后的元素需要注意哪些细节?
  11. 如何让元素只能向某一个方向拖动?
  12. 打印页面有哪些关键点?

以上请联系作者获取详细解答。

xinglie avatar Nov 09 '20 10:11 xinglie

元素添加流程

我们可以在header中通过按下鼠标拖动或点击的方式进行添加元素,所以在header.ts里,使用@:{add.element}<mousedown>来接收鼠标在元素上的按下行为,在这个方法里,我们根据鼠标有没有移动来决定是使用拖动添加还是点击添加元素。

无论是拖动添加还是点击添加,我们都是在松开鼠标时处理,因此在松开鼠标时,我们会根据情况,比如未移动鼠标,则在触发@:{event#toolbox.add.element}事件时,传递不同的参数。

header.ts里,我们仅触发@:{event#toolbox.add.element}事件,然后在stage.ts里监听该事件,然后再由stage.ts调用elements.ts里的方法进行最终元素的添加。

这里通过事件的方式进行通讯连接,在阅读代码上确实会产生“断层”的问题,不过好处是当需要在别的地方添加元素时,只需要派发@:{event#toolbox.add.element}事件即可,比如流程图元素进行连线时,要添加一个连接线元素只需要派发@:{event#toolbox.add.element}事件即可,即并不是所有元素都由header.ts里添加的。

xinglie avatar Feb 23 '21 11:02 xinglie

有开源版本的嘛,目前看起来代码都是压缩的

waerly avatar Jul 07 '21 02:07 waerly