• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

当pinia遇上web-localstorage-plus,打不过就申请加入

武飞扬头像
我不只是切图仔
帮助7

大家好,我是苏先生,一名热爱钻研、乐于分享的前端工程师,跟大家分享一句我很喜欢的话:人活着,其实就是一种心态,你若觉得快乐,幸福便无处不在

github与好文

你可以从本文学到什么

1.如何开发一个pinia插件

2.如何开发一个vite插件

3.如何开发一个webpack插件

前言

学新通

所以,咱们本文的目的就一个,那就是改造web-localStorage-plus,让它能够站在Pinia这个巨人的肩膀上

学新通

思考

俗话说,兵马未动,粮草先行......

学新通

我的意思是,我们要先考虑出来一个方向,然后再动手进行改造🤔

有两个大的方向:

  • 对原有的web-localStorage-plus进行改造

是让Pinia作为web-localStorage-plus的插件还是让web-localStorage-plus作为Pinia的插件,尽管它们本质的实现都一定是一个符合Pinia插件规范的函数,但是这对web-localStorage-plus的接口设计却影响很大。比如,如果让web-localStorage-plus作为Pinia的插件,则必须在web-localStorage-plus内部重新单独导出一个函数,但如果是反过来的话,则恰好可以利用web-localStorage-plus本身的use接口

想要实现这一点,只需要对原有的use接口进行改造即可,如下

// src/core/api/use.ts
function use(pinia:Pinia):FalsyValue;
function use(...):FalsyValue;
function use(type: PluginCb | Pinia, framework?: "customer" | "buildIn") {
  if(typeof type === 'function'){
    ...
    return 
  }
  runAsPiniaPlugin(type,native)
}
  • 新开发一个npm

考虑到需要对热更新进行支持,如果采取方案一,则会让web-localStorage-plus包变的不纯粹,因为它不应该与piniavitewebpack强相关

实现

补充Pinia实例类型

首先,我们找到PiniadefineStore的类型定义

export declare function defineStore<...>(id: Id, options: Omit<DefineStoreOptions<...>, 'id'>): ...;

options即我们要扩展的部分

options: Omit<DefineStoreOptions<Id, S, G, A>

进入DefineStoreOptions,它扩展自DefineStoreOptionsBase

export declare interface DefineStoreOptions<Id extends string, S extends StateTree, G, A> extends DefineStoreOptionsBase<S, Store<Id, S, G, A>> {
    ...
}

找到DefineStoreOptionsBase,它是一个空的interface

export declare interface DefineStoreOptionsBase<S extends StateTree, Store> {
}

故我们借助DefineStoreOptionsBasePinia补充TypeScript类型

// src/helper/types.ts
export interface PersistedStateOptions {
  namespace?: string ;
  paths?: Array<string>;
}

export interface DefineStoreOptionsBase<S extends StateTree, Store> {
    persist?: boolean | PersistedStateOptions 
}

同理,借助PiniaCustomPropertieshmr设计TypeScript类型

export interface PiniaCustomProperties {
    $hydrate: (payload: {
      state: StateTree;
      persist: boolean | PersistedStateOptions;
    }) => void;
    $discard: (id: string) => void;
}

初始化

首先,使用web-localStorage-plus创建一个命名空间,后续pinia相关的状态都设置到该空间下

function initSpaceForPinia(ctx: This) {
  const hasSpace = ctx.getItem(NAMESPACE);
  if (hasSpace) return;
  ctx.setItem(NAMESPACE, {});
}

接着,将其注册为pinia插件

pinia.use(internalPiniaPlugin(ctx));

状态激活

当刷新页面后,我们从web-localStorage-plus存储中取出对应的状态并重新设置给pinia,这其实分为两种情况,当store已经存在时,此时用于从web-localStorage-pluspinia激活,否则说明是进行初始化,需要将pinia中的状态保存到web-localStorage-plus

function activateState(payload: Params) {
  const { key, ctx, piniaCtx, paths, state } = payload;
  if (spaceToStoreId.has(key)) {
    const store = ctx.getItem(key, NAMESPACE);
    if (store) {
      const latest = updateStore(paths, store, ctx, key, state);
      piniaCtx.$patch(latest);
      return;
    }
    persistState(payload);
  }
}

保持响应

Pinia中的状态发生改变时,我们要对其进行同步更新,这只需要监听store.$subscribe方法,当其回调后调用persistState即可

如下,我们实际上是将state中的值按paths排除后重新向本地更新了一份

function persistState(payload: Omit<Params, "piniaCtx">) {
  const { key, ctx, paths, state } = payload;
  if (spaceToStoreId.has(key)) {
    for (let i = 0; i < paths.length; i  ) {
      const v = paths[i];
      const rest = paths.slice(i   1);
      const index = rest.findIndex((r) => r.startsWith(v));
      if (index > -1) {
        paths.splice(i   index, 1);
        i--;
      }
    }
    ctx.setItem(key, pick(state, paths), NAMESPACE);
  }
}

paths配置项的更新

