沐游虞笔记
  • 前端面试题

    • HTML面试题汇总(无答案)
    • HTML面试题汇总
    • CSS 面试题汇总(无答案)
    • CSS 面试题汇总
    • javascript 面试题汇总(无答案)
    • javascript 面试题汇总
    • promise 面试题(无答案)
    • promise 面试题
    • 浏览器面试题汇总(无答案)
    • 浏览器面试题汇总
    • 网络面试题汇总(无答案)
    • 网络面试题汇总
    • 工程化面试题汇总(无答案)
    • 工程化面试题汇总
    • VUE面试题汇总(无答案)
    • VUE面试题汇总
  • 直播课文件

    • 静态页面学习指导
    • 属性的计算过程
    • 层叠继承规则总结
    • BFC
    • JS基础知识回顾
    • DOM 事件的传播机制
    • DOM 事件的注册和移除
    • 阻止事件默认行为
    • 基础领航考试题
    • 基础领航考试题(答案)
    • 2024前端发展
    • JS核心概念学习指导
    • 第三方库与工程化学习指导
    • Vue入门学习指导
    • vue进阶学习指导
    • 前端性能优化
  • 笔面试环节知识讲解

    • 目录
    • 图像处理
    • 图像处理(面试)
    • Webpack构建优化
    • Webpack构建优化(面试)
    • TTS性能优化
    • TTS性能优化(面试)
    • 实时协作
    • 实时协作(面试)
    • 网页复制图片到剪贴板
    • 网页复制图片到剪贴板(面试)
    • vite插件
    • vite插件(面试)
    • 表单数据同步与保持
    • 表单数据同步与保持(面试)
    • 优化虚拟列表
    • 优化虚拟列表(面试)
    • 微前端解决巨石应用
    • 微前端解决巨石应用(面试)
    • DNS解析与优化
    • DNS解析与优化(面试)
    • 前端监控
    • 前端监控(面试)
    • 12.跨标签页通信
    • 12.跨标签页通信(面试)
    • 13.Vite相关优化
    • 13.Vite相关优化(面试)
    • 14.计时器节流问题
    • 14.计时器节流问题(面试)
    • 15.多文件预览支持
    • 15.多文件预览支持(面试)
    • 16.defer优化白屏时间
    • 16.defer优化白屏时间(面试)
  • Vue3整体变化
  • Vue2响应式回顾
  • Vue3响应式变化
  • nextTick实现原理
  • 两道代码题
  • Vue运行机制
  • 渲染器核心功能
  • 事件绑定与更新
  • computed面试题
  • watch面试题
  • 图解双端diff
  • 图解快速dff
  • 最长递增子序列
  • 模板编译器
  • 模板编译提升
  • 组件name作用
  • 路由传参方式
  • 基础篇

    • 序章React介绍
    • JSX基础语法
    • React基本介绍
    • 表单
    • 生命周期
    • 组件与事件绑定
    • 组件状态与数据传递
    • Hooks
    • React--redux介绍
    • React-router介绍
  • 就业篇

    • 属性默认值和类型验证
    • 高阶组件
    • Ref
    • Context
    • Render Props
    • Portals
    • 错误边界
    • 组件渲染性能优化
    • 前端框架的理解
    • Reacti和Vue描述页面的区别
    • 前端框架的分类
    • 虚拟DoM
    • React整体架构
    • React渲染流程
    • Fiber双缓冲
    • MessageChannel
    • Scheduleri调度普通任务
    • Scheduleri调度延时任务
    • 最小堆
    • React中的位运算
    • beginWork工作流程
    • completeWork工作流程
    • 图解diff算法
    • commit工作流程
    • lane模型
    • React中的事件
    • Hooks原理
    • useStateuseReducer.
    • effect相关hook
    • useCallbackuseMemo
    • useRef
    • Update
    • 性能优化策略之eagerState
    • 性能优化策略之bailout
    • bailoutContextAPl
    • 性能优化对日常开发启示
  • 前端监控概述
  • 错误监控
  • 数据上报
  • 页面性能监控
  • 用户行为收集与埋点
  • CSS3手册
  • HTML5手册
  • JavaScript语言提升

    • es补充
    • 事件循环
    • promise基础
    • Promise的链式调用
    • Promise的静态方法
    • async和await
    • Promise相关面试题
  • 网络

    • 客户端与服务器
    • 关于 Apifox 的使用
  • git文档
  • 工程化

    • CommonJS
    • ES module
    • npm文档(包管理)
    • Lass笔记
    • webpack工具
  • canvas详解
  • uinapp笔记
  • 自动化测试
  • oauth2令牌

    • 认识Oauth2
    • 三方应用实现github授权
    • 微信三方应用登录实现
    • 支付宝沙箱支付功能
  • 前端面试题

    • HTML面试题汇总(无答案)
    • HTML面试题汇总
    • CSS 面试题汇总(无答案)
    • CSS 面试题汇总
    • javascript 面试题汇总(无答案)
    • javascript 面试题汇总
    • promise 面试题(无答案)
    • promise 面试题
    • 浏览器面试题汇总(无答案)
    • 浏览器面试题汇总
    • 网络面试题汇总(无答案)
    • 网络面试题汇总
    • 工程化面试题汇总(无答案)
    • 工程化面试题汇总
    • VUE面试题汇总(无答案)
    • VUE面试题汇总
  • 直播课文件

    • 静态页面学习指导
    • 属性的计算过程
    • 层叠继承规则总结
    • BFC
    • JS基础知识回顾
    • DOM 事件的传播机制
    • DOM 事件的注册和移除
    • 阻止事件默认行为
    • 基础领航考试题
    • 基础领航考试题(答案)
    • 2024前端发展
    • JS核心概念学习指导
    • 第三方库与工程化学习指导
    • Vue入门学习指导
    • vue进阶学习指导
    • 前端性能优化
  • 笔面试环节知识讲解

    • 目录
    • 图像处理
    • 图像处理(面试)
    • Webpack构建优化
    • Webpack构建优化(面试)
    • TTS性能优化
    • TTS性能优化(面试)
    • 实时协作
    • 实时协作(面试)
    • 网页复制图片到剪贴板
    • 网页复制图片到剪贴板(面试)
    • vite插件
    • vite插件(面试)
    • 表单数据同步与保持
    • 表单数据同步与保持(面试)
    • 优化虚拟列表
    • 优化虚拟列表(面试)
    • 微前端解决巨石应用
    • 微前端解决巨石应用(面试)
    • DNS解析与优化
    • DNS解析与优化(面试)
    • 前端监控
    • 前端监控(面试)
    • 12.跨标签页通信
    • 12.跨标签页通信(面试)
    • 13.Vite相关优化
    • 13.Vite相关优化(面试)
    • 14.计时器节流问题
    • 14.计时器节流问题(面试)
    • 15.多文件预览支持
    • 15.多文件预览支持(面试)
    • 16.defer优化白屏时间
    • 16.defer优化白屏时间(面试)
  • Vue3整体变化
  • Vue2响应式回顾
  • Vue3响应式变化
  • nextTick实现原理
  • 两道代码题
  • Vue运行机制
  • 渲染器核心功能
  • 事件绑定与更新
  • computed面试题
  • watch面试题
  • 图解双端diff
  • 图解快速dff
  • 最长递增子序列
  • 模板编译器
  • 模板编译提升
  • 组件name作用
  • 路由传参方式
  • 基础篇

    • 序章React介绍
    • JSX基础语法
    • React基本介绍
    • 表单
    • 生命周期
    • 组件与事件绑定
    • 组件状态与数据传递
    • Hooks
    • React--redux介绍
    • React-router介绍
  • 就业篇

    • 属性默认值和类型验证
    • 高阶组件
    • Ref
    • Context
    • Render Props
    • Portals
    • 错误边界
    • 组件渲染性能优化
    • 前端框架的理解
    • Reacti和Vue描述页面的区别
    • 前端框架的分类
    • 虚拟DoM
    • React整体架构
    • React渲染流程
    • Fiber双缓冲
    • MessageChannel
    • Scheduleri调度普通任务
    • Scheduleri调度延时任务
    • 最小堆
    • React中的位运算
    • beginWork工作流程
    • completeWork工作流程
    • 图解diff算法
    • commit工作流程
    • lane模型
    • React中的事件
    • Hooks原理
    • useStateuseReducer.
    • effect相关hook
    • useCallbackuseMemo
    • useRef
    • Update
    • 性能优化策略之eagerState
    • 性能优化策略之bailout
    • bailoutContextAPl
    • 性能优化对日常开发启示
  • 前端监控概述
  • 错误监控
  • 数据上报
  • 页面性能监控
  • 用户行为收集与埋点
  • CSS3手册
  • HTML5手册
  • JavaScript语言提升

    • es补充
    • 事件循环
    • promise基础
    • Promise的链式调用
    • Promise的静态方法
    • async和await
    • Promise相关面试题
  • 网络

    • 客户端与服务器
    • 关于 Apifox 的使用
  • git文档
  • 工程化

    • CommonJS
    • ES module
    • npm文档(包管理)
    • Lass笔记
    • webpack工具
  • canvas详解
  • uinapp笔记
  • 自动化测试
  • oauth2令牌

    • 认识Oauth2
    • 三方应用实现github授权
    • 微信三方应用登录实现
    • 支付宝沙箱支付功能
  • 必看导言

    • 就业的核心问题

      • 课件
    • 表现力的训练

      • 课件
  • 简历制作

    • 课件
    • 简历准备课堂笔记
    • 个人信息课堂笔记
    • 求职意向课堂笔记
    • 技术栈课堂笔记
    • 教育经历课堂笔记
    • 个人优势与评价课堂笔记
    • 工作经历课堂笔记
    • 项目
    • 项目亮难点问题

    • 项目经历

  • 项目准备

    • 课件
    • 课件
  • 技术重点

    • 划重点

    • 非技术环节
  • 简历投递和面试准备

    • 简历投递
    • 面试准备
  • 笔面试环节知识讲解
  • 项目准备
  • 难点攻关
  • Vite插件
  • 课件资料
