HOW - 开源表格组件封装和使用(含国际化配置)

一、介绍

HOW - Canvas 入门系列之基于vue-konva的多维表格(四) 中我们介绍过如何基于 vue-konva 渲染一个多维表格。

今天我们将提供一个开源能力,支持在自己项目中引入 MyKonva 来渲染 rowList,并支持多语言。

import MyKonva from 'MyKonva';
<MyKonva :width="200" :height="100" :row-list="[]" />

二、具体步骤

创建项目

我们选择基于 vite 来构建仓库:

npm create vite@latest

请添加图片描述
输入项目名称,选择框架、语言。

cd my-konva

package.json

{
  "name": "my-konva",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vue-tsc && vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "vue": "^3.4.21"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^5.0.4",
    "typescript": "^5.2.2",
    "vite": "^5.2.0",
    "vue-tsc": "^2.0.6"
  }
}

国际化支持和本地调试

Vue I18n 官方文档

因为我们是一个可以提供给其他项目直接引入 MyKonva 组件进行渲染表格的场景,为了支持根据使用项目的语音环境切换组件里的文案,我们可以参考 Element Plus - 国际化 进行设计:

  1. 支持一个 Vue 插件:用于组件注册
import ElementPlus from 'element-plus'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
app.use(ElementPlus, {
  locale: zhCn,
})
  1. 提供一个 Vue 组件 ConfigProvider 组件:用于全局配置国际化的设置
<template>
  <el-config-provider :locale="locale">
    <app />
  </el-config-provider>
</template>
<script>
  import { defineComponent } from 'vue'
  import { ElConfigProvider } from 'element-plus'
  import zhCn from 'element-plus/es/locale/lang/zh-cn'
  export default defineComponent({
    components: {
      ElConfigProvider,
    },
    setup() {
      return {
        locale: zhCn,
      }
    },
  })
</script>

这里 locale 在真实项目中可以存储在 vuex 或者 pinia 中,然后在页面 Header 摸了提供一个切换语言的 Button,触发切换 store.locale 即可使得 Element 组件文案正常切换。

我们先在项目里引入 vue-i18n 试试,实现将我们的多语言配置正常显示。

首先,安装依赖:

npm install vue-i18n@next

package.json 新增如下依赖:

  "dependencies": {
    "vue": "^3.4.21",
    "vue-i18n": "^10.0.0-alpha.5"
  },

开始创建多语言文案:

// src/locales/en.ts
var English = {
  name: "en",
  konva: {
    refresh: "Refresh"
  } 
}
export { English as default };
// src/locales/zh-cn.ts
var zhCn = {
  name: "zh-cn",
  konva: {
    refresh: "刷新"
  } 
}
export { zhCn as default };

createI18n:

// src/locales/i18n.ts
import { createI18n } from "vue-i18n";
import English from './en';
import zhCn from './zh-cn';
const i18n = createI18n({
  locale: 'en',
  fallbackLocale: 'en',
  messages: {
    'en': English.konva,
    'zh-cn': zhCn.konva
  }
})
export default i18n

i18n 注入:

// src/main.ts
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import i18n from './locales/i18n'

createApp(App).use(i18n).mount('#app')

支持切换语言的 Button:

// src/App.vue
<script setup lang="ts">
import MyKonva from './components/MyKonva.vue';
import { useI18n } from 'vue-i18n';

const { locale } = useI18n();
const changeLanguage = () => {
  locale.value = locale.value === 'en' ? 'zh' : 'en';
};
</script>

<template>
  <div>
    <button @click="changeLanguage">Change Language</button>
  </div>
  <MyKonva :width="200" :height="100" :row-list="[]" />
</template>

<style scoped>
</style><script setup lang="ts">
import MyKonva from './components/MyKonva.vue';
import { useI18n } from 'vue-i18n';

const { locale } = useI18n();
const changeLanguage = () => {
  locale.value = locale.value === 'en' ? 'zh-cn' : 'en';
};
</script>

<template>
  <div>
    <button @click="changeLanguage">Change Language</button>
  </div>
  <MyKonva :width="200" :height="100" :row-list="[]" />
</template>

<style scoped>
</style>

创建 MyKonva 组件并显示文案:

<script setup lang="ts">
import { toRefs } from 'vue';
import { useI18n } from 'vue-i18n';

