3

服务端渲染

服务器知识:koa、node.js
SSR原理:
*将同⼀个组件渲染为服务器端的 HTML 字符串,将它们直接发送到浏览器,最后将这些静态标记"激
活"为客户端上完全可交互的应⽤程序。*
应⽤场景:

  1. 应⽤需要更好的 SEO
  2. 应⽤需要更快的内容到达时间 (⾸屏渲染时间),特别是对于缓慢的⽹络情况或运⾏缓慢的设备。

ssr的局限:

  1. 更⼤的服务器端负载
  2. 较⾼的学习成本
  3. ⼀些外部扩展库使⽤会受限

nuxt安装
npx create-nuxt-app <项⽬名>
启动
npm run dev

⽬录结构

assets:资源⽬录 assets ⽤于组织未编译的静态资源如 LESS 、 SASS 或 JavaScript 。

components:组件⽬录 components ⽤于组织应⽤的 Vue.js 组件。Nuxt.js 不会扩展增强该⽬录下

Vue.js 组件,即这些组件不会像⻚⾯组件那样有 asyncData ⽅法的特性。

layouts:布局⽬录 layouts ⽤于组织应⽤的布局组件。

middleware: middleware ⽬录⽤于存放应⽤的中间件。

pages:⻚⾯⽬录 pages ⽤于组织应⽤的路由及视图。Nuxt.js 框架读取该⽬录下所有的 .vue ⽂

件并⾃动⽣成对应的路由配置。

plugins:插件⽬录 plugins ⽤于组织那些需要在 根vue.js应⽤ 实例化之前需要运⾏的

Javascript 插件。
static:静态⽂件⽬录 static ⽤于存放应⽤的静态⽂件,此类⽂件不会被 Nuxt.js 调⽤ Webpack 进⾏构建编译处理。 服务器启动的时候,该⽬录下的⽂件会映射⾄应⽤的根路径 / 下。
store: store ⽬录⽤于组织应⽤的 Vuex 状态树 ⽂件。 Nuxt.js 框架集成了 Vuex 状态树 的相关功 能配置,在 store ⽬录下创建⼀个 index.js ⽂件可激活这些配置。
nuxt.confifig.js: nuxt.config.js ⽂件⽤于组织Nuxt.js 应⽤的个性化配置,以便覆盖默认配置。

约定优于配置

约定优于配置
路由:pages⽬录中所有 *.vue ⽂件⽣成应⽤的路由配置
导航:

<nuxt-link to="/">⾸⻚</nuxt-link>

嵌套:制造⼀个.vue⽂件和⽂件夹同名

pages/
--| main/
--| main.vue

动态路由:⽂件名或者⽂件夹名称要带_

pages/
--| main/
-----| detail/
--------| _id.vue

⻚⾯

⾃定义布局:

  1. 创建layouts/main.vue
<template>
    <div>
    <nav>
      <n-link to="/main">index</n-link>
      <!-- 别名 -->
      <n-link to="/main/admin" no-prefetch>管理</n-link>
      
      <N-link to="/main/cart">gouwu</N-link>
    </nav>
    <nuxt-child />
    </div>
</template>

<script>
    export default {
        
    }
</script>

<style lang="scss" scoped>

</style>

pages/login.vue

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

<script>
    export default {
        data(){
            return {
                user:{
                    username:"",
                    password:""
                }
            }
        },
        methods:{
            onLogin(){
                this.$store.dispatch("user/login",this.user).then(ok=>{
                    if(ok){
                        this.$router.push("/")
                    }
                })
            }
        }
    }
</script>

<style lang="scss" scoped>

</style>
  1. ⻚⾯中引⼊pages/main.vue
export default {
  layout: 'main',
};

main/admin.vue

<template>
    <div>
        <h1>商品管理页</h1>
    </div>
</template>

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

<style lang="scss" scoped>

</style>

/main/cart.vue

<template>
    <div>
        <h1>购物车</h1>
    </div>
</template>

<script>
    export default {
        
    }
</script>

<style lang="scss" scoped>

</style>

mian/index.vue

<template>
  <div>
    <h1>首页</h1>
    <ul>
      <li v-for="good in goods" :key="good.id">
        <nuxt-link :to="{name:'main-detail-id',params:{id:good.id}}">
        <span>{{good.text}}</span>
        <span>¥{{good.price}}</span>
        <button @click="addCart(good)">加购物车</button>
        </nuxt-link>
      </li>
    </ul>
  </div>
</template>

<script>


export default {
  head(){//head里面项目
    return{
      title:"列表",
      meta:[{name:"description",hid:"description",content:"set page meta"}],
      link:[{rel:"favicon",href:"favicon.ico"}],
      script:[{src:"https://cdn.jsdelivr.net/npm/jquery/dist/jquery.js"}]
    }
  },
  layout:'users',
  // 在这个组件创建之前发送ajax请求 数据回来之后才开始创建 优先级大于下面的data
  async asyncData({$axios,error,redirect}) {
    //asyncData早于时间点 所以不能用this访问组件实例
    const result = await $axios.$get('/api/goods')
    if(result.ok){
      return {goods: result.goods}
    }
    //错误处理
    error({statusCode: 400,message:"查询数据失败"})
  },
  data(){
    return {
      goods:[
        {id:1,text:'Web',price:4444},
        {id:2,text:'移动',price:5555}
      ]
    }
  }
}
</script>

<style>
.container {
  margin: 0 auto;
  min-height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  text-align: center;
}

.title {
  font-family: 'Quicksand', 'Source Sans Pro', -apple-system, BlinkMacSystemFont,
    'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
  display: block;
  font-weight: 300;
  font-size: 100px;
  color: #35495e;
  letter-spacing: 1px;
}

.subtitle {
  font-weight: 300;
  font-size: 42px;
  color: #526488;
  word-spacing: 5px;
  padding-bottom: 15px;
}

.links {
  padding-top: 15px;
}
</style>

mian/detail/_id.vue

<template>
    <div>
        <h2>商品的详情</h2>
        ${{goodInfo}}
    </div>
</template>

<script>
    export default {
        async asyncData(ctx) {
            console.log(ctx)
            const {$axios,params,error} = ctx
            //不能通过this获取组件实例 但是可以通过上下文获取相关数据
            const result = await $axios.$get("/api/detail",{params})
            if(result.data){
                return {goodInfo: result.data}
            }
            error({statusCode:400,message:"商品详情查询失败"})
        },
    }
</script>

<style lang="scss" scoped>

</style>

异步数据:
asyncData
它在组件创建之前执⾏,⾥⾯不要⽤this访问组件实例
第⼀个参数是上下⽂
可以在服务端也可以客户端都执⾏
asyncData会和data融合

<template>
  <div>
    <h1>首页</h1>
    <ul>
      <li v-for="good in goods" :key="good.id">
        <nuxt-link :to="{name:'main-detail-id',params:{id:good.id}}">
        <span>{{good.text}}</span>
        <span>¥{{good.price}}</span>
        <button @click="addCart(good)">加购物车</button>
        </nuxt-link>
      </li>
    </ul>
  </div>
</template>

<script>


export default {
  head(){//head里面项目
    return{
      title:"列表",
      meta:[{name:"description",hid:"description",content:"set page meta"}],
      link:[{rel:"favicon",href:"favicon.ico"}],
      script:[{src:"https://cdn.jsdelivr.net/npm/jquery/dist/jquery.js"}]
    }
  },
  layout:'users',
  // 在这个组件创建之前发送ajax请求 数据回来之后才开始创建 优先级大于下面的data
  async asyncData({$axios,error,redirect}) {
    //asyncData早于时间点 所以不能用this访问组件实例
    const result = await $axios.$get('/api/goods')
    if(result.ok){
      return {goods: result.goods}
    }
    //错误处理
    error({statusCode: 400,message:"查询数据失败"})
  },
  data(){
    return {
      goods:[
        {id:1,text:'Web',price:4444},
        {id:2,text:'移动',price:5555}
      ]
    }
  }
}
</script>

<style>
.container {
  margin: 0 auto;
  min-height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  text-align: center;
}

