乐趣区

关于vuepress:vuepress-插件原理-以及-如何实现一个博客访问验证插件

背景

最近在写博客的时候有这样一个需要:有些博客内容想要通过明码验证的形式来容许他人拜访

试想一下这样的场景:

比方你的个人简历是保护在线上博客的,博客的其余内容所有人都能够拜访,然而像简历这种波及个人信息的内容,就须要通过一种验证机制来限度所有人拜访。如果你在寻找新的工作机会,通过 链接 + 口令 的形式提供给他人本人的简历,是不是也显得极(zhuang)客(bi)了许多。

另一方面,以产品的角度来想,这其实也是变相为你的博客引流的一种形式。

实现上述性能的办法有很多种,如果你的博客刚好也是(或者也想)通过 vuepress + Github Pages 的形式搭建,那我倡议能够持续浏览上来。

我的博客就是应用这种形式构建和部署的,所以遇到这个问题之后,首先就尝试去找是不是 vuepress 有相干的插件能够间接拿来用,不过没有找到本人想要的,所以就只好本人入手撸了一个这样的插件。

vuepress-plugin-posts-encrypt

预览及应用

  1. 插件目前曾经开源,插件的具体装置以及应用形式能够在这里看到。
  2. 想要看成果的话能够间接拜访这个地址,明码:1234
  3. 或者你也能够 clone 下来代码通过上面的形式在本地运行。
  • clone 代码
git clone https://github.com/alphawq/vuepress-plugin-posts-encrypt.git
  • 装置依赖
cd vuepress-plugin-posts-encrypt && yarn
  • 启动服务
yarn dev

不出意外的话,应该就能够关上终端里的链接在本地看到成果了。出意外的话,欢送大家来提 issue。😂

代码仓库是应用 yarn workspaces+lerna 多包治理的形式进行保护,也就是常说的 monorepo。相干的应用教训,其实也能够总结成一篇文章来记录和分享,这个 日后 再说。

实现

以上都是对插件应用上的介绍,如果只是想用这个插件的话,下面的内容曾经足够了。对于实现这一部分,其实次要是记录一下开发过程中遇到的一些问题,以及对应的解决方案,并不会聊太多代码实现相干的内容。

同时,如果你也想要开发一个 vuepress 插件的话,那接下来的内容可能会对你有所帮忙。

码农码码码字码砖都不容易,有帮忙的话,劳烦给个 Star!💋

计划一

看官网文档介绍:VuePress 由两局部组成:第一局部是一个极简动态网站生成器 (opens new window),它蕴含由 Vue 驱动的主题零碎和插件 API,第二局部就不必看了。另外,每一个由 VuePress 生成的页面都带有预渲染好的 HTML,也因而具备十分好的加载性能和搜索引擎优化(SEO)。

看到这里我不得不打断一下,因为一开始这个加密插件的实现思路就是受到这句话的影响,让我误以为 因为 vuepress 构建进去的产物每个 md 页面都会生成一个 html,因而部署到 Github Pages 上之后,它应该是一个 MPA 利用,所以一开始的实现计划就很简略粗犷:

  • 在构建产物生成之后,将须要加密的 html 页面内容加密,而后将密文注入到明码验证的模板里,并重写回文件系统
  • 当用户拜访到这个页面路由的时候,因为是 MPA,所以就会呈现出明码验证模板的内容
  • 待用户输出明码并校验通过之后,再解密出原始内容,通过 document.write 的形式写回去

这个性能实践上就实现了。然而,实际上,文档中还有前面一句话:

同时,一旦页面被加载,Vue 将接管这些动态内容,并将其转换成一个残缺的单页利用(SPA),其余的页面则会只在用户浏览到的时候才按需加载

所以在上述计划实现之后,我部署到线上看成果发现,如果一开始进入的就是这个须要加密拜访的页面是没问题的,可一旦不是这种状况,也就是说一旦页面路由被 vue router 接管,那就 G 了。。。

计划二

