源码解读
准备工作
源码采用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.html、print.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文件!会影响最终打包的代码!
其它事项
- 开发阶段代码未降级!!需要使用较新的浏览器进行开发调试。设计器的源码保持与时俱进,所以很多语法都是新规范的写法
- 打包后,即
gulp dist之后的代码进行了降级,以适应大部分的国内浏览器,默认降级到es2019,可以在配置文件中进行降级配置。
Magix.View
提供区块化的方案,方便对页面进行无限拆分和自由组装功能。传送门:https://thx.github.io/magix/ 可以用
react或vue中的组件概念来理解magix view
生命周期
ctor方法
仅在对象实例化时调用一次,可在该方法内绑定事件及在销毁时做一些清理工作
该方法很少用,它的特点是在继承时,子类无法覆盖也无法阻止父类的ctor方法被调用,请参考下面的init方法
init方法
仅在对象实例化时调用一次,可在该方法内绑定事件及在销毁时做一些清理工作
在有继承时,子类可以覆盖父类的init方法,覆盖后父类的init方法将不再被执行,除非子类显式调用。
assign方法
接收外部(父view)传递进来的数据,在对像生命周期之间,该方法可能会被调用很多次。
在该方法内部处理接收到的外部数据,并做业务处理,不能在该方法内部使用异步,异步请放在render方法中
render方法
渲染view,可在该方法内部使用任何你想要的方式,或同步或异步获取等,组织好数据后渲染界面
数据和更新
set方法
magix采用js和html分离的方式进行开发,因此如果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
通常用于在ctor或init方法中,监听自身销毁时,做一些清理工作,如
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或<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也方便定位和修复。
这也是和团队一起,经过近十年无数个项目得到的开发经验。用目标单纯的工具或库,拒绝大包大揽。可能一键开发上手更快,但是出现问题就是一团麻。
模板语法
请阅读打包工具仓库里的说明:模板语法
设计器
数据
Magix.State是一个用于存放全局数据和带pubsub的一个对象,一共5个方法
- set用于存放全局数据
- get用于读取存放的数据
- on用于监听事件
- off用于取消事件的监听
- fire用于触发一个事件
desinger文件夹,包含整个设计器的基础功能,如标尺,历史记录、工具栏、剪切板等。
同时在该文件夹下,elements.ts、selection.ts,snap.ts,clipboard.ts,generic.ts为重点核心文件。这些文件对存放在Magix.State中的数据进行操作或修改,同时再用事件的方式派发出去。
除此之外的其它如elements元素目录或panels面板目录,这些目录下的文件则多以监听State中事件的变化,然后再取出自己需要的数据进行界面渲染。
这种方式是插件化的基础,把设计器基础功能与插件化的扩展功能分别开来,方便自由拆装。
代码阅读
一次性看懂是不现实的,先对整体的流程有一个概念,比如如何开发,如何打包。然后再逐层深入,看开发命令是怎么启动的,打包是怎么回事,我要修改的话大概会改在哪里,心里先有一个总览。
然后找到入口,跟随入口及关键函数,不需要过多的细致阅读,先总体过一遍,知道哪些区块或界面是在哪个地方被调用的。
designer目录是需要花时间看的。 elements可设计元素,先从简单的看起,比如基础的线条,矩形,圆等。看懂后可以看图片或文本元素,它们比基础的元素多了数据绑定,然后再看单元格元素,它的数据绑定就比单个元素的要复杂些。再就需要看容器元素,这个因为支持嵌套,所以也就更复杂些。最后再看数据表格,因为该元素即包含容器的功能,也包含普通元素的功能。至此元素的学习就结束了。
其它目录都是辅助性的,并不复杂,前期可以跳过不看。
阅读时可以先找到相应功能的入口,打上断点,边看边调试代码,这样效率会快一些。
查看State上绑定的事件和数据
可以在调试工具console面板中,输入以下代码
seajs.require('magix').State
查看State上绑定的事件
输入以下代码
seajs.require('magix').State.get()
查看State中存放的数据
当然,也可以使用设计器自带的资源面板,进行查看,如下图
新版已经统一放在调试面板里,以下功能请在调试面板中查看

Magix.State存放的数据及事件
数据
在tmpl/desinger/index.ts的init方法中,一次性把设计器中需要的数据全部在这里进行了初始化,大约在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对象
元素分类
目前设计器共提供了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来表示整个元素,而元素的内容区域则交给相应的元素来实现。
镂空元素的编辑区域全部属于自己,设计器不会在该区域处理键盘、鼠标、右键等事件。
designer文件解读
@:{get.props}
元素添加到设计器区时,设计器本身并不关心添加进来的是什么元素,有哪些属性。所以当元素添加到设计区中时,需要元素本身向设计器提供自己的默认属性都有哪些。
该方法用于返回某个元素被添加到设计区中时,默认或初始化的数据是什么样的,后续在设计器中所有的修改都是针对这份数据来的。
所有的元素返回的数据中,必须包含x,y,width,height这4个基础数据。
props
主要向属性面板提供如何修改@:{get.props}返回的数据。
例如: 某个元素,自身都有哪些属性出现在属性面板中,这些属性通过何种方式来修改。 比如x,y坐标信息,使用数字输入框来修改,比如颜色使用颜色组件来修改。
props提供了这样一份渲染和修改清单,供属性面板使用。
其它数据是向设计器提供缩放时、改变位置时、设计区渲染时,缩放哪些属性,都移动哪些点以及是否在设计区支持旋转、调整尺寸等信息。
界面细节
- 如何处理元素的拖动响应?
- 贴边滚动都有哪些细节需要处理?
- 如何设置全局鼠标样式?
- 元素从设计区向上拖有什么问题?
- 如何支持其它单位?
- 拉框选择的细节有哪些?
- 如何处理或合并同类型的历史记录?
- 拖动时按了快捷键如何解决?
- 元素是如何缩放的?
- 旋转后的元素需要注意哪些细节?
- 如何让元素只能向某一个方向拖动?
- 打印页面有哪些关键点?
以上请联系作者获取详细解答。
元素添加流程
我们可以在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里添加的。
有开源版本的嘛,目前看起来代码都是压缩的