luzhichang
2024-09-18
目录

06-技术讲解

# Vite插件技术讲解

# 什么问题

平时在进行开发的时候,经常会发现页面级组件(views or pages)的层次结构和路由的配置基本上是一致的。

例如我们现在有这样的目录结构:

  ├── src
  │   ├── router
  │   │   └── index.js
  │   ├── views
  │   │   ├── Home.vue
  │   │   ├── About.vue
  │   │   └── User
  │   │       ├── Index.vue
  │   │       └── Profile.vue
  │   └── main.js
  └── vite.config.js

对应的路由结构大致如下:

import { createRouter, createWebHistory } from 'vue-router'

// 导入页面组件
const Home = () => import('../views/Home.vue')
const About = () => import('../views/About.vue')
const UserIndex = () => import('../views/User/Index.vue')
const UserProfile = () => import('../views/User/Profile.vue')

// 配置路由
const routes = [
  { path: '/', component: Home },
  { path: '/about', component: About, name: 'About', auth: false },
  { path: '/user', component: UserIndex },
  { path: '/user/profile', component: UserProfile }
]

// 创建路由实例
const router = createRouter({
  history: createWebHistory(),
  routes
})

export default router

这是一个很繁琐但是又不得不做的一件事情,每次新增了页面级组件,就得去修改对应的路由配置。

