Giter VIP home page Giter VIP logo

silence717 / taro-designer Goto Github PK

View Code? Open in Web Editor NEW
97.0 4.0 26.0 1.41 MB

基于React开发可视化拖拽,页面搭建,使用tarojs自带组件库,生成源码生成。主要取tarojs组件库支持的 H5和微信小程序的交集进行属性编辑。

Home Page: https://silence717.github.io/taro-designer

JavaScript 93.61% Less 4.74% HTML 1.66%
react react-dnd low-code page-editor tarojs drag-and-drop h5-editor no-code

taro-designer's Introduction

trao-designer

基于 React 开发可视化拖拽,页面搭建,使用 tarojs 自带组件库,生成源码生成。主要取 tarojs 组件库支持的 H5 和微信小程序的交集进行属性编辑。

访问地址

taro-designer

技术栈

react + mobx + cloud-react + tarojs

目的

提高互动多端开发的效率,降低开发门槛,通过拖拉拽就可生成基本的页面 UI

使用

// clone code
http://gitlab.platdep.shuyun.com/fe/public/taro-designer.git
// 进入工程
cd taro-designer
// 安装依赖
yarn install
// 启动
yarn start

项目结构

build-utils  // webpack相关配置
    webpack.dev.js  // webpack development mode config
    webpack.prod.js  // wbepack production mode config

output // 生成源码目录
    index.less // 生成的css文件
    taro.jsx   // 生成的jsx文件
public  // 静态资源目录
    favicon.ico  // 网页图标
    index.html   // 静态页面
    index.js      // 展示网页入口js
scripts  // 项目脚本文件
    tpl  // 通过脚本生成新组件目录
    generate.js // 执行当前脚本生成 src/components/index.js 文件
    new.js // 执行当前脚本,在src/components 下新增加一个组件,模版为tpl下的文件
   
src // 源码目录
    components  // 可供拖拽的组件目录
    container // 页面容器
    utils  // 工具类
    style.less // css
    index.js // 项目主入口文件

.babelrc      // babel配置问考吗
.editorconfig // 编辑器配置
.eslintignore // 配置 eslint 忽略的文件
.eslintrc     // eslint校验规则配置
.gitignore    // 提交到git仓库需要忽略的文件
.prettierrc   // 格式化代码配置
.jsconfig     // 使得编辑器认识项目中配置的别名,可链接查找
README.MD     // 项目说明
server-config.js // node服务相关配置
taro-designer-server.js     // node服务
webpack.config.js // webpack基础配置

一些说明

可进行拖拉拽的组件

视图容器

视图容器 View,可滚动的视图容器 ScrollView,滑块视图容器 Swiper,滑块 SwiperItem

基础内容

图标 Icon,文本 Text,进度条 Progress

表单组件

按钮 Button,多选 Checkbox,表单 Form,输入框 Input,标签 Label,选择器 Picker,单选 Radio,滑动选择器 Slider,开关选择器 Switch,多行输入框 Textarea

媒体组件

图片 Image

组件可配置的属性说明

因为需要兼容多端,而 taro 每个组件 api 对平台的支持程度不一致,在当前项目中我们选取了兼容微信小程序h5两个版本的属性可进行配置。

项目部署说明

node 服务,在项目下使用 pm2 启动 server.js,如果 node 端代码有改动,需要登录服务器重启。

taro-designer's People

Contributors

silence717 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

taro-designer's Issues

taro-designer 可视化拖拽的技术点整理

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

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的项目工程中验证效果

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.