webpack 打包优化(二)

回顾

前面我们已经对我们的项目做过了一次优化,优化后的结果如下图所示:

可以看到 main.***.js 中还有 country.jsi18n.js 两块大家伙。很明显,这两个东西都跟我们的 i18n 翻译数据有关,下面我们就来优化这一块。

处理 process.env

country.jsi18n.js 中都使用了 process.env 这个对象,也就是说资源被重复的使用了,所以我们这里将其提取出来放到 constants/env.js 下面,然后导出供其他模块使用:

1
export default process.env

异步加载翻译文件

我们项目现在的做法是将所有语言都一起打包到了最终的 main 文件中,这显然没有必要,比较合理的做法是只打包该地区的默认语言到 main 文件中,然后根据用户选择的语言来动态加载翻译文件。

打包默认语言

我们在 config 下面新建一个 languages.js,然后生成默认语言及其翻译资源并导出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const localesHash = require('../i18n/localesHash')
const resourcesHash = require('../i18n/resourcesHash')

const COUNTRY = process.env.COUNTRY || 'sg'
const country = (COUNTRY).toUpperCase()
const defaultLng = localesHash[country][0]

const resources = {
[defaultLng]: {
common: resourcesHash[defaultLng]
}
}

exports.resources = resources
exports.defaultLng = defaultLng

然后将它们加入到 env.js 中:

1
2
3
4
LANGUAGE: {
resources: languages.resources,
defaultLng: languages.defaultLng
}

i18n.js 中使用方式跟之前类似,不过这里将之前匹配 app 语言的代码删去了,因为我们需要动态的加载我们的翻译文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import i18next from 'i18next'
import env from '@const/env'
import { firstLetterUpper, getLocale } from './helpers/utils'

let LANGUAGE = env.LANGUAGE
LANGUAGE = typeof LANGUAGE === 'string' ? JSON.parse(LANGUAGE) : LANGUAGE

const { defaultLng, resources } = LANGUAGE

i18next
.init({
lng: defaultLng,
fallbackLng: defaultLng,
defaultNS: 'common',
keySeparator: false,
debug: env.NODE_ENV === 'development',
resources,
interpolation: {
escapeValue: false
},
react: {
wait: false,
bindI18n: 'languageChanged loaded',
bindStore: 'added removed',
nsMode: 'default'
}
})

动态加载翻译文件

说道动态加载,首先想到可以使用 webpack 提供的 import 函数,所以我们在 i18n.js 中增加了如下函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
export const loadResource = lng => {
let p

return new Promise((resolve, reject) => {
if (isMatch(defaultLng, lng)) resolve()

switch (lng) {
case 'th-TH':
p = import('../i18n/locales/th-TH.json')
break
case 'id-ID':
p = import('../i18n/locales/id-ID.json')
break
case 'ms-MY':
p = import('../i18n/locales/ms-MY.json')
break
case 'vi-VN':
p = import('../i18n/locales/vi-VN.json')
break
case 'zh-Hans':
p = import('../i18n/locales/zh-Hans.json')
break
case 'zh-Hant':
p = import('../i18n/locales/zh-Hant.json')
break
default:
p = import('../i18n/locales/en.json')
}

p.then(data => {
i18next.addResourceBundle(lng, 'common', data)
changeLanguage(lng)
})
.then(resolve)
.catch(reject)
})
}

该函数传入需要加载的语言,如果加载的语言与该地区默认语言一致的话,就直接 resolve,否则动态加载相应的翻译文件。

然后将 src/index.js 改成如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
loadResource(lng)
.then(() => {
ReactDOM.render(
<I18nextProvider i18n={i18n}>
<Provider store={store}>
<ConnectedRouter history={history}>
<App />
</ConnectedRouter>
</Provider>
</I18nextProvider>,
document.getElementById('root')
)
})

意思就是要等翻译文件加载完成后,再渲染我们的 react 组件。

问题

按照上面这样修改以后,我们发现我们的首页并没有像预期一样显示正确的内容,原因是我们的首页数据是写在 reducers/landing/index.js 中的,然后这些 reducerssrc/store.js 中被统一导入,而 store.js 又在 src/index.js 中导入,而此时我们的翻译文件并没有加载完成,所以这里的翻译就是使用的旧的资源了。

为了解决这个问题,只好稍微改一下我们的代码了。我们先修改 reducers/landing/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const serviceList = [
{
name: 'phone_credit',
icon: 'phoneCredit',
url: `/phone-credit`
},
{
name: 'data_plan',
icon: 'dataPlan',
url: `/data-plan`
},
{
name: 'game',
icon: 'game',
url: '/game'
// isPromo: true
// forbidden: true
},
...

我们把翻译的工作拿到组件里面去做:

1
2
3
4
const ServiceItem = ({ name, url, icon ...
...
<p>{tUpperCase(name)}</p>
...

这样,我们就大功告成了,让我们看看最后的杰作:

更新(2018-09-03)

上面的问题不仅仅出现在首页,菜单页也有,这样程序中需要改动的地方就比较多了,而且把翻译的工作放到基础组件中去做显然不合适,那有没有更好的做法呢?答案是有的。既然我们需要保证页面中的资源必须要在翻译文件加载完后才能加载,那么继续使用 webpack 的 import 就行了呀。

我们在 src 目录下新建一个 app.js,将 index.js 的内容拷贝到该文件中,然后将 index.js 改为:

1
2
3
4
5
6
7
import { getLocale } from './helpers/utils'
import { loadResource } from './i18n'

loadResource(getLocale())
.then(() => {
import('./app.js')
})

这样,就实现了我们的功能。

不过,index.js 中引入的文件以及他们递归引入的其他文件 (utils.js, env.js) 中不能正常使用新加载的语言,因为他们在 loadResource 执行之前就已经导入了,这里需要格外注意

再次打包看看效果: