由simpleMDE 看Nuxt的cdn模块协作

Nuxt 加载外部cdn模块

Vue加载外部cdn模块的时候,是通过配置vue.config.jsconfigureWebpack.externals,从而告诉webpack跳过模块依赖

// vue.config.js
{
    configureWebpack: {
        externals: {
            axios: 'axios',
            vue: 'Vue',
            loadsh: '_',
        }
    }
}

Nuxt 加载cdn模块比较直接,在nuxt.config.js配置head即可全局加载

如何在 Nuxt.js 应用中使用外部资源?

// 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库为独立组件,并实现按需加载

目前暂时没有找到解决办法

  1. npm安装,封装为组件。实际使用时按需加载chunk。
    • 缺点: 无法使用公共cdn
  2. 全局配置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模式下我们直接访问/aSampleMDE渲染成功。但是如果我们一开始访问的是/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的初始化:

  1. 上策 反馈nuxt,并期望它暴露head中的资源加载完成的钩子,然后在钩子函数中优雅的进行初始化
  2. 中策 在vue-router的钩子中自行实现loadScript的钩子
  3. 下策 定时检查引入的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一起使用?
通过搜索引擎发现网上类似的问题还比较多,refv-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个技能点

  1. Nuxt 的page组件的head加载漏洞 spa模式下,路由跳转导致外部cdn资源异步加载
  2. Promise对象的递归调用
  3. Vue的响应式原理,修改状态与更新dom的时机
posted @ 2020-09-04 02:40  真理君的宿敌  阅读(344)  评论(0编辑  收藏  举报