Nuxt 挂载三方 js 脚本插件

885 阅读4分钟

Nuxt 挂载三方 js 脚本插件

起因

因公司的官网功能迁移,从原来的 SPA 应用迁移至 Nuxt 的 SSR 应用,牵扯到第三方客服脚本的加载,对前端不熟悉的美工特来求助,因此编写了一个 Vue 插件来救同事于水火之中。

TODO List

  • 挂载三方 js 脚本
  • 卸载三方 js 脚本
  • 提供全局方法
  • 初始化传入配置项
  • 使用 Vue.use 注册插件

挂载脚本

const PluginName = 'LoadScriptPlugin'
const prefix = `[${PluginName}] - `

/** @typedef {{name: string, src: string, selector?: string, defer?: boolean, async?: boolean}} ScriptOption */

/**
 * 创建并挂载脚本
 * @param {ScriptOption} option 挂载脚本选项
 * @returns {void}
 */
function createScript(option = {}) {
  if (typeof option !== 'object' || option === null) {
    option = {}
  }

  const scriptId = `${prefix}${name}`
  const script = document.createElement('script')
  const parentEl = document.querySelector(selector) || document.body

  script.id = scriptId
  script.src = option.src
  script.defer = option.defer
  script.async = option.async
  
  parentEl.append(script)
}

上面代码通过 createElement 创建了 script 标签,并通过接收外部传递的参数来设置 script 的属性实现挂载脚本的过程。

为了方便后面卸载脚本,我们还需记录每次挂载脚本的元素及自身元素。添加如下代码:

/**
 * 所有脚本信息
 * @description
 * parent: 父级元素
 * self: 自身元素(脚本元素)
 *
 * @type {{parent: HTMLElement, self: HTMLElement}}
 */
const scriptMap = {}

