本文使用到的开发平台是阿里的低代码引擎,下面是官方文档地址
目前开发暂时有两个项目,一个是编辑器,一个是物料库,两者的搭建在我前一篇文章中都有介绍
https://blog.csdn.net/luoluoyang23/article/details/135047766
本篇主要介绍物料仓库的插件拓展开发,并且是上一篇文章的基础上进行的,请看完上一篇 did-meta 拓展部分的内容
从接口注释生成设置器描述 & 传参
在上一篇文章当中,我们添加了额外的文件 props-params.json 来给设置器传参,这样太麻烦了,这次直接从组件接口注释内容给设置器传参
下面是最终效果图参考
为了方便,我们不从零开始写,而是直接借用引擎插件的代码
当我们的目录中没有 lowcode/xxx/meta.ts 的时候,执行 dev 或者 build 命令时,引擎都会为我们生成对应组件的 meta.ts 文件,查看这个文件我们就可以发现,引擎其实已经为我们解析了组件的接口注释,并生成对应的中文
在刚开始我以为可以直接在注释中添加参数就实现传递,反复试了多次并没有效果,因此我决定查看一下源代码
很明显,引擎是有解析注释这个动作的,且这个动作一定是由 @alifd/build-plugin-lowcode 这个插件引起的,
找源头这个过程就省略了,最终我们能够定位到 node_modules@alilc\lowcode-material-parser\lib\parse\ts\index.js 当中,而 parseTS 就是我们要找的方法
能够发现下面的代码
··· // 省略其他代码
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);
}
}
}
这样就结束了