由simpleMDE 看Nuxt的cdn模块协作
Nuxt 加载外部cdn模块
Vue
加载外部cdn模块
的时候,是通过配置vue.config.js
的configureWebpack.externals
,从而告诉webpack
跳过模块依赖
// vue.config.js
{
configureWebpack: {
externals: {
axios: 'axios',
vue: 'Vue',
loadsh: '_',
}
}
}
Nuxt 加载cdn模块比较直接,在nuxt.config.js
配置head
即可全局加载
// nuxt.config.js
{
head: {
script: [
{
src: 'https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js'
}
],
link: [
{
rel: 'stylesheet',
href: 'https://fonts.googleapis.com/css?family=Roboto'
}
]
}
}
或者直接在page组件中配置head(),可实现局部加载
// pages/example.vue
export default {
head: () => {
return {
//...
}
}
}
封装sampleMDE 组件
封装cdn库为独立组件,并实现按需加载
目前暂时没有找到解决办法
- npm安装,封装为组件。实际使用时按需加载chunk。
- 缺点: 无法使用公共cdn
- 全局配置nuxt的head,引入cdn,再将sampleMDE封装为独立组件
- 缺点: 无法按需加载cdn
局部加载引入sampleMDE的cdn
考虑在page视图里通过局部加载cdn
// pages/a.vue
<template>
<textarea ref="editor"></textarea>
</template>
<script>
export default {
mounted() {
let editor = new SimpleMDE({ element: this.$refs.editor })
},
// spa模式下,由其他页面路由跳转,将进行异步加载,无法预知什么时候加载完成(nuxt是否有hook?)
head: () => {
return {
script: [
{
src: 'https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.js',
},
],
link: [
{
rel: 'stylesheet',
href: 'https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.css',
},
],
}
},
}
</script>
// pages/b.vue
<template>
<nuxt-link :to="/a">Go to pageA</nuxt-link>
</template>
spa
模式下我们直接访问/a
,SampleMDE
渲染成功。但是如果我们一开始访问的是/b
,然后通过route
跳转到/a
,会发现SampleMDE
不存在。稍加思索可以想到
- 直接访问
/a
的时候,nuxt
发现page
组件里的head
,于是等待cdn
模块加载完成后触发mounted
- 先访问
/b
,此时页面未加载head
中的cdn
route
跳转到/a
时,nuxt
开始异步加载head
中的cdn
,同时触发mounted
,因此SimpleMDE is not defined
找到原因之后,那就想办法在cdn
模块加载完成之后再进行editor
的初始化:
- 上策 反馈nuxt,并期望它暴露head中的资源加载完成的钩子,然后在钩子函数中优雅的进行初始化
- 中策 在vue-router的钩子中自行实现loadScript的钩子
- 下策 定时检查引入的cdn是否加载完成
所以我立马选择了下策,顺便演练了一下如何递归的调用一个Promise
// pages/a.vue
mounted() {
this.$nextTick(() => {
this.detectMDE().then(() => {
this.simpleMDE = new SimpleMDE({ element: this.$refs.editor })
})
})
},
methods: {
detectMDE() {
console.log('detecting...')
return new Promise((resolve) => {
setTimeout(() => {
if (typeof SimpleMDE === 'undefined') {
resolve(false)
} else {
resolve(true)
}
}, 100)
}).then((r) => {
// 演示如何递归调用一个promise
return r ? true : this.detectMDE()
})
},
},
然后证明下策是行得通的!
更优雅的交互?
没错,在等待simpleMDE
加载完成的过程中,不妨应用一下骨架
// pages/a.vue
<template>
<div>
<textarea v-if="loadMDE" ref="editor" style="width: 100%; height: calc(100% - 40px)"></textarea>
<v-skeleton-loader v-else class="mx-auto" max-width="100%" type="card-heading, image"> </v-skeleton-loader>
</div>
</template>
// pages/a.vue
mounted() {
this.$nextTick(() => {
this.detectMDE().then(() => {
this.loadMDE = true
this.simpleMDE = new SimpleMDE({ element: this.$refs.editor })
})
})
},
然后很不幸,这个时候报错this.$refs.editor is undefined
。
难道ref
不能和v-if
一起使用?
通过搜索引擎发现网上类似的问题还比较多,ref
与v-if
,v-show
,v-else
一起使用时通常获取不到dom。
其实这个问题很简单,让我们回忆一下vue的响应式原理
可能你还没有注意到,Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。
例如,当你设置 vm.someData = 'new value',该组件不会立即重新渲染。当刷新队列时,组件会在下一个事件循环“tick”中更新。多数情况我们不需要关心这个过程,但是如果你想基于更新后的 DOM 状态来做点什么,这就可能会有些棘手。为了在数据变化之后等待 Vue 完成更新 DOM,可以在数据变化之后立即使用 Vue.nextTick(callback)。这样回调函数将在 DOM 更新完成后被调用。
在v-if
的状态改变时,dom
还未被更新,因此ref
引用为空。
网上有用watch
监听来处理,找到原因之后发现完全没必要。我们只需要再套一层$nextTick
mounted() {
this.$nextTick(() => {
this.detectMDE().then(() => {
this.loadMDE = true
// dom将在下一个事件循环中更新,因此此时 ref 为undefined
console.log(this.$refs.editor) // undefined
this.$nextTick(() => {
// eslint-disable-next-line no-undef
this.simpleMDE = new SimpleMDE({ element: this.$refs.editor })
})
})
})
},
小结
由于莫名其妙的坚持按需cdn加载,以及剑走偏锋的脑回路检测cdn模块加载完成,倒是误打误撞解锁/复习了3个技能点
- Nuxt 的page组件的head加载漏洞
spa模式下,路由跳转导致外部cdn资源异步加载
- Promise对象的递归调用
- Vue的响应式原理,
修改状态与更新dom的时机