2019-09-21 | react | UNLOCK

使用nextjs做国际化网站时的一些总结

国际化实现 (react-intl)


import {
  addLocaleData,
  IntlProvider
} from 'react-intl'

import zh_CN from '../locale/zh_CN';//个人配置
import en_US from '../locale/en_US';//个人配置
import zh from 'react-intl/locale-data/zh'; //react-intl语言包
import en from 'react-intl/locale-data/en'; //react-intl语言包
addLocaleData([...en, ...zh]);//需要放入本地数据库


// 判断cookie中设置的语言是否存在,不存在则默认取浏览器的语言, 如果是中文默认为中文,如果不是中文默认为英文
if (!lang) {
  if (typeof window !== 'undefined') {
    language = (navigator.language || navigator.userLanguage).substr(0, 2)
    language = language == 'zh' ? 'zh' : 'en'
    Cookie.set('hpbLanguage', language)
  }
}

return {
    <IntlProvider locale={lang ? lang : language} messages={messages[lang ? lang : language]}>
          <Component {...pageProps} />
    </IntlProvider>
}

// IntlProvider 有三个配置参数:
//  locale, <string>, 语言标记,例如 'zh-CN' 'en-US'
//  messages, <object>, 国际化所需的 key-value 对象
//  formats, <object>, 自定义 format,比如日期格式、货币等

在页面中如何使用

<FormattedMessage
  tagName="p"
  id="example"
  values={{
    name: 'React'
  }}
  defaultMessage="{name} 默认信息。"
  description="{name} 描述?"
/>
  • id 指代的是这个字符串在配置文件中的属性名
  • description 指的是对于这个位置替代的字符串的描述,便于维护代码,不写的话也不会影响输出的结果
  • defaultMessage 当在 locale 配置文件中没有找到这个 id 的时候,输出的默认值
  • tagName 实际生成的标签,默认是 span
  • values 动态参数. 格式为对象

默认语言的设置

由于一般不会设置很多种语言资源库,不能完全和浏览器的默认语言相匹配,如果出现语言库不存在的情况,回导致页面中显示出语言对应的 key,也就是 FormattedMessage 组件中对应的 id,导致页面显示问题,所以设置默认语言

switch (navigator.language.split('-')[0]) {
  case 'en':
    return en_US
    break
  case 'zh':
    return zh_CN
    break
  default:
    return en_US
    break
}

解决页面手动刷新时,由于默认语言的原因导致的页面闪烁,解决方法:在服务端也储存 cookie,解决刷新的问题


// serve.js

const cookie = require('cookie-parser')

app
  .prepare()
  .then(() => {
    const server = express()
    server.use(cookie())  // use cookie
    ....
    }).catch(ex => {
    console.error(ex.stack)
    process.exit(1)
  })

在客户端只需要按照正常的存 cookie 的方法即可

import Cookie from 'js-cookie'

Cookie.set(key, value) // cookie-parser可以保持客户端和服务端cookie统一

在对应的页面设置

static async getInitialProps({ req }) {
    // 获取cookie中设置的语言,渲染不同语言的页面
    const lang = req
      ? req.cookies.hpbLanguage || ''
      : Cookie.get('hpbLanguage') || ''
    return { lang }
  }

路由跳转

