在0代码&低代码系统中,如何去实现物料的开发&管理

928 阅读5分钟

物料的存储

物料的数量直接决定了一个0代码或者低代码系统的丰富性,所以物料的存储也显的犹为重要。

方式一:直接存储在代码当中

我之前接手过的一个0代码平台,最初的时候是吧物料全部存储在代码当中。
---components
---ComponentA
---index.tsx
---config.ts

export default {
  componentCode: 'ComponentA',
  componentName: '组件名称-ComponentA',
  componentType: '组件类型',
  icon: '',
  // ...  
}
import config from './config';

class ComponentA {
  static componentConfig = config;
}

当时的情况是,平台所有的组件都在一个仓库的 components 文件夹下,组件代码有一个 static componentConfig 属性存放组件的注入组件名称、类型这样的配置。
然后在 Webpack 打包的时候,打成 umd 的包,将组件全都打包到全局变量的某一个字段下,如 _components__,上面 componentCode 为 ComponentA 的组件就可以通过 window.components.CompondntA加载到。其组件配置可以通过 window.components.CompondntA.componentConfig加载到。

方式二:组件信息存储在服务端

按照方式1的方法去开发,随着项目运行的时间变长就会暴露出以下缺点:

  1. 物料变的越来越多,最终产物变的越来越大。
  2. 只修改了其中一个组件,就要所有的组件全部重新打包。
  3. 最终只产出了一个包,根本没办法针对单个物料做版本管理
  4. 组件只能交给一个某团队去维护,就算其他团队有类似自定义物料的需求,也必须在同一个仓库下开发,负责维护的团队还是要肩负起帮忙 code review、组件发布的问题,太耗费人力。这样也根本没办法让开发者都参与进来去共建平台的物料也很困难。

所以最终我们尝试将物料拆开,每个物料仓库和产物都是单独的。然后将物料的信息在服务端存储下来:

组件表

字段名类型含义
componentCodestring组件code,可作为组件唯一标识
componentNamestring组件名字,用于前台展示
packageNamestring组件 npm 包名
previewImagestring组件预览图 url,用于前台展示
versionIdstring当前的版本主键id

版本表

字段名类型含义
idstring主键id
versionstring版本名,如:1.1.1
resourceUrlstring组件资源地址,组件打成umd后上传到OSS后的资源地址,前台使用组件的时候加载此地址
statusnumber枚举,当前版本的状态:1 初始化 2 开发中 3 已发布 4 使用中 等等... 可随业务需要进一步细化
branchNamestring开发分支

组件跟版本的关系是一对多的。

物料的开发

物料的开发流程包括:初始化、本地开发、物料发布。
在这个阶段我们要做的事情就是:
提供一个脚手架,功能包括:初始化组件模板、组件本地开发、组件打包、单元测试能力(非必要) 等。 然后再结合公司的 devOps 平台实现组件的发布流程,组件在发布流程中实现发布 npm 包产物、发布静态资源产物然后将静态资源产物地址存储到组件的版本表中。

初始化组件模板

初始化组件模板,就是要创建出一个最简单的 demo,可以让开发者直接基于这个 demo 进行开发。
这里可以将模板托管的 git 上,然后脚手架中的初始化模板其实就是将 git 上的项目下载到执行者本地即可。
需要注意的点是,模板中有一些文件中可能有些变量是需要被替换的,比如有一个配置文件:

{
  componentCode: {{componentCode}}
}

这里的 compoenntCode 需要被替换成真实的组件code,这里我一般会采用 handlebars 去处理。
比如,此时初始化一个组件的命令为:

npx studio-cli create ComponentCode --packageName moduleName

就可以拿着命令行传入的 ComponentCode 和 --packageName 去替换模板中对应的变量。

组件本地开发

组件本地开发,就要满足开发者能够在开发阶段在本地去预览组件。
这里的做法是,项目里面搞一个 example 目录,然后再 example 目录里边引入这个组件,在使用 webpack 去启动一个服务即可。比如当前项目结构是这样:
-example
-index.html
-src
-index.tsx
-webpack.config.js
-tsconfig.json
-src (组件源码目录)
-node_modules
在 example 中创建 webpack 的配置文件:

