Skip to content

包体积优化

Data Loaders (Vue Routers)

使用 Vue Router 的 Data Loaders,按需加载对应文件的 frontmatter。(支持并行的数据获取) 否则初始打包会合并在一个文件中。

vue
<script>
import { defineBasicLoader } from 'unplugin-vue-router/data-loaders/basic'

export const usePageData = defineBasicLoader('/relativePath', async (_to) => {
  // custom basic loader
}, {
  lazy: true,
})
</script>
ts
export function injectPageDataCode() {
  const vueContextImports = [
    `import { provide } from 'vue'`,
    `import { useRoute } from 'vue-router'`,

    'const { data: pageData } = usePageData()',
    'const route = useRoute()',
    // $frontmatter contain runtime added data
    // for example, $frontmatter.partiallyEncryptedContents
    `const $frontmatter = Object.assign(route.meta.frontmatter || {}, pageData.value.frontmatter || {})
    route.meta.frontmatter = $frontmatter

    provide('pageData', pageData)
    provide('valaxy:frontmatter', $frontmatter)
    `,
  ]

  return vueContextImports
}

const loaderVuePath = path.resolve(options.pkgRoot, 'node/templates/loader.vue')
let loaderVue = fs.readFileSync(loaderVuePath, 'utf-8')
loaderVue = loaderVue
  .replace('/relativePath', pageData.relativePath.slice('/pages'.length - 1, -'.md'.length))
  .replace('// custom basic loader', `return ${transformObject(pageData)}`)
code = loaderVue + code

manualChunks

手动分包

  • 框架
  • 各类库
ts
import type {
  Rollup,
} from 'vite'
import type { ResolvedValaxyOptions } from '../options'
import path from 'node:path'

// ref vitepress
const cache = new Map<string, boolean>()
const cacheTheme = new Map<string, boolean>()

// https://github.com/sindresorhus/escape-string-regexp/blob/ba9a4473850cb367936417e97f1f2191b7cc67dd/index.js
export function escapeRegExp(str: string) {
  return str.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&').replace(/-/g, '\\x2d')
}

// https://github.com/vitejs/vite/blob/d2aa0969ee316000d3b957d7e879f001e85e369e/packages/vite/src/node/plugins/splitVendorChunk.ts#L14
// eslint-disable-next-line regexp/no-unused-capturing-group
const CSS_LANGS_RE = /\.(css|less|sass|scss|styl|stylus|pcss|postcss|sss)(?:$|\?)/

/**
 * Check if a module is statically imported by at least one entry.
 */
export function isEagerChunk(id: string, getModuleInfo: Rollup.GetModuleInfo) {
  if (
    id.includes('node_modules')
    && !CSS_LANGS_RE.test(id)
    && staticImportedByEntry(id, getModuleInfo, cache)
  ) {
    return true
  }
}

function staticImportedByEntry(
  id: string,
  getModuleInfo: Rollup.GetModuleInfo,
  cache: Map<string, boolean>,
  entryRE: RegExp | null = null,
  importStack: string[] = [],
): boolean {
  if (cache.has(id)) {
    return !!cache.get(id)
  }
  if (importStack.includes(id)) {
    // circular deps!
    cache.set(id, false)
    return false
  }
  const mod = getModuleInfo(id)
  if (!mod) {
    cache.set(id, false)
    return false
  }

  if (entryRE ? entryRE.test(id) : mod.isEntry) {
    cache.set(id, true)
    return true
  }
  const someImporterIs = mod.importers.some((importer: string) =>
    staticImportedByEntry(
      importer,
      getModuleInfo,
      cache,
      entryRE,
      importStack.concat(id),
    ),
  )
  cache.set(id, someImporterIs)
  return someImporterIs
}

export function getRollupOptions(options: ResolvedValaxyOptions) {
  // these deps are also being used in the client code (outside of the theme)
  // exclude them from the theme chunk so there is no circular dependency
  const excludedModules = [
    '/@siteData',
    'node_modules/@vueuse/core/',
    'node_modules/@vueuse/shared/',
    'node_modules/vue/',
    'node_modules/vue-demi/',
    options.clientRoot,
  ]

  const themeEntryRE = new RegExp(
    `^${escapeRegExp(
      path.resolve(options.themeRoot, 'index.ts').replace(/\\/g, '/'),
    )}`,
  )

  const rollupOptions: Rollup.RollupOptions = {
    external: [],
    output: {
      manualChunks(id, ctx) {
        // move known framework code into a stable chunk so that
        // custom theme changes do not invalidate hash for all pages
        if (id.startsWith('\0vite')) {
          return 'framework'
        }
        if (id.includes('plugin-vue:export-helper')) {
          return 'framework'
        }
        if (
          isEagerChunk(id, ctx.getModuleInfo)
          // eslint-disable-next-line regexp/no-unused-capturing-group
          && /@vue\/(runtime|shared|reactivity)/.test(id)
        ) {
          return 'framework'
        }

        if (
          (id.startsWith(options.themeRoot)
            || !excludedModules.some(i => id.includes(i)))
          && staticImportedByEntry(
            id,
            ctx.getModuleInfo,
            cacheTheme,
            themeEntryRE,
          )
        ) {
          return 'theme'
        }

        if (id.startsWith('vue-i18n'))
          return 'vue-i18n'
      },
    },
  }
  return rollupOptions
}

date-fns vs dayjs?

尽管 date-fns 支持 ESM,而 dayjs 不支持 ESM。

但 Valaxy 所使用到的函数,打包分析后,date-fns 的 chunk 约为 30KB,而 dayjs 的 chunk 约为 21KB

因此 dayjs 占用体积仍小于 date-fns,且 dayjs 全局的特性使得 API 更加简洁。