需求:期望详情页的路由是 blog/id 而不是 `/blog?query=id

routerLink = id => {
  if ('scrollRestoration' in history) {
    history.scrollRestoration = 'manual' // 跳转的时候页面回到顶部
  }
  // console.log(window.location)

  if (typeof window !== 'undefined') {
    // 后来的需求是要在新页面打开,当时考虑是使用a标签打开还是window.open,最终选择了window.open
    window.open(`${window.location.origin}/post-${id}`, '_blank')
  } else {
    Router.push(
      {
        pathname: '/post',
        query: {
          id
        }
      },
      `/post-${id}` // 伪造路由,实际的路由还是通过 `query` 去传参的,障眼法而已
    )
  }
}

通过 express 做服务端的数据请求和重定向

devProxy 代理设置

const devProxy = {
  '/api': {
    target: 'ip:3000/',
    //  pathRewrite: { '^/api': '/' },
    changeOrigin: true
  }
}

自定义服务端的路由和重定向

app
  .prepare()
  .then(() => {
    const server = express()
    server.use(cookie())
    const proxyMiddleware = require('http-proxy-middleware') // 接口地址代理的中间件
    Object.keys(devProxy).forEach(function(context) {
      server.use(proxyMiddleware(context, devProxy[context]))
    })

    server.get('/post-:id', (req, res) => {
      // 配合伪造路由,解析路由参数,确保接口的请求参数是正确的
      const actualPage = '/post'
      const queryParams = { id: req.params.id }
      app.render(req, res, actualPage, queryParams)
    })
    server.get('/post/:id', (req, res) => {
      // 301重定向  /post/:id => /post-:id
      res.redirect(301, `/post-${req.params.id}`)
    })
    server.get('*', (req, res) => {
      return handle(req, res)
    })
    server.listen(3001, err => {
      if (err) throw err
      console.log(`> Ready on port ${port} [${env}]`)
    })
  })
  .catch(ex => {
    console.error(ex.stack)
    process.exit(1)
  })

antd 默认样式修改

因为 antd 的样式是全局的,所以在局部作用域内修改会无法生效,可以使用 less 中的全局的方法来修改

<div className="title">
  <Row gutter={24}>
    <Col xl={6} md={6} xs={6}>
      111111
    </Col>
    <Col xl={12} md={12} xs={12}>
      22222
    </Col>
    <Col xl={6} md={6} xs={6} className="right">
      333333
    </Col>
  </Row>
</div>
.title {
  max-width: 1200px;
  line-height: 60px;
  height: 62px;
  margin: 60px auto 0;
  background: #3d4864;
  font-size: 14px;
  text-align: center;
  color: #fff;
  .right {
    line-height: 20px;
    padding-top: 20px;
  }
  :global(.ant-row) {
    <!--全局的样式-->margin: 0 !important;
  }
}

全局图片路径

大坑: 当时做全局部署的时候,需要改图片路径。

export const imgHost = {
  // "local_page": 'https://dapp-prod-fileserver01.oss-cn-hongkong.aliyuncs.com/hpbsite3.0',
  local_page: '../static',
  // "local_component": 'https://dapp-prod-fileserver01.oss-cn-hongkong.aliyuncs.com/hpbsite3.0',
  local_component: '../static'
  // "local_component": '../../../',
  //https://dapp-prod-fileserver01.oss-cn-hongkong.aliyuncs.com/hpbsite3.0/images/sc_github.png
}

// 组件内使用
import { imgHost } from '../../common/config/imgHost'
;<img src={`${imgHost.local_component}/images/team/${name}.jpg`} />

css 样式的预加载

出现的问题: ssr 部署到服务器上之后,第一次加载会出现没有样式的页面
期望: 在 css 和 js 都加载完成后才显示页面
解决方案:使用 css 预加载

Twitter 和 Telegram 分享链接加载的是卡片效果

官方文档 官方提供 4 种类型,Summary card、Summary with large image、Player card、App card。

image

import React, { Component } from 'react'
import Head from 'next/head'

class Header extends Component {
  constructor(props) {
    super(props)
    this.state = {
      visible: false
    }
  }
  render() {
    let {
      description = '',
      title = '',
      descTitle = '',
      //   url = 'url',
      // img = 'https://dapp-prod-fileserver01.oss-cn-hongkong.aliyuncs.com/hpbsite3.0/images/logo/indexIcon2.png'
      img = '',
      keywords = ''
    } = this.props.data || {}
    return (
      <Head>
        <meta name="description" content={description} />
        <meta name="keywords" content={keywords} />
        <meta
          name="viewport"
          content="width=device-width, initial-scale=1, maximum-scale=1,user-scalable=no"
          className="next-head"
        ></meta>
        <meta property="og:type" content="article" />
        <meta property="og:title" content={descTitle} />
        {/* <meta property="og:url" content={url} /> */}
        <meta property="og:description" content={description} />
        {/* <meta property="og:site_name" content="" /> */}
        <meta property="og:image" content={img} />
        <meta property="og:image:width" content="770" />
        <meta property="og:image:height" content="433" />
        {/* <meta property="og:locale" content="en_US" /> */}
        {/* <meta name="twitter:creator" content="" />
        <meta name="twitter:site" content="" /> */}
        <meta name="twitter:text:title" content={descTitle} />
        <meta name="twitter:image" content={img} />
        <meta name="twitter:card" content="summary_large_image" />
        {/* <meta property="article:publisher" content="hwq" /> */}
        {/* <meta name="twitter:app:id:iphone" content="586939626" /> */}
        {/* <meta property="article:author" content="hwq" /> */}
        <title>{title}</title>
      </Head>
    )
  }
}

export default Header