其实这里可以采用一种“约定大于配置”的方式,自动生成路由。

约定大于配置(Convention over Configuration)

这是一种软件设计原则,旨在通过遵循约定和标准化来减少开发者需要编写的配置代码。这种方法提倡使用默认的行为和规则,以简化开发过程,使得开发人员能够专注于业务逻辑而不是配置细节。

常见的框架:

  • Nuxt.js
  • Next.js
  • Umi.js
  • ....

普通 Vite 搭建的项目并没有这个功能,这正是我们要解决的问题。

# 解决思路

1. 使用 import.meta.glob 读取文件

首先使用 import.meta.glob 读取 views 目录下所有的 .vue 文件,这些文件将被用作路由组件。

使用 import.meta.glob 不使用 fs 的原因?

  1. import.meta.glob 是 Vite 提供的内置功能,专门用于在前端项目中动态导入模块。它可以在开发时和构建时都正常工作,并且与 Vite 的热模块替换(HMR)功能无缝集成。fs 读取文件需要编写更多的代码来处理文件路径和模块导入,并且需要手动实现热更新等功能。
  2. import.meta.glob 可以直接生成按需导入的模块映射表,支持代码分割和懒加载,从而优化前端性能。而使用 fs 读取文件后,还需要额外的逻辑来实现按需导入和懒加载。