const props = defineProps<{ 
  width: number,
  height: number,
  rowList: string[],
}>()

const { t } = useI18n();
const { width, height, rowList } = toRefs(props);

const refresh = () => {
  // Refresh logic here
  console.log('Refreshed', rowList.value);
};
</script>

<template>
  <div>
    <button @click="refresh">{{ t('refresh') }}</button>
    <v-stage :config="{ width: width, height: height }">
      <!-- Konva content goes here -->
    </v-stage>
  </div>
</template>

<style scoped>
/* Add any necessary styles here */
</style>

最终效果:
请添加图片描述

点击切换语言:
请添加图片描述

国际化支持外部项目

OK. 我们的语言包可以正常使用。

接下来,为了在其他项目也支持 MyKonva 国际化,我们需要基于上述多语言配置进行改造。

1. 支持一个 Vue 插件

package.json

{
	"main": "lib/index.js",
	"module": "es/index.mjs",
	"types": "es/index.d.ts",
}

src/index.ts

//参考:node_modules/element-plus/es/index.mjs
export { default } from './defaults.mjs';

src/defaults.ts

//参考:node_modules/element-plus/es/defaults.mjs
import { makeInstaller } from './make-installer.mjs';
var installer = makeInstaller();
export { installer as default };

// installer 使用示例
//import MyKonva from 'my-konva'
//import zhCn from 'my-konva/es/locale/lang/zh-cn';
//app.use(MyKonva, { locale: zhCn });

src/make-installer.ts

//参考:node_modules/element-plus/es/make-installer.mjs
import { MyKonva } from '../components/my-konva/index.ts';
const makeInstaller = () => {
  const install(app, options = {}) {
    // 选项配置(含多语言)
    if (options) {
    	// 入参:config, app, global = false
    	// global = true 代表为全局注册模式
    	provideGlobalConfig(options, app, true);
    }
    // 注册组件
    app.component('MyKonva', MyKonva);
  }
  return {
  	install,
  }
};
export { makeInstaller };

接下来是关键:src/components/config-provider/src/hooks/use-global-config.ts

这个文件其实也是后续要提供的 ConfigProvider 组件中一个内置 hook:用于处理选项配置。具体来说:

  1. provideGlobalConfig 方法会将全局配置信息以 Vue 3 中 provide/inject 的方式提供给组件。这些全局配置信息包括诸如主题色、默认尺寸等与样式相关的配置。
  2. 当组件使用时,它们会通过 inject 方法获取到这些全局配置信息。如果组件内部没有对某些配置进行自定义,那么将使用全局配置信息中的默认值。

通过这种方式,Element-Plus 提供了一种灵活的配置方式,允许开发者在整个应用中统一管理样式相关的配置,同时也可以在需要时对特定组件进行个性化配置。接下来我们学习具体实现:

//参考:node_modules/element-plus/es/components/config-provider/src/hooks/use-global-config.mjs
import { getCurrentInstance, provide } from 'vue';
import { localeContextKey } from '../../../../hooks/use-locale/index.mjs';

const useGlobalConfig = () => {}
const mergeConfig = (a,b) => {
	const keys = [...new Set([...keysOf(a), ...keysOf(b)])];
	const obj = {};
	for (const key of keys) {
		obj[key] = b[key] !== void 0 ? b[key] : a[key];
	}
	return obj;
}

const provideGlobalConfig = (config, app, global = false) => {
  var _a;
  // 检查是否在 setup 中执行
  const inSetup = !!getCurrentInstance();
  const oldConfig = inSetup ? useGlobalConfig() : void 0;
  // 确定使用哪个 provide 函数。如果传入了 app 且 app.provide 存在,则使用 app.provide。否则,如果在 setup 中,则使用 Vue 的 provide 函数。
  const provideFn = (_a = app == null ? void 0 : app.provide) != null ? _a : inSetup ? provide : void 0;
  if (!provideFn) {
    debugWarn("provideGlobalConfig", "provideGlobalConfig() can only be used inside setup().");
    return;
  }
  // 创建合并后的配置对象
  // 如果旧配置存在,则与新的 config 合并,否则直接使用新的 config。
  const context = computed(() => {
    const cfg = unref(config);
    if (!(oldConfig == null ? void 0 : oldConfig.value))
      return cfg;
    return mergeConfig(oldConfig.value, cfg);
  });
  // 提供全局配置
  // 这里我们可以先只关注多语言
  //provideFn(configProviderContextKey, context);
  provideFn(localeContextKey, computed(() => context.value.locale));
  //provideFn(namespaceContextKey, computed(() => context.value.namespace));
  //provideFn(zIndexContextKey, computed(() => context.value.zIndex));
  //provideFn(SIZE_INJECTION_KEY, { size: computed(() => context.value.size || "") });
  
  if (global || !globalConfig.value) {
    globalConfig.value = context.value;
  }
  return context;
}
export { provideGlobalConfig };

