服务端渲染ssr-nuxt实战总结

195 阅读5分钟

wanandroid-nuxt

前言

基于鸿洋大佬的 api ,从零开始实现一个 nuxt ,偏实战,项目的设计思路和主要实现步骤,列出坑点, 基础 api 请参照官方文档。有需要的小伙伴可以从零实现自己 nuxt(实现起来并不复杂,该教程只是提供思路,并非最佳实践)

Github:传送门

演示地址:传送门

nuxt 官网:Nuxt.js docs.

预览

技术栈


前端:

vue: 三选一,毕竟是 nuxt。

vuex: 状态管理

sass: 样式

element-ui: 优秀

lodash: 工具类

swiper: 知名滑动插件, 不要用最新的 6 版本, 有 坑

服务端

nginx: 端口转发

docker: 部署

工程搭建


基于 nuxt 官网搭建,详细步骤请查看官网, 我这里只展示有差异的地方。

我的触发点是搭建一套环境,多项目部署,省事。使用到了npm-run-all

|--v1
  |-- api                 // 接口目录
  |-- assets              // 文件
  |-- components          // 组件
  |-- layout              // 页面布局
  |-- middleware          // 中间件
  |-- page                // 页面
  |-- plugins             // 插件
  |-- static              // 静态文件
  |-- store               // 状态管理
  |-- util                // 常用工具集合
|--v2

这里简单介绍下使用npm-run-all进行多项目并行打包,结合Dockerfile打包镜像,部署项目,实现简化版本 CI/CD

使用--parallel参数执行并行操作 cx:v1是本地测试用的,本地调试也就是 npm dev:v1

// .package.json
"dev:v1": "nuxt --config-file v1/nuxt.config.js",
"build:v1": "nuxt build --config-file v1/nuxt.config.js",
"start:v1": "nuxt start --config-file v1/nuxt.config.js",
"cx:v1": "npm run build:v1 && npm run start:v1",
"dev:v2": "nuxt --config-file v2/nuxt.config.js",
"build:v2": "nuxt build --config-file v2/nuxt.config.js",
"start:v2": "nuxt start --config-file v2/nuxt.config.js",
"cx:v2": "npm run build:v2 && npm run start:v2",
"all": "npm-run-all --parallel cx:*",
"all-start": "npm-run-all --parallel start:*",

打包成 docker 容器,拷贝项目到创建的文件夹,npm i, 设置环境变量, 打包项目 暴漏端口,坐等启动,这里需要 dockerhub 账号关联 github,监听分支提交,自动打包。github 今年新出的 action 也很好用。小伙伴们可以试一试

// .Dockerfile
RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app

# 拷贝
COPY package*.json /usr/src/app/
COPY yarn.lock /usr/src/app/
RUN npm install

ENV NODE_ENV production
ENV NUXT_HOST 0.0.0.0

# 拷贝
COPY . /usr/src/app/

# build
RUN npm run build:v1
RUN npm run build:v2
EXPOSE 3001 3002

# Running the app
CMD "npm" "run" "all-start"

简单介绍下 nuxt.config.js 的属性

  1. server : 端口设置

    server: {
        port: 3001, // default: 3000
        host: '0.0.0.0', // default: localhost
      },
    
  2. buildDir: 启动路径

     buildDir: `.nuxt/v1`,
    
  3. head: 网站 seo 设置

      head: {
        title: process.env.npm_package_name || '',
        meta: [
          { charset: 'utf-8' },
          { name: 'viewport', content: 'width=device-width, initial-scale=1' },
          {
            hid: 'description',
            name: 'description',
            content: process.env.npm_package_description || '',
          },
        ],
        link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }],
      },
    
  4. css : 样式文件

    css: [
        'element-ui/lib/theme-chalk/index.css',
        '~/static/iconfont/iconfont.css',
        {
          src: '~/assets/style/reset.scss',
          lang: 'scss',
        },
        {
          src: '~/assets/style/common.scss',
          lang: 'scss',
        },
      ],
    
  5. public: 插件

     plugins: [
        '~/plugins/element-ui',
        '~/plugins/swiper',
        '~/plugins/vue-global.js',
        '~/plugins/axios',
        // '~/plugins/sticky',
        // { src: '~/plugins/sticky', ssr: false },
        { src: '~/static/iconfont/iconfont.js', ssr: false },
      ],
    
  6. modules: nuxt 提供的插件 axios 处理前后接口,auth 官方提供的权限管理,proxy 提供处理跨域

    modules: [
        // Doc: https://axios.nuxtjs.org/usage
        // Doc: https://github.com/nuxt/content
        '@nuxtjs/axios',
        '@nuxtjs/auth',
        '@nuxtjs/proxy',
        '@nuxtjs/pwa',
        '@nuxtjs/svg',
        '@nuxt/content',
        [
          '@nuxtjs/component-cache',
          {
            max: 10000,
            maxAge: 1000 * 60 * 60,
          },
        ],
      ],
    
  7. axios: 接口

      axios: {
        proxy: true,
        credentials: true,
      },
    
  8. proxy:转发解决跨域

     proxy: {
        '/api': {
          target: baseURL,
          pathRewrite: {
            '^/api': '/',
          },
        },
      },
    
  9. build: 引入 UI 组件

    build: {
       transpile: [/^element-ui/],
       extend(config, ctx) {
       }
    }
    
  10. router: 路由, 这里会用到中间件,处理路由拦截

    router: {
      middleware: ['auth'],
    },
    
  11. auth:路由的中间件, 拦截路由,里面配置比较多 配置说明请看 官方 auth 验证模块

      auth: {
        plugins: ['~/plugins/auth.js'], // 扩展auth插件
        localStorage: false,
        cookie: {
          prefix: '123cookie.', // 自定义cookie名称
          options: {
            path: '/',
            expires: 2 / 24 / 60, // 两分钟 兼容ie 用户不操作后 用户操作就一直更新
            // maxAge: 2 * 60, // 两分钟
          },
        },
        redirect: {
          login: '/login',
          logout: '/',
          callback: '/login',
          home: '/',
        },
        strategies: {
          local: {
            autoFetchUser: false, // 登录后请求用户接口
            // tokenRequired: false, // 此选项可用于禁用所有令牌处理
            // tokenType: Bearer, // 在axios请求中使用的授权标头类型
            globalToken: false, // 为所有axios请求设置授权标头
            endpoints: {
              login: {
                url: '/api/user/login',
                method: 'post',
                propertyName: false,
                headers: {
                  'Content-Type': 'application/x-www-form-urlencoded',
                  // Referer: 'https://www.wanandroid.com/index',
                },
              },
              logout: { url: '/api/user/logout/json', method: 'get' },
              user: { url: '/api/friend/json', method: 'get', propertyName: false },
            },
          },
        },
      },
    

