前面两篇文章总结了 Vue 开发的大部分技巧和内容,最后一篇文章来对它进行一个收尾
这篇文章我们来谈谈一些 Vue 理解和实践要求高一点的问题
首先是生命周期这一块内容,随着实践越多它的意义越大,理解也越深刻
mixin 功能强大,对代码复用组织都有很高的要求,算是 Vue 后期发力的高级技巧
服务端渲染可能是学习 Vue 最后一块阵地了,对于 SPA 框架的一个里程碑
最后,总结一下我在使用 Vue 中使用的技巧和经验
常规操作,先点赞后观看哦!你的点赞是我创作的动力之一!
我将从 16 个方面来论述 Vue 开发过程中的一些技巧和原理。如果你还未观看上节文章,可以移步至
Vue 生命周期大概就是:一个从 Vue 实例的创建到组件销毁的一个的过程。
具体情况下,我们分为几个核心的阶段,并且每个阶段都有一套钩子函数来执行我们需要的代码。
我们整理分类一下这些生命周期钩子,为了记忆方便分为 4 大核心阶段:
方便读者记忆,这里尽量使用图示:
可以看到每一个阶段中的钩子命名都很好记忆,阶段开始前使用
beforeXxx
,阶段后结束后使用xxxed
除这 8
个核心钩子,另外还有 3
个新增功能型钩子,目前总共是 11
个钩子
顺带提一下这 3
个钩子的功能
activated
与 disactivated
,这两个钩子也是一对的,分别表示被 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
和 disactivated
等等)总得来说,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 多天禁足的日子。希望以后多多关注!