2. 生成路由配置

对路由信息做二次处理

  1. 生成对应的路由配置对象
  2. 将配置对象组成路由数组

3. 创建 Vite 插件

4. 发布插件

# 解决细节

# 1. 读取文件

import.meta.glob. 这是 Vite 提供的一个功能,用于批量导入指定目录中的文件。它可以根据指定的模式动态生成导入的模块映射表。这个功能在处理静态资源和文件系统操作时非常有用,尤其是在构建自动化流程(如自动生成路由)时。

使用 import.meta.glob 来导入所有 .vue 文件:

const modules = import.meta.glob('../views/**/*.vue');

console.log(modules);
// 输出如下对象
// {
//   '../views/Home.vue': () => import('../views/Home.vue'),
//   '../views/About.vue': () => import('../views/About.vue'),
//   '../views/User/Index.vue': () => import('../views/User/Index.vue'),
//   '../views/User/Profile.vue': () => import('../views/User/Profile.vue')
// }

每个文件路径对应一个函数,该函数返回一个 Promise,当调用该函数时,会动态导入相应的模块。

支持的参数:

  • eager:默认情况下,import.meta.glob 返回的值是一个函数,需要手动调用以触发模块加载。如果设置 eager: true,则模块会在生成映射表时立即加载。
const modules = import.meta.glob('../views/**/*.vue', { eager: true });

console.log(modules);
// 输出如下对象
// {
//   '../views/Home.vue': Module { ... },
//   '../views/About.vue': Module { ... },
//   '../views/User/Index.vue': Module { ... },
//   '../views/User/Profile.vue': Module { ... }
// }
  • import:可以指定导入模块的特定命名导出。默认情况下,导入整个模块对象。如果只需要模块的特定部分,可以使用 import 选项。
const modules = import.meta.glob('../views/**/*.vue', { import: 'default' });

console.log(modules);
// 输出如下对象,导入模块的特定部分,这里是导入模块的 default 部分
// {
//   '../views/Home.vue': () => import('../views/Home.vue').then(mod => mod.default),
//   '../views/About.vue': () => import('../views/About.vue').then(mod => mod.default),
//   '../views/User/Index.vue': () => import('../views/User/Index.vue').then(mod => mod.default),
//   '../views/User/Profile.vue': () => import('../views/User/Profile.vue').then(mod => mod.default)
// }

# 2. 生成路由配置

对读取到的内容进行一些二次处理。

要拿到路径信息,路径对应的组件,组装成对象,最终在形成数组。

import { createRouter, createWebHistory } from 'vue-router'

// 动态导入所有页面组件
const modules = import.meta.glob('../views/**/*.vue', { eager: false })

// 生成路由配置
const routes = Object.keys(modules).map((path) => {
  // 生成路由路径
  const routePath = path
    .replace('../views', '')
    .replace(/\.vue$/, '')
    .toLowerCase()
    .replace(/\/index$/, '') // 移除文件名中的 index
    .replace(/\/_/g, '/:') // 将文件名中的下划线转换为动态路由参数

  return {
    path: routePath || '/',
    component: modules[path]
  }
})

// 添加默认根路径路由
if (!routes.some(route => route.path === '/')) {
  routes.push({
    path: '/',
    component: modules['../views/Home.vue'] // 设置默认首页组件
  })
}

console.log(routes, '生成的路由配置')

// 创建路由实例
const router = createRouter({
  history: createWebHistory(),
  routes
})

export default router

如何增加额外的元数据信息?因为根据路径读取出来的信息,只能提取出来路径以及组件

为每一个页面组件配套一个 XXX.meta.js 文件。

整个目录结构如下:

/vue-project
  ├── src
  │   ├── router
  │   │   └── index.js
  │   ├── views
  │   │   ├── About
  │   │   │   ├── About.meta.js
  │   │   │   └── About.vue
  │   │   ├── Home
  │   │   │   ├── Home.meta.js
  │   │   │   └── Home.vue
  │   │   └── User
  │   │       ├── Index
  │   │       │   ├── Index.meta.js
  │   │       │   └── Index.vue
  │   │       ├── Profile
  │   │       │   ├── Profile.meta.js
  │   │       │   └── Profile.vue
  │   └── main.js
  └── vite.config.js

