诗号:六道同坠,魔劫万千,引渡如来。

/img/bdx/yiyeshu-001.jpg

该脚手架详情请查看官方文档: gcclll/vitesse: 🏕 Opinionated Vite Starter Template

本文是使用和学习过程中的一些笔记!

项目目录结构:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
 vitesse git:(main) ✗ tree -I "node_modules|dist"
.
├── cypress
│   ├── integration
│   │   └── basic.spec.ts
│   └── tsconfig.json
├── cypress.json
├── index.html # 模板文件
├── locales # 多语言国际化
│   ├── README.md
│   └── zh-CN.yml
├── netlify.toml
├── package.json
├── pnpm-lock.yaml
├── public
│   ├── _headers
│   ├── favicon.svg
│   ├── pwa-192x192.png
│   ├── pwa-512x512.png
│   ├── robots.txt
│   └── safari-pinned-tab.svg
├── src
│   ├── App.vue
│   ├── auto-imports.d.ts # 自动引入声明,该文件中声明,在项目中可以直接使用而不需要通过 import 引入
│   ├── components # 组件目录,该目录下的组件也可以直接使用而不需要 import 引入
│   │   ├── Counter.vue
│   │   ├── Footer.vue
│   │   └── README.md
│   ├── components.d.ts
│   ├── composables # 多主题
│   │   ├── dark.ts
│   │   └── index.ts
│   ├── layouts # 页面布局
│   │   ├── 404.vue
│   │   ├── README.md
│   │   ├── default.vue
│   │   └── home.vue
│   ├── main.ts
│   ├── modules # 项目中使用到了一些插件模块化
│   │   ├── README.md
│   │   ├── i18n.ts
│   │   ├── nprogress.ts
│   │   ├── pinia.ts
│   │   └── pwa.ts
│   ├── pages # 页面目录,自动路由化,会根据目录结构和文件名称生成对应的路由
│   │   ├── README.md
│   │   ├── [...all].vue
│   │   ├── about.md # 如: /about 加载这个
│   │   ├── hi
│   │   │   └── [name].vue # 如: /hi/100 加载这个页面
│   │   └── index.vue
│   ├── shims.d.ts
│   ├── stores # pinia 状态管理插件
│   │   └── user.ts
│   ├── styles # 全局样式
│   │   ├── main.css
│   │   └── markdown.css
│   └── types.ts # ts类型声明
├── test
│   ├── __snapshots__
│   │   └── component.test.ts.snap
│   ├── basic.test.ts
│   └── component.test.ts
├── tsconfig.json
├── vite.config.ts # vite 配置
└── windi.config.ts # windi css 框架配置

