之前的文章中,我们介绍了低代码开发平台iVX的实现思路,今天我们来继续探索一款社区开源产品——sparrow,项目源码见github.com/sparrow-js/…。
当前,对于前端开发,有纯代码开发、低代码开发、无代码开发、自驱式开发四种形态。
图引自:2020中国低代码平台指数测评报告
Sparrow是个场景化低代码搭建工作台,它基于Vue.js和element-ui开发,能实时输出可二次开发的源代码(即代码可读性好的源代码,非编译后的代码),支持基于原子、区块粒度的搭建。之所以特别强调场景化,是因为一般性通用组件搭建平台的提效效果比较一般,通过场景化的粗粒度封装可以进一步达到提效的目的。笔者任务,作者将场景化这样一个特点重视起来,是非常明智的。实际上,像宜搭、简道云、活字格等市面上的低代码产品,也都是在切场景。而像iVX那样的产品,上手门槛还是太高,非专业人员很难大面积地使用起来。要想让非专业人员大面积用起来,场景化似乎是一条必由之路。
下面是演示效果:
大体上,Sparrow的实现方案如下图所示:
该项目的代码目录结构如下:
看起来像是采用了monorepo的方式来管理项目,但是在项目中并未看到关于lerna之类的monorepo管理工具的配置。
本文主要分析plugin-demo、plugins、sparrow-cli、sparrow-utils这四个文件夹。
一、plugin-demo
该目录下给出了两个插件(实际上就是组件)样例:sparrow-test-box和sparrow-test-component。
其中,sparrow-test-box的目录结构为:
sparrow-test-component的目录结构为:
两个插件的目录结构基本上类似,都是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中代码所对应的前后台服务。效果如下图所示:
其中:
- 本地加载时先需要获取源文件压缩包路径,实现采用了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文件夹下找到如下图所示文件/文件夹:
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!