读取内容做二次处理时也需要一些修改:

import { createRouter, createWebHistory } from 'vue-router'

// 动态导入所有页面组件
const modules = import.meta.glob('../views/**/**/*.vue', { eager: false })
// 动态导入所有元数据文件
const metaModules = import.meta.glob('../views/**/**/*.meta.js', { eager: true })

// 生成路由配置
const routes = Object.keys(modules).map((filePath) => {
  // 生成路由路径
  let routePath = filePath
    .replace('../views', '')
    .replace(/\.vue$/, '')
    .replace(/\/_/g, '/:') // 将文件名中的下划线转换为动态路由参数
  // 拆分路径并处理 index 文件名
  const parts = routePath.split('/').filter(Boolean)
  // 移除重复的目录名和文件名
  if (parts.length > 1 && parts[parts.length - 1] === parts[parts.length - 2]) {
    parts.pop()
  }

  if (parts.length > 1 && parts[parts.length - 1].toLowerCase() === 'index') {
    parts.pop()
  }

  routePath = '/' + parts.join('/').toLowerCase()

  // 导入元数据文件
  const metaFilePath = filePath.replace('.vue', '.meta.js')
  const meta = metaModules[metaFilePath] ? metaModules[metaFilePath].default : {}

  return {
    path: routePath,
    component: modules[filePath],
    ...meta
  }
})

// 确保有一个根路径('/')的路由,如果没有,则添加默认的根路径路由
if (!routes.some((route) => route.path === '/')) {
  routes.push({
    path: '/',
    component: modules['../views/Home/Home.vue'], // 设置默认首页组件
    title: 'Home', // 设置默认首页标题
    requiresAuth: false // 默认首页不需要认证
  })
}

// 创建路由实例
const router = createRouter({
  history: createWebHistory(),
  routes
})

export default router

# 3. 创建 Vite 插件

1. 创建新的插件项目

初始化项目

mkdir vite-plugin-auto-routes
cd vite-plugin-auto-routes
npm init -y
npm install vite

项目的目录结构如下:

/vite-plugin-auto-routes
  ├── src
  │   └── index.js
  ├── package.json
  └── README.md

2. 迁移自动生成路由的逻辑

// src/index.js
const fs = require("fs");
const path = require("path");

// 生成路由数组的方法
function generateRoutes(dir, basePath) {
  const files = fs.readdirSync(dir);
  return files.flatMap((file) => {
    const fullPath = path.resolve(dir, file);
    const relativePath = path.relative(basePath, fullPath);
    if (fs.statSync(fullPath).isDirectory()) {
      return generateRoutes(fullPath, basePath);
    }
    if (file.endsWith(".vue")) {
      const routePath = relativePath
        .replace(/\\/g, "/")
        .replace(/\.vue$/, "")
        .replace(/\/index$/, "")
        .replace(/\/_/g, "/:");

      const parts = routePath.split("/").filter(Boolean);
      if (
        parts.length > 1 &&
        parts[parts.length - 1] === parts[parts.length - 2]
      ) {
        parts.pop();
      }
      const finalPath = "/" + parts.join("/").toLowerCase();

      const metaFilePath = fullPath.replace(".vue", ".meta.js");
      const meta = fs.existsSync(metaFilePath) ? require(metaFilePath) : {};

      return [
        {
          path: finalPath,
          component: fullPath,
          ...meta,
        },
      ];
    }
    return [];
  });
}

module.exports = function VitePluginAutoRoutes(options = {}) {
  const { pagesDir = "src/views", routesFile = "src/router/autoRoutes.js" } =
    options;
  return {
    name: "vite-plugin-auto-routes",
    configResolved(config) {
      const pagesPath = path.resolve(config.root, pagesDir);
      // 生成路由数组
      const routes = generateRoutes(pagesPath, pagesPath);

      const routesFileContent = `
        export const routes = ${JSON.stringify(routes, null, 2)};
      `;

      fs.writeFileSync(
        path.resolve(config.root, routesFile),
        routesFileContent
      );
    },
  };
};

3. 打包和压缩

选择使用 rollup 来进行打包和压缩。