api 自动化, 这里没有另外引入 axios, 毕竟 nuxt 已经提供了封装好的 axios

// api 自动化
const ctx = require.context('.', false, /.api.js$/)
const APIS = {}
ctx.keys().forEach((item) => {
  const name = item.split('.')[1].slice(1)
  const config = ctx(item)
  const dft = config.default || config
  APIS[name] = dft
})
export default APIS

api 设计 增删改查

// ./api/repository
export default ($axios) => (resource) => ({
  index() {
    return $axios.$get(`${resource}`)
  },

  show(id) {
    return $axios.$get(`${resource}/${id}`)
  },

  create(payload) {
    return $axios.$post(`${resource}`, payload)
  },

  update(id, payload) {
    return $axios.$post(`${resource}/${id}`, payload)
  },

  delete(id) {
    return $axios.$delete(`${resource}/${id}`)
  },
})

处理 async/await 异常 , 让代码更优雅

// ./api/test.api.js
const to = (promise) => {
  return promise
    .then((data) => {
      return [null, data]
    })
    .catch((err) => [err, null])
}
// 首页banner
getBanner() {
   return to($axios.$get(`${resource}/banner/json`))
},

使用

// ./page/index.vue
const [err, resList] = await ctx.app.$api_wanandroid.getAllIndex(page - 1)
if (err) return

组件自动化,省事省心

// ./components/common/index.js
// 组件自动化
export default {
  install(Vue) {
    const components = require.context('~/components/common', false, /\.vue$/)
    // components.keys() 获取文件名数组
    components.keys().forEach((path) => {
      // 获取组件文件名
      const fileName = path.replace(/(.*\/)*([^.]+).*/gi, '$2')
      // components(path).default 获取 ES6 规范暴露的内容,components(path) 获取 Common.js 规范暴露的内容
      Vue.component(fileName, components(path).default || components(path))
    })
  },
}

部署

镜像名称:caniuse/web-nuxt


- 停止容器:docker stop <CONTAINER>
- 删除容器:docker rm <CONTAINER>
- 更新镜像:docker pull <IMAGE>
- 启动容器:docker run <ARG> ... <IMAGE>

// 单项目部署
docker run --name web-nuxt01 -p 43001:3001 -d caniuse/web-nuxt:0.0.1
// 多项目部署
docker run --name web-nuxt01 -p 43001:3001 -p 43002:3002 -d caniuse/web-nuxt:0.0.1

watchtower

这里推荐一款自动更新所有镜像的镜像 watchtower 常用的命令

每小时更新一次

docker run -d \
    --name watchtower \
    --restart always \
    -v /var/run/docker.sock:/var/run/docker.sock \
    containrrr/watchtower \
    --cleanup \
    -i 3600

每天凌晨一点更新(北京时间)

docker run -d \
    --name watchtower \
    --restart always \
    -e TZ=Asia/Shanghai \
    -v /var/run/docker.sock:/var/run/docker.sock \
    containrrr/watchtower \
    --cleanup \
    -s "0 0 1 * * *"

watchtower 手动更新

docker run --rm \
    -v /var/run/docker.sock:/var/run/docker.sock \
    containrrr/watchtower -cR \
    web01
docker run --rm \
    -v /var/run/docker.sock:/var/run/docker.sock \
    containrrr/watchtower \
    --cleanup \
    --run-once \
    web01

注意指定容器需填写 容器名 ,并非镜像名.由于部分容器启动时可能没有定义 --name 参数,请执行 docker ps 查询核对容器名.

手动更新所有容器

docker run --rm \
    -v /var/run/docker.sock:/var/run/docker.sock \
    containrrr/watchtower \
    --cleanup \
    --run-once

官方文档

auth 验证模块 auth.nuxtjs.org/#getting-st…

$axios 接口模块

axios.nuxtjs.org/helpers

websocket 文档 nuxt-socket-io.netlify.app/

社区好文章

选用@auxtjs/axios 而非 axios 作为 nuxt 的请求模块 方案 blog.lichter.io/posts/nuxt-…

jwt+cookie 的安全处理 stormpath.com/blog/where-…

权限验证 dev.to/mandeepm91/…

// Nuxt.js + SockJs 实现 webSocket 订单实时消息 语音播报 blog.csdn.net/lq099526/ar…

待解决 待学习

nuxt sticky 粘性插件没找到合适的 github.com/mehwww/vue-…

nuxt 的性能问题 www.zhihu.com/question/32…

nuxt 性能 www.cnblogs.com/lessfish/p/…

nuxt next 性能对比 ssr 方案讨论

blog.fundebug.com/2019/05/23/…

cnodejs.org/topic/5e0c5…