Jul 06, 2020 in Dev

Links in Saber

虽然 Saber 是基于 Vue 的,但你不需要使用 <router-link> 之类的 Link 组件,你可以直接用 <a> 标签然后 Saber 会自动使用相应的组件并做一些处理,比如为外部链接加上 rel="noopener noreferrer"

这是通过 PostHTML 实现的,Saber 会用一个 webpack loader 处理每一个 Vue 组件中的 <template> 部分:

// webpack config 中的 loader 部分
{
  resourceQuery: /\?vue&type=template/
  loader: './posthtml-loader.js',
  options: {
    plugins: [
      // 一些 PostHTML 插件
      // Saber 允许用户添加额外的插件
    ]
  }
}
// posthtml-loader.js
const posthtml = require('posthtml')

module.exports = async function(source) {
  const done = this.async()
  try {
    const { plugins } = this.query
    const context = { filename: this.resourcePath }
    const { html } = await posthtml(
      plugins.map(plugin => tree => plugin(tree, context)),
    ).process(source, {
      recognizeSelfClosing: true,
    })
    done(null, html)
  } catch (error) {
    done(error)
  }
}

Saber 内置了一些 PostHTML 插件,其中一个就是用来把所有 <a> 标签转换成 <saber-link> 组件的。

这个 loader 对 Markdown 页面也有效,所有 Markdown Link [text](link) 会先转换成 <a> 然后被这个 loader 转换成 <saber-link>,因为 Markdown page 会被 Saber 转换成 Vue 组件来解析。这也是 Markdown 页面支持 <script><style scoped> 这些 Vue SFC 功能的原理。

这里有一个测试可以展示一下转换的效果:

test('basic', async () => {
  const html = await transform(`
  <a href="foo">foo</a>
  <a href="https://example.com">foo</a>
  <a href="mailto:i@example.com">foo</a>
  <saber-link to="/foo">foo</saber-link>
  <saber-link :to="foo">foo</saber-link>
  `)
  expect(html).toBe(`
  <saber-link to="foo">foo</saber-link>
  <saber-link to="https://example.com">foo</saber-link>
  <saber-link to="mailto:i@example.com">foo</saber-link>
  <saber-link to="/foo">foo</saber-link>
  <saber-link :to="foo">foo</saber-link>
  `)
})

test('ignore', async () => {
  const html = await transform(`<a href="foo" saber-ignore>foo</a>`)
  expect(html).toBe(`<a href="foo">foo</a>`)
})

<saber-link> 这个组件主要是用来根据 to 的值来判断什么时候用 <a> 和什么时候用 <router-link>,这一点无法通过这个 PostHTML loader 办到,因为那一步解析发生在编译时,而当你使用 :to 的时候这个值只能在运行时获取到。


Saber 1.0 可能会使用 Vue 3 Compiler 里的 nodeTransforms option 而不是 PostHTML 来处理 template:

// There are two types of transforms:
//
// - NodeTransform:
//   Transforms that operate directly on a ChildNode. NodeTransforms may mutate,
//   replace or remove the node being processed.
export type NodeTransform = (
  node: RootNode | TemplateChildNode,
  context: TransformContext,
) => void | (() => void) | (() => void)[]