src/hooks/use-locale/index.ts

// 参考:node_modules/element-plus/es/hooks/use-locale/index.mjs
const localeContextKey = Symbol("localeContextKey");
export { localeContextKey };

通过上述实现,我们已经实现 locale 的 provide 依赖注入了。接下来看看在组件里如何使用的吧:

//node_modules/element-plus/es/components/pagination/index.mjs
import '../../utils/index.mjs';
import Pagination from './src/pagination.mjs';
export { paginationEmits, paginationProps } from './src/pagination.mjs';
export { elPaginationKey } from './src/constants.mjs';
import { withInstall } from '../../utils/vue/install.mjs';

const ElPagination = withInstall(Pagination);

export { ElPagination, ElPagination as default };
//node_modules/element-plus/es/components/pagination/src/pagination.mjs
import { useLocale } from '../../../hooks/use-locale/index.mjs';

var Pagination = defineComponent({
	set(props, { emit, slots }) {
		return () => {
			debugWarn(componentName, t("el.pagination.deprecationWarning"));
		}
	}
})

下面我们分析多语言 hook 的完整实现:

//node_modules/element-plus/es/hooks/use-locale/index.mjs
import { unref, computed, isRef, ref, inject } from 'vue';
import { get } from 'lodash-unified'; // 用于在对象中安全地获取嵌套属性
import English from '../../locale/lang/en.mjs';

// translate 函数根据 path(譬如 t("el.pagination.deprecationWarning") 中的 'el.pagination.deprecationWarning') 获取 locale 对象中的对应字符串
// 并判断是否有 option,有则使用 option 中的值替换字符串中的占位符
// 使用 lodash-unified 的 get 函数从 locale 对象中获取指定路径的值,如果路径不存在,则返回路径本身
// 使用正则表达式匹配占位符 {key} 并替换为 option 中对应的值,如果 option 中没有该值,则保留原样
const translate = (path, option, locale) => get(locale, path, path).replace(/\{(\w+)\}/g, (_, key) => {
  var _a;
  return `${(_a = option == null ? void 0 : option[key]) != null ? _a : `{${key}}`}`;
});
// buildTranslator 接受一个 locale 对象,返回一个翻译函数
// 返回的翻译函数接受 path 和 option 两个参数,并调用 translate 函数进行翻译
const buildTranslator = (locale) => (path, option) => translate(path, option, unref(locale));
// buildLocaleContext 接受一个 locale 对象,返回一个包含语言环境的上下文对象
const buildLocaleContext = (locale) => {
  // 我们前面定义过多语言文案文件
  // var English = { name: "en", konva: { refresh: "Refresh" } };
  const lang = computed(() => unref(locale).name);
  const localeRef = isRef(locale) ? locale : ref(locale);
  return {
    lang,
    locale: localeRef,
    t: buildTranslator(locale)
  };
};
const localeContextKey = Symbol("localeContextKey");
const useLocale = (localeOverrides) => {
  // 注意,这里就用到了我们前面实现的依赖注入:inject(localeContextKey)
  const locale = localeOverrides || inject(localeContextKey, ref());
  // 构建语言环境上下文,并确保语言环境有一个默认值(如 English)
  return buildLocaleContext(computed(() => locale.value || English));
};

export { buildLocaleContext, buildTranslator, localeContextKey, translate, useLocale };

通过上面的学习,我们也知道 vue-i18n 下面三个特性的具体原理了:

  • 为什么用 t:return { lang, locale: localeRef, t: buildTranslator(locale)};
  • 如何根据 path 替换文案:t("el.pagination.deprecationWarning")
  • 如何根据占位符来替换变量:t('apples', { count: 5 }) 在文案里定义的是 apples: "You have {count} apples."

