🌞

通过内存文件系统优化组件开发

背景对于一个开发工具,管理多个组件的构建,组件构建本身是一个独立进程,上层命令需要管理这些构建过程。需要构建过程(产物)与上层管理实现强耦合。例如一个服务形态如下: 以上结构造成的开发构建组件的问题:

文链接在语雀:https://www.yuque.com/wumingshi/rkh1qq/

背景

对于一个开发工具,管理多个组件的构建,组件构建本身是一个独立进程,上层命令需要管理这些构建过程。需要构建过程(产物)与上层管理实现强耦合。

例如一个服务形态如下:

image.png 


以上结构造成的开发构建组件的问题:

  • 直接使用 webpack-dev-server,导致构建的产物无法通过其他进程实例进行管理。生成的文件也是通过自持服务托管,当启动多组件时,需要绑定多端口,无法使用上层服务作统一封装
  • 选择构建出文件到统一文件夹下,从而使上层服务可以获取到文件内容进行操作。这就带来的一个问题,构建效率的问题

尽管在大部分场景构建出文件对于开发端的影响并不明显,但是文件的IO相比内存处理,在文件大/多的情况下,差距还是比较明显的

要解决的问题

  • 内存文件系统
  • webpack 构建至内存
  • 上层服务托管内存中的文件
  • 主/子进程如何进行内存文件的同步
  • 兼容文件构建/内存构建两种方式
  • 组件本身(开发者)如何尽可能弱感知

内存文件系统

关于内存文件系统,其实原理很简单——无非就是对标 fs 模拟一个对象,实现内容的增删改查等。

现成的轮子主要有:memfsmemory-fs

这里直接使用webpack官方的 memory-fs,而且 webpack 构建需要依赖一个join方法,这个memfs是没有实现的

注意的是memory-fs 并没有单独打包,大概就是因为比较简单,可以直接源码引入,方便后续的更改

引入之后,需要做一下更改

// 自定义合并方法
function merge(o) {
    if (!isPlainObject(o)) { 
        return {}; }
    var args = arguments;
    var len = args.length - 1;
  
    for (var i = 0; i < len; i++) {
      var obj = args[i + 1];
  
      if (isPlainObject(obj)) {
        for (var key in obj) {
          if (hasOwn(obj, key)) {
            if (isPlainObject(obj[key])) {
                if (isPlainObject(o[key])) {
                    o[key] = merge(o[key], obj[key]);
                } else {
                    o[key] = obj[key] || o[key];
                }
            } else {
              o[key] = obj[key] || o[key];
            }
          }
        }
      }
    }
    return o;
}

class MemoryFileSystem {
    constructor(data) {
        this.data = data || {};
        this.join = join;
        this.pathToArray = pathToArray;
        this.normalize = normalize;
        this.mergeData = this.mergeData.bind(this);
    }

    mergeData(sourceData) {
        const newObj = merge(this.data, sourceData);
        this.data = newObj;
        return newObj;
    }
}

这里在实例上增加了一个mergeData的方法。方便数据同步,这个下文再说。

webapck 构建至内存

内存文件模型选好了,接下来需要改造webpack构建至内存。强大的webpack提供了node构建接口,只需要修改 outputFileSystem 即可

const compiler = webpack(config);
compiler = memoryfs // memofs 就是memory-fs构建出的实例

服务托管内存中的文件

构建出的内存在内存中,如何将内存托管出去呢?

技术选型:koa + koa-static / koa-send


静态文件的托管直接通过koa-static,使用很方便。但这里我们涉及到内容出处的改造,修改是少不了。观察下源码我们发现koa-static是koa-send的封装,这里直接拿出koa-send进行源码修改。


koa-send 主要就是实现了文件查找以及创建文件读句柄,提供给ctx.body; 而且整个操作依赖的是 fs 这个node module。yep!直接把 fs 替换成memoryf-fs是不是就可以了!


组件本身可能是内存构建也可能是文件构建,这里的独文件可以同时兼容两种读取方式,先查内存,没有再去文件中查找,则实现了两种文件系统的兼容

image.png


在使用的过程中发现,文件托管到内存中,输出的文件在服务端的读取时间大幅延长。达到5s+,最终排查发现,因为memory-fs中没有对输出的字节数做映射

// memory-fs MemoryFileSystem.js

statSync(_path) {
        let current = this.meta(_path);
        if(_path === "/" || isDir(current)) {
            return {
                isFile: falseFn,
                isDirectory: trueFn,
                isBlockDevice: falseFn,
                isCharacterDevice: falseFn,
                isSymbolicLink: falseFn,
                isFIFO: falseFn,
                isSocket: falseFn,
                size: Buffer.byteLength(current, 'utf8'), //增加此处
            };
        } else if(isFile(current)) {
            return {
                isFile: trueFn,
                isDirectory: falseFn,
                isBlockDevice: falseFn,
                isCharacterDevice: falseFn,
                isSymbolicLink: falseFn,
                isFIFO: falseFn,
                isSocket: falseFn,
                size: Buffer.byteLength(current, 'utf8'), //增加此处
            };
        } else {
            throw new MemoryFileSystemError(errors.code.ENOENT, _path, "stat");
        }
    }

即对stats对象增加 size 属性(内容buffer的字节数)即可。

主/子进程如何进行内存文件的同步

如果你仔细看了上文,会注意到一个问题,就是组件本身的构建以及koa服务是两个进程,内存不一致,这里的内存文件如何进行共享呢。node 多进程中进行内存共享的方式,这里直接跳过(因为我没找到很合适的内存共享策略,比如我研究了下shm-typed-array,最终一知半解,放弃了)。


这里选择了使用 socket 做数据同步。即子进程每次构建结束后,将内容同步到主服务进程。主服务进程可以再将内容广播出去,通知每个子进程进行数据同步,如果子进程直接的内容互不影响,广播这一步也可以省略。


子进程的构建是不同步的,每次更新需要对于内容进行文件替换,文件夹需要合并,体现在内存文件操作上,即上文提到的memory-fs中追加的mergeData方法。

从而在内存层面上实现了一个一致性的文件系统。

image.png


组件开发者弱感知

以上,整体功能已经完成,最后需要考虑的一点是,如何让开发者感知不到这种变化呢。好在开发过程中,组件的启动是通过主进程控制的,及主进程创建一个子进程并执行npm script。这里可以通过在启动的时候,创建一个方法,替代执行脚本。


由于组件的 webpack 以及其他 node_module 是组件内持有的(各个组件互不影响),则模块的引用需要依赖组件内部,实现思路:

  • 通过组件内组件一个配置项,导出webpack及webpack.config.js
  • cli中通过导出的webpack实例,以及webpack配置文件进行自定义执行。这里要注意的是webpack配置文件需要通过绝对地址引入,以用来使配置中的插件可以正确的查找

最后

做出来回看似乎很简单,但是从一开始往下想,似乎并没有很清晰。

解决实际问题总会依赖各种各样的思路想法,只要明确最终的目的,一切以使用者的角度出发,最终的结果一般不会太差。

updatedupdated2023-10-312023-10-31