taro-designer可視化ドラッグの技術点整理


いきなり可視化ドラッグの風がフロントエンドの隅々で吹いているようで、自分でも煽ってみましたが、コードはほぼ開発済みで、整理してみました.
githubプロジェクトアドレス:taro-designer
オンライン体験アドレス:taro-desiger
主な技術点は以下の通りである.
  • バックグラウンド
  • テクノロジスタック
  • ドラッグ
  • パッケージ
  • データ構造
  • エディタ
  • 単一コンポーネント動作
  • taroのソースコード
  • を生成する.
  • ソースコード
  • のプレビューとダウンロード

    背景


    会社の一部の業務はインタラクティブな開発をしています.例えば、サイン、贈り物の両替などです.インタラクティブなビジネスは迅速な反復が必要であり、H 5、微信ウィジェット、淘宝ウィジェットをサポートする必要があるため、フロントエンドはtaroをベースフレームワークとして多端なニーズを満たすために採用している.そこで,基礎となるコンポーネントを可視化してドラッグ&ドロップし,ページレイアウトを直接生成し,開発効率を向上させるかどうかを考える.
    プロジェクトの様々な限界に直面して、taro 2を採用した.xライブラリ、およびtaroが持参したコンポーネントライブラリ、taro-uiではありません.taroがサポートするプロパティはバラツキがあるため、ビジネス側と議論した後、tarojsコンポーネントライブラリがサポートするh 5と微信ウィジェットの交差を取ってプロパティ編集を行います.

    テクノロジースタック


    react、mobx、cloud-react、tarojs

    ドラッグ&ドロップ


    左側の選択可能なコンポーネントからエディタに要素をドラッグ&ドロップし、エディタで2回のドラッグ&ドロップソートを行い、ドラッグ位置エラーを解決し、再ドラッグの問題を削除する必要があります.
    ドラッグ&ドロップのベースライブラリとして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 ;
        }
    }
    
    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 {
    '; let configString = 'export const CONFIGS = {
    '; folders.forEach(folder => { if (!ignores.includes(folder)) { importString += `import ${folder} from './${folder}';
    `; requireString += `const ${folder.toLowerCase()}Config = require('./${folder}/config.json');
    `; defaultString += `${folder},
    `; configString += `${folder}: ${folder.toLowerCase()}Config,
    `; } }); return { importString, requireString, defaultString, configString }; } function generateFile() { const { importString, requireString, defaultString, configString } = getStringCodes(); const code = `${importString}
    ${requireString}
    ${defaultString}
    };

    ${configString}
    };
    `; 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の2つのコンポーネントのネストを使用してデータ構造を生成し、Itemコンポーネントのうちtype値に基づいて現在のコンポーネント、renderから現在のページを取得する.コアコードは次のとおりです.
    // index.js
    
    // tree.js
    render() {
            const { parentId, items, move } = this.props;
            return (
                <>
                    {items && items.length
                        ? items.map(item => {
                                return ;
                          })
                        : null}
                >
            );
        }
    const CurrentComponet = Components[type];
    
    
    return (
                 this.handleClick({ id, parentId, type }, event)}>
                    
                
            );

    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でコンポーネントを新しい親コンポーネントの下に移動する
  • ドラッグしているコンポーネントとその親コンポーネントを見つけ、ターゲットコンポーネントとその親コンポーネント
  • を見つけます.
  • は、ターゲットコンポーネントが配置可能なタイプのコンポーネントであるかどうかを判断する.はい、ターゲットコンポーネントに直接プッシュします.そうでなければ、現在親アセンブリにある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を更新します.
    スタイルの編集
    一般的な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メソッドを呼び出し、生成されたjsxおよびcss文字列
  • を取得する.
  • は、format apiを呼び出し、jsxおよびcss文字列をフォーマットする
  • prettierbabelを使用してjsx
  • を美化する.
  • prettierlessを使用してcss
  • を美化する.
  • apiから戻る結果をコードプレビュー領域
  • に表示する.
  • は、ワンタッチレプリケーションjsxおよびcssの機能
  • を提供する.
    ソースのダウンロード
  • renderJSONtoJSXメソッドを呼び出し、生成されたjsxおよびcss文字列
  • を取得する.
  • 呼び出しdownload api
  • response headerが設けるContent-Typeapplication/zip
  • である.
  • 呼び出しfs.truncateSync前回生成したファイル
  • を削除する.
  • プリセットは、codeという名前のフォルダ
  • を生成する.
  • は、jsxおよびcss文字列を美化し、対応するファイル
  • に書き込む.
  • codeフォルダにtaro.jsxindex.cssフォルダ
  • を追加する.
  • 生成base64型のzipファイルは
  • を返す.
  • インタフェースから戻るdataのデータを取得し、base64でロードし、blobファイルを作成し、
  • をダウンロードする.
    検証#ケンショウ#
    生成されたコードをtaro-cliを使用したプロジェクトプロジェクトにコピーして効果を検証します.