服务端渲染之 nuxt.js + koa

特性

  • 基于 Vue.js
  • 自动代码分层
  • 服务端渲染
  • 强大的路由功能,支持异步数据
  • 静态文件服务
  • ES2015+ 语法支持
  • 打包和压缩 JS 和 CSS
  • HTML 头部标签管理
  • 本地开发支持热加载
  • 集成 ESLint
  • 支持各种样式预处理器: SASS、LESS、 Stylus 等等
  • 支持 HTTP/2 推送

nuxt渲染流程

  • 客户端发起请求;
  • 调用nuxtServerInit可以将服务端的数据(比如登录信息)传到客户端的storeaction中;
  • 接下来调用middleware俗称中间件,中间件会在页面渲染之前运行我们定义的函数,比如用于权限控制和校验;
  • 然后再validate执行的时候对客户端携带的参数进行校验;
  • asyncData在渲染组件前向服务端获取数据,把请求到的数据合并到Vue中的data中;
  • render页面渲染,内部跳转<nuxt-link>继续循环执行。

nuxt安装

  • 运行create-nuxt-app 确保安装了npx(npx在NPM版本5.2.0默认安装了): npx create-nuxt-app <项目名>
  • 选项

  • 目录结构
nuxt-test
├── assest # 存放被webpack打包的静态资源文件
├── components # vue组件,不会像页面组件那样有 asyncData 方法的特性
├── layouts # 布局页
├── middleware # 中间件
├── pages # 页面目录,该目录下所有的 .vue 文件并自动生成对应的路由配置。
├── plugins # 运行应用之前,运行的 Javascript 插件。
├── static # 不会被webpack打包的静态资源
├── store #  Vuex 状态树 
├── nuxt.config.js #  Nuxt.js 应用的个性化配置,以便覆盖默认配置
└── package.json # 用于描述应用的依赖关系和对外暴露的脚本接口

复制代码
  • 运行项目 npm run dev

路由

路由生成

pages目录中所有*.vue文件自动生成应用的路由配置,新建:

  • pages/admin.vue 商品管理页
  • pages/cart.vue 购物车页

在.nuxt/router.js会生成自动路由

动态路由

以下划线作为前缀的.vue文件或目录会被定义为动态路由,如下面结构:

pages/
--| detail/
----| _id.vue
复制代码

会生成如下路由配置:

{
path: "/detail/:id?",
component:_9c9d895e,
name: "detail-id"
}

复制代码

路由路径带有 :id? 参数,表示该路由是可选的。如果你想将它设置为必选的路由,需要在 detail 目录内创建一个 index.vue 文件。

嵌套路由

创建内嵌子路由,你需要添加一个 .vue 文件,同时添加一个与该文件同名的目录用来存放子视图组件。

文件结构如下:

pages/
--| detail/
----| _id.vue
--| detail.vue
复制代码

生成的路由配置如下:

{
path: '/detail',
component: 'pages/detail.vue',
children: [
	{path: ':id?', name: "detail-id"}
 ]
}
复制代码

别忘了在父组件(.vue文件) 内增加 <nuxt-child/> 用于显示子视图内容

视图

为指定的路由配置数据和视图,包括应用模板、页面、布局和 HTML 头部等内容。

默认布局

  • 添加路由导航 layouts/default.vue
<div>
    <nuxt-link to="/">首页</nuxt-link>
    <nuxt-link to="/admin">管理</nuxt-link>
    <nuxt-link to="/cart">购物车</nuxt-link>
    <nuxt />
</div>
复制代码

别忘了在布局文件中添加 <nuxt/> 组件用于显示页面的主体内容。

npm run dev查看

自定义布局

创建空白布局页layouts/blank.vue,用于login.vue

<template>
  <div>
  	<nuxt />
  </div>
</template>

复制代码

页面pages/login.vue使用自定义布局:

export default {
	layout: 'blank'
}

复制代码

自定义错误页面

创建layouts/error.vue

<template>
  <div>
      <h1 v-if="error.statusCode === 404">页面不存在</h1>
      <h1 v-else>应用发生错误异常</h1>
      <p>{{error}}</p>
      <nuxt-link to="/">首页</nuxt-link>
  </div>
