一、介绍
在 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"
}
}
国际化支持和本地调试
因为我们是一个可以提供给其他项目直接引入 MyKonva 组件进行渲染表格的场景,为了支持根据使用项目的语音环境切换组件里的文案,我们可以参考 Element Plus - 国际化 进行设计:
- 支持一个 Vue 插件:用于组件注册
import ElementPlus from 'element-plus'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
app.use(ElementPlus, {
locale: zhCn,
})
- 提供一个 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:用于处理选项配置。具体来说:
provideGlobalConfig
方法会将全局配置信息以 Vue 3 中provide/inject
的方式提供给组件。这些全局配置信息包括诸如主题色、默认尺寸等与样式相关的配置。- 当组件使用时,它们会通过
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>