node.js react typescript 构建拖拽生成html文件项目

1,151 阅读3分钟

近期抽空写了了一个可拖拽生成html文件的项目,项目截图:

利用了html5拖拽的属性,拖拽的流程分为:

1.可拖拽元素:设置拖拽属性draggable=“true”;

2.可释放元素:设置ondrop事件,设置dragover事件,dragover的默认行为是拒绝接受任何被拖放的元素。因此,我们必须阻止浏览器这种默认行为。e.preventDefault();

例子:

<!DOCTYPE html><html lang="en"><head>    <meta charset="UTF-8">    <meta http-equiv="X-UA-Compatible" content="IE=edge">    <meta name="viewport" content="width=device-width, initial-scale=1.0">    <title>demo 演示</title>    <link rel="stylesheet" href="./a.css"></head><style>    body {        background: black;        display: flex;    }    * {        box-sizing: border-box;    }    #page {        width: 375px;        height: 750px;        overflow: auto;        margin: 30px;        background: #ddd;        padding: 20px;    }    #page div {        min-height: 10px;        outline: 1px solid red;        padding: 20px;        resize: both;        overflow: auto;    }    #page span {        outline: 1px solid blue;        padding-left: 10px;        padding-right: 10px;    }    #drag {        background: #aaa;        flex: 1;    }    .tag {        width: 120px;        border: 1px solid white;        padding: 0;    }    .tag .li {        list-style-type: none;        margin: 10px;        background: blue;        color: white;        text-align: center;        padding: 10px;        border-radius: 4px;        cursor: pointer;        display: block;    } </style><body>    <div id="page" ondragover="dragover(event)" ondrop='drop(event)'>     </div>    <div id="drag">        <div class="tag">            <div class='li' contenteditable="false"  draggable="true" ondragstart="dragstart(event)" id='id1'>                div            </div>            <span class='li' contenteditable="false"   draggable="true" ondragstart="dragstart(event)" id='id2' >                span            </span>        </div>    </div></body></html><script>    function dragover(e){        e.preventDefault();    }    function dragstart(e){        e.dataTransfer.setData('msg',e.target.id)    }function drop(e){    e.preventDefault();    e.stopPropagation();        var id =e.dataTransfer.getData('msg')        var node = document.getElementById(id).cloneNode(true);        node.id +='1'        node.draggable=false;        node.ondrop = drop;        node.ondragover = dragover;        node.setAttribute('contenteditable','true')        node.innerText=''        e.target.appendChild(node)    }</script>

开发环境

开发环境选用了

create-react-app typescript。

1.安装react:运行 npx create-react-app projectName。

2.打开项目文件,运行npm i react @types/react @types/react-dom安装react。运行npm run eject生成webpack配置文件,注销这行// .filter(ext => useTypeScript ||!ext.includes('ts'))

主界面设计

1.左边:html绘制区域

2.右边功能区域,分为主功能区、元素列表区、html元素属性设置区功能设计与实现

元素模型设计

每个元素本身不存储状态,只需接受处理父组件传参属性(样式、属性、方法等),需定义专属样式、专属属性、元素类型,比如基础元素div:

interface Type{   
    css:any;    
    attr:any;
    [props:string]:any
}
export class Div extends Component<Type>{   
render(){
   const {css,attr}:any = {...(this.props)}
   return(
      <div nDragOver={e=>{e.preventDefault();e.stopPropagation()}}
                 onDrop={e=>{e.stopPropagation();this.props.drop(this.props.id);}}
                 className='use-outline'
                 style={css}
                 onClick={this.props.onClick}
            >
          {attr.text !==""?attr.text:null}
          {this.props.children}
       </div>
  )
  }
  static label='div'
  //默认样式
  static css:any = {
     "text":"",
     "width":'100%',
     "minHeight":'20px'
  }
  //属性
  static attr:any={
      "text":""
  }}

元素属性面板设计

设置元素的属性面板,属性分为所有元素的基础css属性,和父组件传递的属性(样式属性和专属功能属性),属性修改时通知父组件修改状态,代码如下:

import './cssMode.css';
import { mode } from "./interface";
import { Component } from 'react'; 
//所有元素的基础样式
const d_mode:mode={
    'borderLeftWidth':'0px',
    'borderRightWidth':'0px',
    'borderTopWidth':'0px',
    'borderBottomWidth':'0px',
    'borderStyle':'none',    
    'borderColor':'none',
    'paddingLeft':'0px',
    'paddingRight':'0px',
    'paddingTop':'0px',
    'paddingBottom':'0px',
    'marginLeft':'0px',
    'marginRight':'0px',
    'marginTop':'0px',
    'marginBottom':'0px'
} 
export class CssMode extends Component{
    state:{css:any,attr:any,[prop:string]:any};
    props:any;
    constructor(props:any){
      super(props);
      this.props = props;
      let css= Object.assign({},props.css,d_mode,props.css);
      let attr = Object.assign({},props.attr)
      this.state={
            css,
            attr
      };
     this.changeAttr = this.changeAttr.bind(this);
     this.changeCss = this.changeCss.bind(this); }
  render(){
    const {css,attr} = this.state    
    return (
      <>
        <button onClick={this.props.delete}>删除</button>
        {
           Object.keys(attr).map((item:string,index:number)=>
                <div className='css-mode' key={index}>
                     <label>{item}</label>
                    <input type="text" value={attr[item]} onChange={(e)=>this.changeAttr(e,item)} />
              </div>)
        }
        <div style={{"borderBottom":"1px solid grey","margin":"20px 0"}}></div>
        {
             Object.keys(css).map((item:string,index:number)=>
                <div className='css-mode' key={index}>
               <label>{item}</label>
                <input type="text" value={css[item]} onChange={(e)=>this.changeCss(e,item)} />
                        </div>
                    )
          }
    </>
  )}
//修改样式
  changeCss(e:any,item:string){
    const css = {...(this.state.css)}
    css[item]=e.target.value
    this.setState({css},()=>{
     this.props.changeCss && this.props.changeCss(this.state.css);
    })  
  }  
   //修改属性
  changeAttr(e:any,item:string){
   const attr = {...(this.state.attr)}
    attr[item]=e.target.value
    this.setState({attr},()=>{
     this.props.changeAttr && this.props.changeAttr(this.state.attr);    
  })
 }
}

3. 元素列表注册与展示:在定义元素模板时设置了每个元素的标签、样式、专属属性这几个字段,根据这几个进行元素列表的注册与展示:

元素列表注册:定义了元素标签label、展示组件component、属性组件cssModey已经样式和属性

import * as El from './baseEl'
import { CssMode } from './cssMode';
let Ell:any={...El}const labels:any=[];
Object.keys(Ell).map((item:string)=>{
  const el = Ell[item];
  labels.push({
    label:el.label,
    type:el.label,
    component:el,
    cssMode:CssMode,
    css:el.css,
    attr:el.attr
  }) 
})

元素列表展示:

{
    list.map((li:any,index:number)=>
   <button
      key={index}
      draggable='true'
      onDragStart={()=>this.drag_start(li.type)}
  >     
  {li.label}     
  </button>)  
}

拖拽生成展示元素与展示元素状态管理

拖拽列表元素进可放入区域时会生成展示元素的id,并将拖拽元素的注册信息(元素标签、展示组件、属性面板组件、样式和属性)存入展示元素状态,页面根据展示元素的状态展示元素:

存入展示元素状态

drop(e:any){
   e.preventDefault();
   const {list,el} = this.state;
   let node = {...(list.filter((item:any)=>item.type === this.targetType)[0])};
   node.id = `el_${el.length}`;
   node.children=[]
   el.push(node);
   this.setState({      el    })
 }

页面展示

list.map((item:any,key:number)=>
    <item.component
        key={key}
        css={item.css}
        attr={item.attr}
        id={item.id}
        onClick={(e:any)=>
        {this.chooseEL(e,item)}}
       > 
       </item.component>
 )

5. 点击展示元素时,聚焦该展示组件,将该展示组件的属性传递给展示组件,调起该展示元素的属性展示面板,修改属性后,更新改展示元素的属性和样式,点击展示元素外区域取消属性面板的展示

聚焦展示元素并调起属性面板

async chooseEL(e:any,item:any){
    e.stopPropagation();
    await this.dropEl();
    //获取元素组件根节点
    const getNode=(el:any)=>{
       if(el.classList.contains('use-outline')){
        return el      
       }else{return el.parentNode}
    }
    let target = getNode(e.target);
    target.classList.add('el-focus');
    this.setState({
      target:item
    })
  }

属性面板组件与展示:样式修改时调用changeCss修改展示元素状态,属性修改时调用changAttr修改展示组件属性

{
  target?
  <target.cssMode  
    css={target.css}
    attr={target.attr}
    changeCss={(data:any)=>{this.setCss(data,target,'css')}}
    changeAttr={(data:any)=>{this.setCss(data,target,'attr')}}
    delete={()=>{this.delete(target)}} 
  />:null  
}

6.目标展示元素被拖入新元素时,将子元素状态存入目标元素的children(子元素),并更新展示

目标元素拖入子元素