module.exports = {
  mode: 'development',
  entry: path.resolve(__dirname, './src/index.tsx'),
  module: {
    rules: [
      {
        test: /\.(js|mjs|jsx|ts|tsx)$/,
        use: [ {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env', '@babel/preset-react'],
          }
        }, {
          loader: 'ts-loader',
          options: {
            configFile: path.resolve(process.cwd(), 'example/tsconfig.json'),
            transpileOnly: true,
          },
        }],
      },
      // ... 其他想要什么 loader 自己加
    ]
  },
  resolveLoader: {
    modules: [path.resolve(process.cwd(), 'node_modules'), path.resolve(process.cwd(), 'node_modules/@tuya-fe/txp-web-tools/node_modules')],
  }, 
  plugins: [
    new HtmlWebpackPlugin({
      template: path.join(__dirname, './index.html'),
      filename: 'index.html',
    }),
  ],
  output: {
    path: path.resolve(__dirname, './dist')
  },
  devServer: {
    static: {
      directory: path.join(__dirname),
    },
    compress: true,
    port: 3333,
  },
  resolve: {
    alias: {
      'react': path.resolve(__dirname, '../node_modules/react'),
      // 这里是组件的 npm 包名,将其映射到源码目录
      '@tuya-fe/ipc-countdown': path.resolve(__dirname, '../src/index.tsx'),
    },
    extensions: ['.js', '.jsx', '.ts', '.tsx', '.json', '.wasm'],
  },
}

如果支持 typescript 还得配置下 tsconfig.json。

{
  "paths": {
      "@tuya-fe/ipc-countdown": ["../src"]
    }
}

然后再 index.tsx 中去引用组件,就可以引用到源码目录下的组件,这个时候就可以使用各种姿势去调用组件且在本地预览了。

这里每个组件的 npm 包名肯定都是不同的,所以需要模板中去填充变量,在初始化的时候将变量替换成实际的组件的 npm 包名。

组件打包

提供打包能力是脚手架的重中之重,这里需要打包出两个产物:

  1. umd 产物的静态资源;
  2. npm包;

前者用于上传到组件-版本表中的 resourceUrl 字段,可供前台系统动态加载组件,后者后续会借助 devOps 平台将其发布到 npm,作为组件包标准的一种安装方式。

打出 umd 产物

借助 webpack 实现打出 umd 产物,只需要配置 output:

output: {
  path: 'xxx',
  filename: 'xxx',
  library: ['a', 'b'],
  libraryTarget: 'umd',
},

比如以上配置,最终 webpack 会生成 umd 的产物,当前台加载了 js 文件之后,就可以通过 window.a.b 去使用组件。

这里还有一个需要注意的点,就是要提取组件的公共依赖。
大家都知道 webpack 打包的时候,是会将当前 module 依赖的资源一起打包进去的。那当前组件如果依赖了 react,不做任何处理的情况下就会将 react 的代码也打包到最终的产物当中。
那如果每一个组件的产物中都包含 react,那就太浪费资源了,这个时候就需要将 react 提取成公共依赖,在 webpack 打包的时候将其剔除,在前台加载组件的时候,先加载公共依赖。

{
  externals: {
    react: {
      root: ['React'],
      amd: ['React'],
      commonjs: 'react',
      commonjs2: 'react',
    },
  }
}

要将哪些提取成公共依赖,在组件的量起来之后值得深挖。 比如,如果能有一种手段检测到当前捂脸系统中,80%的组件都在安装一个依赖,就值得将其提取成公共依赖。目前我们的系统中还没实现,不过我个人觉得这事情可以在 devOps 的发布流程中去监控。

打出 npm 包

这里我直接借用了 umijs 的 father 去做这个事情,其配置文件:

// .fatherrc.js
export default {
  // 以下为 esm 配置项启用时的默认值,有自定义需求时才需配置
  esm: {
    input: 'src', // 默认编译目录
    platform: 'browser', // 默认构建为 Browser 环境的产物
    transformer: 'babel', // 默认使用 babel 以提供更好的兼容性
    output: './es/',
    sourcemap: false,
  },
  // 以下为 cjs 配置项启用时的默认值,有自定义需求时才需配置
  cjs: {
    input: 'src', // 默认编译目录
    platform: 'node', // 默认构建为 Node.js 环境的产物
    transformer: 'esbuild', // 默认使用 esbuild 以获得更快的构建速度
    output: './lib/',
    sourcemap: false,
  }
};

单元测试能力

单元测试能力,不是组件上线的必须能力,但是单元测试能够有效的保证组件在迭代过程中的质量(前提是单测覆盖率是足够的)。
在这里我希望单测能力对于通过脚手架初始化出来的组件是开箱即用的,无需任何配置,所以我将单测能力集成到了脚手架里面,单测框架使用的是 jest。
首先是,脚手架里面就默认有了一份配置,可供开发者再不做任何事情的情况下就将单元测试能力使用起来,然后是肯定也要提供给开发者自定义 jest 配置的能力。

