前面两篇文章总结了 Vue 开发的大部分技巧和内容,最后一篇文章来对它进行一个收尾css
这篇文章咱们来谈谈一些 Vue 理解和实践要求高一点的问题html
首先是生命周期这一块内容,随着实践越多它的意义越大,理解也越深入前端
mixin 功能强大,对代码复用组织都有很高的要求,算是 Vue 后期发力的高级技巧vue
服务端渲染多是学习 Vue 最后一块阵地了,对于 SPA 框架的一个里程碑node
最后,总结一下我在使用 Vue 中使用的技巧和经验webpack
常规操做,先点赞后观看哦!你的点赞是我创做的动力之一!git
我将从 16 个方面来论述 Vue 开发过程当中的一些技巧和原理。若是你还未观看上节文章,能够移步至github
Vue 生命周期大概就是:一个从 Vue 实例的建立到组件销毁的一个的过程。web
具体状况下,咱们分为几个核心的阶段,而且每一个阶段都有一套钩子函数来执行咱们须要的代码。面试
咱们整理分类一下这些生命周期钩子,为了记忆方便分为 4 大核心阶段:
方便读者记忆,这里尽可能使用图示:
能够看到每个阶段中的钩子命名都很好记忆,阶段开始前使用
beforeXxx
,阶段后结束后使用xxxed
除这 8
个核心钩子,另外还有 3
个新增功能型钩子,目前总共是 11
个钩子 顺带提一下这 3
个钩子的功能
activated
与 deactivated
,这两个钩子也是一对的,分别表示被 keep-alive
缓存的组件激活和停用时调用。errorCaptured
,对组件中出现对异常错误进行处理,使用较少。咱们看看官方的图解,在 Vue 教程实例这节 官方教程直接跳转
官方图解并无详细解释这张图。我猜一方面缘由是这个图里面涉及的细节都是在 vue 源码里面体现,要真正解释起来也没那么简单。另外一方面是但愿咱们多在实践中去理解里面的意义。
对于上面那张图的理解,咱们须要对 Vue 源码进行梳理,才能真正的理解。大概根据现有的源码,我梳理了一下大体的流程:
咱们能够清楚的看到,从 Vue 实例建立、组件挂载、渲染的一些过程当中,有着明显的周期节点。
结合上面源码的流程和相关实践,简化每个阶段作了哪些时期,每个钩子里面是组件处于什么状态。
下面咱们提出一些问题:
<template>
<div>
<div class="message">
{{message}}
</div>
</div>
</template>
<script>
export default {
data() {
return {
message: '1'
}
},
methods: {
printComponentInfo(lifeName) {
console.log(lifeName)
console.log('el', this.$el)
console.log('data', this.$data)
console.log('watch', this.$watch)
}
}
}
</script>
复制代码
// ...
beforeCreate() {
this.printComponentInfo('beforeCreate')
},
created() {
this.printComponentInfo('created')
},
beforeMount() {
this.printComponentInfo('beforeMount')
},
mounted() {
this.printComponentInfo('mounted')
},
beforeUpdate() {
this.printComponentInfo('beforeUpdate')
},
updated() {
this.printComponentInfo('updated')
},
beforeDestroy() {
this.printComponentInfo('beforeDestroy')
},
destroyed() {
this.printComponentInfo('destroyed')
},
// ...
复制代码
beforeCreate
中methods
中方法直接报错没法访问,直接访问 el
和 data
后
发现只能访问到
watch
, el
和 data
均不能访问到
created
时期 el
没法访问到,可是能够访问到 data
了
beforeMount
中能够访问 data
可是仍然访问不到 el
mounted
中能够访问到 el
了
首次加载页面,更新阶段和销毁阶段到钩子都未触发
咱们增长一行代码
this.message = this.message + 1
复制代码
若是增长在 created
阶段,发现 update
钩子仍然未触发,可是 el
和 data
的值都变成了 2
若是增长在 mounted
阶段,发现 update
钩子此时触发了
怎样触发销毁的钩子呢? 大概有这几种方法
$destory
v-if
与 v-for
指令,(v-show
不行)mounted
钩子里面增长一行代码手动销毁当前组件,或者跳转路由this.$destory('lifecycle')
复制代码
发现beforeDestory
和 destoryed
都触发了,并且el
、data
都同样仍是能够访问到
beforeCreate
没法访问到 this
中的 data
、method
// 错误实例
beforeCreate() {
// 容许
console.log('ok')
// 不容许
this.print() // 报错找不到
this.message = 1 // 报错找不到
}
复制代码
created
能够访问 this,但没法访问 dom,dom 未挂载
created() {
// 容许并推荐
this.$http.get(xxx).then(res => {
this.data = res.data
})
// 不容许
this.$el
this.$ref.demo
const a = document.getElementById('demo')
}
复制代码
mounted
已经挂载 dom
,能够访问 this
mounted() {
// 容许
this.$el
this.$ref.demo
let a = document.getElementById('')
}
复制代码
生命周期相关demo 代码见github-lifecycle-demo
当组件使用混入对象时,全部混入对象的选项将被“混合”进入该组件自己的选项 大体原理就是将外来的组件、方法以某种方式进行合并。合并的规则有点像继承和扩展。
当组件和混入对象含有同名选项时,这些选项将以恰当的方式进行“合并”
咱们看一下一个组件里面有哪些东西是能够合并的
// mixins/demo
export default {
data() {
return {}
},
mounted() {},
methods: {},
computed: {},
components: {},
directives: {}
}
复制代码
data
、methods
、computed
、directives
、components
生命周期钩子
没错这些均可以混入
import demoMixin form '@/mixins/demo'
export default {
mixins: [demoMixin]
}
复制代码
这样看来,不少页面重复的代码咱们均可以直接抽取出来
或者是封装成一个公共的 mixin
好比咱们作 H5 页面,里面不少短信验证的逻辑固有逻辑,可是须要访问到 this
。使用工具函数确定不行。
这时候就能够考虑使用 mixin,封装成一个具备响应式的模块。供须要的地方进行引入。
首先是优先级的问题,当重名选项时选择哪个为最后的结果
默认规则我这里分为 3
类
data
混入: 以当前组件值为最后的值methods
、computed
、directives
、components
这种健值对形式,同名key
,通通以当前组件为准固然若是想改变规则,也能够经过配置来改变规则
Vue.config.optionMergeStrategies.myOption = function (toVal, fromVal) {
// 返回合并后的值
}
复制代码
咱们知道 Vue 最能复用代码的就是组件。通常状况,咱们经过 props
来控制组件的,将原有组件封装成 HOC 高阶组件。而控制 props
的生成不同的功能的代码仍是写在基础组件里。
<template lang="pug">
div.dib
van-button.btn(
@click="$emit('click')"
:class="getClass" v-bind="$attrs"
:style="{'width': size === 'large' ? '345px': '', 'backgroundColor': getBgColor, borderColor: getBgColor, color: getBgColor}")
slot
</template>
<script>
import { Button } from 'vant'
import Vue from 'vue'
import { getColor } from '@/utils'
Vue.use(Button)
export default {
name: 'app-button',
props: {
type: {
type: String,
default: 'primary'
},
theme: {
type: String,
default: 'blue'
},
size: {
type: String,
default: ''
}
}
}
复制代码
以这个组件为例,咱们仍是经过公共组件内部的逻辑,来改变组件的行为。
可是,使用 mixin 提供了另外一个思路。咱们写好公共的 mixin,每个须要使用 mixin 的地方。咱们进行扩展合并,不一样与公共 mixin 的选项咱们在当前组件中进行自定义,也就是扩展。咱们新的逻辑是写在当前组件里面的,而非公共 mixins 里。
画个图理解一下:
最后总结一下 mixin 的优势
this
, 能够操做响应式代码第一,千万不能滥用全局 mixins 由于会影响全部多子组件
第二,因为 mixins 的合并策略固有影响,可能在一些极端状况达不到你想要的效果。
好比:我已经存在一个 mixins
,个人页面里面也有一些方法,我要引入mixins
就要作不少改动,来保证个人代码按个人须要运行。
页面 data
有一个 message
值,mixins
里面一样有一个。
按照默认规则,mixins
里面的 message
会被页面里面 message
覆盖。可是这两个 message
可能表明的意义不同,都须要存在。那么我就须要改掉其中的一个,若是业务太深的话,可能这个 message
没那么好改。
有时候须要考虑这些问题,致使使用 mixins
都会增长一些开发负担。固然也是这些问题可使用规范来规避。
SSR 是 Serve Side Render 的缩写,翻译过来就是咱们常说的服务端渲染
可是它还存在如下问题
activated
和 deactivated
等等)总得来讲,SSR 是必要的但不是充分的,SPA 的 SEO 如今没有更好的方案,有这方面强烈需求的网站来讲,SSR 确实颇有必要
经过上面图咱们能够得大体几点内容
分别尝试用这 3 种方式搭建 SSR
这里主要加深理解,vue-cli3+ 实现基本 SSR
分为 2
个入口,将 main.js
定为通用入口, 并额外增长entry-client.js
和 entry-serve.js
两个
1.改造主要入口,建立工厂函数
// main.js
import Vue from 'vue'
import App from './App.vue'
import { createRouter } from "./router"
// app、router
export function createApp () {
const router = createRouter()
const app = new Vue({
router,
render: h => h(App)
})
return { app, router }
}
复制代码
2.客户端入口
// client.js
import { createApp } from './main'
// 客户端特定引导逻辑
const { app } = createApp()
app.$mount('#app')
复制代码
3.服务端入口
// serve.js
import { createApp } from "./main";
export default context => {
// 由于有可能会是异步路由钩子函数或组件,因此咱们将返回一个 Promise
return new Promise((resolve, reject) => {
const { app, router } = createApp();
// 设置服务器端 router 的位置
router.push(context.url);
// 等到 router 将可能的异步组件和钩子函数解析完
router.onReady(() => {
const matchedComponents = router.getMatchedComponents();
// 匹配不到的路由,执行 reject 函数
if (!matchedComponents.length) {
return reject({
code: 404
});
}
// Promise 应该 resolve 应用程序实例,以便它能够渲染
resolve(app);
}, reject);
});
};
复制代码
const VueSSRServerPlugin = require("vue-server-renderer/server-plugin");
const VueSSRClientPlugin = require("vue-server-renderer/client-plugin");
const nodeExternals = require("webpack-node-externals");
const merge = require("webpack-merge");
const TARGET_NODE = process.env.WEBPACK_TARGET === "node";
const target = TARGET_NODE ? "server" : "client";
module.exports = {
configureWebpack: () => ({
entry: `./src/entry-${target}.js`,
devtool: 'source-map',
target: TARGET_NODE ? "node" : "web",
node: TARGET_NODE ? undefined : false,
output: {
libraryTarget: TARGET_NODE ? "commonjs2" : undefined
},
externals: TARGET_NODE
? nodeExternals({
whitelist: [/\.css$/]
})
: undefined,
optimization: {
splitChunks: undefined
},
plugins: [TARGET_NODE ? new VueSSRServerPlugin() : new VueSSRClientPlugin()]
}),
//...
};
复制代码
export function createRouter(){
return new Router({
mode: 'history',
routes: [
//...
]
})
}
复制代码
这一步主要是让 node 服务端响应 HTML 给浏览器访问
const Vue = require('vue')
const server = require('express')()
const renderer = require('vue-server-renderer').createRenderer()
server.get('*', (req, res) => {
const app = new Vue({
data: {
url: req.url
},
template: `<div>访问的 URL 是: {{ url }}</div>`
})
renderer.renderToString(app, (err, html) => {
if (err) {
res.status(500).end('Internal Server Error')
return
}
res.end(`
<!DOCTYPE html>
<html lang="en">
<head><title>Hello</title></head>
<body>${html}</body>
</html>
`)
})
})
server.listen(8080)
复制代码
简单几步体验下 nuxt
简单看了一下源码,nuxt 把咱们以前提到的重要的改造,所有封装到
.nuxt
文件夹里面了
跑一下 dev 发现有两个端,一个 clinet 端,一个 server 端
最后查看一下效果,整个过程挺丝滑的。目录结构也比较符合个人风格,新项目须要 SSR 会考虑使用 nuxt
解决 SEO 问题是否是只有 SSR 呢?其实预渲染也能作到,首先
prerender-spa-plugin
yarn prerender-spa-plugin
复制代码
const path = require('path')
const PrerenderSPAPlugin = require('prerender-spa-plugin')
const Renderer = PrerenderSPAPlugin.PuppeteerRenderer
module.exports = {
plugins: [
//...
new PrerenderSPAPlugin({
staticDir: path.join(__dirname, 'dist'),
outputDir: path.join(__dirname, 'prerendered'),
indexPath: path.join(__dirname, 'dist', 'index.html'),
routes: [ '/', '/about', '/some/deep/nested/route' ],
postProcess (renderedRoute) {
renderedRoute.route = renderedRoute.originalPath
renderedRoute.html = renderedRoute.html.split(/>[\s]+</gmi).join('><')
if (renderedRoute.route.endsWith('.html')) {
renderedRoute.outputPath = path.join(__dirname, 'dist', renderedRoute.route)
}
return renderedRoute
},
minify: {
collapseBooleanAttributes: true,
collapseWhitespace: true,
decodeEntities: true,
keepClosingSlash: true,
sortAttributes: true
},
renderer: new Renderer({
inject: {
foo: 'bar'
},
maxConcurrentRoutes: 4
]
}
复制代码
从 5 个大的角度来提高开发效率和体验,代码美观和代码质量,用户体验
* n
mixins
抽离公共逻辑,代码效率 * n
filter
编码效率+sass
复用 css
,编码体验、效率+eslint + prettier
,代码风格+、基础语法错误-typescript
,代码质量+test
,代码质量+vue
,渲染性能+vuex
减小请求,使用图片懒加载,加载性能+vue-cli4
,webpack
配置效率+vue-h5-template
,vue
配置效率+pug
,HTML
编写效率+css
编写 sass
,CSS
编写效率+mock
,脱离后端开发效率+HOC
,组件开发,页面编写效率+history
使用,服务端配置相关,URL
美观+SEO
与首屏加载、服务端渲染 SSR
基本解决做者面临 “失业” 和 “禁足” 在家里的双重打击下,仍然坚持完成了这个系列的最后一篇文章。
若是能对你有帮助,即是它最大的价值。都看到这里还不点赞,太过不去啦!😄
因为技术水平有限,文章中若有错误地方,请在评论区指出,感谢!
以后做者应该会去巩固基础知识,打算写一个《考古文章系列》,一方面增强本身基础,方便于面试。另外一方面,想沉下心来面对这剩下 10 多天禁足的日子。但愿之后多多关注!