elDrop(id:any,item:any){
   const {list,el} = this.state;
   const node = {...(list.filter((item:any)=>item.type === this.targetType)[0])};
   node.id = `${id}_${item.children.length}`;
   node.children=[];
   item.children.push(node);
   this.setState({  el })
}

修改目标元素展示用来展示子元素

render(){
    let {list,el,target} = this.state;
    const deepEl:any = (list:any)=>{
    return list.map((item:any,key:number)=>
      item.delete?null:
      <item.component
        key={key}
        css={item.css}
        attr={item.attr}
        id={item.id}
        drop={(id:any)=>{this.elDrop(id,item)}}
        onClick={(e:any)=>{this.chooseEL(e,item)}}
    >
    {deepEl(item.children)}
    </item.component>)
   }
   return (
     <div className="App" >
      <div id="page" onDragOver={e=>e.preventDefault()} onDrop={this.drop} onClick={this.dropEl}>
      {
            deepEl(el) 
      } 
      </div>

其它功能:加展示元素outline用于调试、删除展示元素等

实现编译、导出html

上述功能实现后,点击生成html文件,发送ajax请求,将展示元素的状态传给node.js处理,

1. 安装express服务

npm i express -S

2. 起服务,express收到接口请求后开始编译

const express = require('express');
const app = new express();
app.use(express.static(path.resolve(__dirname,'../static')));
 app.all('*', (req, res, next) => {
    res.header("Access-Control-Allow-Origin", "*");
    res.header("Access-Control-Allow-Headers", "X-Requested-With");
    res.header("Access-Control-Allow-Methods","PUT,POST,GET,DELETE,OPTIONS");
    res.header("X-Powered-By",' 3.2.1')
    res.header("Content-Type", "application/json;charset=utf-8");
    next();
});
//生成html文件接口请求
app.get('/build_file', (req, res,next) => {
    parseData.init(JSON.parse(req.query.msg))
    res.send({code:0});
}); 
app.listen('9090', () => {
    console.log(`Server running at http://localhost:9090/`);
});

3.处理展示元素状态生成css文件,根据转递过来的展示元素id生成样式class类名,展示元素的css属性生成css属性

//构建css    
build_css(list) {
   if(list.length == 0) return ''
   //css module大写转横杠
   const transStyle = str => {
       return str.replace(/([A-Z])/g, '-$1').toLowerCase();        
   }
   let cssMsg = fs.readFileSync(path.resolve(__dirname,'../src/modern/base.css'),'utf-8')
   list.map(item => {
     cssMsg += '.' + item.id + '{';
     let css = item.css;
     Object.keys(css).map(li => {
      cssMsg += transStyle(li) + ':' + css[li] + ';'
     })
     cssMsg += '} '
     cssMsg += this.build_css(item.children)
    })
    return cssMsg 
}

4.处理展示元素html标签与属性,根据每个元素模板的构成编译生成html文件

class HtmlBuild {
   static init(list){
      let msg = `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"<meta http-equiv="X-UA-Compatible" content="IE=edge">            <meta name="viewport" content="width=device-width, initial-scale=1.0">            <title>drag html</title>            <link rel="stylesheet" href="./index.css"></head><body>`
      list.map(li => {
        msg += HtmlBuild[li.type](li)
      })    
      return msg +`</body></html>`;
  }
  static div(item) {
    let msg = `<div class="${item.id}">${item.attr.text}`
    item.children.map(li => {
      msg += HtmlBuild[li.type](li)
    })
    msg += `</div>`   return msg + '\n';
  }
  static span(item) {
     return `<span class="${item.id}">${item.attr.text}</span>\n`
  }
  static img(item) {
     return `<img class="${item.id}" src="${item.attr.src}" />\n` 
 }  
 static listItem(item) {
    return`<div  class="list-item ${item.id}">
      <img src="${item.attr.img}" alt="" />
       <h6>${item.attr.title}</h6>
         <p>${item.attr.desc}</p>
     </div>\n` 
 }
 static detail(item) {
     let msg = `<div  class="detail-item ${item.id} "> 
        <img src=${item.attr.img} alt="" />
          <p class='name'>${item.attr.name}</p> 
            <div class='price'><span></span>${item.attr.price}</div> 
             <div class = 'mid'> 
               <span>产品详情</span>
                <div class="line"></div>
            </div>
            <div class="detail">`
      item.children.map(li => { 
         msg += HtmlBuild[li.type](li)
      })
       msg+=`</div></div>`
       return msg + '\n';  
   }
}

/dists文件夹下可看到生成的文件,index.html, 运行npm run build 可将编译功能界面打包,下次直接点开打包文件就可以了

github地址:github.com/nicole11223…