2. 提供一个 ConfigProvider 组件

前面实现了内部组件根据 locale 加载不同文案。

还需要提供一个支持外部项目修改全局配置的能力,如:

<el-config-provider :locale="locale">
  <app />
</el-config-provider>

具体实现:

//node_modules/element-plus/es/components/config-provider/index.mjs
import '../../utils/index.mjs';
import ConfigProvider from './src/config-provider.mjs';
export { messageConfig } from './src/config-provider.mjs';
export { configProviderProps } from './src/config-provider-props.mjs';
export { configProviderContextKey } from './src/constants.mjs';
export { provideGlobalConfig, useGlobalComponentSettings, useGlobalConfig } from './src/hooks/use-global-config.mjs';
import { withInstall } from '../../utils/vue/install.mjs';

const ElConfigProvider = withInstall(ConfigProvider);

export { ElConfigProvider, ElConfigProvider as default };
//node_modules/element-plus/es/components/config-provider/src/config-provider.mjs
import { defineComponent, watch, renderSlot } from 'vue';
import { provideGlobalConfig } from './hooks/use-global-config.mjs';
import { configProviderProps } from './config-provider-props.mjs';

//用于存储消息相关的全局配置
const messageConfig = {};
const ConfigProvider = defineComponent({
  name: "ElConfigProvider",
  props: configProviderProps,
  setup(props, { slots }) {
    watch(() => props.message, (val) => {
      Object.assign(messageConfig, val != null ? val : {});
    }, { immediate: true, deep: true });
    //调用 provideGlobalConfig 函数,将 props 传入以生成全局配置对象 config 如 { locale: { name: 'en', konva: {} } }
    const config = provideGlobalConfig(props);
    // 返回一个渲染函数,渲染插槽内容并传入 config 作为插槽的上下文
    // function renderSlot(slots, name, props, fallback) {}
    // 1. slots 组件中定义的插槽集合
    // 2. 要渲染的插槽名
    // 3. 传递给插槽内容的 props
    // 4. 没有匹配插槽时的备选内容
    return () => renderSlot(slots, "default", { config: config == null ? void 0 : config.value });
  }
});
export { ConfigProvider as default, messageConfig };

至此,开源表格组件含多语言支持的完整能力已实现。

打包发布

构建

npm run build

发布

npm login
npm publish

在其他项目引入

// src/main.js
import { createApp } from 'vue';
import App from './App.vue';
import MyKonva from 'my-konva';
import zhCn from 'my-konva/es/locale/lang/zh-cn';
//import euLang from 'my-konva/es/locale/lang/eu';

const app = createApp(App);
app.use(MyKonva, { locale: zhCn });

// 使用 pinia 或其他状态管理工具来管理全局的语言状态
import { createPinia } from 'pinia';
const pinia = createPinia();
app.use(pinia);

app.mount('#app');
//src/pinia/locale.ts
import { defineStore } from 'pinia'
export const useLocaleStore = defineStore('locale', {
  // other options...
  const locale = ref('zh-cn');
  return  { locale };
})
//src/App.vue
<template>
<my-konva-config-provider :locale="localeConfig">
  <div>
    <MyKonva :width="800" :height="600" :row-list="rowList"/>
    <button @click="changeLanguage">Change Language</button>
  </div>
</my-konva-config-provider>
</template>

<script>
import { ref } from 'vue';

import { MyKnovaConfigProvider } from 'my-konva';
import zhCn from 'my-konva/es/locale/lang/zh-cn';
import euLang from 'my-konva/es/locale/lang/eu';

import { useLocaleStore } '@/pinia/locale';
import { storeToRefs } from 'pinia'

export default {
  setup() {
  	const localeConfigMap = {
		'zh-cn': zhCn,
		'en': euLang,
	}
	const localeConfig = ref(zhCn);
  	
    const rowList = ref([]);

	const store = useLocaleStore();
	const { locale } = storeToRefs(store);

    const changeLanguage = () => {
      locale.value = locale.value === 'en' ? 'zh-cn' : 'en';
      localeConfig.value = localeConfigMap[locale.value];
    };

    return {
      rowList,
      changeLanguage
    };
  }
};
</script>
  • 17
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值