低代码平台开发 - 物料拓展

本文使用到的开发平台是阿里的低代码引擎,下面是官方文档地址

https://lowcode-engine.cn/site/docs/guide/quickStart/start

目前开发暂时有两个项目,一个是编辑器,一个是物料库,两者的搭建在我前一篇文章中都有介绍

https://blog.csdn.net/luoluoyang23/article/details/135047766

本篇主要介绍物料仓库的插件拓展开发,并且是上一篇文章的基础上进行的,请看完上一篇 did-meta 拓展部分的内容

从接口注释生成设置器描述 & 传参

在上一篇文章当中,我们添加了额外的文件 props-params.json 来给设置器传参,这样太麻烦了,这次直接从组件接口注释内容给设置器传参
下面是最终效果图参考
image.png
image.png
为了方便,我们不从零开始写,而是直接借用引擎插件的代码
当我们的目录中没有 lowcode/xxx/meta.ts 的时候,执行 dev 或者 build 命令时,引擎都会为我们生成对应组件的 meta.ts 文件,查看这个文件我们就可以发现,引擎其实已经为我们解析了组件的接口注释,并生成对应的中文
image.png
image.png
在刚开始我以为可以直接在注释中添加参数就实现传递,反复试了多次并没有效果,因此我决定查看一下源代码
很明显,引擎是有解析注释这个动作的,且这个动作一定是由 @alifd/build-plugin-lowcode 这个插件引起的,
找源头这个过程就省略了,最终我们能够定位到 node_modules@alilc\lowcode-material-parser\lib\parse\ts\index.js 当中,而 parseTS 就是我们要找的方法
image.png
能够发现下面的代码

···	// 省略其他代码
const react_docgen_typescript_1 = require("react-docgen-typescript");
···	// 省略其他代码

class MyParser extends react_docgen_typescript_1.Parser {
  // 里面写的两个方法并没有用到
}
···	// 省略其他代码
const info = parser.getComponentInfo(sym, sourceFile);

如果是这样解析出来的代码,为什么最后会只剩一串中文呢,为什么我们传的其他参数并没有在任何地方得到体现呢?我们继续往下看

···	// 省略其他代码
const coms = result.reduce((res, info) => {
  ···	// 省略其他代码
  const item = transform_1.transformItem(name, info.props[name]);
  acc.push(item);
···	// 省略其他代码

很明显,这里对这个 info 对象做了处理,我们定位过去

···	// 省略其他代码
if (type) {
    result.propType = transformType({
        ...type,
        ...lodash_1.omit(others, ['name']),
        required: !!required,
    });
}
if (description) {
    if (description.includes('\n')) {	// 就是这里
        result.description = description.split('\n')[0];
    }
    else {
        result.description = description;
    }
}
···	// 省略其他代码

破案了,引擎做了换行的解析,只取了换行的第一行内容,拿来做物料的属性中文描述,并没有做其他处理,因此我们无论怎么改注释也是不会有效果的
现在,我们重新创建一个文件,改一下这块的功能,过滤掉冗余的内容,只拿到 info 处的内容即可

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const path = tslib_1.__importStar(require("path"));
const react_docgen_typescript_1 = require("react-docgen-typescript");
const typescript_1 = tslib_1.__importStar(require("typescript"));
const lodash_1 = require("lodash");
const fs_extra_1 = require("fs-extra");
const find_config_1 = tslib_1.__importDefault(require("find-config"));

const blacklistNames = [
  'prototype',
  'getDerivedStateFromProps',
  'propTypes',
  'defaultProps',
  'contextTypes',
  'displayName',
  'contextType',
  'Provider',
  'Consumer',
];

class MyParser extends react_docgen_typescript_1.Parser { }

const defaultTsConfigPath = path.resolve(__dirname, '../tsconfig.json');
function parseTS(filePath, args) {
  if (!filePath)
    return [];
  let basePath = args.moduleDir || args.workDir || path.dirname(filePath);
  let tsConfigPath = find_config_1.default('tsconfig.json', { cwd: basePath }); // path.resolve(basePath, 'tsconfig.json')
  if (!tsConfigPath ||
    !fs_extra_1.existsSync(tsConfigPath) ||
    (args.accesser === 'online' && tsConfigPath === 'tsconfig.json')) {
    tsConfigPath = defaultTsConfigPath;
  }
  else {
    basePath = path.dirname(tsConfigPath);
  }
  const { config, error } = typescript_1.default.readConfigFile(tsConfigPath, (filename) => fs_extra_1.readFileSync(filename, 'utf8'));
  if (error !== undefined) {
    const errorText = `Cannot load custom tsconfig.json from provided path: ${tsConfigPath}, with error code: ${error.code}, message: ${error.messageText}`;
    throw new Error(errorText);
  }
  const { options, errors } = typescript_1.default.parseJsonConfigFileContent(config, typescript_1.default.sys, basePath, {}, tsConfigPath);
  if (errors && errors.length) {
    throw errors[0];
  }
  const program = typescript_1.default.createProgram([filePath], options);
  const parser = new MyParser(program, {});
  const checker = program.getTypeChecker();
  const result = [filePath]
    .map((fPath) => program.getSourceFile(fPath))
    .filter((sourceFile) => typeof sourceFile !== 'undefined')
    .reduce((docs, sourceFile) => {
      const moduleSymbol = checker.getSymbolAtLocation(sourceFile);
      if (!moduleSymbol) {
        return docs;
      }
      const exportSymbols = checker.getExportsOfModule(moduleSymbol);
      for (let index = 0; index < exportSymbols.length; index++) {
        const sym = exportSymbols[index];
        const name = sym.getName();
        if (blacklistNames.includes(name)) {
          continue;
        }
        // polyfill valueDeclaration
        sym.valueDeclaration =
          sym.valueDeclaration || (Array.isArray(sym.declarations) && sym.declarations[0]);
        if (!sym.valueDeclaration) {
          continue;
        }
        const info = parser.getComponentInfo(sym, sourceFile);
        if (info === null) {
          continue;
        }
        const exportName = sym.meta && sym.meta.exportName;
        const meta = {
          subName: exportName ? name : '',
          exportName: exportName || name,
        };
        if (docs.find((x) => lodash_1.isEqual(x.meta, meta))) {
          continue;
        }
        docs.push({
          ...info,
          meta,
        });
        // find sub components
        if (!!sym.declarations && sym.declarations.length === 0) {
          continue;
        }
        const type = checker.getTypeOfSymbolAtLocation(sym, sym.valueDeclaration || sym.declarations[0]);
        Array.prototype.push.apply(exportSymbols, type.getProperties().map((x) => {
          x.meta = { exportName: name };
          return x;
        }));
      }
      return docs;
    }, []);
  return result;
}
exports.default = parseTS;

这样我们得到的 result 中就会含有完整的注释,然后我们自行对其进行解析,再利用我们上一篇文章写好的 did-meta 插入进物料描述当中
首先在 did-meta 当中引入我们刚刚写好的方法,然后这个方法需要传两个路径和一个包名,路径就是入口路径

const path = require('path');

const mainEntry = path.join(__dirname.split('plugins')[0], 'src\\index.tsx');
const workDir = __dirname.split('plugins')[0];

const pkgName = 'editor-components';

然后调用我们改造的 parseTS

const parseTS = require('./parse-props');

// 获取 props 的 map
function getPropsMap () {
  const propsParseRes = parseTS.default(mainEntry, {
    pkgName,
    mainFileAbsolutePath: mainEntry,
    mainFilePath: mainEntry,
    npmClient: 'npm',
    workDir,       
  })
}

接着就是解析逻辑了,我们约定以 “@”开头的都是要传递的参数,并且多的参数分行写
这样的话我们就能直接用 split(‘@’) 直接进行分割,整体的逻辑如下

function getPropsMap () {
  const propsParseRes = parseTS.default(mainEntry, {
    pkgName,
    mainFileAbsolutePath: mainEntry,
    mainFilePath: mainEntry,
    npmClient: 'npm',
    workDir,       
  });
  const propDescriptionMap = {};
  for (const propMap of propsParseRes) {
    const props = {};
    for (const key of Object.keys(propMap.props)) {
      const descriptions = propMap.props[key]?.description.split('@');
      descriptions.shift();	// 去除第一行,因为第一行会被用作中文提示
      props[key] = {};
      descriptions.map(des => {
        const noLineDes = des.replace('\n', '');	// 去除换行符
        const head = /(.*?) /.exec(noLineDes)[1];	// 获取字段名
        const value = noLineDes.split(head)[1];	// 获取传递的参数值
        try {
          if (head === 'props') props[key][head] = JSON.parse(value);	// props 是一个对象,要注意写法
          else if (/^[0-9]*$/.test(value.trim())) props[key][head] = parseInt(value, 10);	// 数字的情况
          else props[key][head] = value.trim();
        } catch (e) {
          error(`${propMap.displayName}${key} 属性接口声明存在错误,请检查`);
          throw e;
        }
      })
      if (JSON.stringify(props[key]) === '{}') Reflect.deleteProperty(props, key);
    }
    if (JSON.stringify(props) !== '{}') propDescriptionMap[propMap.displayName] = props;
  }
  return propDescriptionMap;
}

还有 Boolean 和 null 等情况,这里只是举个例子,请自行完善
最后还要改一下下面具体给设置器插入参数的代码,最后 did-meta 的代码如下

const fs = require('fs')
const metaConfig = require('../inject.config.json');
const parseTS = require('./parse-props');
const { error } = require('console');
const path = require('path');

const mainEntry = path.join(__dirname.split('plugins')[0], 'src\\index.tsx');
const workDir = __dirname.split('plugins')[0];

const pkgName = 'editor-components';

// 获取 props 的 map
function getPropsMap () {
  const propsParseRes = parseTS.default(mainEntry, {
    pkgName,
    mainFileAbsolutePath: mainEntry,
    mainFilePath: mainEntry,
    npmClient: 'npm',
    workDir,       
  });
  const propDescriptionMap = {};
  for (const propMap of propsParseRes) {
    const props = {};
    for (const key of Object.keys(propMap.props)) {
      const descriptions = propMap.props[key]?.description.split('@');
      descriptions.shift();
      props[key] = {};
      descriptions.map(des => {
        const noLineDes = des.replace('\n', '');
        const head = /(.*?) /.exec(noLineDes)[1];
        const value = noLineDes.split(head)[1];
        try {
          if (head === 'props') props[key][head] = JSON.parse(value);
          else if (/^[0-9]*$/.test(value.trim())) props[key][head] = parseInt(value, 10);
          else props[key][head] = value.trim();
        } catch (e) {
          error(`${propMap.displayName}${key} 属性接口声明存在错误,请检查`);
          throw e;
        }
      })
      if (JSON.stringify(props[key]) === '{}') Reflect.deleteProperty(props, key);
    }
    if (JSON.stringify(props) !== '{}') propDescriptionMap[propMap.displayName] = props;
  }
  return propDescriptionMap;
}

module.exports = async () => {
  const propsMap = getPropsMap();
  const files = await fs.readdirSync('lowcode/');
  for (const file of files) {
    const contextBuffer = await fs.readFileSync(`lowcode/${file}/meta.ts`);
    const context = contextBuffer.toString();
    const after = context.split('IPublicTypeComponentMetadata = ')[1];
    let jsonTarget;
    let metaJson;
    try {
      jsonTarget = after.split(';\n')[0];
      metaJson = JSON.parse(jsonTarget);
    } catch {
      jsonTarget = /(.*?);[\n|\r| ]*const/.exec(after)[1];
      metaJson = JSON.parse(jsonTarget);
    }
    const config = metaConfig[file];

    if (config) {
      // 插入主配置
      for (const key of Object.keys(config)) {
        if (key === 'configure') {
          // 主配置不允许写入 props
          Reflect.deleteProperty(config[key], 'props');
          metaJson[key] = Object.assign(metaJson[key], config[key]);
        } else if (key !== 'snippets') {
          metaJson[key] = config[key];
        }
      }

      // 给设置器传参
      if (propsMap[metaJson.componentName]) {
        for (let i = 0; i < (metaJson.configure.props || []).length; i++) {
          const prop = metaJson.configure.props[i];
          // const paramsConfig = propsParams[file];
          const paramsConfig = propsMap[metaJson.componentName];
          if (paramsConfig) {
            const params = paramsConfig[prop.name];
            if (metaJson.configure.props[i].setter) {
              metaJson.configure.props[i].setter = Object.assign(
                metaJson.configure.props[i].setter,
                params
              )
            }
          }
        }
      }
  
      // 插入 snippets 配置
      const snippetsContext = context.split('IPublicTypeSnippet[] = [')[1];
      const snippetsJson = JSON.parse(snippetsContext.split('];')[0]);
      for (const key of Object.keys(config.snippets || [])) {
        snippetsJson[key] = config.snippets[key];
      }
  
      const targetContext = `${context.split('IPublicTypeComponentMetadata = ')[0]}IPublicTypeComponentMetadata = ${JSON.stringify(metaJson)};
  
const snippets: IPublicTypeSnippet[] = [
  ${JSON.stringify(snippetsJson)}
];
${snippetsContext.split('];')[1]}`;
      await fs.writeFileSync(`lowcode/${file}/meta.ts`, targetContext);
    }
  }
}

这样就结束了

  • 22
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值