既然是单页,那也好办,首先想到就是 vue router 的导航守卫,如果能够在 beforeEnter 钩子外面加上权限校验,那就能在跳转到须要验证明码的页面之前做拦挡,让其重定向到验证页,验证通过后再跳回指标地址就能够了。实践上实现起来应该很容易,接下来就看 vuepress 是不是提供给了咱们相应的能力。

vupress 架构

这是 vuepress 的官网架构图,能够看到 插件 局部是运行在 node 端的,这个官网文档里也有阐明。那插件运行在 node 端,接下来就要思考上面几个问题:

问题一:前端路由是如何生成的?

咱们都晓得 vue router 的实现中依赖浏览器 historypopstate 或者 hashhashchange 事件监听,所以能够必定的是,router 实例的创立必定还是在 Browser 端运行的。然而 route 的生成就不是了,因为 vuepress 是依据约定的目录构造来生成最终页面的,在 vuepress 的设计理念中称其为 约定大于配置。而想要读取文件目录,并生成最终路由 map 的话,这部分只能是在 node 端进行。

针对这个问题,先来看下 vuepress@1.x 的相干实现(卷起来):

vupress 也是一个应用 lerna + yarn workspacesmonorepo 我的项目,packages 目录下有三个包,

其中:

  • @vuepress 是外围实现
  • docs 是用 vuepress 搭建的 vuepress 的官网文档我的项目
  • vuepressCLI 的实现

这里只须要关注 @vuepress 里的实现即可。

看命名就晓得 core 目录就是外围逻辑了,其余的根本都是内置的一些插件以及工具包了。

开发插件的时候,像 shared-utils 工具包里的内容咱们都是能够拿过去用的

core 目录里的内容也很简洁,分为 clientnode,别离是运行在 Browsernode 环境中的代码模块。残缺的实现咱们不一一去看,只看外围流程。

1. index.js 入口文件

这里次要就是创立 App 实例的逻辑,依据环境不同,调用不同的办法。

  • 开发模式下是 dev
  • 生成模式下是 build

能够看到,实例化之后,立刻调用了 process 办法,该办法外部做了很多事件,捡重点说一下:

// index.js 创立 App 实例
function createApp(options) {return new App(options)
}
// 开发模式
async function dev(options) {const app = createApp(options)
  await app.process()
  return app.dev()}
// 生成模式
async function build(options) {const app = createApp(options)
  await app.process()
  return app.build()}

情谊提醒

  • 以下波及到的目录,默认都是在 core/lib 目录下
  • 代码有删减和变更,为了不便了解

正文中波及到的 生命周期 Option API 相干的局部,其实就是官网文档中 插件开发 局部提到的,能够在插件外部定义的 API 的分类

  • 生命周期 API
  • Option API
  • Context 其实就是 App 实例,下面挂载了很多属性和办法

2. App 类 – node/App.js

class App {process() {
    // ======== 1. 初始化 ======== //
    this.pages = [] // Array<Page>
    // 创立 PluginAPI 实例
    this.pluginAPI = new PluginAPI(this)

    // ===== 2. 外部通过调用 this.pluginAPI.use() 办法注册插件 ====== //
    // 先解决外部插件
    this.applyInternalPlugins()
    // 再解决用户插件
    this.applyUserPlugins()

    // ====== 3. 遍历注册实现的插件,将每个插件中波及到的所有无关 ===== //
    // ====== 生命周期 和 Option API 相干的办法提取进去,寄存到 ===== //
    // ====== pluginAPI 实例上的 options 属性中的 items 数组中去 ===== //
    this.pluginAPI.initialize()

    // =========== 这部分是生成 page 的中央 ========= //
    await this.resolvePages()

    // ====== 4. 这里才是真正的通过 key 去调用 插件中定义了这个 [key] 属性的属性值(是 function 的话,就调用,不是的话间接返回值)//
    await this.pluginAPI.applyAsyncOption('additionalPages', this)
    await Promise.all(
      this.pluginAPI
        .getOption('additionalPages')
        .appliedValues.map(async (options) => {await this.addPage(options)
        })
    )
    await this.pluginAPI.applyAsyncOption('ready')
    await Promise.all([this.pluginAPI.applyAsyncOption('clientDynamicModules', this),
      this.pluginAPI.applyAsyncOption('enhanceAppFiles', this),
      this.pluginAPI.applyAsyncOption('globalUIComponents', this),
    ])
  }
}

