feat: refine theme api (#1319)

This commit is contained in:
ULIVZ
2019-02-26 23:53:38 +08:00
committed by GitHub
parent 40b3da8167
commit d16d3d574e
47 changed files with 1014 additions and 399 deletions
Regular → Executable
-45
View File
@@ -22,51 +22,6 @@
### Features
* **$core:** support global layout (close: [#1226](https://github.com/vuejs/vuepress/issues/1226)) ([c91f55a](https://github.com/vuejs/vuepress/commit/c91f55a))
From now on, users have the ability to use a custom global layout component via [siteConfig](https://v1.vuepress.vuejs.org/miscellaneous/glossary.html#siteconfig) or [themeEntryFile](https://v1.vuepress.vuejs.org/miscellaneous/glossary.html#themeentryfile):
```js
module.exports = {
globalLayout: '/path/to/your/global/vue/sfc'
}
```
Here is the [content of default global layout component](https://github.com/vuejs/vuepress/blob/master/packages/%40vuepress/core/lib/app/components/GlobalLayout.vue), an example of setting global header and footer:
```vue
<template>
<div id="global-layout">
<header><h1>Header</h1></header>
<component :is="layout"/>
<footer><h1>Footer</h1></footer>
</div>
</template>
<script>
export default {
computed: {
layout () {
if (this.$page.path) {
if (this.$vuepress.isLayoutExists(this.$page.frontmatter.layout)) {
return this.$page.frontmatter.layout
}
return 'Layout'
}
return 'NotFound'
}
}
}
</script>
```
Also, you can follow the convention, directly create a component `.vuepress/components/GlobalLayout.vue` or `themePath/layouts/GlobalLayout.vue` without any config. the loading priority is as follows:
- siteConfig
- siteAgreement
- themeEntryFile
- themeAgreement
- default
* **$theme-default:** disable search box via frontmatter (close: [#1287](https://github.com/vuejs/vuepress/issues/1287)) ([#1288](https://github.com/vuejs/vuepress/issues/1288)) ([54e9eb0](https://github.com/vuejs/vuepress/commit/54e9eb0))
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
extend: 'vuepress-theme-parent'
}
+1
View File
@@ -0,0 +1 @@
module.exports = {}
@@ -0,0 +1,27 @@
jest.mock('vuepress-theme-parent')
jest.mock('vuepress-theme-child')
import ThemeAPI from '../../lib/theme-api'
import { resolve } from 'path'
const theme = {
path: resolve(process.cwd(), '__mocks__/vuepress-theme-child'),
name: 'vuepress-theme-child',
shortcut: 'child',
entryFile: require('vuepress-theme-child')
}
const parent = {
path: resolve(process.cwd(), '__mocks__/vuepress-theme-parent'),
name: 'vuepress-theme-parent',
shortcut: 'parent',
entryFile: {}
}
describe('ThemeAPI', () => {
test('extend', async () => {
const themeAPI = new ThemeAPI(theme, parent)
console.log(themeAPI.theme.entry)
})
// loadTheme('vuepress-theme-child')
})
+8 -6
View File
@@ -4,12 +4,14 @@ module.exports = (options, context) => ({
name: '@vuepress/internal-enhance-app',
enhanceAppFiles () {
const { sourceDir, themePath } = context
const { sourceDir, themeAPI } = context
const enhanceAppPath = path.resolve(sourceDir, '.vuepress/enhanceApp.js')
const themeEnhanceAppPath = path.resolve(themePath, 'enhanceApp.js')
return [
enhanceAppPath,
themeEnhanceAppPath
]
const files = [enhanceAppPath]
if (themeAPI.existsParentTheme) {
files.push(path.resolve(themeAPI.parentTheme.path, 'enhanceApp.js'))
}
const themeEnhanceAppPath = path.resolve(themeAPI.theme.path, 'enhanceApp.js')
files.push(themeEnhanceAppPath)
return files
}
})
@@ -1,22 +1,13 @@
module.exports = (options, ctx) => {
const { layoutComponentMap } = ctx
const componentNames = Object.keys(layoutComponentMap)
return {
name: '@vuepress/internal-layout-components',
async clientDynamicModules () {
const componentNames = Object.keys(ctx.themeAPI.layoutComponentMap)
const code = `export default {\n${componentNames
.map(name => ` ${JSON.stringify(name)}: () => import(${JSON.stringify(layoutComponentMap[name].path)})`)
.map(name => ` ${JSON.stringify(name)}: () => import(${JSON.stringify(ctx.themeAPI.layoutComponentMap[name].path)})`)
.join(',\n')} \n}`
return { name: 'layout-components.js', content: code, dirname: 'internal' }
},
chainWebpack (config, isServer) {
const setAlias = (alias, raw) => config.resolve.alias.set(alias, raw)
componentNames.forEach(name => {
setAlias(`@${name}`, layoutComponentMap[name].path)
})
}
}
}
@@ -20,7 +20,7 @@ module.exports = (options, ctx) => ({
// 2. write palette.styl
const { sourceDir, writeTemp } = ctx
const themePalette = path.resolve(ctx.themePath, 'styles/palette.styl')
const themePalette = path.resolve(ctx.themeAPI.theme.path, 'styles/palette.styl')
const userPalette = path.resolve(sourceDir, '.vuepress/styles/palette.styl')
const themePaletteContent = fs.existsSync(themePalette)
@@ -34,8 +34,8 @@ module.exports = (options, ctx) => ({
// user's palette can override theme's palette.
let paletteContent = themePaletteContent + userPaletteContent
if (ctx.parentThemePath) {
const parentThemePalette = path.resolve(ctx.parentThemePath, 'styles/palette.styl')
if (ctx.themeAPI.existsParentTheme) {
const parentThemePalette = path.resolve(ctx.themeAPI.parentTheme.path, 'styles/palette.styl')
const parentThemePaletteContent = fs.existsSync(parentThemePalette)
? `@import(${JSON.stringify(parentThemePalette.replace(/[\\]+/g, '/'))})`
: ''
@@ -1,12 +1,16 @@
const { fs, path, logger, chalk } = require('@vuepress/shared-utils')
/**
* @param options
* @param {AppContext} ctx
*/
module.exports = (options, ctx) => ({
name: '@vuepress/internal-style',
enhanceAppFiles: [path.resolve(__dirname, 'client.js')],
async ready () {
const { sourceDir, writeTemp } = ctx
const { sourceDir, writeTemp, themeAPI } = ctx
const overridePath = path.resolve(sourceDir, '.vuepress/override.styl')
const hasUserOverride = fs.existsSync(overridePath)
@@ -15,7 +19,7 @@ module.exports = (options, ctx) => ({
logger.tip(`${chalk.magenta('override.styl')} has been deprecated from v1.0.0, using ${chalk.cyan('.vuepress/styles/palette.styl')} instead.\n`)
}
const themeStyle = path.resolve(ctx.themePath, 'styles/index.styl')
const themeStyle = path.resolve(themeAPI.theme.path, 'styles/index.styl')
const userStyle = path.resolve(sourceDir, '.vuepress/styles/index.styl')
const themeStyleContent = fs.existsSync(themeStyle)
@@ -28,8 +32,8 @@ module.exports = (options, ctx) => ({
let styleContent = themeStyleContent + userStyleContent
if (ctx.parentThemePath) {
const parentThemeStyle = path.resolve(ctx.parentThemePath, 'styles/index.styl')
if (themeAPI.existsParentTheme) {
const parentThemeStyle = path.resolve(themeAPI.parentTheme.path, 'styles/index.styl')
const parentThemeStyleContent = fs.existsSync(parentThemeStyle)
? `@import(${JSON.stringify(parentThemeStyle.replace(/[\\]+/g, '/'))})`
: ''
@@ -84,7 +84,11 @@ module.exports = class PluginAPI {
if (isPlainObject(pluginRaw) && pluginRaw.$$normalized) {
plugin = pluginRaw
} else {
plugin = this.normalizePlugin(pluginRaw, pluginOptions)
try {
plugin = this.normalizePlugin(pluginRaw, pluginOptions)
} catch (e) {
logger.warn(e.message)
}
}
if (plugin.multiple !== true) {
@@ -114,8 +118,7 @@ module.exports = class PluginAPI {
normalizePlugin (pluginRaw, pluginOptions = {}) {
let plugin = this._pluginResolver.resolve(pluginRaw)
if (!plugin.entry) {
console.warn(`[vuepress] cannot resolve plugin "${pluginRaw}"`)
return this
throw new Error(`[vuepress] cannot resolve plugin "${pluginRaw}"`)
}
plugin = flattenPlugin(plugin, pluginOptions, this._pluginContext, this)
plugin.$$normalized = true
+34 -60
View File
@@ -94,7 +94,7 @@ module.exports = class AppContext {
this.resolveConfigAndInitialize()
this.resolveCacheLoaderOptions()
this.normalizeHeadTagUrls()
await this.resolveTheme()
this.themeAPI = loadTheme(this)
this.resolveTemplates()
this.resolveGlobalLayout()
@@ -137,7 +137,7 @@ module.exports = class AppContext {
)
this.pluginAPI
// internl core plugins
// internl core plugins
.use(require('../internal-plugins/siteData'))
.use(require('../internal-plugins/routes'))
.use(require('../internal-plugins/rootMixins'))
@@ -153,8 +153,8 @@ module.exports = class AppContext {
.use('@vuepress/register-components', {
componentsDir: [
path.resolve(this.sourceDir, '.vuepress/components'),
path.resolve(this.themePath, 'global-components'),
this.parentThemePath && path.resolve(this.parentThemePath, 'global-components')
path.resolve(this.themeAPI.theme.path, 'global-components'),
this.themeAPI.existsParentTheme && path.resolve(this.themeAPI.parentTheme.path, 'global-components')
]
})
}
@@ -167,11 +167,12 @@ module.exports = class AppContext {
applyUserPlugins () {
this.pluginAPI.useByPluginsConfig(this.cliOptions.plugins)
if (this.parentThemePath) {
this.pluginAPI.use(this.parentThemeEntryFile)
if (this.themeAPI.existsParentTheme) {
this.pluginAPI.use(this.themeAPI.parentTheme.entry)
}
this.pluginAPI
.use(this.themeEntryFile)
.use(this.themeAPI.theme.entry)
.use(this.themeAPI.vuepressPlugin)
.use(Object.assign({}, this.siteConfig, { name: '@vuepress/internal-site-config' }))
}
@@ -221,41 +222,26 @@ module.exports = class AppContext {
*/
resolveTemplates () {
const { siteSsrTemplate, siteDevTemplate } = this.siteConfig
this.devTemplate = this.resolveCommonAgreementFilePath(
'devTemplate',
{
defaultValue: path.resolve(__dirname, '../app/index.dev.html'),
siteAgreement: 'templates/dev.html',
themeAgreement: 'templates/dev.html'
}
)
const templateDir = path.resolve(this.vuepressDir, 'templates')
const siteSsrTemplate2 = path.resolve(templateDir, 'ssr.html')
const siteDevTemplate2 = path.resolve(templateDir, 'dev.html')
this.ssrTemplate = this.resolveCommonAgreementFilePath(
'ssrTemplate',
{
defaultValue: path.resolve(__dirname, '../app/index.ssr.html'),
siteAgreement: 'templates/ssr.html',
themeAgreement: 'templates/ssr.html'
}
)
const themeSsrTemplate = path.resolve(this.themePath, 'templates/ssr.html')
const themeDevTemplate = path.resolve(this.themePath, 'templates/dev.html')
const parentThemeSsrTemplate = path.resolve(this.themePath, 'templates/ssr.html')
const parentThemeDevTemplate = path.resolve(this.themePath, 'templates/dev.html')
const defaultSsrTemplate = path.resolve(__dirname, '../app/index.ssr.html')
const defaultDevTemplate = path.resolve(__dirname, '../app/index.dev.html')
const ssrTemplate = fsExistsFallback([
siteSsrTemplate,
siteSsrTemplate2,
themeSsrTemplate,
parentThemeSsrTemplate,
defaultSsrTemplate
])
const devTemplate = fsExistsFallback([
siteDevTemplate,
siteDevTemplate2,
themeDevTemplate,
parentThemeDevTemplate,
defaultDevTemplate
])
logger.debug('SSR Template File: ' + chalk.gray(ssrTemplate))
logger.debug('DEV Template File: ' + chalk.gray(devTemplate))
this.devTemplate = devTemplate
this.ssrTemplate = ssrTemplate
logger.debug('SSR Template File: ' + chalk.gray(this.ssrTemplate))
logger.debug('DEV Template File: ' + chalk.gray(this.devTemplate))
}
/**
@@ -266,14 +252,12 @@ module.exports = class AppContext {
*/
resolveGlobalLayout () {
const GLOBAL_LAYOUT_COMPONENT_NAME = `GlobalLayout`
this.globalLayout = this.resolveCommonAgreementFilePath(
'globalLayout',
{
defaultValue: path.resolve(__dirname, `../app/components/${GLOBAL_LAYOUT_COMPONENT_NAME}.vue`),
siteAgreement: `components/${GLOBAL_LAYOUT_COMPONENT_NAME}.vue`,
themeAgreement: `layouts/${GLOBAL_LAYOUT_COMPONENT_NAME}.vue`
defaultValue: path.resolve(__dirname, `../app/components/GlobalLayout.vue`),
siteAgreement: `components/GlobalLayout.vue`,
themeAgreement: `layouts/GlobalLayout.vue`
}
)
@@ -354,17 +338,6 @@ module.exports = class AppContext {
this.pages.push(page)
}
/**
* Resolve theme
*
* @returns {Promise<void>}
* @api private
*/
async resolveTheme () {
Object.assign(this, (await loadTheme(this)))
}
/**
* Get config value of current active theme.
*
@@ -374,7 +347,8 @@ module.exports = class AppContext {
*/
getThemeConfigValue (key) {
return this.themeEntryFile[key] || this.parentThemeEntryFile[key]
return this.themeAPI.theme.entry[key]
|| this.themeAPI.existsParentTheme && this.themeAPI.parentTheme.entry[key]
}
/**
@@ -386,12 +360,12 @@ module.exports = class AppContext {
*/
resolveThemeAgreementFile (filepath) {
const current = path.resolve(this.themePath, filepath)
const current = path.resolve(this.themeAPI.theme.path, filepath)
if (fs.existsSync(current)) {
return current
}
if (this.parentThemePath) {
const parent = path.resolve(this.parentThemePath, filepath)
if (this.themeAPI.existsParentTheme) {
const parent = path.resolve(this.themeAPI.theme.path, filepath)
if (fs.existsSync(parent)) {
return parent
}
+66 -142
View File
@@ -5,11 +5,13 @@
*/
const {
fs, path,
fs,
path: { resolve, parse },
moduleResolver: { getThemeResolver },
datatypes: { isString },
logger, chalk
} = require('@vuepress/shared-utils')
const ThemeAPI = require('../theme-api')
/**
* Resolve theme.
@@ -25,164 +27,86 @@ const {
* @param {string} theme
* @param {string} sourceDir
* @param {string} vuepressDir
* @returns {Promise}
* @returns {ThemeAPI}
*/
module.exports = async function loadTheme (ctx) {
const { siteConfig, cliOptions, sourceDir, vuepressDir, pluginAPI } = ctx
const theme = siteConfig.theme || cliOptions.theme
module.exports = function loadTheme (ctx) {
const themeResolver = getThemeResolver()
const localThemePath = path.resolve(vuepressDir, 'theme')
const useLocalTheme
= !fs.existsSync(theme)
&& fs.existsSync(localThemePath)
&& (fs.readdirSync(localThemePath)).length > 0
let themePath = null // Mandatory
let themeEntryFile = {} // Optional
let themeName
let themeShortcut
let parentThemePath = null // Optional
let parentThemeEntryFile = {} // Optional
if (useLocalTheme) {
themePath = localThemePath
logger.tip(`Apply theme located at ${chalk.gray(themePath)}...`)
} else if (isString(theme)) {
const resolved = themeResolver.resolve(theme, sourceDir)
const { entry, name, shortcut } = resolved
if (entry === null) {
throw new Error(`Cannot resolve theme ${theme}.`)
}
themePath = normalizeThemePath(resolved)
themeName = name
themeShortcut = shortcut
logger.tip(`Apply theme ${chalk.gray(themeName)}`)
} else {
const theme = resolveTheme(ctx, themeResolver)
if (!theme.path) {
throw new Error(`[vuepress] You must specify a theme, or create a local custom theme. \n For more details, refer to https://vuepress.vuejs.org/guide/custom-themes.html#custom-themes. \n`)
}
logger.tip(`Apply theme ${chalk.gray(theme.name)}`)
theme.entry.name = '@vuepress/internal-theme-entry-file'
try {
themeEntryFile = pluginAPI.normalizePlugin(themePath, ctx.themeConfig)
} catch (error) {
themeEntryFile = {}
let parentTheme = {}
if (theme.entry.extend) {
parentTheme = resolveTheme(ctx, themeResolver, true, theme.entry.extend)
parentTheme.entry.name = '@vuepress/internal-parent-theme-entry-file'
}
themeEntryFile.name = '@vuepress/internal-theme-entry-file'
themeEntryFile.shortcut = null
// handle theme api
const layoutDirs = [
path.resolve(themePath, '.'),
path.resolve(themePath, 'layouts')
]
if (themeEntryFile.extend) {
const resolved = themeResolver.resolve(themeEntryFile.extend, sourceDir)
if (resolved.entry === null) {
throw new Error(`Cannot resolve parent theme ${themeEntryFile.extend}.`)
}
parentThemePath = normalizeThemePath(resolved)
try {
parentThemeEntryFile = pluginAPI.normalizePlugin(parentThemePath, ctx.themeConfig)
} catch (error) {
parentThemeEntryFile = {}
}
parentThemeEntryFile.name = '@vuepress/internal-parent-theme-entry-file'
parentThemeEntryFile.shortcut = null
layoutDirs.unshift(
path.resolve(parentThemePath, '.'),
path.resolve(parentThemePath, 'layouts'),
)
themeEntryFile.alias = Object.assign(
themeEntryFile.alias || {},
{ '@parent-theme': parentThemePath }
)
}
// normalize component name
const getComponentName = filename => {
filename = filename.slice(0, -4)
if (filename === '404') {
filename = 'NotFound'
}
return filename
}
const readdirSync = dir => fs.existsSync(dir) && fs.readdirSync(dir) || []
// built-in named layout or not.
const isInternal = componentName => componentName === 'Layout'
|| componentName === 'NotFound'
const layoutComponentMap = layoutDirs
.map(
layoutDir => readdirSync(layoutDir)
.filter(filename => filename.endsWith('.vue'))
.map(filename => {
const componentName = getComponentName(filename)
return {
filename,
componentName,
isInternal: isInternal(componentName),
path: path.resolve(layoutDir, filename)
}
})
)
.reduce((arr, next) => {
arr.push(...next)
return arr
}, [])
.reduce((map, component) => {
map[component.componentName] = component
return map
}, {})
const { Layout = {}, NotFound = {}} = layoutComponentMap
// layout component does not exist.
if (!Layout || !fs.existsSync(Layout.path)) {
throw new Error(`[vuepress] Cannot resolve Layout.vue file in \n ${Layout.path}`)
}
// use default 404 component.
if (!NotFound || !fs.existsSync(NotFound.path)) {
layoutComponentMap.NotFound = {
filename: 'NotFound.vue',
componentName: 'NotFound',
path: path.resolve(__dirname, '../app/components/NotFound.vue'),
isInternal: true
}
}
return {
themePath,
layoutComponentMap,
themeEntryFile,
themeName,
themeShortcut,
parentThemePath,
parentThemeEntryFile
}
logger.debug('theme', theme.name, theme.path)
logger.debug('parentTheme', parentTheme.name, parentTheme.path)
return new ThemeAPI(theme, parentTheme, ctx)
}
function normalizeThemePath (resolved) {
const { entry, name, fromDep } = resolved
if (fromDep) {
const pkgPath = require.resolve(name)
return path.parse(pkgPath).dir
let packageRootDir = parse(pkgPath).dir
// For those cases that "main" field was set to non-index file
// e.g. `layouts/Layout.vue`
while (!fs.existsSync(`${packageRootDir}/package.json`)) {
packageRootDir = resolve(packageRootDir, '..')
}
return packageRootDir
} else if (entry.endsWith('.js') || entry.endsWith('.vue')) {
return path.parse(entry).dir
return parse(entry).dir
} else {
return entry
}
}
function resolveTheme (ctx, resolver, ignoreLocal, theme) {
const { siteConfig, cliOptions, sourceDir, vuepressDir, pluginAPI } = ctx
const localThemePath = resolve(vuepressDir, 'theme')
theme = theme || siteConfig.theme || cliOptions.theme
let path
let name
let shortcut
let entry = {}
// 1. From local
if (!ignoreLocal
&& !fs.existsSync(theme)
&& fs.existsSync(localThemePath)
&& (fs.readdirSync(localThemePath)).length > 0
) {
path = localThemePath
name = shortcut = 'local'
logger.tip(`Apply local theme at ${chalk.gray(path)}...`)
// 2. From dep
} else if (isString(theme)) {
const resolved = resolver.resolve(theme, sourceDir)
if (resolved.entry === null) {
throw new Error(`Cannot resolve theme: ${theme}.`)
}
path = normalizeThemePath(resolved)
name = resolved.name
shortcut = resolved.shortcut
} else {
return {}
}
try {
entry = pluginAPI.normalizePlugin(path, ctx.themeConfig)
} catch (error) {
entry = {}
}
return { path, name, shortcut, entry }
}
@@ -0,0 +1,3 @@
<template>
<Content/>
</template>
@@ -0,0 +1,152 @@
const { logger, fs, path: { resolve }} = require('@vuepress/shared-utils')
const readdirSync = dir => fs.existsSync(dir) && fs.readdirSync(dir) || []
module.exports = class ThemeAPI {
constructor (theme, parentTheme) {
this.theme = theme
this.parentTheme = parentTheme || {}
this.existsParentTheme = !!this.parentTheme.path
this.vuepressPlugin = {
name: '@vuepress/internal-theme-api',
alias: {}
}
this.init()
}
setAlias (alias) {
this.vuepressPlugin.alias = {
...this.vuepressPlugin.alias,
...alias
}
}
init () {
const alias = {
'@current-theme': this.theme.path
}
if (this.existsParentTheme) {
alias['@parent-theme'] = this.parentTheme.path
}
this.componentMap = this.getComponents()
this.layoutComponentMap = this.getLayoutComponentMap()
Object.keys(this.componentMap).forEach((name) => {
const { filename, path } = this.componentMap[name]
alias[`@theme/components/${filename}`] = path
})
Object.keys(this.layoutComponentMap).forEach((name) => {
const { filename, path } = this.layoutComponentMap[name]
alias[`@theme/layouts/${filename}`] = path
})
alias['@theme'] = this.theme.path
this.setAlias(alias)
}
getComponents () {
const componentDirs = [
resolve(this.theme.path, 'components')
]
if (this.existsParentTheme) {
componentDirs.unshift(
resolve(this.parentTheme.path, 'components'),
)
}
return resolveSFCs(componentDirs)
}
getLayoutComponentMap () {
const layoutDirs = [
resolve(this.theme.path, '.'),
resolve(this.theme.path, 'layouts')
]
if (this.existsParentTheme) {
layoutDirs.unshift(
resolve(this.parentTheme.path, '.'),
resolve(this.parentTheme.path, 'layouts'),
)
}
// built-in named layout or not.
const layoutComponentMap = resolveSFCs(layoutDirs)
const { Layout = {}, NotFound = {}} = layoutComponentMap
// layout component does not exist.
if (!Layout || !fs.existsSync(Layout.path)) {
const fallbackLayoutPath = resolve(__dirname, 'Layout.fallback.vue')
layoutComponentMap.Layout = {
filename: 'Layout.vue',
componentName: 'Layout',
path: fallbackLayoutPath,
isInternal: true
}
logger.warn(
`[vuepress] Cannot resolve Layout.vue file in \n ${Layout.path},`
+ `fallback to default layout: ${fallbackLayoutPath}`
)
}
if (!NotFound || !fs.existsSync(NotFound.path)) {
layoutComponentMap.NotFound = {
filename: 'NotFound.vue',
componentName: 'NotFound',
path: resolve(__dirname, '../app/components/NotFound.vue'),
isInternal: true
}
}
return layoutComponentMap
}
}
/**
* Resolve Vue SFCs, return a Map
*
* @param dirs
* @returns {*}
*/
function resolveSFCs (dirs) {
return dirs.map(
layoutDir => readdirSync(layoutDir)
.filter(filename => filename.endsWith('.vue'))
.map(filename => {
const componentName = getComponentName(filename)
return {
filename,
componentName,
isInternal: isInternal(componentName),
path: resolve(layoutDir, filename)
}
})
).reduce((arr, next) => {
arr.push(...next)
return arr
}, []).reduce((map, component) => {
map[component.componentName] = component
return map
}, {})
}
/**
* normalize component name
*
* @param {strin} filename
* @returns {string}
*/
function getComponentName (filename) {
filename = filename.slice(0, -4)
if (filename === '404') {
filename = 'NotFound'
}
return filename
}
/**
* Whether it's agreed layout component
*
* @param name
* @returns {boolean}
*/
function isInternal (name) {
return name === 'Layout' || name === 'NotFound'
}
@@ -15,7 +15,6 @@ module.exports = function createBaseConfig ({
sourceDir,
outDir,
base: publicPath,
themePath,
markdown,
tempPath,
cacheDirectory,
@@ -52,7 +51,6 @@ module.exports = function createBaseConfig ({
config.resolve
.set('symlinks', true)
.alias
.set('@theme', themePath)
.set('@source', sourceDir)
.set('@app', path.resolve(__dirname, '../app'))
.set('@temp', tempPath)
@@ -50,8 +50,8 @@
</template>
<script>
import NavLink from './NavLink.vue'
import DropdownTransition from './DropdownTransition.vue'
import NavLink from '@theme/components/NavLink.vue'
import DropdownTransition from '@theme/components/DropdownTransition.vue'
export default {
components: { NavLink, DropdownTransition },
@@ -50,7 +50,7 @@
</template>
<script>
import NavLink from './NavLink.vue'
import NavLink from '@theme/components/NavLink.vue'
export default {
components: { NavLink },
@@ -34,9 +34,9 @@
</template>
<script>
import DropdownLink from './DropdownLink.vue'
import DropdownLink from '@theme/components/DropdownLink.vue'
import { resolveNavLinkItem } from '../util'
import NavLink from './NavLink.vue'
import NavLink from '@theme/components/NavLink.vue'
export default {
components: { NavLink, DropdownLink },
@@ -37,10 +37,10 @@
</template>
<script>
import SidebarButton from './SidebarButton.vue'
import AlgoliaSearchBox from '@AlgoliaSearchBox'
import SearchBox from '@SearchBox'
import NavLinks from './NavLinks.vue'
import SidebarButton from '@theme/components/SidebarButton.vue'
import NavLinks from '@theme/components/NavLinks.vue'
export default {
components: { SidebarButton, NavLinks, SearchBox, AlgoliaSearchBox },
@@ -8,8 +8,8 @@
</template>
<script>
import SidebarLinks from './SidebarLinks.vue'
import NavLinks from './NavLinks.vue'
import SidebarLinks from '@theme/components/SidebarLinks.vue'
import NavLinks from '@theme/components/NavLinks.vue'
export default {
name: 'Sidebar',
@@ -55,7 +55,7 @@
<script>
import { isActive } from '../util'
import DropdownTransition from './DropdownTransition.vue'
import DropdownTransition from '@theme/components/DropdownTransition.vue'
export default {
name: 'SidebarGroup',
@@ -22,8 +22,8 @@
</template>
<script>
import SidebarGroup from './SidebarGroup.vue'
import SidebarLink from './SidebarLink.vue'
import SidebarGroup from '@theme/components/SidebarGroup.vue'
import SidebarLink from '@theme/components/SidebarLink.vue'
import { isActive } from '../util'
export default {
@@ -48,10 +48,10 @@
</template>
<script>
import Home from '../components/Home.vue'
import Navbar from '../components/Navbar.vue'
import Page from '../components/Page.vue'
import Sidebar from '../components/Sidebar.vue'
import Home from '@theme/components/Home.vue'
import Navbar from '@theme/components/Navbar.vue'
import Page from '@theme/components/Page.vue'
import Sidebar from '@theme/components/Sidebar.vue'
import { resolveSidebarItems } from '../util'
export default {
@@ -7,8 +7,8 @@
<script>
import ParentLayout from '@parent-theme/layouts/Layout.vue'
import CarbonAds from '../components/CarbonAds.vue'
import BuySellAds from '../components/BuySellAds.vue'
import CarbonAds from '@theme/components/CarbonAds.vue'
import BuySellAds from '@theme/components/BuySellAds.vue'
export default {
components: {
+2 -1
View File
@@ -160,7 +160,8 @@ function getThemeSidebar (groupA, introductionA) {
'using-a-theme',
'writing-a-theme',
'option-api',
'default-theme-config'
'default-theme-config',
'inheritance'
]
},
]
+29 -20
View File
@@ -6,25 +6,27 @@ sidebar: auto
You may encounter some unfamiliar concepts in the documentation. This section lists the common terms in the documentation for easy access, learning and plugin/theme development.
## layout
- Access: `$page.frontmatter.layout`
Name of the layout component used by the current page.
## frontmatter
> Access: `$page.frontmatter`
- Access: `$page.frontmatter`
Configuration wrapped by `---` in the `markdown` file of the current page, which is generally used to do some page-level configuration.
::: tip
Features such as VuePress's dynamic layout system are based on `frontmatter`. You can use the plugin API [extendPageData](../plugin/option-api.md#extendpagedata) to dynamically modify the value of frontmatter during build time.
:::
Configuration wrapped by `---` in the `markdown` file of the current page, which is generally used to do some page-level configuration. For more details, please see [Front Matter](../guide/frontmatter.md).
## permalink
> Access: `$page.frontmatter.permalink`
- Access: `$page.frontmatter.permalink`
For permalinks, see [permalinks](../guide/permalinks.md) for more.
Permalinks. For more details, please head [permalinks](../guide/permalinks.md).
## regularPath
> Access: `$page.regularPath`
- Access: `$page.regularPath`
The current page is based on the URL generated by the directory structure.
@@ -34,36 +36,43 @@ When dynamically generating routes during the build period, a page's URL (`$page
## headers
> Access: `$page.headers`
- Access: `$page.headers`
That is, those titles defined by one or more `#` in `markdown`.
## siteConfig
> Access: `$site | Context.siteConfig`
- Access: `$site | Context.siteConfig`
`.vuepress/config.js`, i.e., `site configuration`
## themeConfig
> Access: `$site | Context.themeConfig`
- Access: `$themeConfig | Context.themeConfig`
Value of `themeConfig` in `.vuepress/config.js`, i.e., `user's theme configuration`.
Value of `themeConfig` in `.vuepress/config.js`, i.e., user's theme configuration.
## themePath
> Access: `Context.themePath`
- Access: `Context.themeAPI.theme.path`
Root path (absolute path) of the currently used theme.
## themeEntryFile
## themeEntry
> Access: `Context.themeEntryFile`
- Access: `Context.themeAPI.theme.entry`
Theme's configuration file (`themePath/index.js`).
Theme's configuration file - `themePath/index.js`.
## layout
## parentThemePath
> Access: `$page.frontmatter.layout`
- Access: `Context.themeAPI.parentTheme.path`
If the current theme is a derived theme, `parentThemePath` refers to the absolute path of the parent theme.
## parentThemeEntry
- Access: `Context.themeAPI.parentTheme.entry`
If the current theme is a derived theme, `parentThemePath` refers to the theme configuration of the parent theme.
Name of the layout component used by the current page.
-6
View File
@@ -42,12 +42,6 @@ Root directory where the temporary files are located.
Output path.
## ctx.themePath
- Type: `string`
The path of the currently active theme.
## ctx.base
- Type: `string`
+2 -2
View File
@@ -117,7 +117,7 @@ We can set aliases via [chainWebpack](#chainwebpack):
```js
module.exports = (options, context) => ({
chainWebpack (config) {
config.resolve.alias.set('@theme', context.themePath)
config.resolve.alias.set('@pwd', process.cwd())
}
})
```
@@ -127,7 +127,7 @@ But `alias` option makes this process more like configuration:
```js
module.exports = (options, context) => ({
alias: {
'@theme': context.themePath
'@pwd': process.cwd()
}
})
```
+182
View File
@@ -0,0 +1,182 @@
# Theme Inheritance <Badge type="warn" text="beta" />
## Motivation
We have two main reasons to support this feature:
1. VuePress provides users with a [default theme](./default-theme-config.md), which meets the needs of document writers in most scenarios, even so, there are still many users who choose to `eject` and modify it, even if they may only need to make minor changes to one of the components.
2. In [0.x](https://vuepress.vuejs.org/guide/custom-themes.html#site-and-page-metadata), only one `Layout.vue` is needed for a theme, so we can achieve simple expansion by directly wrapping `Layout.vue` of another theme.
By 1.x, the elements of a theme has become more complex, we have started to have [theme level configuration](./option-api.md), which supports plugins, custom global layout, etc. In addition, we have also introduced the [directory structure conventions](./writing-a-theme.md#directory-structure) on theme development, such as `styles/index.styl`, under this background, we can not achieve inheritance as 0.x did.
Therefore, we need to provide a reasonable and reliable theme inheritance strategy.
## Concepts
To introduce this section, let's start with a few basic concepts:
- **Atomic theme**i.e. the parent theme, which is implemented entirely from scratch, like the default theme.
- **Derived theme**i.e. the child theme, which is created based on parent theme.
::: tip
For now theme inheritance doesn't support high-order inheritance, that means, a derived theme cannot be inherited.
:::
## Usage
Suppose you want to create a theme inherited from the default theme, you just need to configure the [extend](./option-api.md#extend) option in your theme configuration:
```js
module.exports = {
extend: '@vuepress/theme-default'
}
```
## Inheritance Strategy
All the capabilities of the parent theme will be `"passed"` to the child theme. For file-level conventions, child theme can override it by creating a file with the same name in the same location. For some theme configuration options, such as [globalLayout](./option-api.md/globallayout), child theme can override it by the same name configuration.
The [file-level conventions](./writing-a-theme.md#directory-structure) are as follows:
- **Global Components**i.e. the Vue components under `theme/global-components`.
- **Components**i.e. the Vue components under `theme/components`.
- **Global Style and Palette**i.e. `index.styl` and `palette.styl` under `theme/styles`.
- **HTML Template**i.e. `dev.html` and `ssr.html` under `theme/templates`.
- **Theme-Level App Enhancement File**i.e. `theme/enhanceApp.js`
For theme configuration, the configuration options that can be overrode by child theme are as follows:
- [devTemplate](./option-api.md#devtemplate)
- [ssrTemplate](./option-api.md#ssrtemplate)
- [globalLayout](./option-api.md#globallayout)
Theme configuration options that cannot be overrode by child theme:
- [extend](./option-api.md#extend)
Theme configuration options requiring special treatment:
- [plugins](./option-api.md#plugins)See [Override Plugins](#override-plugins)。
## Override Plugins
For [plugins](./option-api.md#plugins) in the parent theme, the child theme cannot override it intuitively, but the options of plugin can be overrode by creating plugin configuration with the same name.
For example, if the parent theme has the following configuration:
```js
// parentThemePath/index.js
module.exports = {
plugins: [
['@vuepress/search', {
searchMaxSuggestions: 5
}]
]
}
```
The child theme can modify the options of plugin in the following ways:
```js
// themePath/index.js
module.exports = {
plugins: [
['@vuepress/search', {
searchMaxSuggestions: 10
}]
]
}
```
Child theme can even disable it:
```js
// themePath/index.js
module.exports = {
plugins: [
['@vuepress/search', false]
]
}
```
::: warning
Normally, you don't need to do this unless you know clearly that disabling plugins in parent themes won't cause problems.
:::
## Override Components
You may want to override the same-name components in the parent theme. By default, when the components in the parent theme use relative paths to reference other components, you will not be able to do this because you cannot modify the code of the parent theme at runtime.
VuePress achieves this requirement in a clever way, but there is a requirement for the parent theme - **All components must use the `@theme` alias to refer to other components**.
For example, if you are developing an atomic theme with the following structure:
::: vue
theme
├── components
│   ├── `Home.vue`
│   ├── `Navbar.vue`
│   └── `Sidebar.vue`
├── layouts
│   ├── `404.vue`
│   └── `Layout.vue`
├── package.json
└── index.js
:::
Then, in any Vue components on the theme, **you should access the theme root directory through `@theme`**:
```vue
<script>
import Home from '@theme/components/Navbar.vue'
// ...
</script>
```
On this premise, when you create a `Navbar` component in the same place in the child theme
::: vue
theme
└── components
   └── `Navbar.vue`
:::
`@theme/components/Navbar.vue` will automatically map to the Navbar component in the child theme. and when you remove the component, `@theme/components/Navbar.vue` will automatically restore to the Navbar component in the parent theme.
In this way, you can easily "tamper" with some part of an atomic theme.
::: tip
1. You'd better override the component based on the code of the corresponding component in the parent theme.
2. Currently, when developing theme locally, you need to manually restart dev server when a component is created or removed.
:::
## Access Parent Theme
You can use `@parent-theme` to access the root path of the parent theme. The following example shows creating a layout component with the same name in a child theme and simply using slots in the parent theme. [@vuepress/theme-vue](https://github.com/vuejs/vuepress/tree/master/packages/%40vuepress/theme-vue) is created in this way.
```vue
<!-- themePath/components/Foo.vue -->
<template>
<ParentLayout>
<Foo slot="foo"/>
</ParentLayout>
</template>
<script>
import ParentLayout from '@parent-theme/layouts/Layout.vue'
import Foo from '@theme/components/Foo.vue'
export default {
components: {
ParentLayout,
Foo
}
}
</script>
```
Vendored Regular → Executable
+98 -8
View File
@@ -1,8 +1,33 @@
---
metaTitle: Option API | Theme
metaTitle: Configuration | Theme
---
# Option API
# Theme Configuration
As with plugins, the theme configuration file `themeEntry` should export a `plain JavaScript object`(`#1`). If the plugin needs to take options, it can be a function that exports a plain object(`#2`). The function will be called with the `siteConfig.themeConfig` as the first argument, along with [ctx](./context-api.md) which provides some compile-time metadata.
``` js
// #1
module.exports = {
// ...
}
```
``` js
// #2
module.exports = (themeConfig, ctx) => {
return {
// ...
}
}
```
::: tip
1. You should see the difference between `themeEntry` and `themeConfig`, the former is a configuration for ths theme itself, which is provided by VuePress. the latter is the user's configuration for the theme, which is implemented by the currently used theme, e.g. [Default Theme Config](./default-theme-config.md).
2. In addition to the options listed in this section, `themeEntry` also supports all [Option API](../plugin/option-api.md) and [Life Cycle](../plugin/life-cycle.md) supported by plugins.
:::
## plugins
@@ -11,9 +36,33 @@ metaTitle: Option API | Theme
**Also see:**
- [Plugin > Using a plugin](../plugin/using-a-plugin.md).
- [Plugin > Using a Plugin](../plugin/using-a-plugin.md).
## extend
---
::: warning
You probably don't need to use following options tagged with <Badge text="Danger Zone" vertical="middle"/> unless you know what you are doing!
:::
## devTemplate <Badge text="Danger Zone"/>
- Type: `String`
- Default: undefined
HTML template path used in `dev` mode, default template see [here](https://github.com/vuejs/vuepress/blob/master/packages/%40vuepress/core/lib/app/index.dev.html)
## ssrTemplate <Badge text="Danger Zone"/>
- Type: `String`
- Default: undefined
HTML template path used in `build` mode, default template see [here](https://github.com/vuejs/vuepress/blob/master/packages/%40vuepress/core/lib/app/index.ssr.html)
**Also see:**
- [Vue SSR Guide > template](https://ssr.vuejs.org/api/#template).
## extend <Badge text="Danger Zone"/>
- Type: `String`
- Default: undefined
@@ -24,11 +73,52 @@ module.exports = {
}
```
VuePress supports a theme to be inherited from another theme. VuePress will follow the principle of `override` to automatically help you resolve the priorities of various theme attributes, such as styles, layout components.
Note that in the child theme, VuePress will apply a `@parent-theme` [alias](../plugin/option-api.md#alias) pointing to the package directory of parent theme.
VuePress provides the ability to inherit one theme from another. VuePress will follow the concept of `override` and automatically help you prioritize various thematic attributes, e.g. styles and layout components.
**Also see:**
- [Example: `@vuepress/theme-vue`](https://github.com/vuejs/vuepress/tree/master/packages/@vuepress/theme-vue)
- [Theme Inheritance](./inheritance.md)
- [Design Concepts of VuePress 1.x](../miscellaneous/design-concepts.md)
## globalLayout <Badge text="Danger Zone"/>
- Type: `String`
- Default: undefined
```js
// themePath/index.js
module.exports = {
globalLayout: '/path/to/your/global/vue/sfc'
}
```
Global layout component is a component responsible for the global layout strategy. The [default global layout](https://github.com/vuejs/vuepress/blob/master/packages/%40vuepress/core/lib/app/components/GlobalLayout.vue) will help you render different layouts according to [$frontmatter.layout](../guide/frontmatter.md#layout), so in most cases you do not need to configure this option.
For example, when you want to set a global header and footer for your theme, you can do this:
```vue
<!-- themePath/layouts/GlobalLayout.vue -->
<template>
<div id="global-layout">
<header><h1>Header</h1></header>
<component :is="layout"/>
<footer><h1>Footer</h1></footer>
</div>
</template>
<script>
export default {
computed: {
layout () {
if (this.$page.path) {
if (this.$vuepress.isLayoutExists(this.$frontmatter.layout)) {
return this.$frontmatter.layout
}
return 'Layout'
}
return 'NotFound'
}
}
}
</script>
```
+2 -2
View File
@@ -14,7 +14,7 @@ module.exports = {
## Theme Shorthand
If you prefix the plugin with `vuepress-theme-`, you can use a shorthand to leave out that prefix:
If you prefix the theme with `vuepress-theme-`, you can use a shorthand to leave out that prefix:
``` js
module.exports = {
@@ -47,5 +47,5 @@ module.exports = {
```
::: warning Note
The plugin whose name starts with `@vuepress/theme-` is an officially maintained theme.
The theme whose name starts with `@vuepress/theme-` is an officially maintained theme.
:::
+15 -12
View File
@@ -34,22 +34,22 @@ Just one `Layout.vue` might not be enough, and you might also want to define mor
So it's time to reorganize your theme, an agreed theme directory structure is as follows:
::: vue
themePath
├── `global-components` _(**Optional**)_
theme
├── `global-components`
│ └── xxx.vue
├── `components` _(**Optional**)_
├── `components`
│ └── xxx.vue
├── `layouts`
│   ├── Layout.vue _(**Required**)_
│   └── 404.vue _(**Optional**)_
├── `styles` _(**Optional**)_
│   ├── Layout.vue _(**必要的**)_
│   └── 404.vue
├── `styles`
│   ├── index.styl
│   └── palette.styl
├── `templates` _(**Optional**)_
├── `templates`
  ├── dev.html
  └── ssr.html
├── `index.js` _(**Only required when you publish it as an npm package**)_
├── `enhanceApp.js` _(**Optional**)_
├── `index.js`
├── `enhanceApp.js`
└── package.json
:::
@@ -62,12 +62,11 @@ themePath
- `theme/enhanceApp.js`: Theme level enhancements.
::: warning Note
When you want to publish your theme as an npm package, make sure the package has `index.js`, and set `"main"` field at `package.json` to `index.js` so that VuePress can resolve and get the correct [themePath](../miscellaneous/glossary.md#theme-side).
When you publish your theme as an NPM package, if you don't have any theme configuration, that means you don't have `theme/index.js`, you'll need to set the `"main"` field to `layouts/Layout.vue` in `package.json`, only in this way VuePress can correctly resolve the theme.
```json
{
...
"main": "index.js"
"main": "layouts/Layout.vue",
...
}
```
@@ -96,6 +95,10 @@ layout: AnotherLayout
---
````
::: tip
Each layout component may render distinct pages. If you want to apply some global UI (e.g. global header), consider using [globalLayout](./option-api.md#globallayout)。
:::
## Apply plugins
You can apply some plugins to the theme via `theme/index.js`.
+36 -25
View File
@@ -6,64 +6,75 @@ sidebar: auto
你可能会在文档中碰到一些陌生的概念,本节列出了文档中常见的术语,方便查阅、学习、插件/主题开发之用。
## layout
- Access: `$page.frontmatter.layout`
当前页面所使用的布局组件名。
## frontmatter
> Access: `$page.frontmatter`
- Access: `$page.frontmatter`
当前页面的 `markdown` 文件中包裹在 `---` 中的配置,一般用于做一些页面级别的配置。
::: tip
VuePress 的动态布局系统等特性是基于 `frontmatter` 实现的,你可以使用插件 API [extendPageData](../plugin/option-api.md#extendpagedata) 在构建期间动态修改 frontmatter 的值。
:::
当前页面的 `markdown` 文件中包裹在 `---` 中的配置,一般用于做一些页面级别的配置,参考 [Front Matter](../guide/frontmatter.md) 一节了解更多
## permalink
> Access: `$page.frontmatter.permalink`
- Access: `$page.frontmatter.permalink`
永久链接,参考 [permalinks](../guide/permalinks.md) 了解更多。
永久链接,参考 [Permalinks](../guide/permalinks.md) 一节了解更多。
## regularPath
> Access: `$page.regularPath`
- Access: `$page.regularPath`
当前页面基于目录结构生成的 URL。
::: tip
在构建期动态生成路由时,一个页面的 URL (`$page.path`) 将优先使用 `$page.frontmatter.permalink`,若不存在则降级到 `$page.regularPath`
:::
## path
- Access: `$page.path`
当前页面的实际 URL。在构建期生成路由时,一个页面的 URL 将优先使用 `permalink`,若不存在则降级到 `regularPath`
## headers
> Access: `$page.headers`
- Access: `$page.headers`
`markdown` 中那些以一个或多个 `#` 定义的标题。
## siteConfig
> Access: `$site | Context.siteConfig`
- Access: `$site | Context.siteConfig`
`.vuepress/config.js`,译为`站点配置`
`.vuepress/config.js`,译为 `站点配置`
## themeConfig
> Access: `$site | Context.themeConfig`
- Access: `$themeConfig | Context.themeConfig`
`.vuepress/config.js``themeConfig` 的值,译为`用户的主题配置`
`.vuepress/config.js``themeConfig` 的值,是用户对当前所使用的主题配置。
## themePath
> Access: `Context.themePath`
- Access: `Context.themeAPI.theme.path`
当前使用的主题的根路径(绝对路径
当前使用的主题的所在的绝对路径。
## themeEntryFile
## themeEntry
> Access: `Context.themeEntryFile`
- Access: `Context.themeAPI.theme.entry`
主题的配置文件 (`themePath/index.js`)
主题的配置文件 `themePath/index.js`
## layout
## parentThemePath
> Access: `$page.frontmatter.layout`
- Access: `Context.themeAPI.parentTheme.path`
如果当前使用的主题是一个派生主题,那么 `parentThemePath` 就是指父主题的所在绝对路径。
## parentThemeEntry
- Access: `Context.themeAPI.parentTheme.entry`
如果当前使用的主题是一个派生主题,那么 `parentThemePath` 就是指父主题的主题的配置文件 `parentThemePath/index.js`
当前页面所使用的布局组件名。
@@ -42,12 +42,6 @@ VuePress 是否运行在生产环境模式下。
输出目录。
## ctx.themePath
- 类型: `string`
当前应用的主题的根路径。
## ctx.base
- 类型: `string`
+2 -2
View File
@@ -121,7 +121,7 @@ module.exports = (options, context) => ({
```js
module.exports = (options, context) => ({
chainWebpack (config) {
config.resolve.alias.set('@theme', context.themePath)
config.resolve.alias.set('@pwd', process.cwd())
}
})
```
@@ -131,7 +131,7 @@ module.exports = (options, context) => ({
```js
module.exports = (options, context) => ({
alias: {
'@theme': context.themePath
'@theme': context.themeAPI.themePath
}
})
```
+8
View File
@@ -3,3 +3,11 @@
::: tip
主题组件受到相同的 [浏览器的 API 访问限制](../guide/using-vue.md#浏览器的API访问限制).
:::
本栏的主要内容如下:
- [使用主题](./using-a-theme.md)
- [开发主题](./writing-a-theme.md)
- [主题的配置](./option-api.md)
- [主题的继承](./inheritance.md)
- [默认主题配置](./default-theme-config.md)
+181
View File
@@ -0,0 +1,181 @@
# 主题的继承 <Badge type="warn" text="beta" />
## 动机
我们有两个主要的理由来支持这个特性:
1. VuePress 为开发者提供了一个[默认主题](./default-theme-config.md),它能在大多数场景下满足了文档编写者的需求。即便如此,仍然还是会有不少用户选择将其 eject 出来进行修改,即使他们可能只是想要修改其中的某个组件。
2. 在 [0.x](https://vuepress.vuejs.org/guide/custom-themes.html#site-and-page-metadata) 中,主题的入口只需要一个 `Layout.vue`,所以我们可以通过包装另一个主题的 `Layout.vue` 来实现简单的拓展。
到了 1.x 中,一个主题的元素开始变得复杂,我们开始有了[主题级别的配置](./option-api.md),它支持为主题添加插件、自定义 GlobalLayout 等。除此之外,我们还有一些引入了主题开发的 [目录结构的约定](./writing-a-theme.md#目录结构),如 `styles/index.styl`,在这样的背景下,我们无法使用 0.x 的方式来实现继承了。
因此,我们需要提供一种合理、可靠的主题继承方案。
## 概念
为了介绍本节,我们先几个基本概念:
- **原子主题**:即父主题,类似默认主题这种完全从头实现的主题。
- **派生主题**:即子主题,基于父主题创建的主题;
::: tip 提示
主题继承暂时不支持高阶继承,也就是说,一个派生主题无法被继承。
:::
## 使用
假设你想创建一个继承自 VuePress 默认主题的派生主题,你只需要在你的主题配置中配置 [extend](./option-api.md#extend) 选项:
```js
module.exports = {
extend: '@vuepress/theme-default'
}
```
## 继承策略
父主题的所有能力都会"传递"给子主题,对于文件级别的约定,子主题可以通过在同样的位置创建同名文件来覆盖它;对于某些主题配置选项,如 [globalLayout](./option-api.md#globallayout),子主题也可以通过同名配置来覆盖它。
[文件级别的约定](./writing-a-theme.md#目录结构)如下:
- **全局组件**,即放置在 `theme/global-components` 中的 Vue 组件。
- **组件**,即放置在 `theme/components` 中的 Vue 组件。
- **全局的样式和调色板**,即放置在 `theme/styles` 中的 `index.styl``palette.styl`
- **HTML 模板**,即放置在 `theme/templates` 中的 `dev.html``ssr.html`
- **主题水平的客户端增强文件**,即 `theme/enhanceApp.js`
对于主题配置,能被子主题覆盖的配置选项如下:
- [devTemplate](./option-api.md#devtemplate)
- [ssrTemplate](./option-api.md#ssrtemplate)
- [globalLayout](./option-api.md#globallayout)
无法被子主题覆盖的主题配置选项如下:
- [extend](./option-api.md#extend)
需要特殊处理的主题选项:
- [plugins](./option-api.md#plugins):参见 [插件的覆盖](#插件的覆盖)。
## 插件的覆盖
对于父主题中的 [plugins](./option-api.md#plugins), 子主题不会直接覆盖它,但是插件的选项可以通过创建同名的插件配置来覆盖。
举例来说,如果父主题具有如下配置:
```js
// parentThemePath/index.js
module.exports = {
plugins: [
['@vuepress/search', {
searchMaxSuggestions: 5
}]
]
}
```
那么子主题可以通过如下方式来修改该插件的默认值:
```js
// themePath/index.js
module.exports = {
plugins: [
['@vuepress/search', {
searchMaxSuggestions: 10
}]
]
}
```
也可以选择禁用它:
```js
// themePath/index.js
module.exports = {
plugins: [
['@vuepress/search', false]
]
}
```
::: warning
一般情况下,你都不需要这样做,除非你明确知道禁用父主题中的插件不会带来问题。
:::
## 组件的覆盖
你可能想要在子主题中覆盖父主题中的同名组件,默认情况下,当父主题中的组件都使用相对路径引用其他组件时,你将不可能做到这一点,因为你无法在运行时修改父主题的代码。
VuePress 则通过一种巧妙的方式实现了这种需求,但这对父主题有一定的要求——**所有的组件都必须使用 `@theme` 别名来引用其他组件**。
举例来说,如果你正在开发的一个原子主题的结构如下:
::: vue
theme
├── components
│   ├── `Home.vue`
│   ├── `Navbar.vue`
│   └── `Sidebar.vue`
├── layouts
│   ├── `404.vue`
│   └── `Layout.vue`
├── package.json
└── index.js
:::
那么,在该主题中的任意 Vue 组件中,**你都应该通过 `@theme` 来访问主题根目录**
```vue
<script>
import Home from '@theme/components/Navbar.vue'
// ...
</script>
```
在这样的前提下,当你在子主题中同样的位置创建一个 `Navbar` 组件时:
::: vue
theme
└── components
   └── `Navbar.vue`
:::
`@theme/components/Navbar.vue` 会自动地映射到子主题中的 Navbar 组件,当你移除这个组件时,`@theme/components/Navbar.vue` 又会自动恢复为父主题中的 Navbar 组件。
如此一来,就可以实现轻松地“篡改”一个原子主题的某个部分。
::: tip
1. 组件的覆盖,最好直接基于父主题中对应组件的代码来修改;
2. 目前,在本地开发子主题,每次创建或移除组件时,你需要手动重启 Dev Server。
:::
## 访问父主题
你可以使用 `@parent-theme` 来访问父主题的根路径,下述示例展示了在子主题中创建一个同名的布局组件,并简单使用父主题中的 slot,[@vuepress/theme-vue](https://github.com/vuejs/vuepress/tree/master/packages/%40vuepress/theme-vue) 便是通过这种方式创造的。
```vue
<!-- themePath/components/Foo.vue -->
<template>
<ParentLayout>
<Foo slot="foo"/>
</ParentLayout>
</template>
<script>
import ParentLayout from '@parent-theme/layouts/Layout.vue'
import Foo from '@theme/components/Foo.vue'
export default {
components: {
ParentLayout,
Foo
}
}
</script>
```
+106 -5
View File
@@ -1,8 +1,31 @@
---
metaTitle: Option API | Theme
metaTitle: Configuration | Theme
---
# Option API
# 主题的配置
和插件几乎一样,主题的配置文件 `themeEntry` 应该导出一个普通的 JavaScript 对象(`#1`),它也可以是一个返回对象的函数(`#2`),这个函数接受用户在 `siteConfig.themeConfig` 为第一个参数、包含编译期上下文的 [ctx](./context-api.md) 对象作为第二个参数。
``` js
// #1
module.exports = {
// ...
}
```
``` js
// #2
module.exports = (themeConfig, ctx) => {
return {
// ...
}
}
```
::: tip
1. 你应该能看到 `themeEntry` 和 `themeConfig` 的区别,前者是一个主题本身的配置,这些配置由 VuePress 本身提供;而后者则是用户对主题的配置,这些配置选项则由当前使用的主题来实现,如 [默认主题配置](./default-theme-config.md)。
2. 除了本节列出的选项,`themeEntry` 也支持插件支持的所有 [配置选项](../plugin/option-api.md) 和 [生命周期](../plugin/life-cycle.md)。
:::
## plugins
@@ -13,11 +36,89 @@ metaTitle: Option API | Theme
- [插件 > 使用插件](../plugin/using-a-plugin.md).
VuePress 支持一个主题继承于另一个主题。VuePress 将遵循 `override` 的方式自动帮你解决各种主题属性(如样式、布局组件)的优先级。
---
值得注意的是,在子主题中,VuePress 将注入一个指向父主题包目录根路径的 [alias](../plugin/option-api.md#alias) `@parent-theme`
::: warning 注意
你可能不需要使用下面这些带有 <Badge text="Danger Zone" vertical="middle"/> 的选项,除非你知道你在做什么!
:::
## devTemplate <Badge text="Danger Zone"/>
- 类型: `String`
- 默认值: undefined
dev 模式下使用的 HTML 模板路径,默认模板见 [这里](https://github.com/vuejs/vuepress/blob/master/packages/%40vuepress/core/lib/app/index.dev.html)。
## ssrTemplate <Badge text="Danger Zone"/>
- 类型: `String`
- 默认值: undefined
build 模式下使用的 HTML 模板路径,默认模板见 [这里](https://github.com/vuejs/vuepress/blob/master/packages/%40vuepress/core/lib/app/index.ssr.html)。
**参考:**
- [Vue SSR Guide > template](https://ssr.vuejs.org/zh/api/#createrenderer).
## extend <Badge text="Danger Zone"/>
- 类型: `String`
- 默认值: undefined
```js
module.exports = {
extend: '@vuepress/theme-default'
}
```
VuePress 支持一个主题继承于另一个主题。VuePress 将遵循 `override` 的理念自动帮你解决各种主题属性(如样式、布局组件)的优先级。
**参考:**
- [主题继承](./inheritance.md)
- [例子: `@vuepress/theme-vue`](https://github.com/vuejs/vuepress/tree/master/packages/@vuepress/theme-vue)
- [Design Concepts of VuePress 1.x](../miscellaneous/design-concepts.md)
## globalLayout <Badge text="Danger Zone"/>
- 类型: `String`
- 默认值: undefined
```js
// themePath/index.js
module.exports = {
globalLayout: '/path/to/your/global/vue/sfc'
}
```
全局布局组件是负责管理全局布局方案的一个组件,VuePress [默认的 globalLayout](https://github.com/vuejs/vuepress/blob/master/packages/%40vuepress/core/lib/app/components/GlobalLayout.vue)会帮你根据 [$frontmatter.layout](../guide/frontmatter.md#layout) 来渲染不同的布局,所以大部分情况下你不要配置此选项。
举例来说,当你想为当前主题设置全局的 header 和 footer 时,你可以这样做:
```vue
<!-- themePath/layouts/GlobalLayout.vue -->
<template>
<div id="global-layout">
<header><h1>Header</h1></header>
<component :is="layout"/>
<footer><h1>Footer</h1></footer>
</div>
</template>
<script>
export default {
computed: {
layout () {
if (this.$page.path) {
if (this.$vuepress.isLayoutExists(this.$frontmatter.layout)) {
return this.$frontmatter.layout
}
return 'Layout'
}
return 'NotFound'
}
}
}
</script>
```
+5 -5
View File
@@ -1,10 +1,10 @@
# 使用主题
使用一个主题和使用一个插件几乎一致。
使用一个主题和使用一个插件的方式几乎一致。
## 使用 dependency 中的主题
## 使用来自依赖的主题
一个插件可以在以 `vuepress-theme-xxx` 的形式发布到 npm,你可以这样使用它:
一个主题可以在以 `vuepress-theme-xxx` 的形式发布到 npm,你可以这样使用它:
``` js
module.exports = {
@@ -14,7 +14,7 @@ module.exports = {
## 主题的缩写
如果你的插件名以 `vuepress-theme-` 开头,你可以使用缩写来省略这个前缀:
如果你的主题名以 `vuepress-theme-` 开头,你可以使用缩写来省略这个前缀:
``` js
module.exports = {
@@ -47,5 +47,5 @@ module.exports = {
```
::: warning 注意
以 `@vuepress/plugin-` 开头的插件是官方维护的插件
以 `@vuepress/theme-` 开头的主题是官方维护的主题
:::
+15 -11
View File
@@ -33,27 +33,27 @@
## 目录结构
随着需求的变化,只有一个布局组件 `Layout.vue` 可能还不够,你可能想要定义更多的布局组件用于不同的页面,你可能还想要自定义一个[调色板](../config/README.md#palette-styl), 甚至应用一些插件。
随着需求的变化,只有一个布局组件 `Layout.vue` 可能还不够,你可能想要定义更多的布局组件用于不同的页面,你可能还想要自定义一个[调色板](../config/README.md#palette-styl)甚至应用一些插件。
那么是时候重新组织你的主题了!一个约定的主题的目录结构如下:
::: vue
themePath
├── `global-components` _(**可选的**)_
theme
├── `global-components`
│ └── xxx.vue
├── `components` _(**可选的**)_
├── `components`
│ └── xxx.vue
├── `layouts`
│   ├── Layout.vue _(**必要的**)_
│   └── 404.vue _(**可选的**)_
├── `styles` _(**必要的**)_
│   └── 404.vue
├── `styles`
│   ├── index.styl
│   └── palette.styl
├── `templates` _(**必要的**)_
├── `templates`
  ├── dev.html
  └── ssr.html
├── `index.js` _(**当你将主题发布为一个 npm 包时,这是必须的!**)_
├── `enhanceApp.js` _(**必要的**)_
├── `index.js`
├── `enhanceApp.js`
└── package.json
:::
@@ -66,12 +66,12 @@ themePath
- `theme/enhanceApp.js`: 主题水平的客户端增强文件。
::: warning 注意
当你将你的主题以一个 npm 包的形式发布时,请确保这个包包含 `index.js`同时将 `package.json` 中的 `"main"` 字段设置为 `index.js`,如此一来 VuePress 才能获取到正确的 [themePath](../miscellaneous/glossary.md#theme-side).
当你将你的主题以一个 npm 包的形式发布时,如果你没有任何主题配置,即没有 `theme/index.js`那么你需要将 `package.json` 中的 `"main"` 字段设置为 `layouts/Layout.vue`,只有这样 VuePress 才能正确地解析主题。
```json
{
...
"main": "index.js"
"main": "layouts/Layout.vue",
...
}
```
@@ -100,6 +100,10 @@ layout: AnotherLayout
---
````
::: tip
每个 layout 组件都可能会渲染出截然不同的页面,如果你想设置一些全局的 UI(如全局的 `<header>`),可以考虑使用 [globalLayout](./option-api.md#globallayout)。
:::
## 使用插件
你可以通过主题的配置文件 `themePath/index.js` 来给主题应用一些插件: