theme: juejin
小知识,大挑战!本文正在参与「程序员必备小知识」创作活动。
本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。
首先带大家认识什么是服务端渲染以及常见的前端服务端渲染框架,然后再围绕服务端渲染如何与 vue 搭配使用,大家一起来看看吧。
什么是服务端渲染
简单理解是将组件或页面通过服务器生成html字符串,再发送到浏览器,最后将静态标记"混合"为客户端上完全交互的应用程序。
- 传统
PHP、JSP、ASP
的模版渲染 - 前端
Express、Koa、Egg
等基于Node.js
的MVC
框架 React、Vue
的同构框架
关于Express & Koa & Egg
Express
– 基于 Node.js 平台,快速、开放、极简的 Web 开发框架Koa
– 基于 Node.js 平台的下一代 web 开发框架Egg
– 为企业级框架和应用而生
快速上手Koa2
koa2
与koa1
的最大区别是koa2
实现异步是通过async/await
,koa1
实现异步是通过generator/yield
,而express
实现异步是通过回调函数的方式。
学习目标:
- 快速搭建服务
- 理解洋葱模型
koa+pug
服务端渲染的简单实现
app.use(views(resolve(__dirname,'../views'), {
extension: 'pug'
}));
我们的服务端渲染实践
- 从
Express + Jade
框架开始前后端分离。 - 从服务端渲染转到浏览器渲染(
Vue
)框架。
Express
转Vue
的几点原因:
Express
比较吃服务器Express
项目前端页面与服务端耦合度高,服务挂掉会影响全局Express
开发调试不方便,数据问题排除依赖日志Vue
组件化效果更好,数据驱动
服务端渲染完败???
客户端渲染(CSR)的不足
1.SEO能力不足
虽然Google开始通过爬虫技术直接从js里面抓取关键字,但是实际效果依然与使用SSR页面有着明显的差距。
国外一个技术博客Spectrum
的开发者之一Max Stoiber
,也是 styled-components
的作者,在分享网站搭建历程时说,后悔刚开始没有采用Next.js
。他们在九月引入SSR后,网站收录有了质的影响。
2.首次加载白屏,加载慢
目前在我们项目中也有类似的问题,打包的主js比较大,纯浏览器渲染需要等待js加载完之后,才开始页面渲染。
哔哩哔哩(B站)的前端之路 - Crazy-吴俊毅的文章 - 知乎地址
如何优化
- 使用
vue-server-render
, 官方Vue-SSR指南 - 使用
prerender-spa-plugin
预渲染,Git地址
// Basic Usage
const path = require('path')
const PrerenderSPAPlugin = require('prerender-spa-plugin')
module.exports = {
plugins: [
...
new PrerenderSPAPlugin({
// Required - The path to the webpack-outputted app to prerender.
staticDir: path.join(__dirname, 'dist'),
// Required - Routes to render.
routes: [ '/', '/about', '/some/deep/nested/route' ],
})
]
}
- 同构渲染
同构渲染之Nuxt
来自Vue官网大评价: 从头搭建一个服务端渲染的应用是相当复杂的。幸运的是,我们有一个优秀的社区项目 Nuxt.js 让这一切变得非常简单。Nuxt 是一个基于 Vue 生态的更高层的框架,为开发服务端渲染的 Vue 应用提供了极其便利的开发体验。更酷的是,你甚至可以用它来做为静态站生成器。推荐尝试。
附:nuxt中文文档
实战项目分析:Nuxt + ZanUI
如何新增插件
- 使用第三方模块
npm install --save axios
<template>
<h1>{{ title }}</h1>
</template>
<script>
import axios from 'axios'
export default {
async asyncData({ params }) {
let { data } = await axios.get(`https://my-api/posts/${params.id}`)
return { title: data.title }
}
}
</script>
- 直接使用链接
head: {
link: [
{
rel: 'stylesheet',
href: '//at.alicdn.com/t/font_1985456_2ushr2u1y69.css',
},
],
script: [
{src: 'https://webapi.amap.com/maps', async: true},
],
},
- 使用 Vue 插件
~/plugins/ui.js
import Vue from 'vue'
import Element from 'element-ui'
import locale from 'element-ui/lib/locale/lang/zh-CN'
Vue.use(Element, { locale })
~/nuxt.config.js
plugins: [
{ src: '~/plugins/ui.js', ssr: true },
],
- 注入 Vue 实例
~/plugins/vue-inject.js
import Vue from 'vue'
Vue.prototype.$myInjectedFunction = string =>
console.log('This is an example', string)
~/nuxt.config.js
export default {
plugins: ['~/plugins/vue-inject.js']
}
- 注入 context
~/plugins/ctx-inject.js
export default ({ app }, inject) => {
// Set the function directly on the context.app object
app.myInjectedFunction = string =>
console.log('Okay, another function', string)
}
~/nuxt.config.js
export default {
plugins: ['~/plugins/ctx-inject.js']
}
- 同时注入
// 引入@nuxtjs/axios 添加到 module
...
// 注册服务
const extendAxiosInstance = axios => {
const axiosModules = <%= serialize(options.axios.modules) %>
if (axiosModules) {
const keys = Object.keys(axiosModules);
for (let module of keys) {
// Request helpers ($get, $post, ...)
axios['$' + module] = {}
for (let method of ['request', 'delete', 'get', 'head', 'options', 'post', 'put', 'patch']) {
axios['$' + module][method] = function() {
if (arguments.length > 0) {
...
}
return axios[method].apply(axios, arguments)
}
}
}
}
}
function AxiosFunction(content) {
const indexAxios = content.app.$axios;
extendAxiosInstance(indexAxios)
...
// 请求拦截
indexAxios.interceptors.request.use(function(config) {
...
return config;
});
// 响应拦截器
indexAxios.interceptors.response.use(response => new Promise((resolve, reject) => {
....
resolve(data.data);
}), error => {
//关闭加载
close(error.config);
const resultData = {}
...
return Promise.reject(resultData)
});
// 自定义拦截器
const interceptors = <%= serialize(options.axios.interceptors) %>
if (interceptors) {
interceptors.forEach(config => {
...
})
}
return indexAxios;
}
// 同时注入
export default (content, inject) => {
inject('http', AxiosFunction(content))
}
服务端获取数据
asyncData ({isDev, route, store, env, params, query, req, res, redirect, error}) {
const detailDate = await $http.get(parkProductDetail + params.id);
const picLab = detailDate.wapPictureLib;
if (detailDate.flashUrl) {
picLab.unshift({
type: 'video',
url: detailDate.flashUrl,
cover: ' ',
});
}
return { parkInfo: detailDate, parkId: params.id, picLab };
fetch({ store, params }) {
const detailDate = await $http.get(parkProductDetail + params.id);
store.commit('setDetail', detailDate)
})
Store的使用
~/store/mdse.js
import vm from 'vue';
export const state = () => ({
orderInfo: {}, // 缓存下单数据
});
export const getters = {
getOrderInfo(_state) {
return _state.orderInfo;
},
};
export const mutations = {
// 更新下单信息
updateOrderInfo(_state, info) {
vm.set(_state, 'orderInfo', info);
},
};
node中间件
~/middleware/auth.js
/**
* 拦截登录
*/
export default async ({ store, route: { path, fullPath }, redirect }) => {
// 商户端
if (path.startsWith('/merchant')) {
const ignoreMerchantCatchPath = [
'/merchant',
....
];
if (
!ignoreMerchantCatchPath.includes(path) &&
!store.getters['user/isMerchantLogin']
) {
redirect(`/merchant/login/shop?redirect=${encodeURIComponent(fullPath)}`);
return;
}
return;
} else {
await store.dispatch('getProjectInfo');
// 核销二维码地址
if (path.indexOf('/t/') >= 0) {
return;
}
// 忽略校验登录地址
const ignoreCatchPath = [
'/',
'/login',
'/login/registry',
....
];
if (
!(
ignoreCatchPath.includes(path) ||
path.indexOf('/detail/') !== -1 ||
path.indexOf('/group/') !== -1
) &&
!store.getters['user/isLogin']
) {
redirect(`/login?redirect=${encodeURIComponent(fullPath)}`);
return;
}
}
};
打包与部署
npm install
npm run build
pm2 start ./ecosystem.config.js
~/ecosystem.config.js
module.exports = {
apps: [
{
name: 'cqyjy-mall-mobile-ui',
script: 'npm start',
log_date_format: 'YYYY-MM-DD HH:mm:ss',
error_file: './logs/app-err.log',
out_file: './logs/app-out.log',
},
],
};
版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 作者: ITEM 原文链接:https://juejin.im/post/7025164660751990798