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-tsc
:vue-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" } }
- 这里使用npm-run-all2去并行执行多个命令,
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
javascriptimport { 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引入
scssif (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" } }