Saving and restoring types to and from localStorag

2019-07-23 19:02发布

I built a dockable panels solution in which I want to store the layout to localStorage, so I can use it again when I reload the page. This is actually working like a charm on my localhost. These are the most important models and how I use them:

export class DockTreeNode {
  parent: DockTreeContainer;
  // methods..
}

export class DockTreePanel extends DockTreeNode {
  type: Type<DockablePanel>;
  args: DockArguments;
  movable: boolean;
}

export class DockTreeContainer extends DockTreeNode {
  private orientation: Orientation;
  private left: DockTreeNode;
  private right: DockTreeNode;
}

dockTree: DockTreeNode = new DockTreeContainer(
  new DockTreePanel(
    TestPanelComponent,
    null,
    false
  ),
  new DockTreePanel(
    BovenbouwingComponent,
    null,
    true
  ),
  Orientation.horizontal
);

The code for saving and restoring my layout is as follows:

private typeMap: Map<string, Type<DockablePanel>> = new Map();

/**
 * Saving is pretty damn simple
 */
public saveDockTree(): void {
  const saveString = JSON.stringify(this.dockTree, (key, value) => {
    if (key === 'parent') {
      return null;
    } else if (key === 'type') {
      return value.name;
    } else {
      return value;
    }
  });
  localStorage.setItem(this.localStorageKey, saveString);
}

/**
 * This method will check local storage for our stored tree and parse everything
 * and be all kinds of cool on local storage.
 */
public setDefaultDockTree(dockTree: DockTreeNode): void {
  this.defaultDockTree = dockTree;

  try {
    // Retrieve the stored item
    const storageItem = localStorage.getItem(this.localStorageKey);
    if (storageItem) {
      const stored = JSON.parse(storageItem, (key, value) => {
        if (key === 'type') {
          if (!this.typeMap.has(value)) {
            throw new Error('Layout types don\t match.');
          }
          return this.typeMap.get(value);
        } else {
          return value;
        }
      });

      // Convert to real dockTree
      const tree: DockTreeNode = this.JSONtoDockTree(stored);
      this.dockTree = tree;
    } else {
      throw new Error('Could not find layout');
    }
  } catch (e) {
    // Anything wrong? Reset the dockTree layout to default
    this.dockTree = this.defaultDockTree;
    this.saveDockTree();
    console.log('Could not parse stored layout. Resetting local storage');
  }
}

/**
 * I think this can be done better, but that's not the problem here.
 */
private JSONtoDockTree(tree: any): DockTreeNode {
  let node: DockTreeNode;

  if (tree.left && tree.right) {
    const left = this.JSONtoDockTree(tree.left);
    const right = this.JSONtoDockTree(tree.right);
    node = new DockTreeContainer(left, right, tree.orientation);
  } else if (tree.type) {
    node = new DockTreePanel(tree.type, tree.args, tree.movable);
  } else {
    throw new Error('Node is not a DockTreePanel or DockTreeContainer.');
  }

  return node;
}

Now in local storage my saved dockTree looks like this:

{
  "left": {
    "type": "TestPanelComponent",
  },
  "right": {
    "type": "SomePanelComponent",
  },
  // More
}

But when I commit it to the svn repository and Jenkins builds with ng build --prod and starts a Docker container running the build, the saved value for type will be complete nonsense:

{
  "left": {
    "type": "",
  },
  "right": {
    "type": "n",
  }
  // More stuff
}

I figured this would have to do with TypeScript compiling everything to JavaScript AOT, so the types don't actually exist when running the code. However, I don't understand that it does work on my localhost by simply using the typeMap<string, Type<DockablePanel> to store my types.

Why is this any different? Is there another way to get a type from a string that I am overlooking?

2条回答
孤傲高冷的网名
2楼-- · 2019-07-23 19:14

I solved it. I left out a critical part in my question:

private typeMap: Map<string, Type<DockablePanel>> = new Map();

  constructor(@Inject('panelTypes') private panelTypes: Type<DockablePanel>[]) {
    for (let i = 0; i < panelTypes.length; i++) {
      this.typeMap.set(panelTypes[i].name, panelTypes[i]);
    }
  }

Here I put the types of the panels into a Map using the name of the type as the key. The solution was to drop this and just use panelTypes itself:

constructor(@Inject('panelTypes') private panelTypes: Type<DockablePanel>[]) {  }

public setDefaultDockTree(dockTree: DockTreeNode): void {
  // more code
  const stored = JSON.parse(storageItem, (key, value) => {
    if (key === 'type') {
      // The changed part:
      const panel: Type<DockablePanel> = this.panelTypes.find((panelType: Type<DockablePanel>) => {
        return panelType.name === value;
      });
      if (!panel) {
        console.log('Couldn\'t find paneltype ' + value);
      } else {
        return panel;
      }
    } else {
      return value;
    }
  });
  // More code
}

With the Map out of the picture, I just use the Type<>.name property to find the correct Type.

查看更多
劫难
3楼-- · 2019-07-23 19:20

The problem is primarily related to this question.

Function name can be used, as long as a developer is aware of consequences. It is rather safe to rely on it in some environments like Node.js. If the code is supposed to be minified (it usually is in client-side JS), function names will be minified, too.

In order to make sure that proper class names are stored and be able to restore them, classes should be definitely mapped to their names (there also can be multiple functions with same name).

Classes should either have static property that will identify them. name isn't safe to be redefined, and displayName is conventional. It may be safer to make name unique, because this way classes that cannot be safely stored should cause an error. A convenient way to implement this would be class decorator.

Another way is to have a map of known class names:

const classMap = {
  TestPanelComponent,
  ...
};

It can be referred when classes are stored. Again, if a class doesn't exist in the map, it should cause an error.

查看更多
登录 后发表回答