最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 低代码平台实现思路探索:Sparrow项目源码分析(上)|牛气冲天新年征文

    正文概述 掘金(DennisWu)   2021-02-14   830

    之前的文章中,我们介绍了低代码开发平台iVX的实现思路,今天我们来继续探索一款社区开源产品——sparrow,项目源码见github.com/sparrow-js/…。

    当前,对于前端开发,有纯代码开发、低代码开发、无代码开发、自驱式开发四种形态。

    低代码平台实现思路探索:Sparrow项目源码分析(上)|牛气冲天新年征文 图引自:2020中国低代码平台指数测评报告

    Sparrow是个场景化低代码搭建工作台,它基于Vue.js和element-ui开发,能实时输出可二次开发的源代码(即代码可读性好的源代码,非编译后的代码),支持基于原子、区块粒度的搭建。之所以特别强调场景化,是因为一般性通用组件搭建平台的提效效果比较一般,通过场景化的粗粒度封装可以进一步达到提效的目的。笔者任务,作者将场景化这样一个特点重视起来,是非常明智的。实际上,像宜搭、简道云、活字格等市面上的低代码产品,也都是在切场景。而像iVX那样的产品,上手门槛还是太高,非专业人员很难大面积地使用起来。要想让非专业人员大面积用起来,场景化似乎是一条必由之路。

    下面是演示效果: 低代码平台实现思路探索:Sparrow项目源码分析(上)|牛气冲天新年征文

    大体上,Sparrow的实现方案如下图所示: 低代码平台实现思路探索:Sparrow项目源码分析(上)|牛气冲天新年征文

    该项目的代码目录结构如下:

    低代码平台实现思路探索:Sparrow项目源码分析(上)|牛气冲天新年征文

    看起来像是采用了monorepo的方式来管理项目,但是在项目中并未看到关于lerna之类的monorepo管理工具的配置。

    本文主要分析plugin-demo、plugins、sparrow-cli、sparrow-utils这四个文件夹。

    一、plugin-demo

    该目录下给出了两个插件(实际上就是组件)样例:sparrow-test-box和sparrow-test-component。

    其中,sparrow-test-box的目录结构为:

    低代码平台实现思路探索:Sparrow项目源码分析(上)|牛气冲天新年征文

    sparrow-test-component的目录结构为:

    低代码平台实现思路探索:Sparrow项目源码分析(上)|牛气冲天新年征文

    两个插件的目录结构基本上类似,都是package.json、tsconfig.json、sparrow.json以及一个src目录和一个dist目录。dist目录是src目录的内容打包后的结果;tsconfig.json是常规的TypeScript打包配置,不必解释。

    1、sparrow.json

    {
      "name": "sparrow-test-box",
      "description": "Test sparrow box",
      "thumb": "https://unpkg.com/@sparrow-vue/images@1.0.26/assets/box.png"
    }
    

    放置的是插件的名称、描述、缩略图等基本声明信息。

    2、package.json

    在其package.json中,都用到了如下两个库:

    • @lukeed/uuid

    这是一个适用于Node.js和浏览器的微型(大约230B)、快速的UUID(V4)生成器。

    • cheerio

    cheerio是jQuery核心功能的一个快速、灵活而又简洁的实现,主要是为了用在服务器端需要对DOM进行操作的场景。

    它们都依赖了 @sparrow-vue/sparrow-utils 这个库。也就是 packages/sparrow-utils 下的这个package。下面会详细讲到。

    3、src/config.ts

    export default {
      model: {
        attr: {
          direction: "",
          'content-position': '',
        },
        custom: {
          label: '输入文本',
        },
      },
      schema: {
        fields: [
          {
            type: 'object',
            label: '',
            model: 'attr',
            schema: {
              fields: [
                {
                  type: "select",
                  label: "direction",
                  model: "direction",
                  multi: true,
                  values: ["horizontal", "vertical", ""]
                },
                {
                  type: "select",
                  label: "content-position",
                  model: "content-position",
                  multi: true,
                  values: ["left", "right", "center", ""]
                },
              ]
            }
          },
          {
            type: 'object',
            label: '',
            model: 'custom',
            schema: {
              fields: [
                {
                  type: "input",
                  inputType: "text",
                  label: "label",
                  model: "label"
                }
              ]
            }
          }
        ]
      },
    }
    

    这应该是插件的属性配置声明文件,用于声明可视化编辑器的属性配置面板中,应该渲染出来哪些配置项用的。

    4、sparrow-test-box/src/index.ts

    该插件继承了@sparrow-vue/sparrow-utils中导出的Box类。

    其中,customAttrHandler对样式进行了处理,比较好理解,可以自行看如下代码:

    public customAttrHandler () {
      const custom = _.get(this.config, 'model.custom');
      const styleKeys = [
        'display',
        'flex-direction',
        'justify-content',
        'align-items',
        'flex-wrap',
        'style',
      ];
    
      const styleArr = [];
    
      styleKeys.forEach(key => {
        if (key === 'style') {
          styleArr.push(custom[key]);
          return;
        }
        if (custom[key]) {
          styleArr.push(`${key}: ${custom[key]}`);
        }
      });
      if (styleArr.length > 0) {
        this.styleStr = `style="${styleArr.join(';')}"`
      }    
    }
    

    5、sparrow-test-component/src/index.ts

    它继承了@sparrow-vue/sparrow-utils中抛出的Component类。

    二、plugins

    目前该目录下还没有实质性的内容,当前应该只是占位。

    三、sparrow-utils

    通过查看其index.ts,我们发现该package主要包含了四个类:Events、Box、Component和VuePress:

    export { default as events } from './events; // 因为Events实际上是个类,所以应该首字母为大写更合理些
    export { default as Component } from './Component';
    export { default as Box } from './Box';
    export { default as VueParse} from './VueParse';
    
    • Events类

    是个事件管理类,包含了on(注册监听)、off(取消监听)、emit(触发)、destroy(清空全部监听)这四个public方法。

    • Box类

    应该是容器类的基类。其中有getFragment、addComponent、renderComp等比较重要的方法以及一些其它方法。

    • Component类

    应该是组件类的基类。其中有getFragment、renderFragment等比较重要的方法以及一些其它方法。

    • VueParse类

    这是一个非常重要的类,它定义了如何对Vue文件进行解析和处理。其中包含了getData、setData、getFormatData、getMethods、getComponents、getImport、getCreated等public方法,此外还有一个init私有方法。

    在init私有方法中,分别用了如下三个正则表达式将Vue.js SFC文件的template、script、style三个部分的内容分别提取出来:

    /<template>([\s\S])*<\/template>/g

    /(?<=<style[\s\S]*>)[\s\S]*(?=<\/style>)/g

    /(?<=<script>)[\s\S]*(?=<\/script>)/g

    然后,通过@babel/parser对script部分解析成抽象语法树(注意这些都是运行在Node.js上的,而非浏览器上):

    this.scriptAst = parser.parse(this.vueScript, {
      sourceType: 'module',
      plugins: [
        "jsx",
      ]
    });
    

    接着在通过@babel/traverse来遍历和更新抽象语法树中的节点:

    public getMethods () {
      let methods:any = [];
      traverse(this.scriptAst, {
        ObjectProperty: (path: any) => {
          const {node} = path;
          if (node.key.name === 'methods') {
            methods = node.value.properties;
          }
        }
      });
      return methods;
    }
    

    PS:@babel/traverse允许我们在语法树中定位特定的节点类型,如上面代码片段中传给traverse方法的第二个参数。

    四、sparrow-cli

    这是一个命令行package,它就是官方文档里npm install -g sparrow-code中所指的那个sparrow-code包。执行npm install -g sparrow-code之后,再执行sparrow就可以在本地启动起来一个可视化编辑器的前后台项目。

    它的主要功能就是从本地加载sparrow-plugins、sparrow-view、sparrow-server这三个包的源文件压缩包并将他们解压、安装依赖。然后启动sparrow-view和sparrow-server中代码所对应的前后台服务。效果如下图所示:

    低代码平台实现思路探索:Sparrow项目源码分析(上)|牛气冲天新年征文

    其中:

    • 本地加载时先需要获取源文件压缩包路径,实现采用了request-promise-native库,不过这个库已经不推荐使用了,因为它是扩展自现在已经不推荐使用的request这个package。具体实现如下:
    const request = require('request-promise-native');
    const semver = require('semver');
    
    module.exports = async function getNpmTarball(npm, version, registry) {
      const url = `${registry}/${npm}`;
      const body = await request({
        url,
        json: true,
      });
    
      if (!semver.valid(version)) {
        version = body['dist-tags'].latest;
      }
    
      if (
        semver.valid(version) &&
        body.versions &&
        body.versions[version] &&
        body.versions[version].dist
      ) {
        const tarball = body.versions[version].dist.tarball;
        return tarball;
      }
    
      throw new Error(`${name}@${version} 尚未发布`);
    };
    
    • 本地加载组合使用了request、request-progress;本地解压使用了zlib。
    const fs = require('fs');
    const mkdirp = require('mkdirp');
    const path = require('path');
    const request = require('request');
    const progress = require('request-progress'); // 可以得到百分比、下载速度和剩余时间
    const zlib = require('zlib');
    const tar = require('tar');
    
    /**
     * Download tarbar content to the specified directory
     *
     * @param {string} tarballURL tarball url
     * @param {string} destDir target directory
     */
    module.exports = function extractTarball({
      tarballURL,
      destDir,
      progressFunc = () => {},
      formatFilename,
    }) {
      return new Promise((resolve, reject) => {
        const allFiles = [];
        const allWriteStream = [];
        const directoryCollector = [];
        progress(
          request({                     // 加载
            url: tarballURL,
            timeout: 100000,
          })
        )
          .on('progress', (state) => {
            progressFunc(state);
          })
          .on('error', (error = {}) => {
            error.name = 'download-tarball-error';
            error.data = {
              url: tarballURL,
            };
            reject(error);
          })
          .pipe(zlib.Unzip())          // 解压
          .on('error', (error) => {
            reject(error);
          })
          .pipe(tar.Parse())
          .on('entry', (entry) => {
            const realPath = entry.path.replace(/^package\//, '');
    
            let filename = path.basename(realPath);
            filename = formatFilename ? formatFilename(filename) : filename;
    
            const destPath = path.join(destDir, path.dirname(realPath), filename);
    
            const needCreateDir = path.dirname(destPath);
            if (!directoryCollector.includes(needCreateDir)) {
              directoryCollector.push(needCreateDir);
              mkdirp.sync(path.dirname(destPath));
            }
    
            allFiles.push(destPath);
            const writeStream = new Promise((streamResolve) => {
              entry
                .pipe(fs.createWriteStream(destPath))
                .on('finish', () => streamResolve());
            });
            allWriteStream.push(writeStream);
          })
          .on('end', () => {
            progressFunc({
              percent: 1,
            });
            Promise.all(allWriteStream)
              .then(() => resolve(allFiles))
              .catch((error) => {
                reject(error);
              });
          });
      });
    };
    
    • 安装依赖是基于cross-spawn实现的,它是node中的spawn和spawnSync的跨平台解决方案。这个比较常见了。具体实现如下:
    const spawn = require('cross-spawn');
    
    module.exports = (cwd, registry)=> {
      return new Promise((resolve, reject) => {
        const child = spawn('npm', ['install', '--loglevel', 'silly', '--registry', registry], {
          stdio: ['pipe'],
          cwd,
        });
    
        child.stdout.on('data', data => {
          console.log(data.toString());
        });
    
        child.stderr.on('data', data => {
          console.log(data.toString());
        });
    
        child.on('error', error => {
          reject(error);
        });
    
        child.on('close', (code) => {
          if (code === 0) {
            console.log('>>> install completed');
            resolve();
          } else {
            reject(new Error('install deps error'));
          }
        });
      });
    }
    
    • 对于要下载到的目标目录,其中使用了user-home来获取系统的user home directory,放在其中的.sparrow目录下。

    你可以执行sparrow命令之后,在你的用户目录下的.sparrow文件夹下找到如下图所示文件/文件夹:

    低代码平台实现思路探索:Sparrow项目源码分析(上)|牛气冲天新年征文


    起源地下载网 » 低代码平台实现思路探索:Sparrow项目源码分析(上)|牛气冲天新年征文

    常见问题FAQ

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

    发表评论

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

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

    联系作者

    请选择支付方式

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