</template>

<script>
export default {
  props:['error'],
  layout:'blank'
}

</script>
<style lang='scss' scoped>
</style>
复制代码

测试:访问一个不存在的页面

页面

页面组件就是 Vue 组件,在pages存放只不过 Nuxt.js 为这些组件添加了一些特殊的配置项 给首页添加标题和meta等,最重要的一个键, 支持asyncData 异步数据处理,另外该方法的第一个参数为当前页面组件的 上下文对象。

如pages/index.vue,配置头部信息:

export default {
  head(){
      return{
        title:"课程列表",
        // vue-meta利用hid确定要更新meta
        meta:[
          {name:"description",hid:"description",content:"set page meta"}
        ],
        link:[{rel:"favicon",href:"favicon.ico"}]
     }
   }
}
复制代码

异步数据获取

asyncData 方法使得我们可以在设置组件数据之前异步获取或处理数据。

示例:获取商品数据

接口准备

  • 安装依赖 npm i koa koa-router koa-bodyparser -S
  • 创建 server/index.js
const Koa = require('koa')
const consola = require('consola')
const { Nuxt, Builder } = require('nuxt')

const app = new Koa()

// Import and Set Nuxt.js options
let config = require('../nuxt.config.js')
config.dev = !(app.env === 'production')

async function start() {
  // Instantiate nuxt.js
  const nuxt = new Nuxt(config)

  const {
    host = process.env.HOST || '127.0.0.1',
    port = process.env.PORT || 3000
  } = nuxt.options.server

  // Build in development
  if (config.dev) {
    const builder = new Builder(nuxt)
    await builder.build()
  } else {
    await nuxt.ready()
  }

  // 监听所有路由
  app.use(ctx => {
    ctx.status = 200
    ctx.respond = false // Bypass Koa's built-in response handling
    ctx.req.ctx = ctx // This might be useful later on, e.g. in nuxtServerInit or with nuxt-stash
    nuxt.render(ctx.req, ctx.res)
  })

  app.listen(port, host)
  consola.ready({
    message: `Server listening on http://${host}:${port}`,
    badge: true
  })
}

start()

复制代码
  • 创建接口文件
// 此文件并非nuxt生成,它为演示项目提供数据服务接口
const Koa = require('koa');
const app = new Koa();
const bodyparser = require("koa-bodyparser");
const router = require("koa-router")({ prefix: "/api" });

// 设置cookie加密秘钥
app.keys = ["some secret", "another secret"];

const goods = [
  { id: 1, text: "衣服", price: 60 },
  { id: 2, text: "鞋子", price: 200 }
];

// 配置路由
// 获取产品列表
// http://localhost:8080/api/goods
router.get("/goods", ctx => {
  ctx.body = {
    ok: 1,
    goods
  };
});

// 产品详情
router.get("/detail", ctx => {
  ctx.body = {
    ok: 1,
    data: goods.find(good => good.id == ctx.query.id)
  };
});

// 登录
router.post("/login", ctx => {
  const user = ctx.request.body;
  if (user.username === "jerry" && user.password === "123") {
    // 将token存入cookie
    const token = 'a mock token';
    ctx.cookies.set('token', token);
    ctx.body = { ok: 1, token };
  } else {
    ctx.body = { ok: 0 };
  }
});

// 解析post数据并注册路由
app.use(bodyparser());
// 注册路由
app.use(router.routes());

app.listen(8080, () => console.log('api服务已启动'))
复制代码

整合axios

  • 安装@nuxt/axios模块: npm install @nuxtjs/axios -S
  • 配置: nuxt.config.js
modules: [
    '@nuxtjs/axios'
  ],
  //配置跨域,如果nginx配置了则不需要
  axios:{
    proxy:true
  },
  proxy:{
    "/api":"http://localhost:8080"
  },
复制代码

测试代码:获取商品列表 pages/index.vue

export default {
  //由于asyncData方法是在组件 初始化 前被调用的,所以在方法内是没有办法通过 this 来引用组件的实例对象。
  // $axios由nuxtjs/axios模块注入
  // $get是axios模块封装的类fetch风格api
  async asyncData({$axios,error}){
    const {ok,goods} = await $axios.$get("api/goods");
    if(ok){
      return {goods}
    }
      error({statusCode:400,message:"数据查询失败"})
  }
}
复制代码

