20、在线商店国际化与服务器端渲染实战

在线商店国际化与SSR实战

在线商店国际化与服务器端渲染实战

1. 国际化功能实现

在开发多语言支持的应用时,我们需要实现动态加载不同语言的翻译内容。以下是创建国际化对象的函数 createI18n 的实现:

export async function createI18n (locale) {
  const { default: localeMessages } = await
  import(`../../i18n/locales/${locale}`)
  const messages = {
    [locale]: localeMessages,
  }
  const i18n = new VueI18n({
    locale,
    messages,
  })
  return i18n
}

上述代码使用了 async/await 语法,当然也可以使用 Promise 来实现:

export function createI18n (locale) {
  return import(`../../i18n/locales/${locale}`)
    .then(module => {
      const localeMessages = module.default
      // ...
    })
}

为了自动加载用户的语言设置,我们可以使用 navigator.language userLanguage (兼容 Internet Explorer)来获取语言代码。以下是 getAutoLang 函数的实现:

export function getAutoLang () {
  let result = window.navigator.userLanguage ||
  window.navigator.language
  if (result) {
    result = result.substr(0, 2)
  }
  if (langs.indexOf(result) === -1) {
    return 'en'
  } else {
    return result
  }
}

src/main.js 文件中,我们需要引入这两个新的工具函数,并修改 main 函数:

import { createI18n, getAutoLang } from './utils/i18n'

async function main () {
  const locale = getAutoLang()
  const i18n = await createI18n(locale)
  await store.dispatch('init')
  // eslint-disable-next-line no-new
  new Vue({
    el: '#app',
    router,
    store,
    i18n, // Inject i18n into the app
    ...App,
  })
}
2. 语言切换页面的添加

为了让用户能够切换语言,我们需要添加一个语言选择页面。具体步骤如下:
1. 在 src/router.js 文件中,导入 PageLocale 组件:

import PageLocale from './components/PageLocale.vue'
  1. routes 数组中添加语言切换路由:
{ path: '/locale', name: 'locale', component: PageLocale },
  1. AppFooter.vue 组件的模板中添加路由链接:
<div v-if="$route.name !== 'locale'">
  <router-link :to="{ name: 'locale' }">{{ $t('change-lang') }}
  </router-link>
</div>
3. 服务器端渲染(SSR)

服务器端渲染(SSR)是指在服务器上运行并渲染应用,然后将 HTML 发送到浏览器。它有两个主要优点:
- 更好的搜索引擎优化(SEO),因为应用的初始内容会在页面 HTML 中渲染。
- 对于较慢的网络或设备,渲染后的 HTML 不需要 JavaScript 就能显示给用户,从而更快地显示内容。

然而,使用 SSR 也有一些权衡:
- 代码需要能够在服务器上运行(除非是在客户端专用的钩子中,如 mounted ),并且一些库可能在浏览器上表现不佳,需要特殊处理。
- 服务器负载会增加,因为它需要做更多的工作。
- 开发设置会更复杂。

4. 通用应用结构

为了编写一个既能在客户端又能在服务器上运行的通用应用,我们需要改变源代码的架构。具体操作如下:
1. 在 src/router.js 文件中,将路由器创建封装到一个新的导出函数 createRouter 中:

export function createRouter () {
  const router = new VueRouter({
    routes,
    mode: 'history',
    scrollBehavior (to, from, savedPosition) {
      // ...
    },
  })
  return router
}
  1. src/store/index.js 文件中,将 Vuex 存储创建封装到一个新的导出函数 createStore 中:
export function createStore () {
  const store = new Vuex.Store({
    strict: process.env.NODE_ENV !== 'production',
    // ...
    modules: {
      cart,
      item,
      items,
      ui,
    },
  })
  return store
}
  1. src/main.js 文件重命名为 src/app.js ,并将 main 函数改为导出的 createApp 函数:
