忽然间可视化拖拽的风如同在前端的各个角落吹起,本人也鼓捣了一下,代码根本开发结束,做一下整顿。

github我的项目地址:taro-designer

在线体验地址:taro-desiger

次要波及技术点如下:

  • 背景
  • 技术栈
  • 拖拽
  • 包装组件
  • 数据结构
  • 编辑器
  • 单个组件操作
  • 生成taro的源码
  • 预览和下载源码

背景

公司有一部分业务是做互动的开发,比方签到、礼品兑换等。因为互动的业务须要疾速迭代,并且须要反对H5、微信小程序、以及淘宝小程序,因而前端采纳了taro作为根底框架来满足多端的需要。因而咱们思考是不是采纳可视化的形式对根底的组件进行利落拽,间接生成页面布局,进步开发效率。

面对我的项目的种种局限,采纳的是taro2.x库,以及taro自带的组件库,非taro-ui。因为taro反对的属性参差不齐,和业务方探讨之后,咱们取tarojs组件库反对的h5和微信小程序的交加进行属性编辑。

技术栈

react、mobx、cloud-react、tarojs

拖拽

从左侧可抉择的组件拖拽元素到编辑器中,在编辑器外面进行二次拖拽排序,解决拖拽地位谬误,须要删除从新拖拽的问题。

咱们采纳react-dnd作为拖拽的根底库,具体用法解说独自有我的项目实际和文章阐明,在此不做赘述。

我的项目代码: react-dnd-nested

demo地址:react-dnd-nested-demo

包装组件

这里包装的是taro的组件,也能够为其余的第三方组件。每个组件蕴含index.js用于包装组件的代码 和config.json文件用于组件配置数据, 举个 Switch 组件的例子:

// Switch index.jsimport React, { Component } from 'react';import PropTypes from 'prop-types';import { Switch } from '@tarojs/components/dist-h5/react';export default class Switch1 extends Component {    render() {        const { style, ...others } = this.props;        return <Switch style={style} {...others} />;    }}Switch1.propTypes = {    checked: PropTypes.bool,    type: PropTypes.oneOf(['switch', 'checkbox']),    color: PropTypes.string,    style: PropTypes.string};Switch1.defaultProps = {    checked: false,    type: 'switch',    color: '#04BE02',    style: ''};
// config.json{    // 组件类型标识    "type": "Switch",    // 组件名称    "name": "开关选择器",    // 是否可搁置其余组件    "canPlace": false,    // 默认的props数据,与index.js中的 defaultProps 根本保持一致    "defaultProps": {        "checked": false,        "type": "switch",        "color": "#04BE02"    },    // 默认款式    "defaultStyles": {},    // props字段的具体配置    "config": [        {            // key值标识            "key": "checked",            // 配置时候采纳的组件:大略有Input、Radio、Checkbox、Select 等            "type": "Radio",            // 文案显示            "label": "是否选中"        },        {            "key": "type",            "type": "Select",            "label": "款式类型",            // 下拉数据源配置            "dataSource": [                {                    "label": "switch",                    "value": "switch"                },                {                    "label": "checkbox",                    "value": "checkbox"                }            ]        },        {            "key": "color",            "label": "色彩",            "type": "Input"        }    ]}

预置脚本

永远深信代码比人更加高效、精确、靠谱。

生成组件模板脚本

每个组件都是包装taro对应的组件,因而咱们预置index.jsconfig.json文件的代码,代码中设置一个__ComponentName__的特殊字符为组件名称,执行生成脚本,从用户的输出读取进来再正则替换,即可生成根底的代码。这块能够查看具体代码,生成脚本如下:

const path = require('path');const fs = require('fs');const readline = require('readline').createInterface({    input: process.stdin,    output: process.stdout});readline.question('请输出组件名称?', name => {    const componentName = name;    readline.close();    const targetPath = path.join(__dirname, '../src/components/');    fs.mkdirSync(`${targetPath}${componentName}`);    const componentPath = path.join(__dirname, `../src/components/${componentName}`);    const regx = /__ComponentName__/gi    const jsContent = fs.readFileSync(path.join(__dirname, '../scripts/tpl/index.js')).toString().replace(regx, componentName);    const configContent = fs.readFileSync(path.join(__dirname, '../scripts/tpl/config.json')).toString().replace(regx, componentName);    const options = { encoding: 'utf8' };    fs.writeFileSync(`${componentPath}/index.js`, jsContent, options, error => {        if (error) {            console.log(error);        }    });    fs.writeFileSync(`${componentPath}/config.json`, configContent, options, error => {        if (error) {            console.log(error);        }    });});

package.json配置如下:

