本期所用到的技术
- VUE3
- VUE2
- VITE
公司最近有一个国际化的需求,调研了市面上的国际化技术,有用现成组件的,比如vue-i18n,react-i18n,也有自己写的。因为我的需求,需要实时请求后端多语言接口,拿到对应语种下的数据,再进行渲染。
所以需要自己单独写一套通用的组件,来让其他人使用,并且要保证最小化影响他人。
考虑到我们部门的技术栈基本都是vue,所以前期先写vue3的版本,一开始也是一筹莫展,不知道该如何下手,后来看到了vue3官网的plugin介绍,一下子就来了灵感。
vue3 plugin链接如下 截图如下
大体上其实就是三步,这也和其他的vue-i18n等组件比较像
- 将原来的原文(要翻译的值),换成一个特定的字符串
- 设置一个$translate函数,用于实时替换原文
- 将这个第二步的函数全局导入到你的vue3实例里,使得你接下来的组件都可以获取到
站在巨人的肩膀上开始拓展
接下来就是我的组件大体运行流程
mindmap
src
EcloudI18n.vue
作用是提供一个最外层的组件,并在这个组件内,进行数据的请求和页面刷新
i18n.js
提供vue plugin注入的函数,作用是提供一系列全局函数和注册全局组件EcloudI18n.vue
http
common.js
提供请求的函数
config.js
封装axios
结合上面两张图片,其实重点就是在i18n.js这个函数中,下面重点介绍一下
// plugins/i18n.js
import EcloudI18n from './Ecloud-i18n.vue'
import { ref, computed } from 'vue'
import axios from './http/config'
export default {
install: (app, options) => {
let $transData = ref({})
let $languageType = ref(undefined)
app.provide('$languageType', $languageType)
// 注入一个全局可用的 $translate() 方法
let $translate = (key) => {
let storageTransData
if ($languageType.value != undefined) {
storageTransData =
localStorage.getItem(`$translateStorage_${$languageType.value}`) != null
? JSON.parse(localStorage.getItem(`$translateStorage_${$languageType.value}`))
: null
}
const storageSourceTransData =
localStorage.getItem(`$translateStorage_source`) != null
? JSON.parse(localStorage.getItem(`$translateStorage_source`))
: {}
if (storageTransData != null && storageTransData[key] != undefined) {
return storageTransData[key]
}
// 这个key如果是当前lang下的value没有,那就看原文有没有,原文没有就展示原来的key
return $transData.value[key] || storageSourceTransData[key] || key
}
app.config.globalProperties.$translate = $translate
app.provide('$translate', $translate)
let $translateComputed = (key) => {
return computed(() => {
if ($languageType) { }
return $translate(key)
})
}
app.provide('$translateComputed', $translateComputed)
let $changeTransData = (data, languageType) => {
if (languageType != undefined) {
localStorage.setItem(`$translateStorage_${languageType}`, JSON.stringify(data))
}
$languageType.value = languageType
$transData.value = data
}
app.provide('$changeTransData', $changeTransData)
// 解耦出来 $translateStorage_source,如果languageType是undefined,则单独赋值
let $changeSourceTransData = (data, languageType, source) => {
// 假如切换后的语种,翻译不全,应该要保持已有的原文不被覆盖
const storageSourceTransData =
localStorage.getItem(`$translateStorage_source`) != null
? JSON.parse(localStorage.getItem(`$translateStorage_source`))
: {}
localStorage.setItem(
`$translateStorage_source`,
JSON.stringify(Object.assign(storageSourceTransData, data))
)
// 如果只是undefined的话,就更新 $transData和$languageType,否则用上面的函数更新,这里就不需要更新了
if (languageType == undefined) {
$languageType.value = languageType
$transData.value = data
localStorage.setItem(
`$translateStorage_sourceFull`,
JSON.stringify(source)
)
}
}
app.provide('$changeSourceTransData', $changeSourceTransData)
app.component('EcloudI18n', EcloudI18n)
// 根据options 传入的值来配置请求url和请求route
if (options) {
const { testIp, routeIp } = options
if (testIp == undefined || routeIp == undefined) {
throw (new Error(`
请同时在配置testIp和routeIp,testIp用于本地测试的ip,routeIp为网关转发路由
参考bcop后台:
app.use(i18nPlugin, {
testIp: 'http://xxxxxx:xxxx',
routeIp: '/xxxxxx/xxxx'
})
`))
}
if (process.env.NODE_ENV == 'test' || process.env.NODE_ENV == 'development') {
axios.defaults.baseURL = testIp + routeIp // 研发环境的
} else {
const url = window.location.protocol + '//' + window.location.host
axios.defaults.baseURL = url + routeIp // 线上环境的
}
}
}
}
即将插入plugin的install函数中,包含下面几个函数和ref
- $translate 这个是最关键的函数,用于翻译原文的函数。 它会先判断当前语种下有localStorage里面有没有存储对应的翻译项数据,如果对应key值对应有值的话,会输出来。如果没有的话,就会看原文下的key有没有值,还是没有的话,就会直接输出key。
- $transData / languageType,利用vue3的ref来监听他们,当两个数据有变化的时候,会全局刷新页面,来达到页面更新的效果(translate函数中含有两个ref,当两个ref更新时,translate函数会重新刷新)
- $changeTransData 这个函数用于更新transData和languageType这个两个ref,并且存储数据到本地
- $changeSourceTransData 这个函数用于在一种特殊情况下更新两个ref,即传入的函数的语种是undefined,也就是没有指定语种,这个时候需要渲染key对应的原文
- 对options的一些特别处理,主要兼容不同使用方下的调用环境
- 最后将上面的函数和Ecloud-i18n组件设置为全局函数和全局组件
- $translateComputed这个函数主要是用来兼容一些constant值,比如rules里面设置的message,
Ecloud-i18n组件
<template>
<div>
<slot></slot>
</div>
</template>
<script setup>
import { onMounted, ref, inject, watch, toRefs } from 'vue'
import axios from './http/config' // 线上
import {getData,optionData} from './http/common';
// import axios from 'axios' //本地
const props = defineProps({
lang: {
type: String, // 参数类型
default: undefined //默认值
},
serviceCode: {
type: String,
default: undefined
}
})
const source = ref({})
const { lang, serviceCode, } = toRefs(props)
let changeTransData = inject('$changeTransData')
let changeSourceTransData = inject('$changeSourceTransData')
let $languageType = inject('$languageType')
// 获取全量数据
async function fetchTransData() {
const params = {
serviceCode: serviceCode.value,
languageType: lang.value
}
return (
getData(params)
.then((res) => {
if (res.status == 200) {
const { data } = res
const { body } = data
source.value = body
init(source.value)
} else {
console.error('多语言请求接口报错')
}
})
.catch((e) => {
console.error(e)
// window.alert('多语言请求接口报错')
})
)
}
// 获取data前的预检
async function optionTransData() {
const params = {
serviceCode: serviceCode.value,
changeId: localStorage.getItem(`i18n_${lang.value}_changeId`)
}
return (
optionData(params)
.then((res) => {
if (res.status == 200) {
console.log(res)
const { data } = res
return data
} else {
console.error('多语言optionTransData接口报错')
}
})
.catch((e) => {
console.error(e)
// window.alert('多语言请求接口报错')
})
)
}
onMounted(() => {
isNeedToFetch()
})
watch(
() => props.lang,
(nv) => {
isNeedToFetch()
}
)
function handleData(data) {
let res = {}
for (let i of data) {
if (i.acceptLanguage == lang.value) {
res[i.transKey] = i.objectValue
}
}
return res
}
function handleDataBySource(data) {
let res = {}
for (let i of data) {
res[i.transKey] = i.sourceValue
}
return res
}
function handleChangeId(data) {
if (data != undefined && data[0] != undefined) {
localStorage.setItem(`i18n_${lang.value}_changeId`, data[0].changeId)
}
}
async function isNeedToFetch() {
$languageType.value = lang.value
if (localStorage.getItem(`i18n_${lang.value}_changeId`)) {
const res = await optionTransData()
if (res != '') {
// 兼容一下失误
setTimeout(fetchTransData,100)
}
} else {
fetchTransData()
}
}
/**
* 1. 如果没有传递lang
* lang 为 undefined 将当前transKey无论是哪个语种的sourceValue都存到一个localStorage里
* 2. 传递lang
* 2.1 lang 有的话 将当前transKey对应下的这个value存到一个localStorage里
* 2.2 当前transKey的sourceValue都存到一个localStorage
*/
function init(data) {
handleChangeId(data)
// lang 为空 要展示的就是原文
const result = handleDataBySource(data)
// 处理接口返回的全量数据,lang是undefined那就是全量的
changeSourceTransData(result, lang.value, data)
if (lang.value != undefined) {
{
// 展示特定语种下的翻译文
const result = handleData(data)
changeTransData(result, lang.value)
}
}
}
</script>
下面介绍一下里面各个函数和props的作用
- props中的lang为传入的当前语种,也就是要进行翻译的目标语种;serviceCode为录入的工程代码名称,后续通过这个serviceCode和lang来请求对应的数据
- fetchTransData函数,获取全量数据,并更新页面
- init函数用于更新数据并刷新页面
- handleChangeId函数,用于记录这个工程在当前语种下的发布版本(每当数据有更新的时候,传入的changeId会有改变),这个changeId用于记录当前工程下的后台多语言数据有没有变化
- handleDataBySource函数,用于存储工程的原文数据,并通过inject注入i18n的changeSourceTransData函数来更新transData数据和本地存储的原文数据
- handleData函数同上操作,只不过是更新特定语种下的翻译数据。
上面7条是基础,后续为了减少每次服务请求,大量拉取数据对服务的影响,特加入了预检函数,也就是利用上面的changeId,在每次请求前进行判断,数据有无改变,如果没有就不再请求全量数据接口。
- isNeedToFetch函数就是在做上面的事
- optionTransData则是拿着工程代码和changeId去请求后端接口
http下的两个文件
common.js 文件主要就是封装了全量请求函数和预检函数
不过这里可以给出返回的data的数据格式,用java来简单表示一下,帮助理解代码:
type Res = {
// 原文
sourceValue: string;
// 目标值
objectValue: string;
// 翻译ID
transKey: string;
// 需要转换的语音类型
acceptLanguage: string;
// 服务编码
serviceCode: string;
// 变更ID
changeId: string;
}
type Data = Array<Res>; // 接口返回的数据格式
config.js 就是给axios加了一些拦截器之类的,不是很刚需了
上面两个文件不是很重要,只是为了后期可能的拓展和代码维护,写出来的。
说了这么多,如何使用
总共就两步
第一,全局注入,在main.js中
import i18nPlugin from 'xxx/xxxx/plugins/src/i18n'
// 当然这里后续可以用npm publish 发布了,然后再下载下来,直接导入,这里就是演示一下,代表引入i18n.js这个文件
。。。。这里省略。。。
const app = createApp(App)
。。。。这里省略。。。
//导入
app.use(i18nPlugin, {
testIp: 'http://xxx.xxx.xxx.xxx:8080',
routeIp: '/点赞点赞点点赞/大佬大佬大大佬'
})
app.mount('#app')
第二步 vue最外层组件内使用,在app.vue中
<EcloudI18n :lang="en-us" serviceCode="serviceCode-123">
**这里是你原来的内容,下面是演示如何利用$translate替换原文**
<h1>{{ $translate('test1') }}</h1>
<h1>{{ $translate('test2') }}</h1>
<h1>{{ $translate('test3') }}</h1>
</EcloudI18n>
这样当你进入页面或者后期更新语种lang的时候页面会更新所有被$translate包裹的key