export async function createApp (context) {
  const router = createRouter()
  const store = createStore()
  sync(store, router)
  const i18n = await createI18n(context.locale)
  await store.dispatch('init')
  const app = new Vue({
    router,
    store,
    i18n,
    ...App,
  })
  return {
    app,
    router,
    store,
  }
}
5. 客户端入口和服务器入口
  • 客户端入口 :创建一个新的 src/entry-client.js 文件,作为客户端包的入口点:
import { createApp } from './app'
import { getAutoLang } from './utils/i18n'
const locale = getAutoLang()
createApp({
  locale,
}).then(({ app }) => {
  app.$mount('#app')
})

同时,在 webpack.config.js 文件中更改入口路径:

entry: './src/entry-client.js',
  • 服务器入口 :创建一个新的 src/entry-server.js 文件,作为服务器包的入口点:
import { createApp } from './app'
export default context => {
  return new Promise(async (resolve, reject) => {
    const { app, router, store } = await createApp(context)
    // Set the current route
    router.push(context.url)
    // TODO get matched components to preload data
    // TODO resolve(app)
  })
}
6. 状态管理

在处理请求时,我们需要在渲染应用之前获取相关组件的数据。为此,我们为组件添加 asyncData 自定义选项。
- 在 PageHome.vue 组件中添加 asyncData 函数:

asyncData ({ store }) {
  return store.dispatch('items/fetchItems')
},
  • PageStoreItem.vue 组件中添加 asyncData 函数:
asyncData ({ store, route }) {
  return store.dispatch('item/fetchStoreItemDetails', {
    id: route.params.id,
  })
},

entry-server.js 文件中,我们可以使用 router.getMatchedComponents() 方法获取与当前路由匹配的组件列表,并调用它们的 asyncData 选项:

export default context => {
  return new Promise(async (resolve, reject) => {
    const { app, router, store } = await createApp(context)
    router.push(context.url)
    // Wait for the component resolution
    router.onReady(() => {
      const matchedComponents = router.getMatchedComponents()
      Promise.all(matchedComponents.map(Component => {
        if (Component.asyncData) {
          return Component.asyncData({
            store,
            route: router.currentRoute,
          })
        }
      })).then(() => {
        // Send back the store state
        context.state = store.state
        // Send the app to the renderer
        resolve(app)
      }).catch(reject)
    }, reject)
  })
}
7. 恢复客户端的 Vuex 状态

服务器会将存储状态序列化为 __INITIAL_STATE__ 变量存储在 HTML 页面中。我们可以在应用挂载之前使用该变量来设置状态:

createApp({
  locale,
}).then(({ app, store }) => {
  if (window.__INITIAL_STATE__) {
    store.replaceState(window.__INITIAL_STATE__)
  }
  app.$mount('#app')
})
8. Webpack 配置

为了适应客户端和服务器的不同需求,我们需要重构 Webpack 配置。具体步骤如下:
1. 安装 webpack-merge webpack-node-externals 包:

npm i -D webpack-merge webpack-node-externals
  1. 在项目根目录下创建一个新的 webpack 文件夹,并将 webpack.config.js 文件移动并重命名为 webpack/common.js
  2. 移除配置中的 entry 选项,并更新 output 选项:
output: {
  path: path.resolve(__dirname, '../dist'),
  publicPath: '/dist/',
  filename: '[name].[chunkhash].js',
},
  1. 创建客户端配置文件 webpack/client.js
const webpack = require('webpack')
const merge = require('webpack-merge')
const common = require('./common')
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')
module.exports = merge(common, {
  entry: './src/entry-client',
  plugins: [
    new webpack.optimize.CommonsChunkPlugin({
      name: 'manifest',
      minChunks: Infinity,
    }),
    // Generates the client manifest file
    new VueSSRClientPlugin(),
  ],
})
  1. 创建服务器配置文件 webpack/server.js
const merge = require('webpack-merge')
const common = require('./common')
const nodeExternals = require('webpack-node-externals')
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
module.exports = merge(common, {
  entry: './src/entry-server',
  target: 'node',
  devtool: 'source-map',
  output: {
    libraryTarget: 'commonjs2',
  },
  // Skip webpack processing on node_modules
  externals: nodeExternals({
    // Force css files imported from no_modules
    // to be processed by webpack
    whitelist: /\.css$/,
  }),
  plugins: [
    // Generates the server bundle file
    new VueSSRServerPlugin(),
  ],
})
9. 服务器端设置