首先看下 process 办法都做了哪些事件

  • 创立了 pluginAPI 实例
  • 注册 外部插件
  • 注册 用户定义的插件
  • 遍历所有注册的插件,并将插件中定义的所有无关 生命周期 & Option API 属性的值,放到 pluginAPI.options 对应 keyitems 数组里

    • 这一步是的具体操作能够在上面的 PluginAPI 类 局部看到如下:
this.pluginAPI.options = {
  ready: {items: [function ready, function ready]
  },
  additionalPages: {items: [function additionalPages, function  additionalPages]
  },
  // ...
}
  • 生成 page, 这部分放到本节最初说
  • 最初,才是通过,如:this.pluginAPI.applyAsyncOption('additionalPages', this) 真正的去调用 所有插件中定义的 additionalPages 这个属性的属性值(是 Function 的话,就调用,不是的话间接返回值)

3. PluginAPI 类 – plugin-api/index.js

PluginAPI 是插件实例的结构器,它的外部应用到了 Option 类以及 ModuleResolver

// ===== PluginAPI ==== //
class PluginAPI {constructor(context) {
    // 这外面是用来寄存 Options 实例的,模式大略是上面这样
    this.options = {}
    // 这外面是用来寄存通过 this._pluginResolver.resolve 办法解决过后的 plugin 的
    this._pluginQueue = []
    // 它是一个 ModuleResolver 实例,有一个 resolve 办法,用来查找不同类型的模块
    this._pluginResolver = new ModuleResolver(
      'plugin',
      'vuepress',
      [String, Function, Object],
      true /* load module */,
      cwd
    )
    // 依据 PLUGIN_OPTION_MAP 中每个对象的 async 配置,决定是初始化一个 AsyncOption 还是 Option
    this.initializeOptions(PLUGIN_OPTION_MAP)
  }

  /**
   * 通过 resolver 的 resolve 办法将原始的 pluginRaw 标准化成如下模式的一个对象,并把它放到 _pluginQueue 里
   *
   * */
  use() {
    // 标准化
    plugin = this._pluginResolver.resolve(pluginRaw)
    // 这外面寄存的是标准化后的 plugin 对象
    this._pluginQueue.push(plugin)
  }

  // 插件的初始化操作
  initialize() {
    this._initialized = true
    this._pluginQueue.forEach((plugin) => this.applyPlugin(plugin))
  }
  /**
   * 从插件对象中构造进去波及到所有无关 生命周期 & Option
   * 将它们一一定义到 this.options 对象上,* this.options 对象是一个 Option 实例,实际上就是放到该实例的 items 数组中去了
   * */
  applyPlugin({
    name: pluginName,
    shortcut,
    ready /* 生命周期相干的 hook*/,
    // ...
    enhanceAppFiles /*Options API 相干的 */,
    // ...
  }) {
    // 很多个
    this.registerOption(PLUGIN_OPTION_MAP.READY.key, ready, pluginName)
      // ...
      .registerOption(
        PLUGIN_OPTION_MAP.ENHANCE_APP_FILES.key,
        enhanceAppFiles,
        pluginName
      )
      // ...
      .registerOption(
        PLUGIN_OPTION_MAP.CLIENT_DYNAMIC_MODULES.key,
        clientDynamicModules,
        pluginName
      )
    // ...
  }

  /**
   * 将每个插件中波及到的 插件 API 存储到 this.options 对象的 items 属性中
   * */
  registerOption(key, value, pluginName) {let option = [key]
    this.options[option.name].add(pluginName, value)
  }

