共计 14184 个字符,预计需要花费 36 分钟才能阅读完成。
背景
最近在写博客的时候有这样一个需要:有些博客内容想要通过明码验证的形式来容许他人拜访。
试想一下这样的场景:
比方你的个人简历是保护在线上博客的,博客的其余内容所有人都能够拜访,然而像简历这种波及个人信息的内容,就须要通过一种验证机制来限度所有人拜访。如果你在寻找新的工作机会,通过
链接 + 口令
的形式提供给他人本人的简历,是不是也显得极(zhuang)客(bi)了许多。
另一方面,以产品的角度来想,这其实也是变相为你的博客引流的一种形式。
实现上述性能的办法有很多种,如果你的博客刚好也是(或者也想)通过 vuepress
+ Github Pages
的形式搭建,那我倡议能够持续浏览上来。
我的博客就是应用这种形式构建和部署的,所以遇到这个问题之后,首先就尝试去找是不是 vuepress
有相干的插件能够间接拿来用,不过没有找到本人想要的,所以就只好本人入手撸了一个这样的插件。
vuepress-plugin-posts-encrypt
预览及应用
- 插件目前曾经开源,插件的具体装置以及应用形式能够在这里看到。
- 想要看成果的话能够间接拜访这个地址,明码:
1234
。 - 或者你也能够
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
的实现中依赖浏览器 history
的 popstate
或者 hash
的 hashchange
事件监听,所以能够必定的是,router
实例的创立必定还是在 Browser
端运行的。然而 route
的生成就不是了,因为 vuepress
是依据约定的目录构造来生成最终页面的,在 vuepress
的设计理念中称其为 约定大于配置
。而想要读取文件目录,并生成最终路由 map
的话,这部分只能是在 node
端进行。
针对这个问题,先来看下 vuepress@1.x
的相干实现(卷起来):
vupress
也是一个应用 lerna + yarn workspaces
的 monorepo
我的项目,packages
目录下有三个包,
其中:
@vuepress
是外围实现docs
是用vuepress
搭建的vuepress
的官网文档我的项目vuepress
是CLI
的实现
这里只须要关注 @vuepress
里的实现即可。
看命名就晓得 core
目录就是外围逻辑了,其余的根本都是内置的一些插件以及工具包了。
开发插件的时候,像 shared-utils
工具包里的内容咱们都是能够拿过去用的
core
目录里的内容也很简洁,分为 client
和 node
,别离是运行在 Browser
和 node
环境中的代码模块。残缺的实现咱们不一一去看,只看外围流程。
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
对应key
的items
数组里- 这一步是的具体操作能够在上面的
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
办法)的内容提取进去 - 通过上面的模式,
push
到this.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
外面有了内容,上面就可以看是如何生成前端路由的了。
能有急躁看到这里的都是壮士,我都快写不上来了 😂😂😂
- 路由的生成
能够说 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.js
的 process
办法的最初 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 + vue3
的 vitepress
。
怎么说呢?集体感觉,技术这种货色,学到了就是不亏。如果之前没有接触过 vuepress 那我倡议,能够间接去玩 vitepress
。
新技术的呈现,多半是因为旧技术存在的缺点和有余,或者是无奈适应以后的大环境而催生进去的,所以理解技术历史其实也有利于咱们去躲避前辈们踩过的坑。
再者,技术中应用的各种编程思维经常是比技术自身更有意义。就比如说 1.x 版本中的插件零碎的实现:主题即插件、配置文件也是插件、而且还能够在插件中应用插件,这种插件零碎的架构和实现给整个利用带来了很强的灵活性和扩展性,这种思维就很值得去学习。
最初,如果感觉本文对你有帮忙,烦请移步 github 给个 star
(低微码农在线求 ⭐️)
如果感觉没有帮忙的话,那肯定是你没有好好看这篇文章,我写的货色怎么可能没有用?作为一名合格的码农,我写的代码怎么可能有 Bug
?那是用户不会用!
哈哈,开个玩笑~
ありがとう~