const path = require('path');
const fs = require('fs-extra');

function getJestConfig() {
  let jestConfig = require("./jest.config");

  const customJestConfigPath = path.resolve(process.env.ROOT_PATH, "custom.jest.config.js");
  if (fs.pathExistsSync(customJestConfigPath)) {
    jestConfig = require(customJestConfigPath)(Object.assign({}, jestConfig));
  }
  return {
    ...jestConfig,
    transform: JSON.stringify(jestConfig.transform),
  };
}

module.exports = {
  getJestConfig
}

如以上代码所示,如果项目根目录存在 custom.jest.config.js的话,就会执行其暴露出来的方法,并将当前的 jest 配置传进去,用来修改 jest 配置。
然后,平时我们在项目中使用 jest 是直接使用 jest-cli 去执行命令,这里将 jest 集成到脚手架中用到了 jest 的脚本:

const { runCLI } = require("jest");

const { getJestConfig } = require('./getJestConfig');

async function test(options) {
  const config = getJestConfig();
  res = await runCLI(
    {
      ...config,
      silent: options.silent,
      cache: options.cache,
      updateSnapshot: options.u,
      coverage: options.coverage,
      clearCache: options.clearCache,
      ci: options.ci,
      debug: options.debug,
    },
    [process.cwd()]
  );
  if (res.results.success) {
    console.log(`Tests completed`);
  } else {
    console.error(`Tests failed`);
    process.exit(1);
  }
}

module.exports = {
  test,
};

需要特别注意的是,使用这种方法去运行 jest 时,transform 的配置一定要转成 json 字符串。
image.png
更多关于如果在项目中使用 jest 的实践可以查看文章:在实际项目中积累的 jest 编写单元测试攻略

调试组件能力

试想一下,现在我们经过开发阶段,将组件发布了然后再 0 代码或者低代码搭建平台上加载了该组件的静态资源,然后再搭建平台上针对该组件的 schema 进行配置,就在这个时候,组件它报错了。
这个时候,能够在平台上直接调试组件代码就很重要了。
第一步,就是要让平台上的组件资源加载时,加载到本地的代码。
这里可以使用 chrome 插件 XSwitch 将组件资源代理到本地。
经过第一步之后,平台上的组件资源加载到本地了,但是要实现在本地畅快的调试的话,还需要一个至关重要的东西:sourcemap。
我们在本地开发时,使用的是 dev 模式,这个时候产物里肯定是有 sourcemap 的:
image.png
其指向的 sourcemap 文件是跟 index.js 同级的。
但是如果是在搭建平台上将组件请求代理到了此文件上,比如搭建平台的 url 是:www.test.com/create,此时其加… sourcemap 文件 url 就变成了:www.test.com/index.js.ma…
所以这里需要将 sourcemapURL 指向本地。
修改 webpack 配置:

const webpack = require('webpack');

const port = 3333;

module.exports = {
  mode: 'development',
  devtool: false,
  plugins: [
    new webpack.SourceMapDevToolPlugin({
      filename: '[file].map',
      publicPath: `http://127.0.0.1:${port}/`,
    })
  ],
  devServer: {
    port,
  }
}

image.png
现在 sourcemap 也能加载到本地的了,就可以愉快的看清错误堆栈或者打断点调试了。

物料管理

急着水文,但又还没写,后面在写。

物料加载

在搭建页面,是不可能一次性将所有物料全都加载的。不管物料是以单个的形式存储(这种情况下有 n 个物料),还是以组件库的形式存储(这种形势下可能存在 n 个组件库),这里都需要动态的去加载物料。
前面已经说了,组件最终发布后会有 umd 产物,通过组件版本的 resourceUrl 字段即可加载到。所以在前台是可以 script 标签去实现组件的动态加载的。
然后前面也说了,有一些高频组件(比如 react)被提取成了公共依赖,所以在加载组件资源之前,一定要先加载 react 资源,比如是这样配置的:

externals: {
  react: {
    root: ['React'],
    amd: ['React'],
    commonjs: 'react',
    commonjs2: 'react',
  }
},
import React from 'react';
window.React = React;

就是这么简单,这么朴实无华。

参考文章

Webpack 打包 commonjs 和 esmodule 模块的产物对比