VVC
介绍
作者: sahadev
演示地址: vcc3.surge.sh/
使用它可以做什么
目前VCC集成了H5基本元素以及ElementUI绝大多数组件,所以可以通过VCC搭建常见的表格与表单页面。就像这样的:
组件库
第一章 源码解析之渲染
代码结构
{
"template": { //根节点
"lc_id": "root",
"__children": [
{
"div": { //组件名/标签名
"class": "container", //class样式
"style": "min-height: 100%; padding-bottom: 100px;", //内联样式
"lc_id": "container", //容器唯一ID
"__text__": "Hello,欢迎使用LCG,请往此区域拖拽组件", //文本内容
"__children": [ //子节点
{
"div": {
"__text__": "{{showText}}", //支持动态变量
"lc_id": "text"
}
},
{
"el-button": { //组件名/标签名
"lc-mark": "", //用于标记
"type": "danger", //组件的属性(prop)
"lc_id": "COAAYXizyI",
"__children": [],
"__text__": "{{showValue}}",
"@click": "hello", //支持绑定方法
"size": "small"
}
}
]
}
}
]
}
}
初始化
渲染容器
<div class="preview-container">
<div id="render-control-panel">
<!--渲染的区域-->
</div>
</div>
渲染处理类
MainPanelProvider
:封装的一个类,将所有页面上的处理逻辑全部在这个类里面
export default{
created() {
this.mainPanelProvider = new MainPanelProvider();
},
mounted(){
this.init()
},
methods:{
init() {
// 先订阅事件再渲染
this.mainPanelProvider
.onRootElementMounted(...) //加载完毕后执行
.onMerged(...) //代码合并后执行
.onCodeCreated(...) //代码生成后执行
.onCodeStructureUpdated(...)//代码结构更新后执行
.onNodeDeleted(...) //节点删除后执行
.onSelectElement(...) //选择某个节点后执行
.saveJSCodeOnly(...) //保存js代码合并后执行
.render( //执行渲染函数
this.initCodeEntity.codeStructure ?this.initCodeEntity.codeStructure : this.getFakeData());
},
}
}
render
步骤1:初始化了一个代码生成器,调用方法,将json转换为vue代码
this.codeGenerator = createNewCodeGenerator();
let code = this.codeGenerator.outputVueCodeWithJsonObj(rawDataStructure);
步骤2:执行正则替换格式化代码,去除多余的属性标记:如lc_id
let codeForShow = code.replace(/\s{1}lc_id=".+?"/g, '');
codeForShow = codeForShow.replace(/\s{1}lc-mark/g, "");
步骤3:调用解析函数,分别取出代码的 template style script内容,也就是上方的 里面的内容
const { template, script, styles, customBlocks } = parseComponent(code);
this.loadStyle(styles);
//通过 scope包, 模拟vue的样式隔离 为每个样式加上前缀来隔离样式 生成对应的文件,并且插入到页面头 用于后续的样式加载
function loadStyle(styles) {
if (styles.length > 0) {
const scopedStyle = styles[0];
this.styleNodeName = `cssScoped${Date.now()}`;// 拼接上前缀
const scopedCss = scope(scopedStyle.content.replace(/::v-deep/g, ''), this.styleNodeName);
const styleNode = document.createElement('style');
styleNode.innerText = scopedCss;
document.head.appendChild(styleNode);
}
}
步骤4:渲染
//通过Vue的compile方法编译 模板字符串 转换为Node节点
const res = Vue.compile(template.content);
//将字符串转换为一个函数
let newScript = script.content.replace(/\s*export default\s*/, "")
const componentOptions = (new Function(`return ${newScript}`))();
componentOptions.template = template.content;
componentOptions.render = function () {
const rootVNode = res.render.apply(this, arguments);
return rootVNode;
};
const readyForMoutedElement = this.createMountedElement(); //获取到渲染容器节点
// 渲染当前代码
new Vue(componentOptions).$mount(readyForMoutedElement);
function createMountedElement() {
const renderControlPanel = document.getElementById('render-control-panel');
if(this.styleNodeName) { //设置上面加载的隔离css的前缀
renderControlPanel.setAttribute('class', this.styleNodeName);
}
const child = document.createElement('div'); //创建一个节点用于挂载
// 清空子节点
while (renderControlPanel.firstChild) {
renderControlPanel.removeChild(renderControlPanel.firstChild)
}
renderControlPanel.appendChild(child);
return child;
}
生命周期触发
使用
eventemitter3
包,功能类似与 $bus 通过订阅,派发,在对应代码执行的过程派发对应的函数
渲染结束
第二章 源码解析之标注
当鼠标拖拽着元素时,悬浮在某个节点上 会计算放置的位置
标注1:辅助插入位置线
<!-- 准备一个容器用于修改left/top值 实现绘制辅助线 -->
<div class="cross-flag">
<div class="x"></div>
</div>
// onRootElementMounted 渲染完成后执行,参数为根节点元素
(rootElement)=>{
// 只针对根div做事件监听
initContainerForLine(rootElement.firstChild, this.currentPointer); //初始化根节点的事件
document.getElementsByTagName('body')[0].addEventListener("click", () => {
this.mainPanelProvider.clearElementSelect(); //用于清除辅助线
})
}
function currentPointer(ele,index){ //接收 initContainerForLine方法提供的元素信息,传递给mainPanelProvider类去接着处理
this.mainPanelProvider.setDropInfo({
target: ele,
index,
});
}
1:鼠标拖拽到元素上
function initContainerForLine(targetElement,_currentPointer = () => {}){
const currentPointer = (...args) => { //回调:传递信息给调用者
_currentPointer(...args);
};
const crossX = document.querySelector(".x");
targetElement.addEventListener("dragover", (event) => {
event.preventDefault();
drawLine(event);
});
...
}
//参数:元素,鼠标x坐标,鼠标y坐标
function judgeTopOrBottom(e, x, y) {
const position = e.getBoundingClientRect(); //浏览器API:获取元素的位置、大小信息
const cutDistance = Math.round((position.bottom - position.top) / 3); //将元素高度分为3块
return {
top: y < position.top + cutDistance, //顶部
middle: y >= position.top + cutDistance && y <= position.top + cutDistance * 2,//中间
bottom: y > position.top + cutDistance * 2,//底部
};
}
function drawLine(event){
const realTarget = event.target; //鼠标当前悬浮的元素
//计算 鼠标在容器的位置
const directionObj = judgeTopOrBottom(realTarget, event.clientX, event.clientY);
if (directionObj.top && targetElement !== realTarget) {
if (currentPostion === 'top' && currentTarget === realTarget) { //节流
return;
}
currentPostion = 'top';
currentTarget = realTarget;
if (preSelectTarget) { //清除悬浮中间的位置信息
preSelectTarget.classList.remove("in-element");
}
//设置辅助线的位置
crossX.style = `
top:${position.top}px;
width:${position.width}px;
left:${position.left}px;
display:block;
`;
currentPointer(realTarget.parentElement, findElementIndex(realTarget)); //将节点的父元素、节点在父元素子元素的第几个元素 传递给调用者
}else if(directionObj.bottom && targetElement !== realTarget){
//底部的处理逻辑
...
}else{
//中间的处理逻辑
currentPostion = 'middle';
currentTarget = realTarget;
realTarget.classList.add("in-element"); //添加一个样式,用于包住元素
preSelectTarget = realTarget;
crossX.style = `display:none;`;
currentPointer(realTarget, -1); //-1代表是该元素的子元素,外面有用
}
}
function clearTargetOutline() {
if (preSelectTarget) {
preSelectTarget.classList.remove("in-element");
}
}
2. 鼠标离开元素
function initContainerForLine(targetElement,_currentPointer = () => {}){
...
targetElement.addEventListener("dragleave", (event) => {
clearTargetOutline();
});
}
标注2:点击
点击的处理逻辑在 _render之后,也就是上一章所说的渲染完成后执行的,通过遍历所有渲染容器内的子节点,为其绑定事件实现剩下的逻辑
// 每个节点都有唯一的 lc_id,通过查找该元素,为其添加一个样式
function initComonentsEvent() {
const renderControlPanel = document.getElementById('render-control-panel');
const elements = renderControlPanel.querySelectorAll("[lc_id]");
elements.forEach(element => {
element.addEventListener("click", event => {
event.stopPropagation();
// 处理之前的状态
if (this.currentEditElement) { //清除上一个标记
this.currentEditElement.classList.remove("mark-element");
}
if (element) {
this.currentEditElement = element; //储存当前点击的节点
element.classList.add("mark-element"); //标记
// 触发元素选择生命周期,传递节点信息给外面
const rawVueInfo = findRawVueInfo(element); //作用:从json树中 找到该节点的信息
const codeRawInfoCopy = cloneDeep(rawVueInfo);
this.eventEmitter.emit("selected", codeRawInfoCopy);
} else {
this.eventEmitter.emit("selected", null);
}
})
...
})
...
}
第三章 组件库
自定义内容,
结构
通过直接将代码写入一个vue文件中,然后直接渲染,由于结构比较简单,就直接介绍拖拽原理。
优点:方便自定义
缺点:编写繁琐,需要带上lc_id标识
初始化
通过递归调用,获取到所有通过 lc_mark标记的所有节点,对所有节点进行初始化
function deepLCEle(rootElement) {
// 对所有含有lc-mark属性的Element实现可拖拽能力
function deep(ele) {
if (ele.attributes["lc-mark"]) {
// 统计标记组件数量
initElement(ele);
}
if (ele.children.length > 0) {
const length = ele.children.length;
for (let i = 0; i < length; i++) {
deep(ele.children.item(i));
}
}
}
deep(rootElement);
}
绑定拖拽信息
function initElement(element) {
element.draggable = true; //启动拖拽
// 给每个组件添加拖拽事件
element.addEventListener("dragstart", function (event) {
... //拖拽开始
});
// 处理组件标记
element.addEventListener("mouseenter", (event) => {
//鼠标进入
event.target.addList.add('mark-element-unit')
...
});
element.addEventListener("mouseleave", (event) => {
//鼠标离开
event.target.addList.remove('mark-element-unit')
...
});
}
拖拽开始
element.addEventListener("dragstart", function (event) {
event.dataTransfer.effectAllowed = "copy"; //设置虚化显示
const raw = generateRawInfo(element);//方法:将元素==>json信息 (方便后续处理)
const str = JSON.stringify(raw); //原本这里传递了一大堆信息,其实可以简化成只传节点的json数据
event.dataTransfer.setData("text/plain", str); //拖拽的属性,将信息设置在 dataTransfer上,在拖拽结束的节点去 绑定 drag事件 就可以拿到传递的数据
event.stopPropagation();
});