服务端渲染
服务器知识:koa、node.js
SSR原理:
*将同⼀个组件渲染为服务器端的 HTML 字符串,将它们直接发送到浏览器,最后将这些静态标记"激
活"为客户端上完全可交互的应⽤程序。*
应⽤场景:
- 应⽤需要更好的 SEO
- 应⽤需要更快的内容到达时间 (⾸屏渲染时间),特别是对于缓慢的⽹络情况或运⾏缓慢的设备。
ssr的局限:
- 更⼤的服务器端负载
- 较⾼的学习成本
- ⼀些外部扩展库使⽤会受限
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
⻚⾯
⾃定义布局:
- 创建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>
- ⻚⾯中引⼊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>
接⼝准备:
- 创建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>
服务端渲染应⽤部署
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; #注意路径后边不要加/
} }
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。