  // 不同类型的 API 创立不同类型的 Option 实例,EnhanceAppFilesOption、DefineOption 等它们都是继承自 Option 的,次要的区别就在于 apply 办法的不同
  initializeOptions() {Object.keys(PLUGIN_OPTION_MAP).forEach((key) => {const option = PLUGIN_OPTION_MAP[key]
      this.options[option.name] = (function instantiateOption({ name, async}) {switch (name) {
          case PLUGIN_OPTION_MAP.ENHANCE_APP_FILES.name:
            return new EnhanceAppFilesOption(name)

          case PLUGIN_OPTION_MAP.CLIENT_DYNAMIC_MODULES.name:
            return new ClientDynamicModulesOption(name)

          case PLUGIN_OPTION_MAP.GLOBAL_UI_COMPONENTS.name:
            return new GlobalUIComponentsOption(name)

          case PLUGIN_OPTION_MAP.DEFINE.name:
            return new DefineOption(name)

          case PLUGIN_OPTION_MAP.ALIAS.name:
            return new AliasOption(name)
          // 不是下面的类型的则依据 async 属性来创立 AsyncOption 或 Option
          default:
            return async ? new AsyncOption(name) : new Option(name)
        }
      })(option)
    })
  }
}
  • this.options 这个对象寄存的是 Option 实例
  • this._pluginResolver 这个对象寄存的是通过 ModuleResolver 类创立的实例
  • use 办法:

    • 用于把通过 this._pluginResolver.resolve 办法标准化解决过后的插件对象寄存到 this._pluginQueue 这个数组里,标准化解决后的模式如下

      {
      // 插件的名称
      name: "@vuepress/internal-routes",
      // 插件中用到的 API
      clientDynamicModules: async clientDynamicModules () {const code = importCode(ctx.globalLayout) + routesCode(ctx.pages)
        return {name: 'routes.js', content: code, dirname: 'internal'}
      },
      // 简化名称
      shortcut: null,
      // 是否可用
      enabled: true,
      // 传给插件的参数
      $$options: {},
      // 是否应标准化过了
      $$normalized: true,
      // 是否能够被屡次调用,为 false 的话,会被去重解决
      multiple: false
      }
  • initialize 办法:

    • 遍历 this._pluginQueue 里存储的插件对象,并将插件对象中定义的 无关 生命周期 以及 Option API(如下面的例子中定义的 clientDynamicModules 办法)的内容提取进去
    • 通过上面的模式,pushthis.options['clientDynamicModules'].items 数组中去
    this.registerOption('CLIENT_DYNAMIC_MODULES', async clientDynamicModules () {const code = importCode(ctx.globalLayout) + routesCode(ctx.pages)
      return {name: 'routes.js', content: code, dirname: 'internal'}
    }, '@vuepress/internal-routes')

下面用到的 PLUGIN_OPTION_MAP 大略长这个样子:

const PLUGIN_OPTION_MAP = {
  // 生命周期相干的
  READY: {
    key: 'READY',
    name: 'ready',
    types: [Function],
    async: true,
  },
  // ...

  // Options API 相干的
  ENHANCE_APP_FILES: {
    key: 'ENHANCE_APP_FILES',
    name: 'enhanceAppFiles',
    types: [String, Object, Array, Function],
  },
}
  • 其中的 type 属性是一个数组,示意这个 API 能够接管什么类型的数据,比方

    • ready 就只反对 函数类型
    • enhanceAppFiles 则反对 字符串、对象、数组、函数多种类型
      这和文档中定义的类型是一一对应的
  • async 属性则是用来标识是创立一个 AsyncOption 实例还是一个 Option 实例

    • 它俩的次要区别在于插件被真正调用求值的时候,是否须要 await 关键字来解决

4. Option 基类 – plugin-api/abstract/Option.js

  • add 办法增加插件
  • apply 办法调用插件
class Option {constructor(key) {
    this.key = key
    this.items = []}

  add(name, value) {if (Array.isArray(value)) {return this.items.push(...value.map((i) => ({value: i, name})))
    }
    this.items.push({value, name})
  }

  syncApply(...args) {
    const rawItems = this.items
    this.items = []
    // 被调用求值后的插件对象寄存在这里
    this.appliedItems = this.items

    for (const { name, value} of rawItems) {
      // 调用插件中定义的 API,并将返回值放到 items 数组中去
      this.add(name, isFunction(value) ? value(...args) : value)
    }
    // 调用完之后,从新将 items 指针指回原来的对象
    this.items = rawItems
  }
}

