From d16d3d574ea30a3597e1919acedd77f6d1c770f2 Mon Sep 17 00:00:00 2001 From: ULIVZ <472590061@qq.com> Date: Tue, 26 Feb 2019 23:53:38 +0800 Subject: [PATCH] feat: refine theme api (#1319) --- CHANGELOG.md | 45 ---- __mocks__/vuepress-theme-child/Layout.vue | 0 .../vuepress-theme-child/components/Home.vue | 0 __mocks__/vuepress-theme-child/index.js | 3 + __mocks__/vuepress-theme-parent/Layout.vue | 0 .../vuepress-theme-parent/components/Home.vue | 0 .../components/Sidebar.vue | 0 __mocks__/vuepress-theme-parent/index.js | 1 + .../theme-api/fixtures/theme/Layout.vue | 0 .../fixtures/theme/components/Home.vue | 0 .../core/__tests__/theme-api/index.spec.js | 27 +++ .../core/lib/internal-plugins/enhanceApp.js | 14 +- .../lib/internal-plugins/layoutComponents.js | 13 +- .../lib/internal-plugins/palette/index.js | 6 +- .../core/lib/internal-plugins/style/index.js | 12 +- .../@vuepress/core/lib/plugin-api/index.js | 9 +- .../@vuepress/core/lib/prepare/AppContext.js | 94 +++----- .../@vuepress/core/lib/prepare/loadTheme.js | 204 ++++++------------ .../core/lib/theme-api/Layout.fallback.vue | 3 + .../@vuepress/core/lib/theme-api/index.js | 152 +++++++++++++ .../core/lib/webpack/createBaseConfig.js | 2 - .../layouts/{Layout.vue => Layout2.vue} | 0 .../theme-default/components/DropdownLink.vue | 4 +- .../theme-default/components/Home.vue | 2 +- .../theme-default/components/NavLinks.vue | 4 +- .../theme-default/components/Navbar.vue | 4 +- .../theme-default/components/Sidebar.vue | 4 +- .../theme-default/components/SidebarGroup.vue | 2 +- .../theme-default/components/SidebarLinks.vue | 4 +- .../theme-default/layouts/Layout.vue | 8 +- .../@vuepress/theme-vue/layouts/Layout.vue | 4 +- packages/docs/docs/.vuepress/config.js | 3 +- packages/docs/docs/miscellaneous/glossary.md | 49 +++-- packages/docs/docs/plugin/context-api.md | 6 - packages/docs/docs/plugin/option-api.md | 4 +- packages/docs/docs/theme/inheritance.md | 182 ++++++++++++++++ packages/docs/docs/theme/option-api.md | 106 ++++++++- packages/docs/docs/theme/using-a-theme.md | 4 +- packages/docs/docs/theme/writing-a-theme.md | 27 +-- .../docs/docs/zh/miscellaneous/glossary.md | 61 +++--- packages/docs/docs/zh/plugin/context-api.md | 6 - packages/docs/docs/zh/plugin/option-api.md | 4 +- packages/docs/docs/zh/theme/README.md | 8 + packages/docs/docs/zh/theme/inheritance.md | 181 ++++++++++++++++ packages/docs/docs/zh/theme/option-api.md | 111 +++++++++- packages/docs/docs/zh/theme/using-a-theme.md | 10 +- .../docs/docs/zh/theme/writing-a-theme.md | 26 ++- 47 files changed, 1012 insertions(+), 397 deletions(-) mode change 100644 => 100755 CHANGELOG.md create mode 100644 __mocks__/vuepress-theme-child/Layout.vue create mode 100644 __mocks__/vuepress-theme-child/components/Home.vue create mode 100644 __mocks__/vuepress-theme-child/index.js create mode 100644 __mocks__/vuepress-theme-parent/Layout.vue create mode 100644 __mocks__/vuepress-theme-parent/components/Home.vue create mode 100644 __mocks__/vuepress-theme-parent/components/Sidebar.vue create mode 100644 __mocks__/vuepress-theme-parent/index.js create mode 100644 packages/@vuepress/core/__tests__/theme-api/fixtures/theme/Layout.vue create mode 100644 packages/@vuepress/core/__tests__/theme-api/fixtures/theme/components/Home.vue create mode 100644 packages/@vuepress/core/__tests__/theme-api/index.spec.js mode change 100644 => 100755 packages/@vuepress/core/lib/internal-plugins/enhanceApp.js mode change 100644 => 100755 packages/@vuepress/core/lib/prepare/AppContext.js mode change 100644 => 100755 packages/@vuepress/core/lib/prepare/loadTheme.js create mode 100644 packages/@vuepress/core/lib/theme-api/Layout.fallback.vue create mode 100644 packages/@vuepress/core/lib/theme-api/index.js rename packages/@vuepress/theme-blog/layouts/{Layout.vue => Layout2.vue} (100%) mode change 100644 => 100755 mode change 100644 => 100755 packages/docs/docs/.vuepress/config.js mode change 100644 => 100755 packages/docs/docs/miscellaneous/glossary.md create mode 100755 packages/docs/docs/theme/inheritance.md mode change 100644 => 100755 packages/docs/docs/theme/option-api.md mode change 100644 => 100755 packages/docs/docs/zh/miscellaneous/glossary.md create mode 100755 packages/docs/docs/zh/theme/inheritance.md mode change 100644 => 100755 packages/docs/docs/zh/theme/option-api.md mode change 100644 => 100755 packages/docs/docs/zh/theme/writing-a-theme.md diff --git a/CHANGELOG.md b/CHANGELOG.md old mode 100644 new mode 100755 index 25b98b18bf..44152f7090 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 - - - - ``` - - 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)) diff --git a/__mocks__/vuepress-theme-child/Layout.vue b/__mocks__/vuepress-theme-child/Layout.vue new file mode 100644 index 0000000000..e69de29bb2 diff --git a/__mocks__/vuepress-theme-child/components/Home.vue b/__mocks__/vuepress-theme-child/components/Home.vue new file mode 100644 index 0000000000..e69de29bb2 diff --git a/__mocks__/vuepress-theme-child/index.js b/__mocks__/vuepress-theme-child/index.js new file mode 100644 index 0000000000..8cf79b81cc --- /dev/null +++ b/__mocks__/vuepress-theme-child/index.js @@ -0,0 +1,3 @@ +module.exports = { + extend: 'vuepress-theme-parent' +} diff --git a/__mocks__/vuepress-theme-parent/Layout.vue b/__mocks__/vuepress-theme-parent/Layout.vue new file mode 100644 index 0000000000..e69de29bb2 diff --git a/__mocks__/vuepress-theme-parent/components/Home.vue b/__mocks__/vuepress-theme-parent/components/Home.vue new file mode 100644 index 0000000000..e69de29bb2 diff --git a/__mocks__/vuepress-theme-parent/components/Sidebar.vue b/__mocks__/vuepress-theme-parent/components/Sidebar.vue new file mode 100644 index 0000000000..e69de29bb2 diff --git a/__mocks__/vuepress-theme-parent/index.js b/__mocks__/vuepress-theme-parent/index.js new file mode 100644 index 0000000000..4ba52ba2c8 --- /dev/null +++ b/__mocks__/vuepress-theme-parent/index.js @@ -0,0 +1 @@ +module.exports = {} diff --git a/packages/@vuepress/core/__tests__/theme-api/fixtures/theme/Layout.vue b/packages/@vuepress/core/__tests__/theme-api/fixtures/theme/Layout.vue new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/@vuepress/core/__tests__/theme-api/fixtures/theme/components/Home.vue b/packages/@vuepress/core/__tests__/theme-api/fixtures/theme/components/Home.vue new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/@vuepress/core/__tests__/theme-api/index.spec.js b/packages/@vuepress/core/__tests__/theme-api/index.spec.js new file mode 100644 index 0000000000..a8e6c7bdd4 --- /dev/null +++ b/packages/@vuepress/core/__tests__/theme-api/index.spec.js @@ -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') +}) diff --git a/packages/@vuepress/core/lib/internal-plugins/enhanceApp.js b/packages/@vuepress/core/lib/internal-plugins/enhanceApp.js old mode 100644 new mode 100755 index 5aa987c38a..7e90d4d751 --- a/packages/@vuepress/core/lib/internal-plugins/enhanceApp.js +++ b/packages/@vuepress/core/lib/internal-plugins/enhanceApp.js @@ -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 } }) diff --git a/packages/@vuepress/core/lib/internal-plugins/layoutComponents.js b/packages/@vuepress/core/lib/internal-plugins/layoutComponents.js index 97f781b933..d8b71a2417 100644 --- a/packages/@vuepress/core/lib/internal-plugins/layoutComponents.js +++ b/packages/@vuepress/core/lib/internal-plugins/layoutComponents.js @@ -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) - }) } } } diff --git a/packages/@vuepress/core/lib/internal-plugins/palette/index.js b/packages/@vuepress/core/lib/internal-plugins/palette/index.js index fc0c2684b7..bd43d5e4fa 100644 --- a/packages/@vuepress/core/lib/internal-plugins/palette/index.js +++ b/packages/@vuepress/core/lib/internal-plugins/palette/index.js @@ -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, '/'))})` : '' diff --git a/packages/@vuepress/core/lib/internal-plugins/style/index.js b/packages/@vuepress/core/lib/internal-plugins/style/index.js index a0646d7d9c..2055be6978 100644 --- a/packages/@vuepress/core/lib/internal-plugins/style/index.js +++ b/packages/@vuepress/core/lib/internal-plugins/style/index.js @@ -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, '/'))})` : '' diff --git a/packages/@vuepress/core/lib/plugin-api/index.js b/packages/@vuepress/core/lib/plugin-api/index.js index 83639fb542..a887fe7dd9 100644 --- a/packages/@vuepress/core/lib/plugin-api/index.js +++ b/packages/@vuepress/core/lib/plugin-api/index.js @@ -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 diff --git a/packages/@vuepress/core/lib/prepare/AppContext.js b/packages/@vuepress/core/lib/prepare/AppContext.js old mode 100644 new mode 100755 index e92f2bc312..f769842f87 --- a/packages/@vuepress/core/lib/prepare/AppContext.js +++ b/packages/@vuepress/core/lib/prepare/AppContext.js @@ -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 - - const templateDir = path.resolve(this.vuepressDir, 'templates') - const siteSsrTemplate2 = path.resolve(templateDir, 'ssr.html') - const siteDevTemplate2 = path.resolve(templateDir, 'dev.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 - ]) + this.devTemplate = this.resolveCommonAgreementFilePath( + 'devTemplate', + { + defaultValue: path.resolve(__dirname, '../app/index.dev.html'), + siteAgreement: 'templates/dev.html', + themeAgreement: 'templates/dev.html' + } + ) - const devTemplate = fsExistsFallback([ - siteDevTemplate, - siteDevTemplate2, - themeDevTemplate, - parentThemeDevTemplate, - defaultDevTemplate - ]) + this.ssrTemplate = this.resolveCommonAgreementFilePath( + 'ssrTemplate', + { + defaultValue: path.resolve(__dirname, '../app/index.ssr.html'), + siteAgreement: 'templates/ssr.html', + themeAgreement: 'templates/ssr.html' + } + ) - 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} - * @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 } diff --git a/packages/@vuepress/core/lib/prepare/loadTheme.js b/packages/@vuepress/core/lib/prepare/loadTheme.js old mode 100644 new mode 100755 index f07804cddb..9a4bbca3d9 --- a/packages/@vuepress/core/lib/prepare/loadTheme.js +++ b/packages/@vuepress/core/lib/prepare/loadTheme.js @@ -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 } - ) - } + logger.debug('theme', theme.name, theme.path) + logger.debug('parentTheme', parentTheme.name, parentTheme.path) + return new ThemeAPI(theme, parentTheme, ctx) +} - // normalize component name - const getComponentName = filename => { - filename = filename.slice(0, -4) - if (filename === '404') { - filename = 'NotFound' +function normalizeThemePath (resolved) { + const { entry, name, fromDep } = resolved + if (fromDep) { + const pkgPath = require.resolve(name) + 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 filename + return packageRootDir + } else if (entry.endsWith('.js') || entry.endsWith('.vue')) { + return parse(entry).dir + } else { + return entry } +} - 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 - }, {}) +function resolveTheme (ctx, resolver, ignoreLocal, theme) { + const { siteConfig, cliOptions, sourceDir, vuepressDir, pluginAPI } = ctx + const localThemePath = resolve(vuepressDir, 'theme') + theme = theme || siteConfig.theme || cliOptions.theme - const { Layout = {}, NotFound = {}} = layoutComponentMap + let path + let name + let shortcut + let entry = {} - // layout component does not exist. - if (!Layout || !fs.existsSync(Layout.path)) { - throw new Error(`[vuepress] Cannot resolve Layout.vue file in \n ${Layout.path}`) - } + // 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)}...`) - // 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 + // 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 {} } - return { - themePath, - layoutComponentMap, - themeEntryFile, - themeName, - themeShortcut, - parentThemePath, - parentThemeEntryFile + try { + entry = pluginAPI.normalizePlugin(path, ctx.themeConfig) + } catch (error) { + entry = {} } -} -function normalizeThemePath (resolved) { - const { entry, name, fromDep } = resolved - if (fromDep) { - const pkgPath = require.resolve(name) - return path.parse(pkgPath).dir - } else if (entry.endsWith('.js') || entry.endsWith('.vue')) { - return path.parse(entry).dir - } else { - return entry - } + return { path, name, shortcut, entry } } diff --git a/packages/@vuepress/core/lib/theme-api/Layout.fallback.vue b/packages/@vuepress/core/lib/theme-api/Layout.fallback.vue new file mode 100644 index 0000000000..34d84c1b5f --- /dev/null +++ b/packages/@vuepress/core/lib/theme-api/Layout.fallback.vue @@ -0,0 +1,3 @@ + diff --git a/packages/@vuepress/core/lib/theme-api/index.js b/packages/@vuepress/core/lib/theme-api/index.js new file mode 100644 index 0000000000..28af84c4ab --- /dev/null +++ b/packages/@vuepress/core/lib/theme-api/index.js @@ -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' +} diff --git a/packages/@vuepress/core/lib/webpack/createBaseConfig.js b/packages/@vuepress/core/lib/webpack/createBaseConfig.js index c6b5a90cc1..bf79b2072f 100644 --- a/packages/@vuepress/core/lib/webpack/createBaseConfig.js +++ b/packages/@vuepress/core/lib/webpack/createBaseConfig.js @@ -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) diff --git a/packages/@vuepress/theme-blog/layouts/Layout.vue b/packages/@vuepress/theme-blog/layouts/Layout2.vue old mode 100644 new mode 100755 similarity index 100% rename from packages/@vuepress/theme-blog/layouts/Layout.vue rename to packages/@vuepress/theme-blog/layouts/Layout2.vue diff --git a/packages/@vuepress/theme-default/components/DropdownLink.vue b/packages/@vuepress/theme-default/components/DropdownLink.vue index ec45fae957..0d360830ee 100644 --- a/packages/@vuepress/theme-default/components/DropdownLink.vue +++ b/packages/@vuepress/theme-default/components/DropdownLink.vue @@ -50,8 +50,8 @@ +``` + +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 + + + + +``` + + + + + diff --git a/packages/docs/docs/theme/option-api.md b/packages/docs/docs/theme/option-api.md old mode 100644 new mode 100755 index 0fbb7aa90c..57ac02cd9c --- a/packages/docs/docs/theme/option-api.md +++ b/packages/docs/docs/theme/option-api.md @@ -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). + +--- + +::: warning +You probably don't need to use following options tagged with unless you know what you are doing! +::: + +## devTemplate + +- 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 + +- 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 +## extend - 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 + +- 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 + + + + +``` diff --git a/packages/docs/docs/theme/using-a-theme.md b/packages/docs/docs/theme/using-a-theme.md index dfefcf2460..7cc37b78da 100644 --- a/packages/docs/docs/theme/using-a-theme.md +++ b/packages/docs/docs/theme/using-a-theme.md @@ -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. ::: diff --git a/packages/docs/docs/theme/writing-a-theme.md b/packages/docs/docs/theme/writing-a-theme.md index a697176579..90b580c9dc 100644 --- a/packages/docs/docs/theme/writing-a-theme.md +++ b/packages/docs/docs/theme/writing-a-theme.md @@ -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`. diff --git a/packages/docs/docs/zh/miscellaneous/glossary.md b/packages/docs/docs/zh/miscellaneous/glossary.md old mode 100644 new mode 100755 index 95419a4aa2..fa60adaf73 --- a/packages/docs/docs/zh/miscellaneous/glossary.md +++ b/packages/docs/docs/zh/miscellaneous/glossary.md @@ -6,64 +6,75 @@ sidebar: auto 你可能会在文档中碰到一些陌生的概念,本节列出了文档中常见的术语,方便查阅、学习、插件/主题开发之用。 -## frontmatter +## layout -> Access: `$page.frontmatter` +- Access: `$page.frontmatter.layout` -当前页面的 `markdown` 文件中包裹在 `---` 中的配置,一般用于做一些页面级别的配置。 +当前页面所使用的布局组件名。 -::: tip -VuePress 的动态布局系统等特性是基于 `frontmatter` 实现的,你可以使用插件 API [extendPageData](../plugin/option-api.md#extendpagedata) 在构建期间动态修改 frontmatter 的值。 -::: +## frontmatter + +- Access: `$page.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`。 -当前页面所使用的布局组件名。 diff --git a/packages/docs/docs/zh/plugin/context-api.md b/packages/docs/docs/zh/plugin/context-api.md index dfee98d9df..af72f8d015 100644 --- a/packages/docs/docs/zh/plugin/context-api.md +++ b/packages/docs/docs/zh/plugin/context-api.md @@ -42,12 +42,6 @@ VuePress 是否运行在生产环境模式下。 输出目录。 -## ctx.themePath - -- 类型: `string` - -当前应用的主题的根路径。 - ## ctx.base - 类型: `string` diff --git a/packages/docs/docs/zh/plugin/option-api.md b/packages/docs/docs/zh/plugin/option-api.md index e92d839814..2ea8e80c0d 100644 --- a/packages/docs/docs/zh/plugin/option-api.md +++ b/packages/docs/docs/zh/plugin/option-api.md @@ -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 } }) ``` diff --git a/packages/docs/docs/zh/theme/README.md b/packages/docs/docs/zh/theme/README.md index 88856355e2..83447d4210 100644 --- a/packages/docs/docs/zh/theme/README.md +++ b/packages/docs/docs/zh/theme/README.md @@ -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) diff --git a/packages/docs/docs/zh/theme/inheritance.md b/packages/docs/docs/zh/theme/inheritance.md new file mode 100755 index 0000000000..cc0816b36b --- /dev/null +++ b/packages/docs/docs/zh/theme/inheritance.md @@ -0,0 +1,181 @@ +# 主题的继承 + +## 动机 + +我们有两个主要的理由来支持这个特性: + +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 + +``` + +在这样的前提下,当你在子主题中同样的位置创建一个 `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 + + + + +``` + + + + + diff --git a/packages/docs/docs/zh/theme/option-api.md b/packages/docs/docs/zh/theme/option-api.md old mode 100644 new mode 100755 index f304f7bd12..3fb8ccd359 --- a/packages/docs/docs/zh/theme/option-api.md +++ b/packages/docs/docs/zh/theme/option-api.md @@ -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` 的方式自动帮你解决各种主题属性(如样式、布局组件)的优先级。 +--- + +::: warning 注意 +你可能不需要使用下面这些带有 的选项,除非你知道你在做什么! +::: + +## devTemplate + +- 类型: `String` +- 默认值: undefined + +dev 模式下使用的 HTML 模板路径,默认模板见 [这里](https://github.com/vuejs/vuepress/blob/master/packages/%40vuepress/core/lib/app/index.dev.html)。 + +## ssrTemplate + +- 类型: `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 -值得注意的是,在子主题中,VuePress 将注入一个指向父主题包目录根路径的 [alias](../plugin/option-api.md#alias) `@parent-theme`。 +- 类型: `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 + +- 类型: `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 + + + + +``` diff --git a/packages/docs/docs/zh/theme/using-a-theme.md b/packages/docs/docs/zh/theme/using-a-theme.md index 3212fd82ea..2c4f729db8 100644 --- a/packages/docs/docs/zh/theme/using-a-theme.md +++ b/packages/docs/docs/zh/theme/using-a-theme.md @@ -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-` 开头的主题是官方维护的主题。 ::: diff --git a/packages/docs/docs/zh/theme/writing-a-theme.md b/packages/docs/docs/zh/theme/writing-a-theme.md old mode 100644 new mode 100755 index 0c586cb115..a40b42d907 --- a/packages/docs/docs/zh/theme/writing-a-theme.md +++ b/packages/docs/docs/zh/theme/writing-a-theme.md @@ -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(如全局的 `
`),可以考虑使用 [globalLayout](./option-api.md#globallayout)。 +::: + ## 使用插件 你可以通过主题的配置文件 `themePath/index.js` 来给主题应用一些插件: