"Hello! こんにちは! انتبه للسلامة!안녕하십니까!你好"
开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第16天,点击查看活动详情
公司级的大型项目或者社区优秀的开源项目,如 Ant Design,一般都会有 "多语言" 配置,而且支持多语言也是组件的基本能力。多语言功能的本质其实是文本替换,一个词汇“Hello”,在英文语境下是“Greeting”,日语语境下是“こんにちは”,在汉语语境下是“你好”等。
本文将基于 类装饰器 特性来实现多语言配置。
类装饰器与HOC
类装饰器其本质是JS函数,接受一个构造函数(类)作为参数,如果类装饰器函数有返回值,那么该返回值将替代原来的构造函数。举个例子,
类装饰器
type Consturctor = { new (...args: unknown[]): unknown };
function addMethod<T extends Consturctor>(ParentClass: T) {
return ChildClass extends ParentClass {
method1() { /** 可以拿到 this */}
};
}
使用方法
@addMethod
class C {}
// 这里new出来的实例是ChildClass的实例
(new C()).method1()
我们发现类装饰器和HOC的使用方式不谋而合。如果项目中支持装饰器语法,那么类装饰器和HOC用法一致(虽然Class的本质也是JS函数,但类装饰器只能作用于Class语法)。
高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。高阶组件将组件(函数)作为参数,返回值为新组件的函数。
加载多语言包
既然类装饰器中返回的子类(新构造函数)可以替代父类(原来的构造函数),那么就可以在子类的实例(this)上挂载多语言包(可以把多语言包理解成一个JS文件,里面存放一个 Object 类型的值),并将该子类返回。
实现多语言加载器
type LocalePackage = Record<string, unknown>;
export function localeLoader<P, S>(locale: LocalePackage) {
return function (Parent: React.ComponentClass<P, S>) {
class Child extends Parent {
public locale: LocalePackage;
constructor(props: Readonly<P>) {
super(props);
this.locale = locale; // 把多语言包挂在子类的实例(this)上
}
}
Child.displayName = Parent.displayName || Parent.name;
return Child;
};
}
实验多语言加载器
import React from 'react';
import { localeLoader } from './localeLoader';
import defaultLocale from './locale/zh_CN';
@localeLoader(defaultLocale)
class Hello extends React.Component {
render() {
return <div>{this.locale.hello}</div>;
}
}
其中,
// ./locale/zh_CN.js 文件
export default {
hello: '您好',
};
测试一下:
describe('localeLoader', () => {
it('测试多语言', () => {
const component = shallow(<Hello />);
console.log(component.debug());
});
});
换一个语言包,
import React from 'react';
import { localeLoader } from './localeLoader';
import englishLocale from './locale/en_US';
@localeLoader<P, S>(englishLocale)
class Hello<P, S> extends React.Component<P, S> {
render() {
return <div>{this.locale.hello}</div>;
}
}
其中,
// ./locale/en_US.js 文件
export default {
hello: 'greeting',
};
再测试一下,
✅ 多语言的雏形算是完成了...但是,每次更换多语言包还要手动更新,也不太方便了,接下来我们继续实现基于类装饰的多语言包切换功能。
多语言包切换
既然要求多语言文案可以更新,那么组件就必须重新渲染,此时我们一定会想到props 或者 state 更新后,React 组件才会重新渲染。同时我们考虑语言包应该属于组件内部属性,应有组件内部控制,调用方只需要告诉组件想用什么语言(通过 props 传参)即可,然后组件内部通过 setState 方法进行重新渲染。
定义多语言类型枚举
const enum LocaleEnum {
EN_US = 'en_US',
ZH_CN = 'zh_CN',
ZH_TW = 'zh_TW',
}
定义多语言配置类型
type LocaleConfigs = {
/** 默认的语言包数据,优先 */
locale: LocalePackage
} & Partial<Record<LocaleEnum, LocalePackage>>;
实现可切换的多语言加载器
子组件在 props 上提供 lang 属性(组件内支持的多语言类型的枚举值),
export function localeLoader<
P extends { lang: LocaleEnum },
S extends { locale: LocalePackage }
>(localeConfig: LocaleConfig) {
return function (ParentClass: React.ComponentClass<P, S>) {
class ChildClass extends ParentClass {
constructor(props: Readonly<P>) {
super(props);
this.state = {
...this.state,
locale: localeConfig[props.lang] ?? localeConfig.locale,
};
}
componentDidUpdate(prevProps: Readonly<P>, prevState: Readonly<S>, snapshot: unknown) {
super.componentDidUpdate?.call(this, prevProps, prevState, snapshot);
const { lang } = this.props;
if (lang === prevProps.lang) {
return;
}
this.loadLocale(lang);
}
loadLocale(lang: LocaleEnum) {
this.setState({
locale: localeConfig[lang] ?? localeConfig.locale,
});
}
}
ChildClass.displayName = ParentClass.displayName || ParentClass.name;
ChildClass.defaultProps ??= {};
ChildClass.defaultProps.lang ??= LocaleEnum.ZH_CN;
return ChildClass;
};
}
实验可切换的多语言加载器
import React from 'react';
import { localeLoader } from './localeLoader';
import defaultLocale from './locale/zh_CN';
import englishLocale from './locale/en_US';
import taiwanLocale from './locale/zh_TW';
@localeLoader({
en_US: englishLocale,
zh_TW: taiwanLocale,
locale: defaultLocale,
})
class Hello extends React.Component {
render() {
const { locale } = this.state;
return <div>{locale.hello}</div>;
}
}
其中,
// ./locale/zh_TW.js 文件
export default {
hello: '雷猴',
};
测试一下:
describe('src/js/decorator/localeLoader.ts', () => {
it('测试多语言', () => {
const component = shallow(<Hello />);
console.log(component.debug());
component.setProps({ lang: 'en_US' });
console.log(component.debug());
component.setProps({ lang: 'zh_TW' });
console.log(component.debug());
});
});
异步加载多语言包
作为汉语App,国内用户在大部分场景下还是选择汉语语言包,可能也不会切换英语或其他语言包,而语言包一般会比较大,所以我们一般会选择同步加载汉语语言包,异步加载其他语言的语言包。
更新多语言配置类型
// locale:同步加载;LocaleEnum:其他语言包异步加载
type LocaleConfig = {
locale: LocalePackage;
} & Partial<Record<LocaleEnum, () => Promise<{ default: LocalePackage }>>>;
实现异步多语言加载器
export function localeLoader<
P extends { lang: LocaleEnum },
S extends { locale: LocalePackage }
>(localeConfig: LocaleConfig) {
return function (ParentClass: React.ComponentClass<P, S>) {
class ChildClass extends ParentClass {
constructor(props: Readonly<P>) {
super(props);
this.state = {
...this.state,
locale: localeConfig.locale,
};
if (props.lang !== LocaleEnum.ZH_CN) {
this.loadLocale(props.lang);
}
}
componentDidUpdate(prevProps: Readonly<P>, prevState: Readonly<S>, snapshot: unknown) {
super.componentDidUpdate?.call(this, prevProps, prevState, snapshot);
const { lang } = this.props;
if (lang === prevProps.lang) {
return;
}
this.loadLocale(lang);
}
loadLocale(lang: LocaleEnum) {
if (lang === LocaleEnum.ZH_CN || !lang) {
this.setState({
locale: localeConfig.locale,
});
return;
}
localeConfig[lang]?.()
.then(({ default: locale }) => {
this.setState({ locale });
})
.catch((error) => {
console.log('多语言加载失败', error);
});
}
}
ChildClass.displayName = ParentClass.displayName || ParentClass.name;
ChildClass.defaultProps ??= {};
ChildClass.defaultProps.lang ??= LocaleEnum.ZH_CN;
return ChildClass;
};
}
实验异步语言包加载器
import React from 'react';
import { localeLoader } from './localeLoader';
import defaultLocale from './locale/zh_CN';
@localeLoader({
en_US: () => import('./locale/en_US'),
zh_TW: () => import('./locale/zh_TW'),
locale: defaultLocale,
})
class Hello extends React.Component {
render() {
const { locale } = this.state;
return <div>{locale.hello}</div>;
}
}
测试一下:
describe('localeLoader.ts', () => {
it('测试多语言', async () => {
const component = shallow(<Hello />);
console.log(component.debug());
component.setProps({ lang: 'zh_TW' });
await new Promise(resolve => setTimeout(resolve));
console.log(component.debug());
component.setProps({ lang: 'en_US' });
await new Promise(resolve => setTimeout(resolve));
console.log(component.debug());
});
});
~~~完美~~~