一、概述
SPA单页面应用优点
- 用户体验好
- 开发效率高
- 渲染性能好
- 可维护性好
SPA单页面应用缺点
- 首屏渲染时间长
- 不利于SEO
最早期,Web 页面渲染都是在服务端完成的,即服务端运行过程中将所需的数据结合页面模板渲染为HTML,响应给客户端浏览器。所以浏览器呈现出来的是直接包含内容的页面。
在今天看来,这种渲染模式是不合理或者说不先进的。因为在当下这种网页越来越复杂的情况下,这种
模式存在很多明显的不足:
- 应用的前后端部分完全耦合在一起,在前后端协同开发方面会有非常大的阻力;
- 前端没有足够的发挥空间,无法充分利用现在前端生态下的一些更优秀的方案;
- 由于内容都是在服务端动态生成的,所以服务端的压力较大;
- 相比目前流行的 SPA 应用来说,用户体验一般;
同构应用
- 通过服务端渲染首屏直出,解决首屏渲染慢以及不利于 SEO 问题
- 通过客户端渲染接管页面内容交互得到更好的用户体验
- 这种方式通常称之为现代化的服务端渲染,也叫同构渲染
- 这种方式构建的应用称之为服务端渲染应用或者是同构应用。
二、什么是渲染
我们这里所说的渲染指的是把(数据 + 模板)拼接到一起的这个事儿。
例如对于我们前端开发者来说最常见的一种场景就是:请求后端接口数据,然后将数据通过模板绑定语
法绑定到页面中,最终呈现给用户。这个过程就是我们这里所指的渲染。
渲染本质其实就是字符串的解析替换,实现方式有很多种;但是我们这里要关注的并不是如何渲染,而
是在哪里渲染的问题?
三、传统的服务端渲染
server.js
const express = require('express')
const fs = require('fs')
const template = require('art-template')
const app = express()
app.get('/', (req, res) => {
// 1. 获取页面模板
const templateStr = fs.readFileSync('./index.html', 'utf-8')
// 2. 获取数据
const data = JSON.parse(fs.readFileSync('./data.json', 'utf-8'))
// 3. 渲染:数据 + 模板 = 最终结果
const html = template.render(templateStr, data)
// 4. 把渲染结果发送给客户端
res.send(html)
})
app.get('/about', (req, res) => {
res.end(fs.readFileSync('./about.html'))
})
app.listen(3000, () => console.log('running...'))
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>传统的服务端渲染</title>
</head>
<body>
<h1>传统的服务端渲染</h1>
<h2>{{ title }}</h2>
<ul>
{{ each posts }}
<li>{{ $value.title }}</li>
{{ /each }}
</ul>
</body>
</html>
在今天看来,这种渲染模式是不合理或者说不先进的。因为在当下这种网页越来越复杂的情况下,这种模式存在很多明显的不足:
- 应用的前后端部分完全耦合在一起,在前后端协同开发方面会有非常大的阻力;
- 前端没有足够的发挥空间,无法充分利用现在前端生态下的一些更优秀的方案;
- 由于内容都是在服务端动态生成的,所以服务端的压力较大;
- 相比目前流行的 SPA 应用来说,用户体验一般;
四、客户端渲染
传统的服务端渲染有很多问题,但是这些问题随着客户端 Ajax 技术的普及得到了有效的解决,Ajax 技术可以使得客户端动态获取数据变为可能,也就是说原本服务端渲染这件事儿也可以拿到客户端做了。
【后端】负责数据处理
【前端】负责页面渲染
【前端】更为独立,不再受限制于【后端】
但是存在不足:
- 首屏渲染慢:因为 HTML 中没有内容,必须等到 JavaScript 加载并执行完成才能呈现页面内容。
- SEO 问题:同样因为 HTML 中没有内容,所以对于目前的搜索引擎爬虫来说,页面中没有任何有用的信息,自然无法提取关键词,进行索引了。
五、现代化的服务端渲染
同构渲染:【服务端渲染】 + 【客户端渲染】
- 首屏渲染慢
- 不利于 SEO
解决办法就是将客户端渲染的工作放到服务端渲染,严格来讲应该叫现代化的服务端渲染,也叫
同构渲染,也就是【服务端渲染】 + 【客户端渲染】。
- isomorphic web apps(同构应用):isomorphic/universal,基于 react、vue 框架,客户端渲染和服务器端渲染的结合
(1)在服务器端执行一次,用于实现服务器端渲染(首屏直出)
(2)在客户端再执行一次,用于接管页面交互 - 核心解决 SEO 和首屏渲染慢的问题。
- 拥有传统服务端渲染的优点,也有客户端渲染的优点
流程:
- 客户端发起请求
- 服务端渲染首屏内容 + 生成客户端 SPA 相关资源
- 服务端将生成的首屏资源发送给客户端
- 客户端直接展示服务端渲染好的首屏内容
- 首屏中的 SPA 相关资源执行之后会激活客户端 Vue
- 之后客户端所有的交互都由客户端 SPA 处理
优点:首屏渲染速度快、有利于 SEO
缺点:
- 开发成本高。
- 涉及构建设置和部署的更多要求。与可以部署在任何静态文件服务器上的完全静态单页面应用程序 (SPA) 不同,服务器渲染应用程序,需要处于 Node.js server 运行环境。
- 更多的服务器端负载。在 Node.js 中渲染完整的应用程序,显然会比仅仅提供静态文件的server 更加大量占用 CPU 资源 (CPU-intensive - CPU 密集),因此如果你预料在高流量环境(high traffic) 下使用,请准备相应的服务器负载,并明智地采用缓存策略
如何实现同构渲染?
使用Vue、React等框架的官方解决方案
- 优点:有助于理解原理
- 缺点:需要搭建环境,比较麻烦
使用第三方解决方案
- React生态的Next.js
- Vue生态的Nuxt.js
六、演示
npm i nuxt --dev
pages----->index.vue
<<template>
<div id='app'>
<h1>home</h1>
</div>
</template>
<script>
export default {
}
</script>
<style scoped>
</style>
"scripts": {
"dev":"nuxt"
},
七、同构渲染应用的问题
-
开发条件所限
- 开发条件所限。浏览器特定的代码,只能在某些生命周期钩子函数 (lifecycle hook) 中使用。
- 一些外部扩展库 (external library) 可能需要特殊处理,才能在服务器渲染应用程序中运行。
- 不能在服务端渲染期间操作DOM
- 某些代码操作需要区分运行环境
-
涉及构建设置和部署的更多要求
-
更多的服务端负载
- 在 Node.js 中渲染完整的应用程序,显然会比仅仅提供静态文件的 server更加大量占用 CPU 资源
- 因此如果你预料在高流量环境 (high traffic)下使用,请准备相应的服务器负载
- 需要更多地服务端渲染优化工作处理
服务端渲染使用建议
- 首屏渲染速度是否真的重要?
- 是否真的需要SEO?
八、Nuxt.js是什么
- 一个基于Vue.js生态的第三方开源服务端渲染应用框架
- 它可以帮我们轻松使用Vue.js技术栈构建同构应用
- 官网:https://zh.nuxtjs.org/
- GitHub仓库:https://github.com/nuxt/nuxt.js
九、Nuxt.js使用方式
- 初始化项目
- 已有的Node.js服务端项目
- 直接把Nuxt当作一个中间件集成到Node Web Server中
- 现有的Vue.js项目
- 非常熟悉Nuxt.js
- 至少百分之10的代码改动
十、 路由-基本路由
Nuxt.js 依据 pages 目录结构自动生成 vue-router 模块的路由配置。
假设 pages
的目录结构如下:
pages/
--| user/
-----| index.vue
-----| one.vue
--| index.vue
那么,Nuxt.js 自动生成的路由配置如下:
router: {
routes: [
{
name: 'index',
path: '/',
component: 'pages/index.vue'
},
{
name: 'user',
path: '/user',
component: 'pages/user/index.vue'
},
{
name: 'user-one',
path: '/user/one',
component: 'pages/user/one.vue'
}
]
}
十一、路由导航
- a 标签 :会刷新整个页面,不要使用
- 组件
- 编程式导航
<template>
<div id='app'>
<h1>about页面</h1>
<!-- a标签跳转 会刷新页面,不要使用 -->
<h3>a标签跳转</h3>
<a href="/">去首页</a>
<!-- router-link跳转 -->
<h3>router-link</h3>
<router-link to="/">去首页</router-link>
<!-- 编程式导航 -->
<h3>编程式导航</h3>
<button @click="onClick">点我跳转</button>
</div>
</template>
<script>
export default {
methods:{
onClick(){
console.log("123")
this.$router.push("/")
}
}
}
</script>
<style scoped>
</style>
十二、动态路由
在 Nuxt.js 里面定义带参数的动态路由,需要创建对应的以下划线作为前缀的 Vue 文件 或 目录。
pages/
--| _slug/
-----| comments.vue
-----| index.vue
--| users/
-----| _id.vue
--| index.vue
Nuxt.js 生成对应的路由配置表为:
router: {
routes: [
{
name: 'index',
path: '/',
component: 'pages/index.vue'
},
{
name: 'users-id',
path: '/users/:id?',
component: 'pages/users/_id.vue'
},
{
name: 'slug',
path: '/:slug',
component: 'pages/_slug/index.vue'
},
{
name: 'slug-comments',
path: '/:slug/comments',
component: 'pages/_slug/comments.vue'
}
]
}
你会发现名称为 users-id 的路由路径带有 :id? 参数,?表示该路由是可选的。如果你想将它设置为必选的路由,需要在 users/_id 目录内创建一个 index.vue 文件。
十三、嵌套路由
创建内嵌子路由,你需要添加一个 Vue 文件,同时添加一个与该文件同名的目录用来存放子视图组件。
Warning: 别忘了在父组件(.vue文件
) 内增加 <nuxt-child/>
用于显示子视图内容。
pages/
--| users/
-----| _id.vue
-----| index.vue
--| users.vue
router: {
routes: [
{
path: '/users',
component: 'pages/users.vue',
children: [
{
path: '',
component: 'pages/users/index.vue',
name: 'users'
},
{
path: ':id',
component: 'pages/users/_id.vue',
name: 'users-id'
}
]
}
]
}
十四、动态路由
创建配置文件,nuxt.config.js
module.exports = {
router: {
//配置根路径
base: '/abc',
//routes:一个数组,路由配置表
//resolve:解析路由组件路径
extendRoutes(routes,resolve){
routes.push({
path: '/test',
name: 'test',
component: resolve(__dirname, 'pages/about.vue')
})
}
}
}
/**
* Nuxt.js 配置文件
*/
module.exports = {
router: {
linkActiveClass: 'active',
// 自定义路由表规则
extendRoutes (routes, resolve) {
// 清除 Nuxt.js 基于 pages 目录默认生成的路由表规则
// 这里不能 routes = [],赋值操作 引用就断掉了,形参传进来的是个数组引用
//你是想清空 routes 数组的值,而不是想让 routes 重新指向空数组,
//你是想让房间里的东西全部扔掉,还是想让房间钥匙恢复出厂设置
routes.splice(0)
//
routes.push(...[
{
path: '/',
component: resolve(__dirname, 'pages/layout/'),
children: [
{
path: '', // 默认子路由
name: 'home',
component: resolve(__dirname, 'pages/home/')
},
{
path: '/login',
name: 'login',
component: resolve(__dirname, 'pages/login/')
},
{
path: '/register',
name: 'register',
component: resolve(__dirname, 'pages/login/')
}
]
}
])
}
},
server: {
host: '0.0.0.0',
port: 3000
},
// 注册插件
plugins: [
'~/plugins/request.js',
'~/plugins/dayjs.js'
]
}
十五、异步数据-asyncData方法
-
基本用法
- 它会将asyncData返回的数据融合组件data方法返回数据一并给组件
- 调用时机:服务端渲染期间和客户端路由更新之前
-
注意事项
- 只能再页面组件中使用(子组件不能使用)
- 没有this,因为他是再组件初始化之前被调用的
<template>
<div id="app">
<h2>{{ title }}</h2>
<ul>
<li
v-for="item in posts"
:key="item.id"
>{{ item.title }}</li>
</ul>
</div>
</template>
async asyncData () {
const { data } = await axios({
method: 'GET',
url: 'http://localhost:3000/data.json'
})
// 这里返回的数据会和 data () {} 中的数据合并到一起给页面使用
return data
}
十六、异步数据-上下文对象
通过参数context获取id, 不能通过this.$route.params,因为获取不到this
async asyncData (context) {
console.log(context)
const { data } = await axios({
method: 'GET',
url: 'http://localhost:3000/data.json'
})
const id = Number.parseInt(context.params.id)
return {
article: data.posts.find(item => item.id === id)
}
}
}
十七、Nuxtjs综合案例
- GitHub仓库:https://github.com/gothinkster/realworld
- 在线示例:https://demo.realworld.io/#/
- 接口文档:https://github.com/gothinkster/realworld/tree/master/api
- 页面模板:https://github.com/gothinkster/realworld-starter-kit/blob/master/FRONTEND_INSTRUCTIONS.md
- 免费的cdn网站:https://www.jsdelivr.com/
请求数据在这里
nuxtServerInit 是一个特殊的 action 方法,这个 action 会在服务端渲染期间自动调用。作用:初始化容器数据,传递数据给客户端使用
store
export const actions = {
// nuxtServerInit 是一个特殊的 action 方法
// 这个 action 会在服务端渲染期间自动调用
// 作用:初始化容器数据,传递数据给客户端使用
nuxtServerInit ({ commit }, { req }) {
let user = null
// 如果请求头中有 Cookie
if (req.headers.cookie) {
// 使用 cookieparser 把 cookie 字符串转为 JavaScript 对象
const parsed = cookieparser.parse(req.headers.cookie)
try {
user = JSON.parse(parsed.user)
} catch (err) {
// No valid cookie found
}
}
// 提交 mutation 修改 state 状态
commit('setUser', user)
}
}
权限问题中间件
在受保护的组件里直接使用就可,
watchQuery API分页 监听路由参数变化, nuxtjs里面 路由参数query的变化默认是不会差法asyncData方法的
统一设置用户Token
用插件,在nuxt.config.js中注册
过滤器
https://www.cnblogs.com/yesyes/p/7977161.html
文章详情-把Markdown转为HTML
http://markdown-it.docschina.org/api/ParserInline.html#parserinline
文章详情-页面设置meta优化SEO
meta标签里面有个content属性
打包
npm run build 生成.nuxt文件 npm run start可以运行打包后的文件
十八、发布部署
最简单的部署方式
- 配置Host + Post
- 压缩发布包
- 把发布包传到服务器
- 解压
- 安装依赖
- 启动服务
1、配置一下这里
不能用localhost,localhost只能本机访问
2、把这几个文件提取出来压缩一下(带上pages和components 不然报错) 或者把所有项目部署上去
3、连接服务端
ssh root@39.105.28.5
4、创建服务端目录
mkdir realword-nuxtjs
5、进入目录
cd realword-nuxtjs/
6、pwd 打印一下路径
7、exit 退出服务端
8、把压缩包 传到远程服务器目录下
9、再连接服务器
ssh root@39.105.28.5
10、进入目录
cd realword-nuxtjs/
11、ls看到列表
12、解压压缩包
unzip realword-nuxtjs.zip
13、ls -a看到隐藏目录
14、 第三方包安装下
npm i
15、启动
npm run start
十九、发布部署-使用PM2启动Node服务
当我们退出服务的时候,程序也就停止了,这是需要一个程序去维护
pm2就是把程序运行在后台,使其保持运行状态
- Github仓库地址:https://github.com/Unitech/pm2
- 官方文档:https://pm2.io/
- 安装:npm install --global pm2
- 启动:pm2 start 脚本路径
1、在服务端项目目录下安装
npm install --global pm2
2、启动
pm2 start npm --start(pm2 start pm2.config.json) (要等待两分钟 资源才能加载出出来)
3、关闭服务npm stop + id
npm stop 6
4、常用命令
pm2 kill
杀死所有进程
调试案例:
https://blog.csdn.net/qq_39905409/article/details/91357691?utm_medium=distribute.pc_aggpage_search_result.none-task-blog-2aggregatepagefirst_rank_ecpm_v1~rank_v31_ecpm-2-91357691.pc_agg_new_rank&utm_term=pm2%E5%90%AF%E5%8A%A8%E7%9A%84node%E9%A1%B9%E7%9B%AE%E8%AE%BF%E9%97%AE%E4%B8%8D%E4%BA%86&spm=1000.2123.3001.4430
二十、传统的部署方式
-
更新
- 本地构建
- 发布
-
更新
- 本地构建
- 发布
-
…
现代化的部署方式 (CI / CD)
二十一、发布部署-准备自动部署内容
使用GitHub Actions实现自动化部署
CI / CD服务
- Jenkins
- GitLab CI
GitHub Actions
- Travis CI
- Circle CI
- …
环境准备
- Linux服务器
- 把代码提交到GitHub远程服务器
配置GitHub Access Token
- 生成:https://github.com/settings/tokens
点击setting 找到
点击
只需要勾选第一个就行
生成的token只显示一次 要复制出来
- 配置到项目的Secrets中:https://github.com/lipengzhou/realworld-nuxtjs/settings/secrets
在远程仓库里面点击setting
点进去 点击secrets
配置GitHub Actions执行脚本
- 在项目根目录下创建 .github/workflows目录
- 下载main.yml到workflows目录中
https://gist.github.com/lipengzhou/b92f80142afa37aea397da47366bd872
name: Publish And Deploy Demo
on:
push:
tags:
- 'v*'
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
# 下载源码
- name: Checkout
uses: actions/checkout@master
# 打包构建
- name: Build
uses: actions/setup-node@master
- run: npm install
- run: npm run build
- run: tar -zcvf release.tgz .nuxt static nuxt.config.js package.json package-lock.json pm2.config.json
# 发布 Release
- name: Create Release
id: create_release
uses: actions/create-release@master
env:
GITHUB_TOKEN: ${{ secrets.TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: Release ${{ github.ref }}
draft: false
prerelease: false
# 上传构建结果到 Release
- name: Upload Release Asset
id: upload-release-asset
uses: actions/upload-release-asset@master
env:
GITHUB_TOKEN: ${{ secrets.TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./release.tgz
asset_name: release.tgz
asset_content_type: application/x-tgz
# 部署到服务器
- name: Deploy
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USERNAME }}
password: ${{ secrets.PASSWORD }}
port: ${{ secrets.PORT }}
script: |
cd /root/realworld-nuxtjs
wget https://github.com/lipengzhou/realworld-nuxtjs/releases/latest/download/release.tgz -O release.tgz
tar zxvf release.tgz
npm install --production
pm2 reload pm2.config.json
- 修改配置
这里要注意
然后配置这些
- 配置PM2配置文件
在根目录创建pm2.config.json
{
"apps": [
{
"name": "RealWorld",
"script": "npm",
"args": "start"
}
]
}
- 提交更新
- 查看自动部署
- 访问网站
- 提交更新…
二十二、发布部署-自动部署完成
首先确保仓库有了最基本的代码提交
git add .
git tag v0.1.0
git push origin v0.1.0
然后查看tags
然后查看Action
如果我们代码有更新
继续
git add.
git commit -m"长威"
git push
git tag v0.1.1
git push origin v0.1.1