最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • taro-designer 可视化拖拽的技术点整理

    正文概述 掘金(silence717)   2021-01-18   499

    突然间可视化拖拽的风好像在前端的各个角落吹起,自己也鼓捣了一下,代码基本开发完毕,做一下整理。

    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.js
    import 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.js
    render() {
            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的项目工程中验证效果


    起源地下载网 » taro-designer 可视化拖拽的技术点整理

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元