在开发环境中,我们不能直接使用 webpack-dev-server 进行 SSR。我们需要使用 Express 服务器和 Webpack 来设置开发环境。具体操作如下:
1. 下载 server.dev.js 文件,并将其放在项目根目录下。
2. 安装开发依赖包:

npm i -D memory-fs chokidar webpack-dev-middleware webpack-hot-middleware
  1. 创建页面模板文件 index.template.html ,并将 body 内容替换为 <!--vue-ssr-outlet-->
  2. 安装 express reify 包:
npm i -S express reify
  1. 下载 server.js 文件,并将其放在项目根目录下。
  2. server.js 文件中替换 // TODO development 注释:
const setupDevServer = require('./server.dev')
readyPromise = setupDevServer({
  server,
  templatePath,
  onUpdate: (bundle, options) => {
    // Re-create the bundle renderer
    renderer = createBundleRenderer(bundle, {
      runInNewContext: false,
      ...options,
    })
  },
})
  1. server.js 文件中替换 // TODO render 注释:
const context = {
  url: req.url,
  // Languages sent by the browser
  locale: req.acceptsLanguages(langs) || 'en',
}
renderer.renderToString(context, (err, html) => {
  if (err) {
    // Render Error Page or Redirect
    res.status(500).send('500 | Internal Server Error')
    console.error(`error during render : ${req.url}`)
    console.error(err.stack)
  }
  res.send(html)
})
  1. 更改 dev 脚本以运行 Express 服务器:
"dev": "node server",

总结

通过以上步骤,我们实现了一个支持多语言切换的在线商店应用,并引入了服务器端渲染(SSR)来提升应用的性能和 SEO。在开发过程中,我们需要注意代码的可移植性和服务器负载问题,同时合理配置 Webpack 以优化构建过程。

流程图

graph LR
    A[开始] --> B[自动加载用户语言]
    B --> C[创建国际化对象]
    C --> D[添加语言切换页面]
    D --> E[实现服务器端渲染]
    E --> F[重构 Webpack 配置]
    F --> G[设置服务器端环境]
    G --> H[运行应用]
    H --> I[结束]

表格

功能 实现步骤
自动加载用户语言 1. 使用 navigator.language 获取语言代码
2. 截取前两个字符
3. 检查是否在支持的语言列表中,否则使用默认语言
创建国际化对象 1. 异步导入语言文件
2. 创建 VueI18n 实例
添加语言切换页面 1. 导入 PageLocale 组件
2. 添加路由
3. 在页脚添加路由链接
实现服务器端渲染 1. 封装路由器和存储创建函数
2. 创建客户端和服务器入口文件
3. 处理状态管理
4. 恢复客户端状态
重构 Webpack 配置 1. 安装相关包
2. 创建通用配置文件
3. 创建客户端和服务器配置文件
设置服务器端环境 1. 下载相关文件
2. 安装依赖包
3. 创建页面模板
4. 配置服务器文件
运行应用 更改 dev 脚本以运行 Express 服务器

在线商店国际化与服务器端渲染实战

10. 详细操作步骤解析

为了更清晰地理解上述步骤,下面对关键操作进行详细解析。

10.1 国际化功能
  • 自动加载用户语言
  • 首先,通过 window.navigator.userLanguage || window.navigator.language 获取用户浏览器设置的语言代码。
  • 然后,截取该代码的前两个字符,因为有些浏览器返回的代码格式是 en-US ,我们只需要 en 这种格式。
  • 最后,检查该代码是否在支持的语言列表 langs 中,如果不在则使用默认语言 en