特性(来自官方),下面会对每个特性使用到的插件进行分析:

  • ⚡️ Vue 3, Vite 2, pnpm, ESBuild - born with fastness

  • 🗂 File based routing

    基于文件结构的路由系统,使用插件 hannoeru/vite-plugin-pages: File system based route generator for ⚡️Vite

    代码入口: vite-plugin-pages/index.ts at main · hannoeru/vite-plugin-pages

    1
    2
    3
    4
    5
    6
    
    async load(id) {
      if (id !== MODULE_ID_VIRTUAL)
         return
    
         return ctx.resolveRoutes()
    },
    

    resolveRoutes() 解析 src/pages 路由:vite-plugin-pages/context.ts at 771e956fd41589d8c9012c05016503e227d15b21 · hannoeru/vite-plugin-pages

    1
    2
    3
    4
    5
    6
    
    async resolveRoutes() {
      if (this.options.resolver === 'vue')
        return await resolveVueRoutes(this)
      if (this.options.resolver === 'react')
        return await resolveReactRoutes(this)
    }
    

    -> resolveVueRoutes(this) vite-plugin-pages/vue.ts at 771e956fd41589d8c9012c05016503e227d15b21 · hannoeru/vite-plugin-pages

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    
    export async function resolveVueRoutes(ctx: PageContext) {
    const { nuxtStyle, caseSensitive } = ctx.options
    
    // ctx.pageRouteMap 这个应该就是 src/pages/* 下所有文件解析出来的结构
    const pageRoutes = [...ctx.pageRouteMap.values()]
      // sort routes for HMR
      .sort((a, b) => countSlash(a.route) - countSlash(b.route))
    
    const routes: Route[] = []
    
    pageRoutes.forEach((page) => {
      const pathNodes = page.route.split('/')
    
      // add leading slash to component path if not already there
      const component = page.path.replace(ctx.root, '')
      const customBlock = ctx.customBlockMap.get(page.path)
    
      const route: Route = {
        name: '',
        path: '',
        component,
        customBlock,
        rawRoute: page.route,
      }
    
      let parentRoutes = routes
    
      for (let i = 0; i < pathNodes.length; i++) {
        // ..., 将 pathNode 解析成 Route 结构
      }
      parentRoutes.push(route)
    })
    
    let finalRoutes = prepareRoutes(ctx, routes)
    
    finalRoutes = (await ctx.options.onRoutesGenerated?.(finalRoutes)) || finalRoutes
    
    let client = generateClientCode(finalRoutes, ctx.options)
    client = (await ctx.options.onClientGenerated?.(client)) || client
    return client
    }
    

    PageContext 通过 setupViteServer 借用 vite 起的服务来监听(setupWatcher)目录文 件的变化(unlink,add,change),执行对应的操作(removePage, addPage, checkCustomBlockChange) 目的都是为了更新 pageRouteMap 这个结构。

    searchGlob 会扫描 options.dirs 指定的目录(默认是 src/pages),找出符合条件的页 面解析成路由。

    简单来说 vite-plugin-pages 就是通过借用 vite 起的服务去监听 src/pages 目录的变 化,如果有文件变化就将其根据其路径和文件名解析成对应的路由对象。

  • 📦 Components auto importing

    使用的插件:antfu/unplugin-vue-components: 📲 On-demand components auto importing for Vue

    src/index.ts -> src/core/unplugin.ts

    给 vite 服务增加 watcher : ctx.setupWatcher(chokidar.watch(ctx.options.globs))

    transform 转换函数:

    1
    2
    3
    4
    5
    6
    7
    
    async transform(code, id) {
    // ...
          const result = await ctx.transform(code, id)
          ctx.generateDeclaration()
          return result
    // ...
    },
    

    可以转换的时候进行转换,然后生成声明,即 src/components.d.ts 中的内容。

    Context 中同样也是通过 vite server 监听文件 unlink,add 变化,执行 removeComponentsaddCompnents, 以 addComponents 为例:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    addComponents(paths: string | string[]) {
    debug.components('add', paths)
    
    const size = this._componentPaths.size
    toArray(paths).forEach(p => this._componentPaths.add(p))
    if (this._componentPaths.size !== size) {
      this.updateComponentNameMap()
      return true
    }
    return false
    }
    

    -> updateComponentNameMap() 更新的是 componentNameMap 这个对象,它会被

    unplugin-vue-components/declaration.ts at 40fb687990071e485a11ddb73b8e1beec1694249 · antfu/unplugin-vue-components

    中的 generateDeclaration 函数解析最后生成对应的代码写入(await fs.writeFile(filepath, code, 'utf-8'))到 src/components.d.ts

  • 🍍 State Management via Pinia

  • 📑 Layout system

  • 📲 PWA

  • 🎨 Windi CSS - next generation utility-first CSS framework

  • 😃 Use icons from any icon sets, with no compromise

    antfu/unplugin-icons: 🤹 Access thousands of icons as components on-demand universally.

    代码入口 src/index.ts loader 函数 -> generateComponentFromPath()

    unplugin-icons/loader.ts at 4fde686174e0d054eac180c179dbae820afefba1 · antfu/unplugin-icons

    1
    2
    3
    4
    5
    6
    
    export async function generateComponentFromPath(path: string, options: ResolvedOptions) {
    const resolved = resolveIconsPath(path)
    if (!resolved)
      return null
    return generateComponent(resolved, options)
    }
    

    loadCollection -> `@iconify-json/${name}/icons.json` -> install await tryInstallPkg(`@iconify-json/${name}`)

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    
    
    export async function tryInstallPkg(name: string) {
    if (pending)
      await pending
    
    if (!tasks[name]) {
      // eslint-disable-next-line no-console
      console.log(cyan(`Installing ${name}...`))
      tasks[name] = pending = installPackage(name, { dev: true, preferOffline: true })
        .then(() => sleep(300))
        .catch((e) => {
          warnOnce(`Failed to install ${name}`)
          console.error(e)
        })
        .finally(() => {
          pending = undefined
        })
    }
    
    return tasks[name]!
    }
    

    使用 antfu/install-pkg: Install package programmatically. 下载安装 icon, 执行:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    
    execa(
      agent,
      [
        agent === 'yarn'
          ? 'add'
          : 'install',
        options.dev ? '-D' : '',
        ...args,
        ...names,
      ].filter(Boolean),
      {
        stdio: options.silent ? 'ignore' : 'inherit',
        cwd: options.cwd,
      },
    )
    

    等于是 yarn add -D @iconify-json/icon-name 安装。

    根据解析出来的路径 resolved 去下载 icon let svg = await getIcon(collection, icon, query, options) 下载成功后转成 svg

    1
    2
    3
    4
    5
    6
    7
    8
    
    return await mergeIconProps(
      `<svg>${body}</svg>`,
      collection,
      id,
      query,
      () => attributes,
      options,
    )
    
  • 🌍 I18n ready

  • 🗒 Markdown Support

  • 🔥 Use the new <script setup> syntax

  • 📥 APIs auto importing - use Composition API and others directly

    antfu/unplugin-auto-import: Auto import APIs on-demand for Vite, Webpack and Rollup

    引入 unplugin-auto-import 然后在 vite.config.ts 中增加:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    plugins: ]
      AutoImport({
        imports: [
          'vue',
          'vue-router',
          'vue-i18n',
          '@vueuse/head',
          '@vueuse/core',
        ],
        dts: 'src/auto-imports.d.ts',
      }),
    ]
    

    其实和 🔗 components auto import 原理差不多, 差异在这个是直接通过 vite.config.ts 中的配置中获取需要自动导入的插件。

    代码: src/index.ts -> transform -> generateConfigFiles()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    
    const generateConfigFiles = throttle(500, false, () => {
      if (resolved.dts)
        fs.writeFile(resolved.dts, generateDeclaration(resolved.imports, resolved.resolvedImports), 'utf-8')
    
      const { eslintrc } = resolved
      if (eslintrc.enabled && eslintrc.filepath)
        fs.writeFile(eslintrc.filepath, generateESLintConfigs(resolved.imports, resolved.resolvedImports, eslintrc), 'utf-8')
    })
    

    根据 resolved.imports 配置,生成声明代码写入到 resolved.dts 指定的文件。后面是 将自动引入的文件加入 eslint 的配置文件。

    最后生成类似下面的代码:

    1
    2
    3
    4
    
    declare global {
    const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
    // ...
    }
    
  • 🖨 Static-site generation (SSG) via vite-ssg

  • 🦔 Critical CSS via critters

  • 🦾 TypeScript, of course

  • ⚙️ Unit Testing with Vitest, E2E Testing with Cypress on GitHub Actions

  • ☁️ Deploy on Netlify, zero-config