background
When writing a blog recently, there is such a demand: Some blog content wants to pass password verification to allow others to access .
Imagine this scenario:
For example, your personal resume is to maintain an online blog, and other content of the blog can be accessed by everyone, but content involving personal information like a resume requires a verification mechanism to restrict everyone's access. If you are looking for a new job opportunity, you link + password. Does it appear to be a lot of customers (bi)?
On the other hand, from a product perspective, this is actually a way to disguise your blog to attract traffic.
There are many ways to achieve the above functions. If your blog happens to be (or also want to) vuepress
+ Github Pages
, then I suggest you to continue reading.
My blog is built and deployed in this way, so after encountering this problem, first try to find out if vuepress
has related plug-ins that can be used directly, but I didn’t find what I wanted, so I had to do it myself I started using such a plug-in.
vuepress-plugin-posts-encrypt
Preview and use
- The plug-in is currently open source. The specific installation and usage of the plug-in can be found at .
- Want to see results, then you can access directly this address , password:
1234
. - Or you can
clone
code from 06143a95a25d22 and run it locally in the following way.
clone
code
git clone https://github.com/alphawq/vuepress-plugin-posts-encrypt.git
- Installation dependencies
cd vuepress-plugin-posts-encrypt && yarn
- Start service
yarn dev
If nothing happens, you should be able to open the link in the terminal and see the effect locally. In the event of an accident, everyone is welcome to mention issue
. 😂
The code repository is using the 16143a95a25e40 yarn workspaces +
lerna multi-package management method, which is often monorepo
. Relevant experience can actually be summarized into an article to record and share, and I will talk about it later.
accomplish
The above is an introduction to the use of the plug-in. If you just want to use this plug-in, the above content is enough. Regarding the implementation of this part, in fact, it is mainly to record some problems encountered during the development process and the corresponding solutions, and will not talk about too much code implementation related content.
At the same time, if you also want to develop a vuepress
plug-in, then the following content may be helpful to you.
Code bricks are not easy, if you have any help, please give Star! 💋
Option One
Look at the official document introduction: VuePress is composed of two parts: The first part is a minimalist static website generator (opens new window), which contains the theme system and plug-in API driven by Vue, and the second part is unnecessary. In addition,
every page generated by VuePress comes with pre-rendered HTML, so it has very good loading performance and search engine optimization (SEO).
See here I have to interrupt, because the beginning of the realization of ideas encryption plug is affected by this sentence, so I mistakenly thought because vuepress
build out products of each md
page will generate a html
, therefore deployed to Github Pages
MPA
, it should be a 06143a95a25ef6 application, so the initial implementation scheme is very simple and rude:
- After the build product is generated, encrypt the
html
page that needs to be encrypted, and then inject the cipher text into the password verification template and rewrite it back to the file system - When the user visits this page route, because it is
MPA
, the content of the password verification template will be presented - After the user enters the password and passes the verification, decrypt the original content and write it back
document.write
This function is theoretically realized. However, in fact, there is the following sentence in the document:
At the same time, once the page is loaded, Vue will take over the static content and convert it into a complete single page application (SPA), and other pages will be loaded on demand only when the user browses to it.
So after the implementation of the above scheme, I deployed it to see the effect online and found that if the page that needs to be encrypted access is entered at the beginning, it is okay, but if this is not the case, that is to say, once the page routing is taken over vue router
That's G. . .
Option II
Since it is a single page, it is easy to handle. The first thing that comes to mind is vue router
. If permission verification can be added to the beforeEnter
hook, then it can be intercepted before jumping to the page that needs to be verified and redirected Go to the verification page, and then jump back to the target address after the verification is passed. In theory, it should be easy to implement. Next, let's see if vuepress
provides us with the corresponding capabilities.
vupress architecture
This is vuepress
. You can see that the plug-in part is running on the node
end, which is also explained in this official document. The plug-in runs on the node
side, and then we have to think about the following questions:
Question 1: How is the front-end routing generated?
We all know that vue router
relies on the browser history
's popstate
or hash
's hashchange
event monitoring, so it is certain that the creation of the router
Browser
side. But route
generated not because vuepress
is to generate the final page of the directory structure of the agreement, in vuepress
called for the design of convention is greater than configuration. If you want to read the file directory and generate the final route
map
, this part can only be done on the node
side.
To address this issue, first look at the vuepress@1.x
associated implementation (of rolled up ):
vupress
also use a lerna + yarn workspaces
of monorepo
project, packages
directory there are three packages,
in:
@vuepress
is the core implementationdocs
is the official document project ofvuepress
built withvuepress
vuepress
is an implementationCLI
Here only need to pay attention to the implementation in @vuepress
Looking at the naming, you know that the core
directory is the core logic, and the others are basically built-in plug-ins and toolkits.
development of plug-in time, like shared-utils
the contents of the bag of tools we can all be used to take over
core
directory is also very concise, divided into client
and node
, which are code modules Browser
and node
We will not look at the complete implementation one by one, only the core process.
1. index.js entry file
Here is mainly App
instance of 06143a95a2624a. According to different environments, different methods are called.
- In development mode, it is
dev
- In the generation mode, it is
build
As you can see, after instantiation, the process
method is called immediately. The method does a lot of things inside, so let’s talk about it:
// 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()
}
Friendly reminder
- The directories involved below are all in the
core/lib
directory by default - The code has been deleted and changed, in order to facilitate understanding
life cycle and
Option API
related parts in the comments are plug-in development section in the official document, and the API classification that can be defined inside the plug-in
- Life Cycle API
- Option API
- Context is actually an App instance with many attributes and methods mounted on it
2. App category- 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),
])
}
}
First look at process
method has done
- Created
pluginAPI
instance - Register internal plug-in
- Register user-defined plugin
Traverse all registered plug-ins, and put all the
life cycle &
Option API
attribute values defined in the plug-in into theitems
array ofpluginAPI.options
corresponding tokey
- The detailed operation of this step can be seen in the
PluginAPI class section below:
- The detailed operation of this step can be seen in the
this.pluginAPI.options = {
ready: {
items: [function ready, function ready]
},
additionalPages: {
items: [function additionalPages, function additionalPages]
},
// ...
}
- generates
page
, this part is put at the end of this section to say - Finally, is passed, such as:
this.pluginAPI.applyAsyncOption('additionalPages', this)
true to call all plug-ins defined inadditionalPages
the property value of the property (is Function, then it is called, is not the case directly to the return value)
3. PluginAPI class- plugin-api/index.js
PluginAPI
is the constructor of the plug-in instance, which uses the Option
and ModuleResolver
types internally
// ===== 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
This object stores theOption
instancethis._pluginResolver
This object stores an instance createdModuleResolver
use
Method:Used to store the plug-in object standardized
this._pluginResolver.resolve
this._pluginQueue
, the standardized form is as follows{ // 插件的名称 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
method:- Traverse the plug-in objects stored in
this._pluginQueue
, and extract the contentslife cycle and
Option API
clientDynamicModules
method defined in the above example) defined in the plug-in object - In the following form, from
push
tothis.options['clientDynamicModules'].items
array
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')
- Traverse the plug-in objects stored in
PLUGIN_OPTION_MAP
used above looks like this:
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],
},
}
The
type
attribute is an array, indicating what type of data this API can receive, such asready
only supports function typesenhanceAppFiles
supports multiple types of strings, objects, arrays, and functions
This corresponds to the type defined in the document one-to-one
async
attribute is used to identify whether to create aAsyncOption
instance or aOption
instanceawait
keyword is needed to process the plug-in when it is actually called for evaluation.
4. Option base class- plugin-api/abstract/Option.js
add
Method to add plug-insapply
method call plugin
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
This mainly resolve
on the 06143a95a269e2 method, skip
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
}
}
Summarize
Let’s review the entire process of plug-in registration and use below.
App
the Examples are by aPlginAPI
class constructedpluginAPI
instance object- There is a
_pluginResolver
pluginAPI
instance, which isModuleResolver
class, mainly using the resolve method on the instance to find the plug-in module and complete the standardization pluginAPI
has one instanceoptions
attributes used to store different types ofoption
example, asAsyncOption/EnhanceAppFilesOption/...
- Different types of
Option
constructors are based on the base classOption
extensions, the mainapply
method, which is the method used when the plug-in is actually called - Each
option
instance has aitems
attribute, which is used to store the plug-in method standardized_pluginResolver.resolve
- Different types of
- After all plug-ins have been processed, different
API
(eg:ready
), will pass differentkey
: Asready
, acquirespluginAPI.options
objects correspondingready
value of the property, and traverse itsitems
properties, cycle call defined insideready
method
The principle of plug-in related is finished here.
This thing is a bit convoluted, it may not be well expressed, just post two pictures to understand:
1). The following figure pluginAPI.options
the content of the 06143a95a26b87 attribute: when you call await this.pluginAPI.applyAsyncOption('ready')
, you will come here. And items
ready
methods defined in all plug-ins
2). This picture is where the final ready
methods are called
the principle of this part, let's go back to the question at the beginning: How is the 16143a95a26bcf front-end route generated?
Previously, in App
, there was a step to generate page
, which was not mentioned, and it is appropriate to put it here.
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 })
}))
}
The logic is very simple. It is to traverse the file directory, process the file content and generate the page
object, and then add it to the this.pages
array.
pageFiles
this.pages
Okay, there this.pages
here, let's see how to generate front-end routing.
can have the patience to see that the people here are all warriors, I can
- Route generation
It can be said that most of the functions in vuepress are implemented through plug-ins. The generation of routing is an internal plug-in, which is defined in:
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' }
},
})
As you can see, the internal implementation of the route
clientDynamicModules
Option API
. routesCode
the implementation of the internal importCode
and 06143a95a26d5e, you can go to the source code to look it up. It is relatively simple, mainly to generate a module that can be called by the front end through string splicing, which is only in the form of a string.
I have already talked about the registration and calling process of the plug-in. clientDynamicModules
take a look at the final result returned when 06143a95a26d95 is called.
The following result is content
attribute in the returned object, but in the code, it is just a string of strings. When written to the file system, it becomes a module that 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,
},
]
Here only the string content of the file is returned, and the operation of writing to the file system is after calling this plug-in, that is, innode/App.js
of the process
method of this.pluginAPI.applyAsyncOption('clientDynamicModules')
, the internal execution logic of the code 06143a95a26df8
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()
)
}
}
}
Finally, the routing file is written to the user's node_modules/@vuepress/core/.temp/internal
directory. This is the end of the generation of front-end routing.
Question 2: vue-router
take over the front-end routing?
The result of building vuepress
internally by webpack
is to generate a html
md
file. This is also the reason why I thought it was a MPA
application at the beginning. The thinking is a bit solidified. After all, we SPA
applications, almost all of them. A index.html
template file. How to do SPA with multiple html
In fact, it is very simple, the same reason as SPA
We know that in the traditional SPA
application, the template file returned by the server will contain several public js
files. The most common ones are app.xxx.js
. Since they are all public, just inject these public files into html
js
n’t the 06143a95a26ebe file okay? In fact, vuepress
does exactly that.
Another advantage of this is that Github Pages
does not need to be supported by the service to achieve SPA
. Who said SPA
must be supported by the server? Hit him!
Compared to Spa-GitHub-Pages and the provided hack
way to achieve SPA, elegance is not on a lot.
said so much, I still return to the topic: How can 16143a95a26f0c implement a plug-in that can authenticate access to private routing?
In fact, the principle is also very simple. Through the above vuepress
plug-in, I believe you already have your own ideas. Let me talk about my implementation:
Step 1: Navigation Guard
Scheme 2 has already been mentioned at the beginning. To verify the routing, the best place is vue router
the navigation guard of vuepress
, and the plug-in system of 06143a95a26f51 also provides us with such an opportunity, so that we can make a fuss router
.
The API used in this step is enhanceAppFiles
, in this API we can get the router
instance:
export default ({
Vue, // VuePress 正在使用的 Vue 构造函数
options, // 附加到根实例的一些选项
router, // 当前应用的路由实例
siteData, // 站点元数据
isServer, // 当前应用配置是处于 服务端渲染 或 客户端
}) => {
// ...
}
To be honest, when I saw this API
, I didn’t understand what he was doing. I saw the implementation of several plug-ins provided by the government. It turns out that it has the same function as the enhanceApp.js
api
defined in the plug-in will be executed later than the user's enhanceApp.js
And it is also the same way as the route
file is generated, it will be written to the @vuepress/core/.temp/app-enhancers
directory node
Step 2: Routing encryption configuration
If you want to implement route interception, you must first know which routes need to be authenticated before you can access, then the part generated by route
Unlike just now, vuepress
does not provide us with hooks to hook into the route
generation logic, but route
page
instances by traversing the file system, and this process has API
can intervene , That is extendPageData
So we only need to extend a tag that requires encrypted access to the page instance after obtaining it here Front Matter
Step 3: Verify page generation
There is a mark that needs to verify access. The next step is how to generate a password verification page, so that those routes that need verification will jump to the password verification page. There are actually many ways to implement this step. At the beginning, I thought of
Vue.extend
, just like our commonly used Modal
pop-up component, it pops up when the password needs to be verified, and it will be hidden after the verification is passed. And jump. One advantage of this is that I can directly use SFC
to define this component, and there is no need to introduce other things, and it is easy to implement.
But this method also has some drawbacks, such as:
1. What if users want to customize their own verification page?
2. What should I do if I am too lazy to write styles by myself and want to use the ready-made component library?
3. The problem of the component library can actually be solved, but vuepress
itself can be applied to other third-party themes. What if the third-party themes also introduce different component libraries and cause conflicts?
4. In this way, almost all the logic is put into the navigation guard to do it, the coupling is serious and not elegant
Therefore, in response to the above problems, we did not adopt this method in the end, but adopted another method, so that the navigation guard is still a full-time jump job, and the logic of password verification is separately extracted to a route vuepress
The system's html
page is coming. In this way, no matter if I refer to third-party component libraries or other resources, I can do it very freely.
Of course, this way is not without problems, because out of vuepress
routing system application, which means I can not pass router
do the page jump programmatic navigation, only through location.replace、location.href
or a
jump page as a tab, so that you do It will cause the page to refresh, and the user experience is definitely not as good as using router
(but after the actual experience, it feels okay).
Final summary
Finally finished, if someone can see here a little bit from beginning to end, then this person must not be me...If it is you, then I will give you a thumbs up and : 16143a95a271aeお Fatty れさまでした~
In fact, vuepress@2.x
now has beta
stage, and now these write-based 1.x
content seems a bit out
, and now has been through vite + vue3
of vitepress
.
how to say? I personally feel that if you learn something like technology, you won’t lose money. If you haven't been in contact with vuepress before, I suggest that you can play vitepress
directly.
The emergence of new technologies is mostly due to the defects and deficiencies of the old technologies, or the inability to adapt to the current environment. Therefore, understanding the history of technology can actually help us avoid the pits that our predecessors have stepped on.
Furthermore, the various programming ideas used in technology are often more meaningful than the technology itself. For example, the implementation of the plug-in system in version 1.x: the theme is the plug-in, the configuration file is also the plug-in, and you can also use the plug-in in the plug-in. The architecture and implementation of this plug-in system brings strong flexibility to the entire application Sexuality and scalability, this kind of thinking is worth learning.
Finally, if you think this article is helpful to you, please move to github to give a star
(Humble code farmers online request ⭐️)
If you feel that it is not helpful, then it must be because you have not read this article carefully. How could the things I wrote be useless? As a qualified code farmer, how can the code I write have Bug
? That is the user will not use it!
Haha, just kidding~
ありがとう~
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。