- 本阶段围绕当下国内最主流的前端核心框架 Vue.js 展开,深入框架内部,通过解读源码或者手写实现的方式,剖析 Vue.js 框架的内部实现原理,让你做到知其所以然。同时我们还会介绍 Vue.js 的进阶用法、周边生态以及性能优化,让你轻松应对更加复杂的项目业务需求。
模块三 Vuex 数据流管理及Vue.js 服务端渲染(SSR)
- 本模块会介绍关于复杂项目中的状态管理方案 Vuex,以及自己来手写一个自己的 Vuex。Vue.js 中服务端渲染的使用尽在这里,本模块会先告诉你为什么要使用 SSR,紧接着带你使用 SSR 框架 Nuxt.js 快速开发一个服务端渲染的项目。
任务一:Vuex 状态管理
- 课程目标
- Vue 组件间通信方式回顾
- Vuex 核心概念和基本使用回顾
- 购物车案例
- 模拟实现 Vuex
- 组件内的状态管理流程
- state 驱动应用的数据源
- view 以声明方式将state映射到视图
- actions 相应在view 上的用户输入导致的状态改变
- 组件间通信方式回顾-父组件给子组件传值
- 父组件给子组件穿值
- 子组件中通过props接收数据
- 父组件中给子组件通过相应属性传值
-
组件间通信方式回顾-子组件给父组件传值
-
组件间通信方式回顾-不相关组件传值
// eventbus.js
import Vue from 'vue'
export default new Vue()import bus from './eventbus.js'
// bus.$emit() bus.$on()
- 组件间通信方式回顾-通过 ref 获取子组件
- 其它常见方式
- $root
- $parent
- $children
- $refs
- 在普通标签上使用ref,获取的是DOM
- 在组件标签上使用ref,获取到的是组件实例
- 简易的状态管理方案
- 问题
- 多个视图依赖同一状态
- 来自不同视图的行为需要变更同一状态
- Vuex 概念回顾
- 什么是Vuex
- Vuex 是专门为Vue.js设计的状态管理库
- Vuex 采用集中式的方式存储需要共享的状态
- Vuex 的作用是进行状态管理,解决复杂组件通信,数据共享
- Vuex 集成到了 devtools 中,提供了time-travel时光旅行历史回滚功能
- 什么情况下使用Vuex
- 非必要的情况不要使用Vuex
- 大型的单页应用程序
- 多个视图依赖同一状态
- 来自不同视图的行为需要变更同一状态
- Vuex 的核心概念
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TDQu1gVm-1627096149838)(./img/1626770990920.jpg)]
- Store 每一个应用仅有一个Store,Store是一个容器,包含应用中的大部分状态,不能直接改变Store中的状态
- State 就是单一状态树,所有的状态都保存在State,会让程序难以维护,可以通过模块解决该问题,这里的状态是响应式的
- Getter 就像Vue中的计算属性,方便从一个属性派生出其它值,它内部可以对计算结果进行缓存,只有当依赖的状态发生改变才重新计算
- Mutation 状态的变化必须提交Mutation完成
- Action 和Mutation类似,不同的是,Action可以进行异步操作,内部改变状态的时候都需要提交 Mutation
- Module 由于使用单一状态树,应该的状态会集中在一个对象上,当应用复杂时,Store对象就可以变得非常臃肿,为了解决这个问题,Vuex 允许我们将 Store 分为模块,每个模块拥有直接的State,Mutation,Action,Getter
- Vuex 的核心概念
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)export default new Vuex.Store({state: {},mutations: {},actions: {},modules: {}
})import store from './store'
new Vue({router,store,render: h => h(App)
}).$mount('#app')
- State
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BQy1pKew-1627096149839)(./img/1626772526939.jpg)]
- Getter
<!-- {{ $store.getters.reverseMsg }} -->
{{reverseMsg}}
export default new Vuex.Store({state: {msg: 'abc'},mutations: {},getters: {reverseMsg(state) {return state.msg.split('').reverse().join('')}},actions: {},modules: {}
})import { mapGetters } from 'vuex'
export default {computed: {...mapGetters(['reverseMsg'])}
}
- Mutation
<!-- <button @click="$store.commit('increate', 2)"></button> -->
<!-- <button @click="increate(2)"></button> --><!-- <button @click="$store.dispatch('increateAsync', 5)"></button> --><button @click="increateAsync(6)">Action</button>
export default new Vuex.Store({state: {count: 0,msg: 'abc'},mutations: {increate(state, payload) {state.count += payload}},getters: {reverseMsg(state) {return state.msg.split('').reverse().join('')}},actions: {increateAsync(context, payload) {setTimeout(() => {context.commit('increate', payload)}, 2000)}},modules: {}
})import { mapMutations } from 'vuex'
export default {methods: {...mapMutations(['increate']),...mapActions(['increateAsync'])}
}
-
Action
-
Module
```html
<!-- {{ $store.state.cart.cart }} -->
{{ cart }}
```
```js
// cart.js
const state = {}
const getters = {}export default {namespaced: true, // 开启命名空间state: {cart: 2},getters
}// index.js
import cart from './cart.js'export default new Vuex.Store({strict: true, // 开启严格模式state: {},mutations: {},getters: {},actions: {},modules: {cart}
})import { mapState } from 'vuex'
export default {computed: {...mapState('cart', ['cart'])}
}
```
- 严格模式
- 如果修改state没有通过mutations,那么devtools就不能跟踪到,这是一种约定
- 严格模式,直接修改state状态,会抛出错误,但数据会被修改。
- 不要在生成环境开启严格模式,严格模式会深度检查状态树,来检查不合规的状态改变,会影响性能
- strict: process.env.NODE_ENV !== ‘production’ 只在生产环境不开启严格模式。开发环境开启
-
购物车案例-演示
-
购物车案例 - 模板
- 模版地址
- https://github.com/goddlts/vuex-cart-demo-template.git
-
购物车案例 - 商品列表
-
购物车案例 - 添加购物车
-
购物车案例 - 我的购物车 - 列表
-
购物车案例 - 我的购物车 - 统计
-
购物车案例 - 我的购物车 - 删除
-
购物车案例 - 购物车组件 - 购物车列表
-
购物车案例 - 购物车组件 - 全选
-
购物车案例 - 购物车组件 - 数字文本框
-
购物车案例 - 购物车组件 - 统计
-
购物车案例-本地存储
- Vuex 插件介绍
- Vuex 的插件就是一个函数
- 这个函数接收一个store的参数
const myPlugin = store => {// 当 stroe 初始化后调用store.subscribe((mutation, state) => {//每次 mutation之后调用// mutatuin 的格式为 { type, payload }}) }export default new Vuex.Store({state: {msg: 'abc'},mutations: {},getters: {reverseMsg(state) {return state.msg.split('').reverse().join('')}},actions: {},modules: {},plugins: [myPlugin] })
-
模拟 Vuex - 基本结构
-
模拟 Vuex - install
-
模拟 Vuex - Store 类
let _Vue = null
class Store {constructor(options) {const {state = {},getters = {},mutations = {},actions = {}} = optionsthis.state = _Vue.observable(state)this.getters = Object.create(null)Object.keys(getters).forEach(key => {Object.defineProperty(this, key, {get() => getters[key](state)})})this._nutations = mutationsthis._actions = actions}commit(type, payload) {this._nutations[type](this.state, payload)}dispatch(type, payload) {this._actions[type](this, payload)}
}
function install (Vue) {_Vue = VueVue.mixin({beforeCreate() {if (this.$options.store) {_Vue.prototype.$store = this.$options.store}}})
}
export default {Store,install
}
任务二:服务端渲染基础
- 概述
- SPA 单页面应用
- 优点 用户体验好 开发效率高 渲染性能好 可维护性好
- 缺点 首屏渲染时间长 不利于SEO
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AkeNHqgJ-1627096149840)(./img/1626854774759.jpg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nSC1lmGn-1627096149842)(./img/1626854851027.jpg)]
- 同构应用
- 通过服务端渲染首屏直出,解决SPA应用首屏渲染慢以及不利于SEO问题
- 通过客户端渲染接管页面内容交互得到更好的用户体验
- 这种方式通常称之为现代化的服务端渲染,也叫同构渲染
- 这种方式构建的应用称之为服务端渲染应用或者是同构应用
- 什么是渲染
- 渲染:把 数据 + 模版 拼接到一起
- 传统的服务端渲染
- nodemon 文件名 对比node,修改js文件后自动重启
- 网页越来越复杂的情况下,存在很多不足
- 前后端代码完全耦合在一起,不利于开发和维护
- 前端没有足够发挥空间
- 服务端压力大
- 用户体验一般
- 只有经过刷新,才能看到一个新的页面
- 客户端渲染
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sR3NaEyK-1627096149843)(./img/1626856803366.jpg)]
- 前端 更为独立,不再受限制于 后端
- 不足之处
- 首屏渲染慢
- 不利于SEO
- 为什么客户端渲染首屏渲染慢
- 服务端渲染页面直出
- 客户端渲染先加载模版页,再加载js,最后通过ajax加载数据。3批串行的网络请求,如果客户端网络不好,更加显慢。
- 为什么客户端渲染不利于 SEO
- SEO 搜索引擎排名,搜索引擎会去爬取网站进行分析,收录
- 搜索引擎不是浏览器,不会去执行js,拿到的只是html静态页面(当成字符串解析)
- 单页面程序seo基本为0
- 现代化的服务端渲染 同构渲染
- 同构渲染 = 后端渲染 + 前端渲染
- 基于 React、 Vue 等框架,客户端渲染和服务器渲染的结合
- 在服务器端执行一次,用于实现服务器端渲染(首屏直出)
- 在客户端再执行一次,用于接管页面交互
- 核心解决SEO和首屏渲染慢的问题
- 拥有传统服务端渲染的优点,也有客户端渲染的优点
- 如何实现同构渲染
- 使用 Vue、React等框架的官方解决方案
- 优点:有助于理解原理
- 缺点:需要搭建环境,比较麻烦
- 使用第三方解决方案
- React 生态的 Next.js
- Vue 生态的 Nuxt.js
- 使用 Vue、React等框架的官方解决方案
- 通过 Nuxt 体验同构渲染
- yarn add nuxt
- “scripts”: { “dev”: “nuxt” }
- asyncData
- 首屏是通过服务端渲染出来的
-
同构渲染的 SPA 应用
-
同构渲染的问题
- 开发条件受限
- 浏览器特定的代码只能在某些生命周期钩子函数中使用
- 一些外部扩展库可能需要特殊处理才能在服务端渲染应用中运行
- 不能在服务端渲染期间操作DOM
- 某些代码操作需要区分运行环境
- 涉及构建和部署的要求更多
-
构建 部署
- 客户端渲染 仅构建客户端应用即可 可以部署在任意web服务器中
- 同构渲染 需要构建两个端 只能部署在 Node.js Server
-
- 更多的服务端负载
- 在 Node 中渲染完整的应用程序,相比仅仅提供静态文件的服务器 需要大量占用CPU资源
- 如果应用在高流量环境下使用,需要准备相应的服务器负载
- 需要更多的服务端渲染优化工作处理
- 服务端渲染使用建议
- 首屏渲染速度是否真的重要?
- 是否真的需要SEO?
任务三:NuxtJS基础
- NuxtJS介绍
- 一个基于Vue.js生态的第三方开源服务端渲染应用框架
- 它可以帮我们轻松的使用Vue.js技术栈构建同构应用
- https://github.com/nuxt/nuxt.js
- 初始化NuxtJS项目
- Nuxt.js的使用方式
- 初始项目
- 已有的Node.js服务端项目
- 直接把Nuxt当作一个中间件集成到 Node Web Server 中
- 现有的Vue.js项目
- 非常熟悉 nuxt.js
- 至少百分之10的代码改动
- 官方文档 [https://zh.nuxtjs.org/docs/2.x/get-started/installation]
- 根据官方文档
- 方式一:使用 create-nuxt-app
- 方式二:手动创建
- 案例代码分支说明
- git branch 分支名称 创建分支
- git branch 查看所有分支
- git checkout 分支名称 切换分支
-
路由-基本路由
-
路由-路由导航
- a 标签
- 它会刷新整个页面,不用使用
- nuxt-link 组件
- 编程式导航 通过js跳转
- 参考 router-link 就行了,基本一致 [https://router.vuejs.org]
- 路由-动态路由
- _id.vue 动态路由,对应 vue-router /:id
- 路由-嵌套路由
- router-link: template占位,children 配置嵌套路由地址
- nuxt: users.vue 文件中占位,同名users/index.vue 文件夹出现的文件就是子路由文件
- 路由-自定义路由配置
- nuxt.config.js 中配置路由规则
- 视图-模板
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-57euJ1gT-1627096149844)(./img/1626875411461.jpg)]
- 最外层的 html 模版是看不到的,可以在src文件夹(默认项目根目录下)下创建 app.html
<!-- 默认模版 -->
<!DOCTYPE html>
<html {{ HTML_ATTRS }}><head {{ HEAD_ATTRS }}>{{ HEAD }}</head><body {{ BODY_ATTRS }}><!-- 渲染的内容最终会注入到这里 -->{{ APP }}</body>
</html>
- 视图-布局
- layouts/default.vue 第二层所有组件的默认父路由,默认布局组件
- 页面出口,类似于子路由出口
- 异步数据-asyncData
- 基本用法
- 它会将asyncData返回的数据融合组件data方法返回数据一并给组件
- 调用时机:服务端渲染期间和客户端路由更新之前
- 注意事项
- 只能在页面组件中使用
- 没有this,因为它是在组件初始化之前被调用的
- asyncdata 当想要动态页面内容有利于SEO或者是提升首屏渲染速度的时候,就在asyncData 中发请求拿数据
- data 如果是非异步数据或者普通数据,则正常初始化到 data中即可
- 异步数据-上下文对象
- asyncDate(context)
任务四:NuxtJS综合案例
- 案例介绍
- 案例名称:realworld
- 一个开源的学习项目,目的就是帮助开发者快速学习新技能
- GitHub 仓库: https://github.com/gothinkster/realworld
- 在线示例: https://demo.realworld.io/#/
- 模版地址 https://github.com/gothinkster/realworld-starter-kit/blob/master/FRONTEND_INSTRUCTIONS.md
- API 地址 https://github.com/gothinkster/realworld/tree/master/api
- 学习收获
- 掌握使用Nuxt.js开发同构渲染应用
- 增强Vue.js实践能力
- 掌握同构渲染应用中常见的功能处理
- 用户状态管理
- 页面访问权限处理
- SEO 优化
- 掌握同构渲染应用的发布于部署
- 项目初始化-创建项目
- mkdir realworld-nuxtjs
- npm init -y
- npm i nuxt
- 配置启动脚本 “scripts”: { “dev”: “nuxt” }
- 创建pages目录,配置初始页面
- 项目初始化-导入样式资源
- 导入样式资源
- 模版地址 中的头部三个css文件
- 创建 app.html 导入nuxt模版,引用这三个css文件
- https://www.jsdelivr.com 这个可以找到资源在国内的cdn地址 如:ionicons
- 第二个css文件,googleapis已经在国内有资源,可以不处理
- 第三个css,直接下载下来,放在static 文件夹下。第一个css不这样做,是因为引用了一些字体,处理起来比较麻烦
- 配置布局组件
- 配置页面组件
- 项目初始化-布局组件
- pages/layout/index.vue
- 把模版的顶部和底部复制过来,中间放子路由
- nuxt.config.js
module.exports = {router: {// 自定义路由表规则extendRoutes(routes, resolve) {// 清除Nuxt.js基于pages目录默认生成的路由表规则routes.splice(0)routes.push(...[path: '/',component: resolve(__dirname, 'pages/layout/'),children: [{path: '', // 默认子路由name: 'home',component: resolve(__dirname, 'pages/home'),}]])}} }
- 项目初始化-导入登录注册页面
- login/index.vue
- 配置 Login/Register url
export default {computed: {// 处理 Login/Register 页面的不同isLogin() {return this.$route.name === 'login'}}
}
-
项目初始化-导入剩余页面
-
项目初始化-处理顶部导航链接
-
项目初始化-处理导航链接高亮
- 配置在router中 linkActiveClass: ‘active’
- 在nuxt-link 标签上写 exact 表示精确匹配(url)
- 项目初始化-封装请求模块
- npm i axios
- 登录注册-实现基本登录功能
- @submit.prevent = “” 表单阻止默认事件(只触发注册的事件)
-
登录注册-封装请求方法
-
登录注册-表单验证
- 给input 标签加上 required 属性,原生的表单验证
- 邮箱 type改成 email就会验证
-
登录注册-错误处理
-
登录注册-用户注册
- minlength=8
-
登录注册-解析存储登录状态实现流程
-
登录注册-将登录状态存储到容器中
-
登录注册-登录状态持久化
- npm i js-cookie
// 仅在客户端加载 js-cookie 包
const Cookie = process.client ? require('js-cookie') : undefined
// store/index.js
const cookieparser = process.server ? require('cookieparser') : undefined
// 在服务端渲染期间运行都是同一个示例
// 为了防止数据冲突,务必要把state 定义成一个函数,返回数据对象
export const state = () => {return {// 当前登陆用户的登陆状态user: null}
}
export const mutations = {setUser(state, data) {state.user = data}
}
export const actions = {// nuxtServerInit 是一个特殊的action 方法// 这个 action 会在服务端渲染期间自动调用// 作用:初始化容器数据,传递数据给客户端使用nuxtServerInit ({ commit }, { req }) {let user = nullif (req.headers.cookie) {const parsed = cookieparser.parse(req.headers.cookie)try {user = JSON.parse(parsed.user)} catch (error) {}}commit('setUser', user)}
}
-
登录注册-处理导航栏链接展示状态
-
登录注册-处理页面访问权限
- middleware/auth.js
export default function ({store, redirect}) {if (!store.state.user) {return redirect('/login')}
}
// 在路由匹配组件渲染之前会先执行中间件处理
// middleware: 'auth'
-
首页-业务介绍
-
首页-展示公共文章列表
-
首页-列表分页-分页参数的使用
-
首页-列表分页-页码处理
- watchQuery: [‘page’]
-
首页-列表分页-页码处理
-
首页-展示文章标签列表
-
首页-优化并行异步任务
- Promise.all
-
首页-处理标签列表链接和数据
-
首页-处理导航栏-业务介绍
-
首页-处理导航栏-展示状态处理
-
首页-处理导航栏-标签高亮及链接
-
首页-处理导航栏-展示用户关注的文章列表
-
首页-统一设置用户Token
- plugins/request.js
- nuxt.config.js plugins: [’~/plugins/request.js’] 注册插件
import axios from "axios";
// 按需导出,因为默认导出被 plugin 占据了
export const request = axios.create({baseURL: 'https://conduit.productionready.io/'
})
export default ({ store }) => {// 添加请求拦截器request.interceptors.request.use(function (config) {// 在发送请求之前做些什么let { user } = store.stateif (user && user.token) config.headers.Authorization = `Token ${user.token}`// 返回config 配置对象return config;}, function (error) {// 对请求错误做些什么,这个是请求发送之前的异常return Promise.reject(error);});
}
- 首页-文章发布时间格式化处理
- dayjs
- npm install dayjs --save
- vue.filter 注册插件
-
首页-文章点赞
-
文章详情-业务介绍
-
文章详情-展示基本信息
-
文章详情-把Markdown转为HTML
- markdown-it
-
文章详情-展示文章作者相关信息
-
文章详情-设置页面meta优化SEO
-
文章评论-通过客户端渲染展示评论列表
-
发布部署-打包
-
发布部署-最简单的部署方式
- 最简单的部署方式
-
配置Host + Port
- nuxt.config.js => server: {host: ‘0.0.0.0’, port: 3000}
-
压缩发布包
- .nuxt static nuxt.config.js package.json package-lock.json => realword-nuxtjs.zip
-
把发布包传到服务端
- ssh root@10.10.10.0 登陆服务器
- mkdir realworld-nuxtjs
- cd realworld-nuxtjs/
- pwd
- exit
- scp ./realword-nuxtjs.zip root@10.10.10.0:/root/realworld-nuxtjs
- ssh root@10.10.10.0 登陆服务器
- cd realworld-nuxtjs/
- unzip realword-nuxtjs.zip
- ls -a 看隐藏文件
- npm i
- npm run start
-
解压
-
安装依赖
-
启动服务
-
- 发布部署-使用PM2启动Node服务
- npm install --global pm2
- pm2 start 脚本路径
- pm2 start npm -- start
- pm2 stop id
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FXX1ZdK9-1627096149845)(./img/1627037127513.jpg)]
- 发布部署-自动化部署介绍
- 现代化的部署方式(CI/CD)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HmcXSRDi-1627096149846)(./img/1627037335411.jpg)]
- 发布部署-准备自动部署内容
- CI/CD 服务+ jenkins+ Gitlab CI+ GitHub Actions+ Travis CI+ Circle CI
- 环境准备+ Linux 服务器+ 把代码提交到GitHub 远程仓库
- 配置 GitHub Access Token+ 生成:https://github.com/settings/tokens+ 配置到项目的Secrets 中:https://github.com/lipengzhou/realworld-nuxtjs/settings/secrets+ 右上角用户》Settings > Developer settings > Personal access tokens > Generate new token > repo 勾选 > 最下方 Generate token- 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- 复制令牌- 打开远程仓库 》 Settings 》Secrets 》 new Secrets 》Name:TOKEN (脚本中要保持一致) Value 放令牌+ 配置GitHub Actios 执行脚本- 在项目根目录创建 .github/workflows 目录- 下载 main.yml到workflows目录中+ https://gist.github.com/lipengzhou/b92f80142afa37aea397da47366bd872- 修改配置- 配置PM2 配置文件+ pm2.config.json { "apps": [ { "name": "RealWorld", "script": "npm", "args": "start" } ] }+ 需要把 pm2.config.json加入到 main.yml 打包构建的文件中去+ 配置服务器信息,打开远程仓库:Settings 》Secrets 》 new Secrets 》- Name:USERNAME Value:root- Name:PORT Value:22- Name:HOST Value: 100.100.99.98- Name: PASSWORD Value:- 提交更新- 查看自动部署状态- 访问网站- 提交更新
- 发布部署-自动部署完成
- git tag v0.1.0 在main.yml中以v开头才会集成
- git tag
- git push origin v0.1.0
- 查看远程仓库 Actions 构建这一栏
- Release 可以查看到构建完成的发布包
- git add . gti commit ‘’ git push ,只是推送了本地的历史记录
- git tag v0.1.1 只有生成一个新的标签并且推送才回构建