Option.prototype.apply = Option.prototype.syncApply

5. ModuleResolver – @vuepress/shared-utils/lib/moduleResolver.js

这个次要看下 resolve 办法就能够了,略过

function tryChain(resolvers, arg) {
  let response
  for (let resolver of resolvers) {if (!Array.isArray(resolver)) {resolver = [resolver, true]
    }
    const [provider, condition] = resolver
    if (!condition) {continue}
    try {response = provider(arg)
      return response
    } catch (e) {}}
}

class ModuleResolver {resolve(req) {const isStringRequest = isString(req)
    const resolved = tryChain(
      [
        /**
         * Resolve non-string package, return directly.
         */
        [this.resolveNonStringPackage.bind(this), !isStringRequest],
        /**
         * Resolve module with absolute/relative path.
         */
        [this.resolvePathPackage.bind(this), isStringRequest],
        /**
         * Resolve module from dependency.
         */
        [this.resolveDepPackage.bind(this), isStringRequest],
      ],
      req
    )
    return resolved
  }
}

总结

上面就来回顾下插件被注册和应用的整个流程

  • App 实例上有一个通过 PlginAPI 类构建的 pluginAPI 实例对象
  • pluginAPI 实例上有一个 _pluginResolver 属性,它是通过 ModuleResolver 类的实例,次要利用实例上的 resolve 办法来查找插件模块,并实现标准化
  • pluginAPI 实例上有一个 options 属性,用来寄存不同类型的 option 实例, 如 AsyncOption/EnhanceAppFilesOption/...

    • 不同类型的 Option 结构器都是基于基类 Option 扩大的,他们之间次要是 apply 办法的不同,也就是插件被真正调用时所要用到的办法
    • 每个 option 实例上都有一个 items 属性,用来寄存通过 _pluginResolver.resolve 办法标准化后的插件办法
  • 在所有插件都通过解决实现之后,不同的 API(如:ready),会通过不同的 key 如:ready,来获取 pluginAPI.options 对象中对应 ready 属性的值,并遍历其 items 属性, 循环调用外面定义的 ready 办法

到这里插件相干的原理就讲完了。

这货色说起来有点绕,可能表白不好,贴两张图就明确了:

1). 下图就是 pluginAPI.options 属性的内容:当调用 await this.pluginAPI.applyAsyncOption('ready') 时,就会走到这里。而items 存储了所有插件中定义的 ready 办法

2). 这张图就是最终的 ready 办法们被调用的中央了

ok,讲完这部分的原理,咱们回到一开始的问题:前端路由是如何生成的?

后面在 App 实例化的过程中,有一步生成 page 的局部没有说,放到这里来说正合适。

  async resolvePages () {
    // ...
    const pageFiles = sort(await globby(patterns, { cwd: this.sourceDir}))
    await Promise.all(pageFiles.map(async (relative) => {const filePath = path.resolve(this.sourceDir, relative)
      await this.addPage({filePath, relative})
    }))
  }

逻辑很简略,就是遍历文件目录,解决文件内容并生成 page 对象,而后增加到 this.pages 数组里就完了,贴两张图看下长啥样就能够了

  • pageFiles

  • this.pages

好了,这里 this.pages 外面有了内容,上面就可以看是如何生成前端路由的了。

能有急躁看到这里的都是壮士,我都快写不上来了 😂😂😂

  1. 路由的生成

能够说 vuepress 中绝大部分性能都是通过插件来实现的,其中路由的生成就是一个外部插件,它的定义在:

core/lib/internal-plugins/route.js

module.exports = (options, ctx) => ({
  name: '@vuepress/internal-routes',
  // @internal/routes
  async clientDynamicModules() {const code = importCode(ctx.globalLayout) + routesCode(ctx.pages)
    return {name: 'routes.js', content: code, dirname: 'internal'}
  },
})