function createScript(option = {}{
  // 尾部追加
  + scriptMap[scriptId] = {
  +  parent: parentEl,
  +  self: script
  + }
}

销毁脚本

我们上面通过 scriptMap 记录了每次挂载脚本的父级元素和自身元素,下面我们通过创建接收 name 标识的函数来销毁脚本。

/**
 * 销毁脚本
 * @param {string} name 脚本名称
 * @returns {string}
 */
function destroyScript(name) {
  const scriptId = `${prefix}${name}`
  const scriptInfo = scriptMap[scriptId]

  scriptInfo.parent.removeChild(scriptInfo.self)
  delete scriptMap[scriptId]
}

定义插件

上面我们经将插件的基本功能实现完成,但是还没有看到我们定义的插件在哪里,接下来需要让插件具象化,同时为 Vue.use 暴露安装接口。

/**
 * Vue 自动挂载三方脚本插件
 * @example
 * Vue.use(Plugin, {})
 * // or
 * Vue.use(Plugin, [{}])
 */
const Plugin = {
  /**
   * 插件名
   */
  namePluginName,
  /**
   * 安装记录
   */
  installedfalse,
  /**
   * 安装行为
   * @param {VueVue
   * @param {ScriptOption|ScriptOption[]|nulloptions
   * @returns {void}
   */
  install(Vue, options) {
    if (Plugin.installed || Vue.$isServerreturn

    if (options) {
      options = Array.isArray(options) ? options : [options]
      options.forEach((opt) => createScript(opt))
    }
    
    Plugin.installed = true

    Vue.prototype.$createScript = createScript
    Vue.prototype.$destroyScript = destroyScript
  }
}

上面代码定义我们插件的基本信息和安装接口,后面可以通过默认导出即可使用 Vue.use(LoadScriptPlugin) 进行注册,并且可以接收 options 选项来在初始化时进行挂载操作。

通过为 Vue 原型挂载我们的脚本函数来方便在后续的业务中动态使用挂载和销毁脚本功能。

处理边界

到这里我们的插件基本已经实现完成,但是仍然存在使用隐患,因为我们开发出来是为其他人员提供便利操作的,在不能完整了解的情况下使用时可能会出现运行时报错,所以需要对接收参数做严格校验。

function log(...messages) {
  console.log(prefix, ...messages)
}

function warn(...messages) {
  console.warn(prefix, ...messages)
}

/**
 * 创建并挂载脚本
 * @param {ScriptOption} option 挂载脚本选项
 * @returns {void}
 */
function createScript(option = {}) {
  if (typeof option !== 'object' || option === null) {
    option = {}
  }

  if ([''nullvoid 0].includes(option.src)) {
    return warn('The src property of the option cannot be falsly value!')
  }

  if ([''nullvoid 0].includes(option.name)) {
    return warn(
      'The name property of the option cannot be falsly value! The name property will be used to identify the current script!'
    )
  }

  const scriptId = getScriptId(option.name)

  if (scriptId in scriptMap) {
    return warn('Duplicate name attribute, please re-enter!')
  }
  
  ...
  
  log(`The ${name} script been created!`)
}

/**
 * 销毁脚本
 * @param {string} name 脚本名称
 * @returns {string}
 */
function destroyScript(name) {
  ...
  
  if (!(scriptId in scriptMap) || scriptInfo === undefined) {
    return warn(`The script with name as ${name} does not exist!`)
  }

  ...
  
  log(`The ${name} script been destroyed!`)
}

完整代码

import Vue from 'vue'

const PluginName = 'LoadScriptPlugin'
const prefix = `[${PluginName}] - `

/**
 * 所有脚本信息
 * @description
 * parent: 父级元素
 * self: 自身元素(脚本元素)
 *
 * @type {{parent: HTMLElement, self: HTMLElement}}
 */
const scriptMap = {}

function log(...messages) {
  console.log(prefix, ...messages)
}

function warn(...messages) {
  console.warn(prefix, ...messages)
}

/**
 * 获取需要挂载的父级元素
 * @param {string} selector 选择器
 * @returns {HTMLElement}
 */
function getParentEl(selector) {
  let el = null
  if (selector) el = document.querySelector(selector)
  return el || document.body
}

/**
 * 获取脚本唯一标识
 * @param {string} name 脚本名称
 * @returns {string}
 */
function getScriptId(name) {
  return `${prefix}${name}`
}

/** @typedef {{name: string, src: string, selector?: string, defer?: boolean, async?: boolean}} ScriptOption */

/**
 * 创建并挂载脚本
 * @param {ScriptOption} option 挂载脚本选项
 * @returns {void}
 */
function createScript(option = {}) {
  if (typeof option !== 'object' || option === null) {
    option = {}
  }

  if ([''nullvoid 0].includes(option.src)) {
    return warn('The src property of the option cannot be falsly value!')
  }

  if ([''nullvoid 0].includes(option.name)) {
    return warn(
      'The name property of the option cannot be falsly value! The name property will be used to identify the current script!'
    )
  }

  const scriptId = getScriptId(option.name)

  if (scriptId in scriptMap) {
    return warn('Duplicate name attribute, please re-enter!')
  }

  const script = document.createElement('script')
  const parentEl = getParentEl(option.selector)

  script.id = scriptId
  script.src = option.src
  script.defer = option.defer
  script.async = option.async

  parentEl.append(script)
  scriptMap[scriptId] = {
    parent: parentEl,
    self: script
  }

  log(`The ${name} script been created!`)
}

/**
 * 销毁脚本
 * @param {string} name 脚本名称
 * @returns {string}
 */
function destroyScript(name) {
  const scriptId = getScriptId(name)
  const scriptInfo = scriptMap[scriptId]

  if (!(scriptId in scriptMap) || scriptInfo === undefined) {
    return warn(`The script with name as ${name} does not exist!`)
  }

  scriptInfo.parent.removeChild(scriptInfo.self)
  delete scriptMap[scriptId]

  log(`The ${name} script been destroyed!`)
}

/**
 * Vue 自动挂载三方脚本插件
 * @example
 * Vue.use(Plugin, {})
 * // or
 * Vue.use(Plugin, [{}])
 */
const Plugin = {
  /**
   * 插件名
   */
  namePluginName,
  /**
   * 安装记录
   */
  installedfalse,
  /**
   * 安装插件
   * @param {VueVue
   * @param {ScriptOption|ScriptOption[]|nulloptions
   * @returns {void}
   */
  install(Vue, options) {
    if (Plugin.installed || Vue.$isServerreturn

    if (options) {
      options = Array.isArray(options) ? options : [options]
      options.forEach((opt) => createScript(opt))
    }

    Plugin.installed = true

    Vue.prototype.$createScript = createScript
    Vue.prototype.$destroyScript = destroyScript
  }
}

// export default Plugin // 导出插件入口

// Nuxt plugin
Vue.use(Plugin, [
  /** 加载客服脚本 */
  {
    name'customService',
    src'xxx
  }
])

最后

每一个问题的解决,都为我实现财富自由前进了一步!

初次写文章,不喜勿喷,有问题欢迎评论区留言交流!