Когда я раньше работал над некоторыми проектами интернационализации внешнего интерфейса, поскольку бизнес был не очень сложным, соответствующие требования, как правило, сводились к переводу копирайтинга, то есть интернационализации на нескольких языках, в основном с использованием соответствующих плагинов I18n для удовлетворения потребности развития. Однако с итерацией бизнеса и увеличением сложности требований эти плагины I18n могут не соответствовать актуальным требованиям для разработки.Далее я расскажу вам о проблемах, возникающих в процессе интернационализации проектов и что они сделали.
Поскольку технологический стек команды в основном основан на Vue, соответствующие решения также основаны на Vue и соответствующих плагинах интернационализации (vue-i18n).
Фаза 1
задний план
Мы используем vue-i18n для завершения соответствующей работы по интернационализации. Когда проект относительно простой и файлов языковых пакетов не так много, не составляет большой проблемы упаковать языковой пакет непосредственно в бизнес-код. Однако, как только файлов языкового пакета станет больше, вы можете рассмотреть возможность упаковки языкового пакета отдельно, чтобы уменьшить объем бизнес-кода и использовать его с помощью асинхронной загрузки. Кроме того, учитывая, что интернационализированные языковые пакеты являются относительно редко изменяемым содержимым, вы можете рассмотреть возможность кэширования языковых пакетов и предпочтительно получать языковые пакеты из кэша каждый раз при отображении страницы, чтобы ускорить ее открытие.
решение
Что касается работы, связанной с субподрядом, веб-пакет можно использовать для автоматического завершения работы по субподряду и асинхронной загрузки. Начиная с версии 1.x, webpack предоставляетrequire.ensure()
Подождите, пока соответствующий API завершит субподрядную работу над языковым пакетом, но в это времяrequire.ensure()
Должен принимать указанный путь, начиная с версии 2.6.0, веб-пакетыimport
Грамматика может указывать различные режимы анализа динамического импорта.Документация. Таким образом, в сочетании с соответствующими API-интерфейсами, предоставляемыми webpack и vue-i18n, можно выполнить подпакет и асинхронную загрузку языковых пакетов, а работу по переключению языков можно выполнить во время выполнения.
Образец кода:
Структура каталога файлов:
src
|--components
|--pages
|--di18n-locales // 项目应用语言包
| |--zh-CN.js
| |--en-US.js
| |--pt-US.js
|--App.vue
|--main.js
main.js:
import Vue from 'vue'
import VueI18n from 'vue-i18n'
import App from './App.vue'
Vue.use(VueI18n)
const i18n = new VueI18n({
locale: 'en',
messages: {}
})
function loadI18nMessages(lang) {
return import(`./di18n-locales/${lang}`).then(msg => {
i18n.setLocaleMessage(lang, msg.default)
i18n.locale = lang
return Promise.resolve()
})
}
loadI18nMessages('zh').then(() => {
new Vue({
el: '#app',
i18n,
render: h => h(App)
})
})
Вышеупомянутое первое решает проблему субподряда и асинхронной загрузки языковых пакетов.
Далее поговорим о том, как кэшировать языковой пакет и связанный с ним механизм кэширования.Общая идея такова:
После открытия страницы сначала определите, есть ли соответствующий файл языкового пакета в localStorage.Если да, то напрямую синхронно получите языковой пакет из localStorage, а затем завершите отрисовку страницы.Если нет, то нужно асинхронно получить файл языковой пакет из CDN и кэшируйте языковой пакет в localStorage, а затем завершите отрисовку страницы.
Безусловно, в процессе реализации необходимо учитывать следующие вопросы:
-
Если языковой пакет обновлен, как мне обновить языковой пакет, кэшированный в localStorage?
Прежде всего, в процессе компиляции кода плагин webpack используется для завершения сбора хеш-значения версии языкового пакета после каждой компиляции и одновременного внедрения его в бизнес-код. Когда страница открывается и бизнес-код начинает работать,Во-первых, он определит, соответствует ли версия языкового пакета в бизнес-коде версии, кэшированной в localStorage., если они совпадают, соответствующие файлы языковых пакетов будут получены синхронно, если они несовместимы, языковые пакеты будут получены асинхронно.
-
Как номера версий и языковые пакеты хранятся в localStorage?
Данные хранятся в localStorage, потому что localStorage разделен по имени домена,Поэтому, если несколько проектов интернационализации развернуты под одним и тем же доменным именем, пространства имен можно разделить по имени проекта, чтобы избежать перезаписи хэша языкового пакета/версии..
Выше приведены некоторые простые оптимизации для проектов интернационализации на начальном этапе. Подводя итог: языковые пакеты упаковываются в чанки по отдельности и предоставляют функции асинхронной загрузки и хранения в localStorage для ускорения открытия следующей страницы.
Фаза II
задний план
С итерацией проектов и увеличением количества проектов интернационализации все больше и больше компонентов разделяются на библиотеки компонентов для повторного использования, а некоторые компоненты также должны поддерживать интернационализацию и многоязычность.
Существующий план
Что касается этой части контента, vue-i18n также поддерживает интернационализацию компонентов на данном этапе.Пожалуйста, обратитесь к документации для конкретного использования, общая идея состоит в том, чтобы обеспечить возможность локальной регистрации объектов экземпляра vue-i18n всякий раз, когда функция перевода вызывается внутри подкомпонента.$t
,$tc
При ожидании сначала будет получен объект vue-i18n, созданный в дочернем компоненте, а затем будет создана карта местного языка.
Метод, который он предоставляет, ограничен только регистрацией локального компонента языкового пакета.В процессе окончательной компиляции и упаковки языковой пакет в конечном итоге будет упакован в бизнес-код, что также несовместимо с нашими первоначальными целями оптимизации для проектов интернационализации. (Конечно, если ваш компонент является асинхронным компонентом, это нормально).
Оптимизация
Чтобы продолжить совершенствовать схему интернационализации компонентов исходя из первоначальной цели, здесь мы пытаемсяОтделить языковой пакет компонента от компонента, то есть компоненту не нужно вводить многоязычный пакет отдельно, а языковой пакет компонента также можно загрузить асинхронно.
Таким образом, в пределах нашего ожидаемого диапазона мы можем столкнуться со следующими проблемами:
- Приложение проекта также будет иметь свой собственный многоязычный язык, так как же управлять многоязычностью приложения проекта и многоязычностью между компонентами?
- Плагин vue-i18n обеспечивает локальный механизм регистрации для компонентов на нескольких языках.Если многоязычный пакет и компонент разделены, как будет переведен многоязычный копирайтинг при рендеринге конечного компонента?
- В библиотеке компонентов также будут родительско-дочерние/вложенные компоненты, так как же управлять и организовывать многоязычные пакеты в библиотеке компонентов?
- ...
Первый в нашей группе,пост-компиляция(Вы можете ткнуть меня по поводу пост-компиляции) должна быть стандартной конфигурацией нашего стека технологий, поэтому наша библиотека компонентов, наконец, выпущена непосредственно в виде исходного кода.Импорт по запросу + пост-компиляцияспособ использования.
Многоязычная организация пакета приложения проекта не должна вызывать затруднений, и обычно оно размещается в отдельном каталоге (di18n-locales):
// 目录结构:
src
├── App.vue
├── di18n-locales
│ ├── en-US.js
│ └── zh-CN.js
└── main.js
// en-US.js
export default {
messages: {
'en-US': {
viper: 'viper',
sk: 'sk'
}
}
}
// zh-CN.js
export default {
messages: {
'zh-CN': {
viper: '冥界亚龙',
sk: '沙王'
}
}
}
Каждый языковой пакет в каталоге di18n-locales в конечном итоге будет упакован в отдельный фрагмент, поэтому здесь мы рассмотрим, может ли языковой пакет каждого компонента в библиотеке компонентов также быть упакован вместе с языковым пакетом в приложении проекта в виде фрагмента. : то есть приложение проектаen-US.js
Соответствует всем компонентам, на которые ссылается проект в библиотеке компонентов.en-US.js
Упакованы вместе, другие языковые пакеты такие же. Цель этого — отделить языковой пакет библиотеки компонентов от компонента (противоположный решению vue-i18n) и в то же время единообразно упаковать языковой пакет приложения проекта для асинхронной загрузки. Для этой цели при планировании каталога библиотеки компонентов мы сделали следующее соглашение: также будет di18n-locales на том же уровне, что и каждый компонент (разумеется, в соответствии с каталогом языкового пакета приложения проекта, он также поддерживает настраиваемый) каталог, в котором хранятся многоязычные пакеты, соответствующие каждому компоненту:
├── node_modules
| ├── @didi
| ├── common-biz-ui
| └── src
| └── components
| ├── coupon-list
| │ ├── coupon-list.vue
| │ └── di18n-locales
| │ ├── en.js // 当前组件对应的en语言包
| │ └── zh.js // 当前组件对应的zh语言包
| └── withdraw
| ├── withdraw.vue
| └── di18n-locales
| ├── en.js // 当前组件对应的en语言包
| └── zh.js // 当前组件对应的zh语言包
├── src
│ ├── App.vue
│ ├── di18n-locales
│ │ ├── en.js // 项目应用 en 语言包
│ │ └── zh.js // 项目应用 zh 语言包
│ └── main.js
Когда компонент из библиотеки компонентов используется в вашем приложении проекта:
// App.vue
<template>
...
</template>
<script>
import couponList from 'common-biz-ui/coupon-list'
export default {
components: {
couponList
}
}
</script>
Затем, не требуя ручного импорта языковых пакетов:
- как получить
coupon-list
Языковой пакет под этот компонент? - будет
coupon-list
Языковой пакет, используемый компонентом, упаковывается в языковой пакет, соответствующий приложению проекта, и выводит фрагмент?
Для этого мы разработали плагин для веб-пакета: di18n-webpack-plugin. Чтобы решить две вышеупомянутые проблемы, давайте взглянем на основной код этого плагина:
compilation.plugin('finish-modules', function(modules) {
...
for(const module of modules) {
const resource = module.resource || ''
if (that.context.test(resource)) {
const dirName = path.dirname(resource)
const localePath = path.join(dirName, 'di18n-locales')
if (fs.existsSync(localePath) && !di18nComponents[dirName]) {
di18nComponents[dirName] = {
cNameArr: [],
path: localePath
}
const files = fs.readdirSync(dirName)
files.forEach(file => {
if (path.extname(file) === '.vue') {
const baseName = path.basename(file, '.vue')
const componentPath = path.join(dirName, file)
const prefix = getComponentPrefix(componentPrefixMap, componentPath)
let componentName = ''
if (prefix) {
// transform to camelize style
componentName = `${camelize(prefix)}${baseName.charAt(0).toUpperCase()}${camelize(baseName.slice(1))}`
} else {
componentName = camelize(baseName)
}
// component name
di18nComponents[dirName].cNameArr.push(componentName)
}
})
...
}
}
})
Принцип заключается в том, что на этапе компиляции финиш-модулей были скомпилированы все модули, затем на этом этапе можно узнать, какие компоненты в библиотеке компонентов используются в приложении проекта, то есть абсолютный путь, соответствующий компонент, потому что мы уже согласовали, что на том же уровне, что и компонент, будет каталог di18n-locales для хранения многоязычных файлов компонента, поэтому мы также можем найти языковой пакет, используемый этим компонентом. Наконец, через такую функцию ловушки путь к компоненту используется в качестве ключа для завершения соответствующей работы по сбору. Это решает первую проблему выше.
Давайте посмотрим на второй вопрос. Когда мы получим, какие компоненты импортируются по запросу через хук finish-modules, мы столкнемся с очень неловкой проблемой, то есть фаза final-modules запускается после компиляции всех модулей. этап уплотнения, но на этапе уплотнения больше не будет выполняться работа, связанная с компиляцией модуля.
Однако, прочитав исходный код веб-пакета, мы обнаружили, что при компиляции определен метод rebootModule. Судя по имени метода, он должен перекомпилировать модуль. В частности, внутренняя реализация метода действительно заключается в вызове buildModule на объект компиляции.метод для компиляции модуля:
class Compilation extends Tapable {
constructor() {
...
}
...
rebuildModule() {
...
this.buildModule(module, false, module, null, err => {
...
})
}
...
}
Потому что с самого начала наша цель — отделить мультиязычные пакеты и компоненты в библиотеке компонентов друг от друга, и в то же время они будут незаметны для приложения проекта, поэтому для завершения работы по упаковке требуется плагин webpack. на этапе компиляции. , поэтому для второго вопроса выше мы пытаемся получить многоязычные пути пакетов всех компонентов, используемых проектом, после завершения этапа финальных модулей, а затем автоматически добавляем многоязычные пакеты компонентов в качестве зависимостей к Приложение проекта В исходном коде языкового пакета языковой пакет приложения проекта перекомпилируется через метод rebootModule, так что незаметный языковой пакет внедряется в языковой пакет приложения проекта в качестве зависимости.
Процесс buildModule веб-пакета:
Мы видим, что в процессе пересборки webpack снова будет использовать загрузчик соответствующего типа файла для загрузки исходного кода соответствующего файла в память, поэтому на этом этапе мы можем завершить добавление зависимых языковых пакетов. Давайте взглянем на основной код плагина di18n-webpack-plugin для этого контента:
compilation.plugin('build-module', function (module) {
if (!module.resource) {
return
}
// di18n rules
if (/src\/di18n-locales\//.test(module.resource) && module.createSource.name !== 'di18nCreateSource') {
...
if (!componentMsgs.length) {
return createSource.call(this, source, resourceBuffer, sourceMap)
}
let vars = []
const varReg = /export\s+default\s+([^{;]+)/
const exportDefaultVar = source.match(varReg)
source = `
${componentMsgs.map((item, index) => {
const varname = `di18n${index + 1}`
const { path, cNameStr } = item
vars.push({
varname,
cNameStr
})
return `import ${varname} from "${path}";`
}).join('')}
${
exportDefaultVar
? source.replace(varReg, function (_, m) {
return `
${m}.components = {
${getComponentMsgMap(vars)}
};
export default ${m}
`
})
: source.replace(/export\s+default\s*\{([^]+)\}/i, function (_, m) {
return `export default {${m},
components: {
${getComponentMsgMap(vars)}
}
}
`
})
}
`
resourceBuffer = new Buffer(source)
return createSource.call(this, source, resourceBuffer, sourceMap)
}
}
})
Принцип заключается в использовании хука build-module, который открывается, когда веб-пакет начинает компилировать модуль. Его параметром обратного вызова является модуль, компилируемый в данный момент. В настоящее время мы реализовали уровень прокси для метода createSource.То есть перед вызовом метода createSource мы завершаем внедрение языкового пакета компонента, переписывая исходный код языкового пакета приложения проекта. Последующий процесс по-прежнему обрабатывается вебпаком, в итоге каждый языковой пакет проектного приложения будет упакован в чанк отдельно, а языковой пакет компонентов, вводимых по требованию, также будет упакован в этот языковой пакет.
Окончательный результат:
// 原始的项目应用中文(zh.js)语言包
export default {
messages: {
zh: {
hello: '你好',
goodbye: '再见'
}
}
}
Пакет китайского языка применяется к проекту, обрабатываемому плагином di18n-webpack-plugin:
// 将项目依赖的组件对应的语言包自动引入项目应用当中的语言包当中并完成编译输出为一个chunk
import bizCouponList from 'xxxx/xxxx/node_modules/xxx/src/components/coupon-list/di18n-locales/zh.js' // 组件语言包的路径为绝对路径
export default {
messages: {
zh: {
hello: '你好',
goodbye: '再见'
}
},
components: {
bizCouponList
}
}
(После того, как мы вводим здесь языковой пакет компонента, в языковой пакет нашего проекта добавляется новое поле компонента, и в качестве ключа используется имя подкомпонента, а в качестве значения используется языковой пакет подкомпонента, который монтируется в поле компонентов.)
Вышеупомянутый процесс решает несколько проблем, поднятых ранее:
- Как получить языковой пакет, используемый компонентом
- Как упаковать языковой пакет, используемый компонентом, в языковой пакет приложения проекта и вывести отдельным чанком
- Как управлять организацией языковых пакетов между проектными приложениями и компонентами
Теперь, с помощью плагина webpack, мы помогли мне решить организацию, сборку и упаковку языковых пакетов проекта и языковых пакетов компонентов в процессе компиляции. Однако остается проблема, которая до сих пор не решена, то есть после того, как мы отделяем языковой пакет компонента от компонента, то есть мы больше не регистрируем мультиязычный метод частичной регистрации, предоставляемый vue-i18n, а сходимся языковой пакет компонента к приложению проекта.Языковой пакет под , то как мы можем завершить копирайт перевод компонента?
Все мы знаем, что Vue создаст уникальное имя компонента для каждого VNode в процессе создания VNode дочернего компонента:
// src/core/vdom/create-component.js
export function createComponent() {
...
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
...
}
В реальном использовании мы требуем, чтобы компоненты имели свои собственные уникальные имена.
Стратегия, предоставляемая vue-i18n, заключается в локальной регистрации объекта экземпляра vue-i18n всякий раз, когда функция перевода вызывается внутри дочернего компонента.$t
,$tc
При ожидании сначала будет получен объект vue-i18n, созданный в дочернем компоненте, а затем будет создана карта местного языка. В это время мы можем изменить образ мышления, мы управляем языковыми пакетами подкомпонентов унифицированным образом, и не регистрируем экземпляр vue-i18n на подкомпоненте, а каждый раз, когда подкомпонент вызывает$t
,$tc
При ожидании функции перевода в это время мы получаем содержимое соответствующего языкового пакета из единого языкового пакета в соответствии с именем компонента этого подкомпонента и завершаем работу по переводу.
Выше мы также упомянули, как мы управляем организацией языковых пакетов между проектными приложениями и компонентами:После того, как мы вводим языковой пакет компонента, в языковой пакет нашего проекта добавляется новое поле компонента, в качестве ключа используется имя субкомпонента, а в качестве значения используется языковой пакет субкомпонента, который монтируется в поле компонентов. Таким образом, когда подкомпонент вызывает метод функции перевода, он всегда сначала находит ключ соответствующего имени компонента в поле компонентов языкового пакета приложения проекта, а затем завершает функцию перевода. нашли, используйте соответствующее поле приложения проекта языковой копии.
Суммировать
Выше приведены наши мысли о некоторых международных проектах, которые мы недавно реализовали.
- Языковые пакеты индивидуально упакованы в куски и загружаются асинхронно.
- Предусмотрена функция локального кэша localStorage, при следующем открытии страницы не нужно отдельно загружать языковой пакет
- Языковой пакет компонента отделен от компонента, компонент не знает о языковом пакете компонента и не требует отдельной регистрации в компоненте.
- Организация и управление компонентными языковыми пакетами и языковыми пакетами проектных приложений осуществляется через плагин webpack.
По сути, вышеуказанная работа заключается в том, чтобы уменьшить зависимость связанных функций от официальных плагинов и предоставить общее решение, сглаживающее технологический стек.