.title {
  font-family: 'Quicksand', 'Source Sans Pro', -apple-system, BlinkMacSystemFont,
    'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
  display: block;
  font-weight: 300;
  font-size: 100px;
  color: #35495e;
  letter-spacing: 1px;
}

.subtitle {
  font-weight: 300;
  font-size: 42px;
  color: #526488;
  word-spacing: 5px;
  padding-bottom: 15px;
}

.links {
  padding-top: 15px;
}
</style>

接⼝准备:

  1. 创建server/api.js
const router = require("koa-router")({prefix:"/api"})
const goods = [
    {id:1,text:"web",price:1999},
    {
      id: 2,
      text: "移动",
      price: 199
    },
];
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}
    }
});
module.exports = router

2. 修改服务器配置,server/index.js

const Koa = require('koa')
const consola = require('consola')
const { Nuxt, Builder } = require('nuxt')
// 解析post的数据
const bodyparser = require("koa-bodyparser")
const api = require('./api')
const app = new Koa()
// 设置cookie加密秘钥
app.keys = ["some secret","another secret"]
// Import and Set Nuxt.js options
const 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

  await nuxt.ready()
  // Build in development
  if (config.dev) {
    const builder = new Builder(nuxt)
    await builder.build()
  }
// 解析post数据并注册路由
  app.use(bodyparser())
  app.use(api.routes())
  // 页面渲染
  
  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()

中间件

中间件会在⼀个⻚⾯或⼀组⻚⾯渲染之前运⾏我们定义的函数,常⽤于权限控制、校验等任务。
⾸⻚重定向,创建middleware/index-redirect.js

export default function({route,redirect}){
    //中间件可以获得上下文,里面有各种有用信息
    //常用的有app,route,redirect,store
    if(route.path === '/'){
        redirect('/main')
    }
}

注册中间件,nuxt.config.js

router: {
  middleware: ['index-redirect']
},

插件

Nuxt.js会在运⾏Vue应⽤之前执⾏JS插件,需要引⼊或设置Vue插件、⾃定义模块和第三⽅模块时特
别有⽤。
创建plugins/api-inject.js

// 注入接口 利用插件机制将服务接口注入组件实例,store实例中
export default({$axios},inject) => {
    inject("login",user => {
        return $axios.$post("/api/login",user)
    })
}

注册插件,nuxt.config.js

plugins: [
  "@/plugins/api-inject"
],

vuex

⽤户登录及登录状态保存,创建store/user.js

// 应用根目录如果存在Store目录,Nuxt.js将启用vuex状态数,定义各状态树时具名导出state,mutations,getters.actions即可
export const state = () => ({
    token:''
})
export const mutations = {
    init(state,token){
        state.token = token
    }
}
export const getters = {
    isLogin(state) {
        return !!state.token
    }
}
export const actions = {
    login({commit},u){
        return this.$login(u).then(({token})=>{
            if(token){
                commit("init",token)
            }
            return !!token
        })
    }
}

使⽤状态,创建中间件middleware/auth.js

export default function({redirect,store}){
    console.log("token:" + store.state.user.token)
    //通过vuex中令牌存在与否判断是否登录
    if(!store.state.user.token){
        redirect("/login")
    }

}

注册该中间件,admin.vue

export default {
  middleware: 'auth'
}

状态初始化,store/index.js

export const actions = {
    nuxtServerInit({commit},{req}){
        // 服务端就将vuex状态填充
        // 参数1是vuex的上下文
        // 参数2是nuxt的上下文
        // req.ctx是KOA的上下文  因为在server/index 赋的值
        const token = req.ctx.cookies.get('token')
        if(token){
            console.log("初始化token")
            console.log(token)
            commit('user/init',token)
        }
    }
}

nuxt.config.js