export function getAutoLang () {
  let result = window.navigator.userLanguage || window.navigator.language
  if (result) {
    result = result.substr(0, 2)
  }
  if (langs.indexOf(result) === -1) {
    return 'en'
  } else {
    return result
  }
}
  • 创建国际化对象
  • 使用 async/await 异步导入对应语言的翻译文件。
  • 将导入的翻译文件作为 messages 对象的属性,键为语言代码。
  • 创建 VueI18n 实例并返回。
export async function createI18n (locale) {
  const { default: localeMessages } = await import(`../../i18n/locales/${locale}`)
  const messages = {
    [locale]: localeMessages,
  }
  const i18n = new VueI18n({
    locale,
    messages,
  })
  return i18n
}
10.2 语言切换页面
  • 路由配置
  • src/router.js 中导入 PageLocale 组件。
  • routes 数组中添加 /locale 路由,指向 PageLocale 组件。
import PageLocale from './components/PageLocale.vue'
const routes = [
  // 其他路由
  { path: '/locale', name: 'locale', component: PageLocale },
  // 其他路由
]
  • 页脚添加链接
  • AppFooter.vue 组件的模板中,使用 router-link 添加语言切换链接。
  • 使用 $t 方法显示翻译后的文本。
<div v-if="$route.name !== 'locale'">
  <router-link :to="{ name: 'locale' }">{{ $t('change-lang') }}</router-link>
</div>
10.3 服务器端渲染
  • 通用应用结构调整
  • 路由器 :在 src/router.js 中创建 createRouter 函数,每次请求时创建新的路由器实例。
export function createRouter () {
  const router = new VueRouter({
    routes,
    mode: 'history',
    scrollBehavior (to, from, savedPosition) {
      // ...
    },
  })
  return router
}
  • 存储 :在 src/store/index.js 中创建 createStore 函数,每次请求时创建新的 Vuex 存储实例。
export function createStore () {
  const store = new Vuex.Store({
    strict: process.env.NODE_ENV !== 'production',
    // ...
    modules: {
      cart,
      item,
      items,
      ui,
    },
  })
  return store
}
  • 根实例 :将 src/main.js 重命名为 src/app.js ,创建 createApp 函数,返回新的根实例、路由器和存储。
export async function createApp (context) {
  const router = createRouter()
  const store = createStore()
  sync(store, router)
  const i18n = await createI18n(context.locale)
  await store.dispatch('init')
  const app = new Vue({
    router,
    store,
    i18n,
    ...App,
  })
  return {
    app,
    router,
    store,
  }
}
  • 客户端和服务器入口
  • 客户端入口 :在 src/entry-client.js 中获取用户语言,调用 createApp 函数并挂载应用。
import { createApp } from './app'
import { getAutoLang } from './utils/i18n'
const locale = getAutoLang()
createApp({
  locale,
}).then(({ app }) => {
  app.$mount('#app')
})
  • 服务器入口 :在 src/entry-server.js 中接收上下文对象,创建应用实例,设置当前路由,处理组件数据预加载。
import { createApp } from './app'
export default context => {
  return new Promise(async (resolve, reject) => {
    const { app, router, store } = await createApp(context)
    router.push(context.url)
    router.onReady(() => {
      const matchedComponents = router.getMatchedComponents()
      Promise.all(matchedComponents.map(Component => {
        if (Component.asyncData) {
          return Component.asyncData({
            store,
            route: router.currentRoute,
          })
        }
      })).then(() => {
        context.state = store.state
        resolve(app)
      }).catch(reject)
    }, reject)
  })
}
10.4 Webpack 配置
  • 通用配置 :将 webpack.config.js 重命名为 webpack/common.js ,移除 entry 选项,更新 output 选项。
output: {
  path: path.resolve(__dirname, '../dist'),
  publicPath: '/dist/',
  filename: '[name].[chunkhash].js',
},
  • 客户端配置 :在 webpack/client.js 中合并通用配置,设置入口和插件。
const webpack = require('webpack')
const merge = require('webpack-merge')
const common = require('./common')
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')
module.exports = merge(common, {
  entry: './src/entry-client',
  plugins: [
    new webpack.optimize.CommonsChunkPlugin({
      name: 'manifest',
      minChunks: Infinity,
    }),
    new VueSSRClientPlugin(),
  ],
})
  • 服务器配置 :在 webpack/server.js 中合并通用配置,设置入口、目标、输出和插件。