paths配置项改变时,应当重新设置web-localStorage-plus下对应命名空间的值,由于将paths设置到localStorage是一个冗余的字段,故需要与localStorage中的存储值进行比较更新,这无外乎有以下几种情况:

  • paths新增了key

此时,需要将新增的key对应的state中的内容更新到localStorage

  • paths删除了key

此时,需要找到localStorage中的key进行删除

  • 使用persist配置项代替对象配置

此时,按照state全量更新到localStorage

虽然,情况是这么个情况,但是在实际开发中,并不需要严格按照此分类进行讨论,笔者这里采取对象合并的形式来进行统一,首先要根据paths初始化一个空对象

let processingObj = helpers.createObjByPaths(paths, state);

接着分别与web-localStorage-pluspiniastate进行对象合并

const _mergeStoreCb = (objValue: any, srcValue: any) => {
    if (isObject(objValue) && isObject(srcValue)) {
      return helpers.mergeDeep(objValue, srcValue, _mergeStoreCb);
    }
    if (objValue === undefined) {
      return deleteFlag;
    }
    if (!helpers.isSameType(objValue, srcValue)) {
      return objValue;
    }
};

最后,重新设置到web-localStorage-plus即可

ctx.setItem(key, processingObj, NAMESPACE);

处理热更新

当热更新时,需要同步更新web-localStorage-plus的存储值。这以存储的唯一凭证id是否改变分为两类:

id不变时,调用hydratestate中的值持久化到本地

ctx.$hydrate?.({
  ...JSON.parse(msg.data),
  id,
});

id改变时,需要打印出提示,并且将原仓库从web-localStorage-plus中删除

if (id !== initialUseStore.$id && initialUseStore) {
    console.warn(
      `[@web-localstorage-plus/pinia]:检测到存储库的id从"${initialUseStore.$id}"变成"${id}"了`
    );
    initialUseStore(pinia, pinia._s.get(initialUseStore.$id)!).$discard?.(initialUseStore.$id);
    useStore(pinia, pinia._s.get(id)!);
}

开发plugin

目前来说,对用户是相当繁琐的存在,因为其不得不手动的在每一个pinia模块内设置和调用

if (import.meta.hot)
  import.meta.hot.accept(acceptHMRUpdateWithHydration(useStore, import.meta.hot))

因此,最好的方式是写一个plugin帮用户做这件事情,笔者这里暂时只提供对vitewebpack的支持。它们的思路很简单,即:对源码进行识别,识别到可用的pinia模块后,将热更新相关的代码帮助用户进行注入即可

export default function transform(code: string, id: string) {
  let { apiName, stopIndex } = extractApi(code);
  if (apiName) {
    const api = extractRegisterApi(code.slice(stopIndex), apiName);
    if (api) {
      const s = new MagicString(code);
      s.prepend(
        `import { acceptHMRUpdateWithHydration } from '@web-localstorage-plus/pinia';\n`
      );
      s.append(`if (import.meta.hot)\n`);
      s.append(
        `  import.meta.hot.accept(acceptHMRUpdateWithHydration(${api}, import.meta.hot));\n`
      );
      return {
        code: s.toString(),
        map: s.generateMap({ source: id, includeContent: true }),
      };
    }
  }
}

至于vitewebpack的支持,笔者并没有选用第三方库来做,因为用到的hook太有限了

  • vite

vite只需要配置transform钩子即可

export default function vitePlugin(folder: string): Plugin {
  return {
    name: "vite:web-localstorage-plus-pinia-hmr",
    transform,
  };
}
  • webpack

webpack则需要将其作为loader使用

compiler.options.module.rules.unshift({
  enforce: "pre",
  use,
});

需要注意的是,由于vitenode内建模块的不兼容,我们需要采取动态导入的形式来生成loader的指向地址

import("node:path").then(mod=>{
    mod.resolve(...)
})

还有一点,就是需要在use函数中生成fileId,因为在transfrom的实际函数体内拿不到id,这会导致在vite中正常运行的transform出错

let fileId = data.resource   (data.resourceQuery || "");

最后,导出一个函数单独处理id的获取并在transform中调用即可

export async function getFileId(id: string) {
  if (webpackContext.folder) {
      return webpackContext.fileId;
  }
  return id;
}

使用

  • 安装依赖
yarn add web-localstorage-plus
yarn add @web-localstorage-plus/pinia
  • main.ts中设置持久化
import createStorage from 'web-localstorage-plus';
import setPiniaPersist from '@web-localstorage-plus/pinia';
// 设置根存储库
createStorage({
    rootName: 'spp-storage',
});
// 将pinia中的数据持久化到本地
setPiniaPersist(pinia);
  • vite.config.ts中引入热更新插件
import { getPlugin } from '@web-localstorage-plus/pinia';
const piniaHmrPlugin = getPlugin('vite');
export default defineConfig({
    ...,
    plugins:[piniaHmrPlugin(resolve(__dirname, 'src/store'))]
})

如果本文对您有用,希望能得到您的点赞和收藏

订阅专栏,每周更新1-2篇类型体操,每月1-3篇vue3源码解析,等你哟😎

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhfhajcg
系列文章
更多 icon
同类精品
更多 icon
继续加载