能够看到,route 插件的外部实现,用到了 clientDynamicModules 这个 Option API。无关外部 importCode 以及 routesCode 的实现,大家能够去源码里翻一下,比较简单,次要就是通过字符串拼接的形式,生成可能被前端调用的一个模块,只不过是字符串的模式。

下面曾经讲过了插件的注册以及调用过程,这里就看下当 clientDynamicModules 被调用时,最初返回的后果就好了。
上面的后果就是返回对象中的 content 属性的值,只不过代码中它只是一串字符串,写到文件系统里就成了一个可被 client 端调用的模块了

import {injectComponentOption, ensureAsyncComponentsLoaded} from '@app/util'
import rootMixins from '@internal/root-mixins'
import GlobalLayout from '/Users/wangqiang/Personal/blog/node_modules/@vuepress/core/lib/client/components/GlobalLayout.vue'

injectComponentOption(GlobalLayout, 'mixins', rootMixins)
export const routes = [
  {
    name: 'v-2da0cf04',
    path: '/',
    component: GlobalLayout,
    beforeEnter: (to, from, next) => {ensureAsyncComponentsLoaded('Layout', 'v-2da0cf04').then(next)
    },
  },
  {
    path: '/index.html',
    redirect: '/',
  },
  {
    path: '*',
    component: GlobalLayout,
  },
]

这里只是返回了文件的字符串内容,而写入文件系统的操作是在调用这个插件之后,也就是在
node/App.jsprocess 办法的最初 this.pluginAPI.applyAsyncOption('clientDynamicModules') 这段代码的外部执行逻辑里

class ClientDynamicModulesOption extends AsyncOption {async apply(ctx) {await super.asyncApply()

    for (const { value, name: pluginName} of this.appliedItems) {const { name, content, dirname = 'dynamic'} = value
      await ctx.writeTemp(`${dirname}/${name}`,
        `
/**
 * Generated by "${pluginName}"
 */
${content}\n\n
        `.trim())
    }
  }
}

最终将路由文件写入到了用户的 node_modules/@vuepress/core/.temp/internal 目录下。到这里对于前端路由的生成就讲完了

问题二:vue-router 是如何接管前端路由的呢

vuepress 外部通过 webpack 构建的后果是为每个 md 文件生成一个 html 页面,这也是一开始为什么我会认为它是一个 MPA 利用的起因,思维有点固化了,毕竟咱们常见的 SPA 利用,简直都是只有一个 index.html 模板文件。那多个 html 页面怎么做 SPA 呢?

其实也很简略,跟 SPA 一个情理。

咱们晓得传统的 SPA 利用,服务端返回的模板文件中都会蕴含几个公共 js 文件,最常见的比方 app.xxx.js,既然都是公共的,那只有给每个产出的 html 文件内都注入进这些公共的 js 文件不就能够了吗?实际上,vuepress 也就是这样做的。

这样做的另外一个益处就是,不须要 Github Pages 服务来反对,就实现了 SPA。谁说 SPA 肯定须要服务端反对的?打他!
相比于 spa-github-pages 提供的 hack 的形式实现 SPA,是不是就优雅了很多。

说了这么多,还是回归主题:怎么去实现一个插件,能够做到针对私密路由的验证拜访呢?

其实原理也很简略。通过上面对 vuepress 插件从注册到调用的整个流程的剖析,置信你也曾经有了本人的思路。上面就说下我的实现:

第一步:导航守卫

计划二一开始曾经提到了,要针对路由做验证,那最好的中央必定就是 vue router 的导航守卫,而 vuepress 的插件体系也恰好为咱们提供了这样的一个契机,让咱们能够在 router 上做文章。

这一步用到的 API 就是 enhanceAppFiles,在这个 API 里咱们能够拿到 router 实例:

export default ({
  Vue, // VuePress 正在应用的 Vue 构造函数
  options, // 附加到根实例的一些选项
  router, // 以后利用的路由实例
  siteData, // 站点元数据
  isServer, // 以后利用配置是处于 服务端渲染 或 客户端
}) => {// ...}

说实话,一开始看到这个 API 齐全不了解他是做什么的,看了官网提供的几个插件的实现才晓得,原来它跟咱们在我的项目外面定义的 enhanceApp.js 的性能是一样的,只不过咱们在插件中定义的这个 api 会晚于用户的 enhanceApp.js 的执行。而且它也跟生成 route 文件的形式一样,会在 node 端被写入到文件系统中的 @vuepress/core/.temp/app-enhancers 目录下

第二步:路由加密配置

想要实现路由拦挡,前提得先晓得哪些路由是须要通过验证能力拜访的,那这里就会波及到 route 生成的局部。与方才不一样的是,vuepress 并没有提供给咱们能够钩入 route 生成逻辑的钩子,但 route 的生成依赖通过对文件系统的遍历生成 page 实例的过程,而这个过程,是有 API 能够进行干涉的,那就是 extendPageData

所以咱们只须要依据用户在 Front Matter 中的配置,在这里获取到之后给 page 实例扩大一个须要加密拜访的 标记 就能够了

第三步:验证页面的生成

须要验证拜访的标记有了,下一步就是如何生成一个 明码验证 的页面,让那些须要验证的路由跳转到明码验证页下来。这一步的实现其实有很多种计划,一开始我想的是通过 Vue.extend 创立一个组件结构器的形式,就像咱们罕用的 Modal 弹层组件一样,在须要验证明码的时候弹出来,验证通过之后暗藏并跳转。这样做的一个益处就是,我能够间接应用 SFC 来定义这个组件,而且也不须要额定引入其余的货色,实现起来也简略。

然而这种形式也有一些弊病,比方:

1、用户如果想定制化本人的验证页面怎么办?
2、我懒得本人写款式,想用现成的组件库怎么办?
3、组件库的问题其实能够解决,然而 vuepress 自身是能够利用其余第三方的主题,如果第三方主题也引入了不同的组件库造成抵触怎么办?
4、这种形式实现,简直所有的逻辑都放到 导航守卫 外面去做,耦合重大且不优雅

所以针对下面存在的几个问题,最终没有采纳这种形式去做,而是采纳了另外一种形式,让 导航守卫 还是专职本人的跳转工作,明码验证的逻辑独自抽离到一个脱离 vuepress 路由零碎的 html 页面中来。这样的话,无论我是援用第三方组件库还是其余资源都能够做到很自在。

当然这种形式也不是没有问题,因为脱离了 vuepress 利用的路由零碎,意味着我没有方法通过 router 编程式的导航做页面跳转,只能通过 location.replace、location.href 或者 a 标签的形式跳转页面,这样做就会导致页面的刷新,用户体验必定不如应用 router 的形式好(不过理论体验下来,感觉也还能够)。

最初总结

终于写完了,如果有人能从头到尾一点点看到这里,那这个人肯定不是我 … 如果是你的话,那我给你点赞,道一声:お疲れさまでした~

其实,vuepress@2.x 当初也曾经在 beta 阶段了,当初写这些基于 1.x 的内容仿佛有点 out,而且当初也曾经有了通过 vite + vue3vitepress

怎么说呢?集体感觉,技术这种货色,学到了就是不亏。如果之前没有接触过 vuepress 那我倡议,能够间接去玩 vitepress

新技术的呈现,多半是因为旧技术存在的缺点和有余,或者是无奈适应以后的大环境而催生进去的,所以理解技术历史其实也有利于咱们去躲避前辈们踩过的坑。

再者,技术中应用的各种编程思维经常是比技术自身更有意义。就比如说 1.x 版本中的插件零碎的实现:主题即插件、配置文件也是插件、而且还能够在插件中应用插件,这种插件零碎的架构和实现给整个利用带来了很强的灵活性和扩展性,这种思维就很值得去学习。

最初,如果感觉本文对你有帮忙,烦请移步 github 给个 star(低微码农在线求 ⭐️)

如果感觉没有帮忙的话,那肯定是你没有好好看这篇文章,我写的货色怎么可能没有用?作为一名合格的码农,我写的代码怎么可能有 Bug?那是用户不会用!

哈哈,开个玩笑~

ありがとう~

退出移动版