const merge = require('webpack-merge')
const common = require('./common')
const nodeExternals = require('webpack-node-externals')
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
module.exports = merge(common, {
  entry: './src/entry-server',
  target: 'node',
  devtool: 'source-map',
  output: {
    libraryTarget: 'commonjs2',
  },
  externals: nodeExternals({
    whitelist: /\.css$/,
  }),
  plugins: [
    new VueSSRServerPlugin(),
  ],
})
10.5 服务器端设置
  • 开发环境设置
  • 下载 server.dev.js 文件,安装开发依赖包。
  • 创建页面模板文件 index.template.html ,替换 body 内容。
  • 安装 express reify 包,下载 server.js 文件。
  • server.js 中配置开发环境和渲染逻辑。
const setupDevServer = require('./server.dev')
readyPromise = setupDevServer({
  server,
  templatePath,
  onUpdate: (bundle, options) => {
    renderer = createBundleRenderer(bundle, {
      runInNewContext: false,
      ...options,
    })
  },
})

const context = {
  url: req.url,
  locale: req.acceptsLanguages(langs) || 'en',
}
renderer.renderToString(context, (err, html) => {
  if (err) {
    res.status(500).send('500 | Internal Server Error')
    console.error(`error during render : ${req.url}`)
    console.error(err.stack)
  }
  res.send(html)
})
11. 注意事项和常见问题
  • 代码可移植性 :在编写代码时,要确保代码能够在服务器和客户端都能正常运行。避免在服务器端使用客户端特有的 API,如 window document
  • 服务器负载 :SSR 会增加服务器的负载,因为服务器需要处理更多的请求和渲染工作。可以通过优化代码、缓存数据等方式来减轻服务器负载。
  • Webpack 配置 :Webpack 配置可能会比较复杂,要确保每个配置文件的设置正确。特别是服务器配置中的 externals 选项,要正确处理 node_modules 中的依赖。
  • 错误处理 :在服务器端渲染过程中,要做好错误处理。当渲染出错时,要返回合适的错误页面或错误信息给客户端。
12. 进一步优化建议
  • 缓存机制 :可以在服务器端实现缓存机制,缓存已经渲染好的页面,减少重复渲染的时间和服务器负载。
  • 代码分割 :使用 Webpack 的代码分割功能,将应用代码分割成更小的块,按需加载,提高应用的加载速度。
  • 性能监控 :使用性能监控工具,如 Google Analytics、New Relic 等,监控应用的性能,找出性能瓶颈并进行优化。

总结

通过实现多语言切换和服务器端渲染,我们提升了在线商店应用的用户体验和搜索引擎优化效果。在开发过程中,我们需要注意代码的可移植性、服务器负载和 Webpack 配置等问题。同时,可以通过进一步优化,如缓存机制、代码分割和性能监控,来提高应用的性能和稳定性。

流程图

graph LR
    A[详细解析开始] --> B[国际化功能解析]
    B --> C[语言切换页面解析]
    C --> D[服务器端渲染解析]
    D --> E[Webpack 配置解析]
    E --> F[服务器端设置解析]
    F --> G[注意事项和优化建议]
    G --> H[总结]
    H --> I[结束]

表格

操作步骤 详细说明
国际化功能 自动加载用户语言,创建国际化对象,使用 async/await 异步导入语言文件
语言切换页面 配置路由,在页脚添加链接,使用 $t 方法显示翻译文本
服务器端渲染 调整通用应用结构,创建客户端和服务器入口,处理组件数据预加载
Webpack 配置 创建通用配置、客户端配置和服务器配置,使用 webpack-merge 合并配置
服务器端设置 下载文件,安装依赖,创建页面模板,配置服务器文件
注意事项 代码可移植性、服务器负载、Webpack 配置、错误处理
优化建议 缓存机制、代码分割、性能监控
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值