module.exports = {
  mode: 'universal',
  /*
  ** Headers of the page
  */
  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' }
    ]
  },
  /*
  ** Customize the progress-bar color
  */
  loading: { color: '#fff' },
  /*
  ** Global CSS
  */
  css: [
    'element-ui/lib/theme-chalk/index.css'
  ],
  //路由配置
  router: {
    // 顺序从前往后执行
    middleware: ['index-redirect']
  },
  /*
  ** Plugins to load before mounting the App
  */
  plugins: [
    '@/plugins/element-ui',
    '@/plugins/api-inject',
    {src:"@/plugins.axios",mode:"client"}//仅客户端执行
  ],
  /*
  ** Nuxt.js dev-modules
  */
  buildModules: [
  ],
  /*
  ** Nuxt.js modules
  */
  modules: ['@nuxtjs/axios'],
  // axios: {
  //   proxy: true
  // },
  // proxy: {
  //   "api": "http://localhost:8080"
  // },
  /*
  ** Build configuration
  */
  build: {
    transpile: [/^element-ui/],
    /*
    ** You can extend webpack config here
    */
    extend (config, ctx) {
    }
  }
}

pluygins/axios.js

export default function({$axios}){
    // 利用$axios模快幫忙方法setToken設置全局請求頭
    // 此處省略token截取邏輯
    $axios.setToken(document.cookie,'Bearer')
}

布局

整体布局 默认的
layouts/default.vue

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

<style>
html {
  font-family: 'Source Sans Pro', -apple-system, BlinkMacSystemFont, 'Segoe UI',
    Roboto, 'Helvetica Neue', Arial, sans-serif;
  font-size: 16px;
  word-spacing: 1px;
  -ms-text-size-adjust: 100%;
  -webkit-text-size-adjust: 100%;
  -moz-osx-font-smoothing: grayscale;
  -webkit-font-smoothing: antialiased;
  box-sizing: border-box;
}

*,
*:before,
*:after {
  box-sizing: border-box;
  margin: 0;
}

.button--green {
  display: inline-block;
  border-radius: 4px;
  border: 1px solid #3b8070;
  color: #3b8070;
  text-decoration: none;
  padding: 10px 30px;
}

.button--green:hover {
  color: #fff;
  background-color: #3b8070;
}

.button--grey {
  display: inline-block;
  border-radius: 4px;
  border: 1px solid #35495e;
  color: #35495e;
  text-decoration: none;
  padding: 10px 30px;
  margin-left: 15px;
}

.button--grey:hover {
  color: #fff;
  background-color: #35495e;
}
</style>

自定义布局
layouts/error.vue

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

<script>
    export default {
        props:['error']
    }
</script>

<style lang="scss" scoped>

</style>

/layouts/users.vue

<template>
<div>
    <h1>自定义布局</h1>
    <nuxt></nuxt>
</div>
</template>

image.png

服务端渲染应⽤部署

npm run build

npm start

静态应⽤部署

准备⼯作: 将接⼝服务器独⽴出来

// api.js

const Koa = require("koa");

const bodyparser = require("koa-bodyparser");

const router = require("koa-router")({ prefix: "/api" });

const app = new Koa();

// 设置cookie加密秘钥

app.keys = ["some secret", "another secret"];

const goods = [

{ id: 1, text: "Web全栈架构师", price: 1000 },

{ id: 2, text: "Python架构师", price: 1000 }

];

// /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 };

} }); 
app.use(bodyparser()); 
app.use(router.routes()); 
app.listen(8080);

// 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()

启动接⼝服务器api.js
代理接⼝,nuxt.confifig.js

axios: {

proxy: true }, proxy: {

"/api": "http://localhost:8080" },

⽣成应⽤的静态⽬录和⽂件

配置服务器默认端⼝,package.json

"scripts": {

"generate": "cross-env PORT=80 nuxt generate" }

执⾏⽣成命令

npm run generate

nginx配置

server {

listen 80;

server\_name localhost;

\# 静态⽂件服务

location / {

root C:\\\\Users\\\\yt037\\\\Desktop\\\\kaikeba\\\\projects\\\\nuxttest\\\\dist;

index index.html;

}

#nginx反向代理,实现接⼝转发

location ^~ /api/ proxy\_pass http://localhost:3000; #注意路径后边不要加/

} }

HappyCodingTop
526 声望847 粉丝

Talk is cheap, show the code!!