"new": "node scripts/new.js",

执行脚本

npm run new
对外输入export脚本

咱们须要把所有组件对外输入都放在components/index.js文件中,每减少一个组件都须要改变这个文件,减少新组件的对外输入和配置文件。因而咱们编写一个脚本,每次生成新组件之后,间接执行脚本,主动读取,改写文件,对外输入:

/** * 动静生成 componets 上面的 index.js 文件 */const path = require('path');const fs = require('fs');const prettier = require('prettier');function getStringCodes() {    const componentsDir = path.join(__dirname, '../src/components');    const folders = fs.readdirSync(componentsDir);    // ignore file    const ignores = ['.DS_Store', 'index.js', 'Tips'];    let importString = '';    let requireString = '';    let defaultString = 'export default {\n';    let configString = 'export const CONFIGS = {\n';    folders.forEach(folder => {        if (!ignores.includes(folder)) {            importString += `import ${folder} from './${folder}';\n`;            requireString += `const ${folder.toLowerCase()}Config = require('./${folder}/config.json');\n`;            defaultString += `${folder},\n`;            configString += `${folder}: ${folder.toLowerCase()}Config,\n`;        }    });    return { importString, requireString, defaultString, configString };}function generateFile() {    const { importString, requireString, defaultString, configString } = getStringCodes();    const code = `${importString}\n${requireString}\n${defaultString}\n};\n\n${configString}\n};\n`;    const configPath = path.join(__dirname, '../.prettierrc');    prettier.resolveConfig(configPath).then(options => {        const content = prettier.format(code, Object.assign(options, { parser: 'babel' }));        const targetFilePath = path.join(__dirname, '../src/components/index.js');        fs.writeFileSync(targetFilePath, content, error => {            if (error) {                console.log(error);            }        });    });}generateFile();

package.json配置如下:

"gen": "node scripts/generate.js"

执行脚本

npm run gen

数据结构

页面的交互数据存储在localstoragecacheData数组外面,每个组件的数据模型:

{    id: 1,    // 组件类型    type: "View",    // 组件props配置    props: {},    // 组件style配置    styles: {},    // 蕴含的子组件列表    chiildrens: []}

简略页面数据示例如下:

[    {        "id": 1,        "type": "View",        "props": {},        "styles": {            "minHeight": "100px"        },        "childrens": [            {                "id": 9397,                "type": "Button",                "props": {                    "content": "ok",                    "size": "default",                    "type": "primary",                    "plain": false,                    "disabled": false,                    "loading": false,                    "hoverClass": "none",                    "hoverStartTime": 20,                    "hoverStayTime": 70                },                "styles": {}            },            {                "id": 4153,                "type": "View",                "props": {                    "hoverClass": "none",                    "hoverStartTime": 50,                    "hoverStayTime": 400                },                "styles": {                    "minHeight": "50px"                },                "childrens": [                    {                        "id": 7797,                        "type": "Icon",                        "props": {                            "type": "success",                            "size": 23,                            "color": ""                        },                        "styles": {}                    },                    {                        "id": 9713,                        "type": "Slider",                        "props": {                            "min": 0,                            "max": 100,                            "step": 1,                            "disabled": false,                            "value": 0,                            "activeColor": "#1aad19",                            "backgroundColor": "#e9e9e9",                            "blockSize": 28,                            "blockColor": "#fff",                            "showValue": false                        },                        "styles": {}                    },                    {                        "id": 1739,                        "type": "Progress",                        "props": {                            "percent": 20,                            "showInfo": false,                            "borderRadius": 0,                            "fontSize": 16,                            "strokeWidth": 6,                            "color": "#09BB07",                            "activeColor": "#09BB07",                            "backgroundColor": "#EBEBEB",                            "active": false,                            "activeMode": "backwards",                            "duration": 30                        },                        "styles": {}                    }                ]            },            {                "id": 8600,                "type": "Text",                "props": {                    "content": "text",                    "selectable": false                },                "styles": {}            },            {                "id": 7380,                "type": "Radio",                "props": {                    "content": "a",                    "checked": false,                    "disabled": false                },                "styles": {}            }        ]    }]

编辑器

实现思路:

1、初始化获取到的值为空时,默认数据为:

[    {        id: 1,        type: 'View',        props: {},        styles: {            minHeight: '100px'        },        childrens: []    }]

2、遍历cacheData数组,应用TreeItem两个组件嵌套生成数据结构,在Item组件中依据type值获取到以后组件,render到以后页面。外围代码如下:

// index.js<Tree parentId={null} items={store.pageData} move={this.moveItem} />
// tree.jsrender() {        const { parentId, items, move } = this.props;        return (            <>                {items && items.length                    ? items.map(item => {                            return <Item parentId={parentId} key={item.id} item={item} move={move} />;                      })                    : null}            </>        );    }
const CurrentComponet = Components[type];return (            <CurrentComponet                id={id}                type={type}                className={classes}                style={parseStyles(styles)}                onClick={event => this.handleClick({ id, parentId, type }, event)}>                <Tree parentId={id} items={childrens} move={move} />            </CurrentComponet>        );

3、从左侧拖拽组件进入编辑器,找到它拖入的父组件id,应用push批改以后的组件childrens减少数据。

add(targetId, type) {    // 递归查找到咱们要push进去的指标组件    const item = findItem(this.pageData, targetId);    const obj = {        // 依据规定生成id        id: generateId(),        type,        // 为组件增加默认的props属性        props: CONFIGS[type].defaultProps || {},        // 为组件增加默认款式        styles: CONFIGS[type].defaultStyles || {}    };    // 如果childrens存在,间接push    if (item.childrens) {        item.childrens.push(obj);    } else {        // 不存在则增加属性        item.childrens = [obj];    }    localStorage.setItem(KEY, JSON.stringify(this.pageData));}

4、在编辑器中拖入组件,应用move形式挪动组件到新的父组件上面

  • 找到正在拖拽的组件和其父组件,找到指标组件和它的父组件
  • 判断指标组件是否为可搁置类型组件。是的话间接push到指标组件。不是的话,找到以后在父组件中的index,而后在指定地位插入
  • 从指标组件的父组件中移除以后组件

5、单击某个组件,右侧编辑器区域呈现对于这个组件所有的propsstyle配置信息。

6、清空工作区,增加二次确认避免误操作,复原页面数据到初始化的默认数据。

单个组件操作

加载组件配置

依据以后组件的id找到以后组件的props和style配置信息,在依据之前config中对于每一个字段的config记录对应的组件去编辑。

删除组件

依据以后组件id和父组件id,删除这个组件,并且清空所有对以后选中组件的保存信息,更新localstorage。

复制组件

依据以后组件id和父亲节点id,找到以后复制组件的所有信息,为其生成一个新id,而后push到父组件中,更新localstorage。

编辑属性props

生成form表单,每个formitem的name设置为以后组件的key-currentId进行拼接, 当form中的item的value产生扭转的时候,咱们获取到整个configform的值,在cacheData中查找到以后组件,更新它的props,从新渲染编辑器,同时更新localstorage

编辑款式style

提供罕用的css配置属性,通过勾选对应的key值在上面生成该属性对应的配置,组成一个表单,item的值产生扭转的时候,收集所有勾选属性的值,更新到以后组件的配置中,从新渲染编辑器,同时更新localstorage

tips:在款式编辑的时候有className的生成到独立的css文件,没有增加则生成行内款式。

生成taro的源码

  • 预置一个模版字符串
  • localstorage外面获取以后页面的配置数据
  • 递归renderElementToJSX将数据转换为jsx字符串

    • 将组件类型type存储到一个数组
    • 判断className是否存在。存在将className称转为驼峰命名,便于css modules应用,调用renderCss办法拼接css字符串。不存在,则调用renderInlineCss生成行内款式,拼接到jsx。
    • 调用renderProps生成每个组件的props配置,并且在外面过滤以后的props值是否与默认的值相等,相等剔除该属性的判断,简化jsx字符串。
    • 以后组件childrens解决,存在childrens或者content字段的时候,解决以后组件的children。否则以后组件就是一个自闭和的组件。
  • 对组件type保留的数据去重
  • 应用生成的jsx字符串和types替换预置模版的占位符

    具体代码查看

预览和下载源码

预览代码
  • 调用renderJSONtoJSX办法,拿到生成的jsxcss字符串
  • 调用formatapi,格式化jsxcss字符串

    • 应用prettierbabel丑化jsx
    • 应用prettierless丑化css
  • api返回的结果显示到代码预览区
  • 提供一键复制jsxcss性能
下载源码
  • 调用renderJSONtoJSX办法,拿到生成的jsxcss字符串
  • 调用downloadapi

    • 设置response headerContent-Typeapplication/zip
    • 调用fs.truncateSync删除上次生成的文件
    • 预置生成一个名称为code的文件夹
    • 丑化jsxcss字符串,并且写入对应的文件
    • code文件夹添入taro.jsxindex.css文件夹
    • 生成base64类型的zip文件返回
  • 获取接口返回的data数据,再以base64进行加载,创立 blob文件, 下载
验证

将生成的代码复制到应用 taro-cli的我的项目工程中验证成果