中间件

中间件会在一个页面或一组页面渲染之前运行我们定义的函数,常用于权限控制、校验等任务。 示例代码:管理员页面保护,创建middleware/auth.js

// 定义中间件,参数是nuxt提供的上下文对象
export default function({route,redirect,store}){
    // 上下文中通过store访问vux在的全局状态
    // 通过vux中令牌存在与否判断用户是否登录
    if(!store.state.user.token){
        redirect("/login?redirect="+route.path);
    }
}
复制代码

注册中间件,pages/admin.vue

<script>
  export default {
  	middleware: ['auth']
  }
</script>

复制代码

全局注册:将会对所有页面起作用,nuxt.config.js

router: {
	middleware: ['auth']
}
复制代码

状态管理 vuex

应用根目录下如果存在 store 目录,Nuxt.js将启用vuex状态树。定义各状态树时具名导出state, mutations, getters, actions即可。导出时state是个函数,其余是对象。

示例代码:用户登录及登录状态保存,创建store/user.js

export const state = () => ({
    token:''
});

export const mutations = {
    init(state,token){
        state.token = token;
    }
};

export const getters = {
    isLogin(state){
        // !!  相当于将state.token直接转换为boolean值
        return !!state.token
    }
};

export const actions = {
    login({commit,getters},u){
        //this.$axios由@nuxt/axios模块提供
        return this.$axios.$post("api/login",u).then(({token}) => {
            if(token){
                commit("init",token)
            }
            return getters.isLogin;
        })
    }
}
复制代码

登录页面逻辑 pages/login.vue

<template>
  <div>
      <h2>用户登录</h2>
      <input v-model="user.username">
      <input type="password" v-model="user.password">
      <button @click="onLogin">登录</button>
  </div>
</template>

<script>
export default {
    layout:'blank',
    data(){
        return{
            user:{
                username:"",
                password:""
            }
        }
    },
    methods:{
        onLogin(){
            this.$store.dispatch("user/login",this.user).then(ok => {
                if(ok){
                    console.log(this.$route.query.redirect)
                    const redirect = this.$route.query.redirect || '/';
                    this.$router.push(redirect);
                }
            })
        }
    }
  
}

</script>
<style lang='scss' scoped>
</style>
复制代码

插件

Nuxt.js会在运行应用之前执行插件函数,需要引入或设置Vue插件、自定义模块和第三方模块时特别有用。

示例代码:添加请求拦截器附加token,创建plugins/interceptor.js

export default function({$axios,store}){
    $axios.onRequest(config => {
        if(store.state.user.token){
            config.headers.Authorization = "Bearer " + store.state.user.token;
        }
        return config;
    })
}
复制代码

注册插件,nuxt.config.js

plugins: ["@/plugins/interceptor"]
复制代码

nuxtServerInit

通过在store的根模块中定义 nuxtServerInit 方法,Nuxt.js 调用它的时候会将页面的上下文对象作 为第2个参数传给它。当我们想将服务端的一些数据传到客户端时,这个方法非常好用。

示例代码: 登陆状态初始化,从服务端cookie中拿token
store/index.js

export const actions = {
    nuxtServerInit({commit},{app}){
        const token = app.$cookies.get("token");
        if(token){
            console.log("nuxtServerInit:token:"+token);
            commit("user/init",token)
        }
    }
}
复制代码
  • 安装依赖模块: cookie-universal-nuxt

npm i cookie-universal-nuxt -S

配置,nuxt.config.js

modules:["cookie-universal-nuxt]

  • nuxtServerInit只能写在store/index.js
  • nuxtServerInit仅在服务端执行

发布部署

  • 服务端渲染应用部署: 先进行构建,然后在启动nuxt服务
npm run build
npm start
复制代码

生成内容在.nuxt/dist中

  • 静态应用部署

Nuxt.js 可依据路由配置将应用静态化,使得我们可以将应用部署至任何一个静态站点主机服务商。

npm run generate

注意渲染和接口服务器都需要处于启动状态

生成内容再dist中

分类:
前端