Vue3脚手架实现(九、渲染typescript配置)

Vue3脚手架实现(一、整体介绍)

Vue3脚手架实现(二、项目环境搭建)

Vue3脚手架实现(三、命令行读取配置)

Vue3脚手架实现(四、模板渲染流程、渲染一个基础项目)

Vue3脚手架实现(五、渲染jsx和prettier配置)

Vue3脚手架实现(六、渲染router和pinia)

Vue3脚手架实现(七、渲染eslint配置)

Vue3脚手架实现(八、渲染vitest配置)

Vue3脚手架实现(九、渲染typescript配置)

typescript的基础配置

  • template\config\typescript\base\env.d.ts:env.d.ts中声明一下Vite特有的环境变量和模块类型声明。里面内容这是TypeScript的三斜线指令,告诉 TypeScript 去加载 vite/client 类型声明文件(由 Vite 官方提供,位于 node_modules/vite/client.d.ts

    ini 复制代码
    /// <reference types="vite/client" />
  • template\config\typescript\base\package.json

    • 这里使用npm-run-all2去并行执行多个命令,run-p就是并行执行,type-check这个是执行对应的vue-tsc --build,这个\"build-only {@}"\,是执行这个vite build进行打包。{@}这个就表示把npm run build的参数传给这个build-only这个任务。这个--是一个明确的分隔符,确保run-p 不会误解析后续参数,而是传给build-only
    • vue-tscvue-tsc 是对 TypeScript 自身命令行界面 tsc 的一个封装。它的工作方式基本和 tsc 一致。除了 TypeScript 文件,它还支持 Vue 的单文件组件。相关可见vue官方文档,这里文档也有其他的ts的配置要求说明。手动配置的注意事项比较多,我们看create-vue的源码,其实引用了@vue/tsconfig的相关的内置配置文件,减少我们心智负担,我们也是按照对应的配置即可。
    sql 复制代码
    {
      "scripts": {
        "build": "run-p type-check \"build-only {@}\" --",
        "build-only": "vite build",
        "type-check": "vue-tsc --build"
      },
      "devDependencies": {
        "@types/node": "^22.13.10",
        "npm-run-all2": "^7.0.2",
        "typescript": "~5.8.0",
        "vue-tsc": "^2.2.8"
      }
    }

tsconfig的配置

tsconfig配置:tsconfig配置,我们就需要拆分配置方便维护。针对浏览器的相关拆分出tsconfig.app.json,针对项目中是node环境的我们要拆分出来tsconfig.node.json,针对vitest的我们也同样拆分出来tsconfig.vitest.json,然后需要在tsconfig.json中进行references引入即可

@tsconfig/node22 是node的基础配置包

@vue/tsconfig 是vue项目推荐extend的tsconfig配置包

paths配置项是为了配置路径别名,需要让ts识别对应的路径别名

tsBuildInfoFile 存储增量编译信息,加速后续构建

noEmit: true 仅做类型检查,不生成编译产物

"module": "ESNext":指定使用的模块系统,详见

"moduleResolution": "Bundler",指定模块解析策略。详见

types配置项是显示增加类型识别,避免报错

"lib": [] 配置可以强制不加载标准库类型,因为Vitest 已经提供了完整的测试环境类型,无需再通过 lib 重复加载。有对应的types配置即可。

  • template\tsconfig\base\package.json:这两个主要是给tsconfig.app.json和tsconfig.node.json继承配置用的,减少配置的心智。

    perl 复制代码
    {
      "devDependencies": {
        "@tsconfig/node22": "^22.0.0",
        "@vue/tsconfig": "^0.7.0"
      }
    }
  • template\tsconfig\base\tsconfig.app.json

    perl 复制代码
    {
      "extends": "@vue/tsconfig/tsconfig.dom.json",
      "include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
      "exclude": ["src/**/__tests__/*"],
      "compilerOptions": {
        "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
    
        "paths": {
          "@/*": ["./src/*"]
        }
      }
    }
  • template\tsconfig\base\tsconfig.node.json

    json 复制代码
    {
      "extends": "@tsconfig/node22/tsconfig.json",
      "include": [
        "vite.config.*",
        "vitest.config.*",
        "eslint.config.*"
      ],
      "compilerOptions": {
        "noEmit": true,
        "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
    
        "module": "ESNext",
        "moduleResolution": "Bundler",
        "types": ["node"]
      }
    }
  • template\tsconfig\vitest\package.json

    perl 复制代码
    {
      "devDependencies": {
        "@types/jsdom": "^21.1.7"
      }
    }
  • template\tsconfig\vitest\tsconfig.vitest.json

    json 复制代码
    {
      "extends": "./tsconfig.app.json",
      "include": ["src/**/__tests__/*", "env.d.ts"],
      "exclude": [],
      "compilerOptions": {
        "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.vitest.tsbuildinfo",
    
        "lib": [],
        "types": ["node", "jsdom"]
      }
    }

code文件

有ts的时候,代码文件不同,我们直接重新申明对应的文件进行覆盖即可

  • template\code\typescript-default\src\components\HelloWorld.vue

    xml 复制代码
    <template>
      <div>hello world</div>
    </template>
    
    <script setup lang="ts"></script>
    
    <style scoped></style>
  • template\code\typescript-default\src\App.vue

    xml 复制代码
    <template>
      <HelloWorld></HelloWorld>
    </template>
    
    <script setup lang="ts">
    import HelloWorld from '@/components/HelloWorld.vue'
    </script>
    
    <style scoped></style>
  • template\code\typescript-router\src\components\HelloWorld.vue

    xml 复制代码
    <template>
      <div>hello world</div>
    </template>
    
    <script setup lang="ts"></script>
    
    <style scoped></style>
  • template\code\typescript-router\src\router\index.ts

    javascript 复制代码
    import { createRouter, createWebHistory } from 'vue-router'
    
    const router = createRouter({
      history: createWebHistory(import.meta.env.BASE_URL),
      routes: [
        {
          path: '/',
          name: 'home',
          component: () => import('@/views/HomeView.vue'),
        },
      ],
    })
    
    export default router
  • template\code\typescript-router\src\views\HomeView.vue

    xml 复制代码
    <template>
      <div>hello home</div>
      <HelloWorld></HelloWorld>
    </template>
    
    <script setup lang="ts">
    import HelloWorld from '@/components/HelloWorld.vue'
    </script>
    
    <style scoped></style>
  • template\code\typescript-router\src\App.vue

    xml 复制代码
    <template><router-view></router-view></template>
    
    <script setup lang="ts"></script>
    
    <style scoped></style>

index.ts中的流程

有了之前的配置支撑,我们需要在index.ts中添加对应的流程

  • 首先需要渲染typescript基础配置,以及渲染tsconfig.json的配置,这里直接通过代码引入,不进行使用模板代码了,因为tsconfig.json的配置较少,主要就是references引入

    scss 复制代码
    if (needsTypeScript) {
      render('config/typescript/base')
      // tsconfig
      render('tsconfig/base')
      const rootTsConfig = {
        files: [],
        references: [{ path: './tsconfig.node.json' }, { path: './tsconfig.app.json' }],
      }
      if (needsVitest) {
        render('tsconfig/vitest')
        rootTsConfig.references.push({
          path: './tsconfig.vitest.json',
        })
      }
      fs.writeFileSync(
        path.resolve(root, 'tsconfig.json'),
        JSON.stringify(rootTsConfig, null, 2) + '\n',
        { encoding: 'utf-8' },
      )
    }
  • 添加code代码,我们需要替换掉之前的渲染逻辑,这里根据配置,看具体渲染哪个模板

    javascript 复制代码
    // 添加code
    // 有router和没有router,会影响到code的结构
    // 有ts和没有ts,也会影响到code的代码
    // 所以这里区分为四个模板,进行渲染
    const codeTemplate = (needsTypeScript ? 'typescript-' : '') + (needsRouter ? 'router' : 'default')
    render(`code/${codeTemplate}`)
  • 要将js的相关后缀改为ts的相关后缀

    javascript 复制代码
    // ts代码是js的超集,对于生成的代码中,还需要对ts进行转换处理,如下转换逻辑
    // 如果有冗余的xxx.ts和xxx.js,直接清理掉xxx.js文件。也就是说,如果生成的代码里面有两种文件,默认选用ts文件覆盖js文件
    // 如果只有js文件,这种文件视为改一下文件名,就可以当作ts文件使用的。因为ts是js的超集
    // jsconfig.json需要删除
    // 相关的js的引用也要改为ts的引用
    if (needsTypeScript) {
      preOrderDirectoryTraverse(
        root,
        () => {},
        (filePath) => {
          if (filePath.endsWith('.js')) {
            const tsFilePath = filePath.replace(/\.js$/, '.ts')
            if (fs.existsSync(tsFilePath)) {
              fs.unlinkSync(filePath)
            } else {
              fs.renameSync(filePath, tsFilePath)
            }
          } else if (path.basename(filePath) === 'jsconfig.json') {
            fs.unlinkSync(filePath)
          }
        },
      )
    
      // index.html里面的js引用更改
      const indexHtmlPath = path.resolve(root, 'index.html')
      const indexHtmlContent = fs.readFileSync(indexHtmlPath, { encoding: 'utf-8' })
      fs.writeFileSync(indexHtmlPath, indexHtmlContent.replace('src/main.js', 'src/main.ts'))
    } else {
      // 一般来说,有ts配置才会生成有ts文件,但是这里做一下冗余处理,删除掉所有ts文件
      preOrderDirectoryTraverse(
        root,
        () => {},
        (filepath) => {
          if (filepath.endsWith('.ts')) {
            fs.unlinkSync(filepath)
          }
        },
      )
    }

相关代码顺序如下:

scss 复制代码
const create = async (name: string, options: Options) => {
  // 项目目录预处理
  await processTargetDirectory(name, options)

  // 询问用户需要的配置
  const { features } = await inquireConfig()
  const needsTypeScript = features.includes('typescript')
  const needsJsx = features.includes('jsx')
  const needsPrettier = features.includes('prettier')
  const needsRouter = features.includes('router')
  const needsPinia = features.includes('pinia')
  const needsVitest = features.includes('vitest')
  const needsEslint = features.includes('eslint')

  // 创建一下目录
  const targetDir = name
  const cwd = process.cwd()
  const root = path.join(cwd, targetDir)
  fs.mkdirSync(root)

  // 生成基础的package.json文件
  const pkgJson = { name: name, version: '0.0.0' } // 写入package.json的name和版本
  fs.writeFileSync(path.resolve(root, 'package.json'), JSON.stringify(pkgJson, null, 2)) // 缩进为2

  // 模板文件位置
  const templateRoot = path.resolve(__dirname, 'template')
  const callbacks: Function[] = []
  const render = function (templateName) {
    const templateDir = path.resolve(templateRoot, templateName)
    renderTemplate(templateDir, root, callbacks)
  }

  // 渲染基础项目
  render('base')

  // 添加对应的配置
  if (needsJsx) {
    render('config/jsx')
  }
  if (needsPrettier) {
    render('config/prettier')
  }
  if (needsRouter) {
    render('config/router')
  }
  if (needsPinia) {
    render('config/pinia')
  }
  if (needsVitest) {
    render('config/vitest')
  }
  if (needsEslint) {
    render('config/eslint')
    render('eslint/base') // eslint基础配置
    if (needsTypeScript) {
      render('eslint/typescript')
    }
    if (needsVitest) {
      render('eslint/vitest')
    }
    if (needsPrettier) {
      render('eslint/prettier')
    }
  }
  if (needsTypeScript) {
    render('config/typescript/base')
    // tsconfig
    render('tsconfig/base')
    const rootTsConfig = {
      files: [],
      references: [{ path: './tsconfig.node.json' }, { path: './tsconfig.app.json' }],
    }
    if (needsVitest) {
      render('tsconfig/vitest')
      rootTsConfig.references.push({
        path: './tsconfig.vitest.json',
      })
    }
    fs.writeFileSync(
      path.resolve(root, 'tsconfig.json'),
      JSON.stringify(rootTsConfig, null, 2) + '\n',
      { encoding: 'utf-8' },
    )
  }

  // 添加入口文件
  render('entry/default')
  if (needsRouter) {
    render('entry/router')
  }
  if (needsPinia) {
    render('entry/pinia')
  }

  // 添加code
  // 有router和没有router,会影响到code的结构
  // 有ts和没有ts,也会影响到code的代码
  // 所以这里区分为四个模板,进行渲染
  const codeTemplate = (needsTypeScript ? 'typescript-' : '') + (needsRouter ? 'router' : 'default')
  render(`code/${codeTemplate}`)

  // 收集所有的ejs的数据
  const dataStore = {}
  for (const cb of callbacks) {
    await cb(dataStore)
  }
  // 根据ejs数据渲染对应的模板文件
  preOrderDirectoryTraverse(
    root,
    () => {},
    (filePath) => {
      if (filePath.endsWith('.ejs')) {
        const template = fs.readFileSync(filePath, { encoding: 'utf-8' })
        const dest = filePath.replace(/\.ejs$/, '')
        const content = ejs.render(template, dataStore[dest])
        fs.writeFileSync(dest, content)
        fs.unlinkSync(filePath)
      }
    },
  )

  // ts代码是js的超集,对于生成的代码中,还需要对ts进行转换处理,如下转换逻辑
  // 如果有冗余的xxx.ts和xxx.js,直接清理掉xxx.js文件。也就是说,如果生成的代码里面有两种文件,默认选用ts文件覆盖js文件
  // 如果只有js文件,这种文件视为改一下文件名,就可以当作ts文件使用的。因为ts是js的超集
  // jsconfig.json需要删除
  // 相关的js的引用也要改为ts的引用
  if (needsTypeScript) {
    preOrderDirectoryTraverse(
      root,
      () => {},
      (filePath) => {
        if (filePath.endsWith('.js')) {
          const tsFilePath = filePath.replace(/\.js$/, '.ts')
          if (fs.existsSync(tsFilePath)) {
            fs.unlinkSync(filePath)
          } else {
            fs.renameSync(filePath, tsFilePath)
          }
        } else if (path.basename(filePath) === 'jsconfig.json') {
          fs.unlinkSync(filePath)
        }
      },
    )

    // index.html里面的js引用更改
    const indexHtmlPath = path.resolve(root, 'index.html')
    const indexHtmlContent = fs.readFileSync(indexHtmlPath, { encoding: 'utf-8' })
    fs.writeFileSync(indexHtmlPath, indexHtmlContent.replace('src/main.js', 'src/main.ts'))
  } else {
    // 一般来说,有ts配置才会生成有ts文件,但是这里做一下冗余处理,删除掉所有ts文件
    preOrderDirectoryTraverse(
      root,
      () => {},
      (filepath) => {
        if (filepath.endsWith('.ts')) {
          fs.unlinkSync(filepath)
        }
      },
    )
  }
}

关于之前讲到的jiti这个包的处理

之前我们有说jiti这个包,我们如果对应的eslint.config.mjs文件改为ts的文件的话,需要引入这个包。如果不引入这个包我们就需要侵入代码,过滤掉这个eslint.config.mjs文件后缀的更改,所以我考虑还是加上jiti这个包配置

  • template\eslint\typescript\package.json

    json 复制代码
    {
      "devDependencies": {
        "jiti": "^2.4.2",
        "typescript-eslint": "^8.35.1"
      }
    }
相关推荐
行板Andante6 分钟前
前端设计中如何在鼠标悬浮时同步修改块内样式
前端
Carlos_sam38 分钟前
Opnelayers:ol-wind之Field 类属性和方法详解
前端·javascript
小毛驴8501 小时前
创建 Vue 项目的 4 种主流方式
前端·javascript·vue.js
誰能久伴不乏1 小时前
Linux如何执行系统调用及高效执行系统调用:深入浅出的解析
java·服务器·前端
涔溪2 小时前
响应式前端设计:CSS 自适应布局与字体大小的最佳实践
前端·css
今禾2 小时前
前端开发中的Mock技术:深入理解vite-plugin-mock
前端·react.js·vite
你这个年龄怎么睡得着的2 小时前
Babel AST 魔法:Vite 插件如何让你的 try...catch 不再“裸奔”?
前端·javascript·vite
我想说一句3 小时前
掘金移动端React开发实践:从布局到样式优化的完整指南
前端·react.js·前端框架
码间舞3 小时前
Zustand 与 useSyncExternalStore:现代 React 状态管理的极简之道
前端·react.js