Vue异步自定义指令
涉及Vue自定义指令生命周期、Vue生命周期、v-html、dom操作、$nextTick等知识点
文章目录
一、场景描述
提供:后台给你返回一段富文本并包含代码块,代码块用类来唯一标识;
需求:是前端在拿到富文本后在页面进行异步渲染,并给代码高亮显示;
二、思路分析
1.思路再现
思路:可以为接收到的富文本定义一个父容器并给其唯一标识,后端返回数据后
v-html
渲染,这样可以很轻松地展示富文本内容;但是在做代码高亮的时候有问题了。。。
2.做代码高亮遇到的问题详解
如果文档同步渲染做代码高亮其实是非常容易的,使用highlight.js即可,但是遇到异步创建dom节点后,问题就出现了。。。
问题一:mounted不能保证子组件全部挂载完成?
我一开始是打算在mounted里面请求富文本数据,然后在成功的回调里面获取v-html渲染的富文本内容,再通过给代码节点添加pre
和code
标签,但是我发现怎么也拿不到异步dom,心里想着mounted里面不是应该渲染完了嘛?
我以前一直认为父子组件的生命周期是:
父beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount->子mounted->父mounted
所以,既然父组件mounted
,那一定是子组件挂载完成了呀?那这里怎么说mounted
不能保证子组件全部挂载完成?
然后我去中文版官方文档找到了相应的出处,如下:
综上可以理解为:如果子组件是一个异步组件,那父组件 mounted 时子组件或许就没有加载完成。
于是我在请求到数据后加了一层$nextTick
getDocList() {
docDetail(this.$route.params.id).then((res) => {
this.docList = res.data;
this.$nextTick(() => {
console.log([...document.getElementById('content').getElementsByClassName('code-block')])
});
});
},
下一步我是继续打算在拿到代码节点后获取节点innerText
再replace
成<pre><code>innerText</code></pre>
,但是这种方法且不说可行性,从性能上来讲可谓是无敌拉胯了;
问题二:vue自定义指令还有生命周期?
于是我又开始面向百度编程,查阅到自定义指令v-highlight
,给需要高亮的代码块加上该指令,然后获取对其子代码块进行操作。
directives: {
highlight: {
inserted(el, bind, vNode, oldvNode) {
let blocks = el.querySelectorAll(".ql-code-block");
blocks.forEach((block) => {
hljs.highlightBlock(block);
});
},
},
},
一开始我以为这样写是没问题的,但是怎么也不生效,也获取不到dom
元素,此时我就纳闷了,再去查资料,看了下自定义指令的声明周期,结果发现:
Vue.directive('focus', {
bind: function(el){}, //每当指令绑定到元素上,立即执行 bind 函数。注意:绑定时 DOM 树还未被构建。
inserted: function(el){}, //inserted 表示元素插入到 DOM 中的时候(已经插入),执行 inserted 函数
updated: function(el){} //当VNode更新时,会执行 updated
componentUpdate : function(el){} //被绑定的元素所在模板完成一次更新更新周期的时候调用
unbind: function(el){} //只调用一次,指令与元素解绑的时候调用
})
原来在inserted
不适用与同步渲染,于是我把它换成了componentUpdate
,让模板在声明周期更新后调用,问题解决。
directives: {
highlight: {
componentUpdate(el, bind, vNode, oldvNode) {
let blocks = el.querySelectorAll(".ql-code-block");
blocks.forEach((block) => {
hljs.highlightBlock(block);
});
},
},
},
三、小的知识点
1、vue中如何使用highlight.js
引入:除了引入Vue-highlight.js文件外,还需要引入样式文件highlight.js。vue-highlight.js只是实现了代码高亮的功能,安装包里是没有css样式文件,因此我们还需要安装一个highlight.js来实现真正的样式。
使用:
<!-- 1.如果你需要高亮的代码是一个变量值,那么你可以这样使用它。 其中 sourcecode 为变量。 -->
<pre v-highlightjs="sourcecode"><code></code></pre>
<!-- 2.如果你需要高亮的代码固定的源代码,那么你可以这样使用它。 -->
<pre v-highlightjs><code>const s = new Date().toString()</code></pre>
2、v-html可能导致的问题
Vue
中的v-html
指令用以更新元素的innerHTML
,其内容按普通HTML
插入,不会作为Vue
模板进行编译,如果试图使用v-html
组合模板,可以重新考虑是否通过使用组件来替代。
描述:
- 易导致XSS攻击
v-html
指令最终调用的是innerHTML
方法将指令的value
插入到对应的元素里,这就是容易造成xss
攻击漏洞的原因了。Vue
在官网对于此也给出了温馨提示,在网站上动态渲染任意HTML
是非常危险的,因为容易导致XSS
攻击,只在可信内容上使用v-html
,永不用在用户提交的内容上。 关于XSS
,跨站脚本攻击XSS
,是最普遍的Web
应用安全漏洞。这类漏洞能够使得攻击者嵌入恶意脚本代码到正常用户会访问到的页面中,当正常用户访问该页面时,则可导致嵌入的恶意脚本代码的执行,从而达到恶意攻击用户的目的。当动态页面中插入的内容含有这些特殊字符如<
时,用户浏览器会将其误认为是插入了HTML
标签,当这些HTML
标签引入了一段JavaScript
脚本时,这些脚本程序就将会在用户浏览器中执行。当这些特殊字符不能被动态页面检查或检查出现失误时,就将会产生XSS
漏洞。 - 不作为模板编译
v-html
更新的是直接使用元素的innerHTML
方法,内容按普通HTML
插入,不会作为Vue
模板进行编译,如果试图使用v-html
组合模板,可以重新考虑是否通过使用组件来替代。另外后端返回<script>
标签中的代码是不会直接执行的,这是浏览器的策略,如果需要的话可以在$nextTick
回调中动态创建<script>
标签然后src
引入代码url
即可。 - scoped样式不能应用
在单文件组件里,scoped
的样式不会应用在v-html
内部,因为那部分HTML
没有被Vue
的模板编译器处理,如果你希望针对v-html
的内容设置带作用域的CSS
,你可以替换为CSS Modules
或用一个额外的全局<style>
元素手动设置类似BEM
的作用域策略。此外提一下关于样式隔离的话,Shadow DOM
也是个不错的解决方案。关于CSS Modules
以及BEM
命名规范可以参考下面的链接。
3、$nextTick()的使用
(1)NextTick是什么
官方对其的定义
在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM
可以理解成,Vue
在更新 DOM
时是异步执行的。当数据发生变化,Vue
将开启一个异步更新队列,视图需要等队列中所有数据变化完成之后,再统一进行更新
举例一下:
Html
结构
<div id="app"> {{ message }} </div>
构建一个vue
实例
const vm = new Vue({
el: '#app',
data: {
message: '原始值'
}
})
修改message
this.message = '修改后的值1'
this.message = '修改后的值2'
this.message = '修改后的值3'
这时候想获取页面最新的DOM
节点,却发现获取到的是旧值
console.log(vm.$el.textContent) // 原始值
这是因为message
数据在发现变化的时候,vue
并不会立刻去更新Dom
,而是将修改数据的操作放在了一个异步操作队列中
如果我们一直修改相同数据,异步操作队列还会进行去重
等待同一事件循环中的所有数据变化完成之后,会将队列中的事件拿来进行处理,进行DOM
的更新
- 为什么要有nexttick?
举个例子
{{num}}
for(let i=0; i<100000; i++){
num = i
}
如果没有 nextTick
更新机制,那么 num
每次更新值都会触发视图更新(上面这段代码也就是会更新10万次视图),有了nextTick
机制,只需要更新一次,所以nextTick
本质是一种优化策略
(2)使用场景
如果想要在修改数据后立刻得到更新后的DOM
结构,可以使用Vue.nextTick()
第一个参数为:回调函数(可以获取最近的DOM
结构)
第二个参数为:执行函数上下文
// 修改数据
vm.message = '修改后的值'
// DOM 还没有更新
console.log(vm.$el.textContent) // 原始的值
Vue.nextTick(function () {
// DOM 更新了
console.log(vm.$el.textContent) // 修改后的值
})
组件内使用 vm.$nextTick()
实例方法只需要通过this.$nextTick()
,并且回调函数中的 this
将自动绑定到当前的 Vue
实例上
this.message = '修改后的值'
console.log(this.$el.textContent) // => '原始的值'
this.$nextTick(function () {
console.log(this.$el.textContent) // => '修改后的值'
})
$nextTick()
会返回一个 Promise
对象,可以是用async/await
完成相同作用的事情
this.message = '修改后的值'
console.log(this.$el.textContent) // => '原始的值'
await this.$nextTick()
console.log(this.$el.textContent) // => '修改后的值'