安装依赖:

npm install --save-dev rollup @rollup/plugin-node-resolve @rollup/plugin-commonjs @rollup/plugin-json @rollup/plugin-terser

项目根目录下创建 rollup.config.js 文件:

主要需要配置打包时,要输出的不同格式

import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import json from '@rollup/plugin-json';
import { terser } from '@rollup/plugin-terser';

export default {
  input: 'src/index.js', // 输入文件路径
  output: [
    {
      file: 'dist/index.cjs.js',
      format: 'cjs', // CommonJS 规范
      sourcemap: true
    },
    {
      file: 'dist/index.esm.js',
      format: 'es', // ES Module 规范
      sourcemap: true
    },
  ],
  plugins: [
    resolve(),
    commonjs(),
    json(),
    terser() // 压缩代码
  ]
};

4. 更新package.json文件

更新入口文件

{
  "name": "vite-plugin-auto-routes",
  "version": "1.0.0",
  "description": "A Vite plugin to auto-generate route configurations based on directory structure.",
  "main": "dist/index.cjs.js",
  "module": "dist/index.esm.js",
  "scripts": {
    "build": "rollup -c"
  },
  "keywords": [
    "vite",
    "plugin",
    "routes",
    "auto",
    "vue-router"
  ],
  "author": "Your Name",
  "license": "MIT",
  "devDependencies": {
    "@rollup/plugin-commonjs": "^20.0.0",
    "@rollup/plugin-json": "^4.1.0",
    "@rollup/plugin-node-resolve": "^13.0.0",
    "@rollup/plugin-terser": "^7.0.0",
    "rollup": "^2.50.0",
    "vite": "^2.0.0"
  }
}

# 4. 测试插件

在 vite.config.js 中引入这个插件:

import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import VitePluginAutoRoutes from 'vite-plugin-auto-routes';

export default defineConfig({
  plugins: [vue(), VitePluginAutoRoutes({
    pagesDir: 'src/views',
    routesFile: 'src/router/autoRoutes.js'
  })]
});

然后在 src/router/index.js 文件中使用插件自动生成的路由配置:

import { createRouter, createWebHistory } from 'vue-router';
import { routes } from './autoRoutes';

const router = createRouter({
  history: createWebHistory(),
  routes
});

export default router;

# 5. 发布 Vite 插件

整体的项目目录如下:

/vite-plugin-auto-routes
  ├── dist
  │   ├── index.cjs.js
  │   ├── index.cjs.js.map
  │   ├── index.esm.js
  │   ├── index.esm.js.map
  ├── src
  │   └── index.js
  ├── package.json
  ├── README.md
  ├── rollup.config.js
  ├── node_modules
  └── package-lock.json

需要发布的:

  1. 打包后的 dist 目录
  2. 必要的配置文件(package.json)

配置发布内容的两种方式:

  1. 白名单:顾名思义只有白名单里面的内容才会被发布到 npm 上面

在 package.json 文件中配置 files 字段,如下所示:

{
  "name": "vite-plugin-auto-routes",
  "version": "1.0.0",
  "description": "A Vite plugin to auto-generate route configurations based on directory structure.",
  "main": "dist/index.cjs.js",
  "module": "dist/index.esm.js",
  "scripts": {
    "build": "rollup -c"
  },
  "keywords": [
    "vite",
    "plugin",
    "routes",
    "auto",
    "vue-router"
  ],
  "author": "Your Name",
  "license": "MIT",
  "files": [
    "dist",
    "README.md"
  ],
  "devDependencies": {
    "@rollup/plugin-commonjs": "^20.0.0",
    "@rollup/plugin-json": "^4.1.0",
    "@rollup/plugin-node-resolve": "^13.0.0",
    "@rollup/plugin-terser": "^7.0.0",
    "rollup": "^2.50.0",
    "vite": "^2.0.0"
  }
}

只有 files 字段对应的目录和文件才会被上传至 npm

  1. 黑名单

在项目根目录下创建一个 .npmignore 文件,用于排除不需要的文件和目录:

/src
/rollup.config.js
/node_modules
/package-lock.json
/.git
/.github
/tests
/examples

-EOF-

Theme by Vdoing | Copyright © 2021-2024 蜀ICP备2024068710号-1
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式