一、项目技术栈 / 依赖
|-----|----------------------------|-------------|---------------------------------|
| 序号 | 技术栈 | 版本 | 解释 |
| 1 | node | 20.14.0 | |
| 2 | vue | 3.4.31 | |
| 3 | vite | 5.3.4 | |
| 4 | TypeScript | 5.2.2 | |
| 5 | @types/node | 22.0.2 | 解决TypeScript项目中缺少对应模块的类型定义文件的问题 |
| 6 | element-plus | 2.7.8 | ui组建 |
| 7 | @types/js-cookie js-cookie | 3.0.6 3.0.5 | |
| 8 | sass | 1.77.8 | |
| 9 | husky | 8.0.0 | |
| 10 | chalk | 5.3.0 | |
| 11 | axios | 1.7.3 | |
| 12 | vue-router | 4.4.2 | |
| 13 | tailwindcss | 3.4.10 | |
| ... | ... | ... | ... |
二、创建项目
2.1、创建项目
pnpm create vite
2.2、输入项目名字
2.3、选择TypeScript
2.4、Enter/回车后
2.5、项目创建成功-目录
2.6、安装依赖
pnpm install
2.7、启动项目
pnpm run dev
2.8、启动成功
三、public目录下的文件可以直接访问
四、vite.config.ts 配置alias
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
'@assets': resolve(__dirname, 'src/assets')
}
}
})
@
@assets
经测试,@、@assets 两个 alias 均使用成功。
五、Element-plus
pnpm add element-plus
安装后在src/main.ts引入并使用
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
const app = createApp(App)
app.use(ElementPlus)
app.mount('#app')
六、使用pinia
nuxt3:使用pinia_nuxt3使用pinia-CSDN博客
七、使用cookie
pnpm add @types/js-cookie
pnpm add js-cookie
// 页面引入
import cookie from 'js-cookie'
<script setup lang="ts">
import cookie from 'js-cookie'
import HelloWorld from './components/HelloWorld.vue'
cookie.set('name', 'snow')
const name: string | undefined = cookie.get('name')
console.log('10name', name)
</script>
测试成功:
八、使用 Tailwind CSS
Tailwind CSS:基础使用/vue3+ts+Tailwind_tailwind中文文档-CSDN博客
九、使用sass
9.1、安装sass
pnpm add sass
9.2、变量文件 assets/scss/variables.scss
// 颜色变量
$primary-color: #42b983; // 主题色
$text-color: #333; // 文本颜色
$border-color: #ddd; // 边框颜色
// 字体样式变量
$font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
$font-size-base: 16px;
$line-height-base: 1.5;
// 间距变量
$padding-base: 10px;
$margin-base: 20px;
// 布局变量
$container-max-widths: (
sm: 540px,
md: 720px,
lg: 960px,
xl: 1140px,
xxl: 1320px
);
// 组件样式变量
$button-padding: 10px 15px;
$button-border-radius: 4px;
$button-background-color: $primary-color;
// 图标和图像变量
$logo-url: '/images/logo.png';
// 媒体查询断点
$screen-xs-min: 600px;
$screen-sm-min: 992px;
$screen-md-min: 1200px;
$screen-lg-min: 1920px;
9.3、vite.config.ts配置
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
'@assets': resolve(__dirname, 'src/assets')
}
},
css: {
// 这里的配置将应用于所有CSS/Sass/Less等预处理器文件
preprocessorOptions: {
scss: {
additionalData: `@import "@/assets/scss/variables.scss";` // 全局引入变量
// 或者你可以通过Vue的<style scoped src="...">来引入全局样式
// 如果你不需要进行特别的Sass配置,这个对象可以留空
}
}
}
})
9.4、App.vue文件使用全局变量,进行测试
<template>
<div class="name">snow</div>
</template>
<style scoped lang="scss">
.name {
color: $primary-color;
}
</style>
测试成功
十、使用ESLint
配置文件 - ESLint - 插件化的 JavaScript 代码检查工具
Configuration Migration Guide - ESLint - Pluggable JavaScript Linter
工程化-vue3+ts:代码检测工具 ESLint_expected to return a value at the end of arrow fun-CSDN博客
十一、使用husky / V.9.1.4 + ESLint
配置文件 - ESLint - 插件化的 JavaScript 代码检查工具
vue3+ts:约定式提交(git husky + gitHooks)_vue husky hook-CSDN博客
项目中使用 Husky 可以帮助你自动地在 Git 钩子(如 pre-commit、pre-push 等)上执行一些脚本,比如代码格式化、linting、测试等,从而确保代码质量。 同时,如果你打算在提交前运行 ESLint 或 Prettier,也需要安装husky。
pnpm add husky
npx husky-init
.husky/pre-commit 文件,增加 pnpm run eslint:fix,这样在提交代码前会先进行ESLint检查。
十二、使用router
12.1、安装router
pnpm add vue-router
12.2、在src目录下创建目录和文件routers/index.ts
// 引入创建路由管理器 引入创建路由模式 history模式
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import Snow from '../views/snow/index.vue';
// 引入路由各页面配置
const routes:Array<RouteRecordRaw> = [
{
path: '/',
redirect: '/snow'
},
{
path: '/index',
redirect: '/snow'
},
{
path: '/snow',
name: 'snow',
component: Snow
},
{
path: '/star',
component: ()=>import('../views/star/index.vue')
},
]
// 创建路由管理器 模式和路由
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
12.3、src/views/snow/index.vue
<template>
<div class="container">
<!-- 水平、垂直 居中 -->
<!-- <div class="flex">
<div class="flex_item"></div>
</div> -->
<div class="flex">
<div class="flex_item">1</div>
<div class="flex_item">2</div>
<div class="flex_item">3</div>
<div class="flex_item">4</div>
<div class="flex_item">5</div>
<div class="flex_item">6</div>
<div class="flex_item">7</div>
</div>
</div>
</template>
<script setup lang="ts">
import cookie from 'js-cookie'
const name:string | undefined = cookie.get('name');
console.log('10name', name)
</script>
<style scoped lang="scss">
.container{
// .flex{
// display: flex;
// justify-content: center; // 水平居中
// align-items: center; // 垂直居中
// width: 200px;
// height: 200px;
// background: #ff0000;
// &_item{
// width: 50px;
// height: 50px;
// background: #b3de1b;
// }
// }
.flex{
display: flex;
width: 200px;
height: 200px;
background: #ff0000;
&_item{
width: 50px;
height: 50px;
background: #b3de1b;
flex-shrink: 0; // 表示Flex项目在空间不足时的缩小比例。flex-shrink的默认值为1,数值越大,缩小比例越多,设置为 0 不缩放 。
}
}
}
</style>
12.4、src/main.ts
12.5、src/App.vue
<template>
<router-view></router-view>
</template>
<script setup lang="ts">
</script>
<style scoped>
</style>
12.6、测试成功
十三、本地代理,配置proxy,配置多环境 / env
vue3+vite:本地代理,配置proxy_vue3代理服务器proxy配置-CSDN博客
13.1、vite.config.ts
import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
'@assets': resolve(__dirname, 'src/assets')
}
},
// 开发服务器配置
server: {
host: '0.0.0.0', // 允许本机
port: 3022, // 设置端口
open: false, // 设置服务启动时自动打开浏览器
hmr: true, // 开启热更新
// cors: true, // 允许跨域
// 请求代理
proxy: {
'/m-staff-center': { // 匹配请求路径,localhost:3000/m-staff-center,如果只是匹配/那么就访问到网站首页了
target: loadEnv(process.argv[process.argv.length-1], './env').VITE_SERVER_NAME, // 代理的目标地址
changeOrigin: true, // 开发模式,默认的origin是真实的 origin:localhost:3000 代理服务会把origin修改为目标地址
}
}
},
css: {
// 这里的配置将应用于所有CSS/Sass/Less等预处理器文件
preprocessorOptions: {
scss: {
additionalData: `@import "@/assets/scss/variables.scss";` // 全局引入变量
// 或者你可以通过Vue的<style scoped src="...">来引入全局样式
// 如果你不需要进行特别的Sass配置,这个对象可以留空
}
}
},
})
13.2、env文件
env/env.dev 其他同理
# 请求接口地址
VITE_REQUEST_BASE_URL = '/m-abc-center/api/v1'
VITE_SERVER_NAME = 'https://md.abc.com.cn/'
# VITE开头的变量才会被暴露出去
env/index.d.ts
/** 扩展环境变量import.meta.env */
interface ImportMetaEnv {
VITE_REQUEST_BASE_URL: string,
VITE_SERVER_NAME: string
}
13.3、package.json/script配置
十四、 封装axios请求
14.1、安装axios
pnpm add axios
14.2、目录结构
14.3、src/api/http/axios.ts
import instance from "./index"
/**
* @param {String} method 请求的方法:get、post、delete、put
* @param {String} url 请求的url:
* @param {Object} data 请求的参数
* @param {Object} config 请求的配置
* @returns {Promise} 返回一个promise对象,其实就相当于axios请求数据的返回值
*/
const axios = async ({
method,
url,
data,
config
}: any): Promise<any> => {
method = method.toLowerCase();
if (method == 'post') {
return instance.post(url, data, { ...config })
} else if (method == 'get') {
return instance.get(url, {
params: data,
...config
})
} else if (method == 'delete') {
return instance.delete(url, {
params: data,
...config
})
} else if (method == 'put') {
return instance.put(url, data, { ...config })
} else {
console.error('未知的method' + method)
return false
}
}
export {
axios
}
14.4、src/api/http/index.ts
import axios from 'axios'
import cookie from 'js-cookie'
//创建axios的一个实例
const instance = axios.create({
// baseURL: import.meta.env.VITE_RES_URL, //接口统一域名
timeout: 6000, //设置超时
headers: {
'Content-Type': 'application/json;charset=UTF-8;',
}
})
//请求拦截器
instance.interceptors.request.use((config: any) => {
// 每次发送请求之前判断是否存在token,如果存在,则统一在http请求的header都加上token,不用每次请求都手动添加了
const token = `Bearer ${cookie.get('token')}`
console.log('16', token)
token && (config.headers.Authorization = token)
//若请求方式为post,则将data参数转为JSON字符串
if (config.method === 'post') {
config.data = JSON.stringify(config.data);
} else if(config.method === 'get'){
console.log('21', config)
}
return config;
}, (error: any) =>
// 对请求错误做些什么
Promise.reject(error));
//响应拦截器
instance.interceptors.response.use((response: any) => {
//响应成功
console.log('响应成功');
return response.data;
}, (error: any) => {
console.log(error)
//响应错误
if (error.response && error.response.status) {
const status = error.response.status
console.log(status);
return Promise.reject(error);
}
return Promise.reject(error);
});
export default instance;
14.5、 src/api/modules/m-abc-center.ts
import { axios } from "../http/axios"
import * as T from '../types/types'
export const getUser = (data: T.userParams) => {
return axios({
method: "get",
url: "/m-abc-center/api/v1/abc/abc",
data,
config: {
timeout: 10000
}
})
}
14.6、src/api/types/types.ts
export interface userParams {
keyword: string
}
14.7、页面使用及测试
<template>
<div class="container">
</div>
</template>
<script setup lang="ts">
import { getUser } from "@/api/modules/m-abc-center.ts";
let params = {
keyword:"snow",
}
getUser(params).then((res: any)=>{
console.log('4res', res)
})
</script>
<style scoped lang="scss">
</style>
14.8、测试成功
十五、接口挂载到全局属性上
15.1、 src/api/modules/m-abc-center.ts
export default ({axios}:any) => ({
getUser(data: T.userParams) {
return axios({
url: '',
data,
method: "get"
})
},
getUser2(data: T.userParams) {
return axios({
url: '',
data,
method: "get"
})
},
})
15.2、src/api/index.ts
import { axios } from './http/axios'
const files:any = import.meta.glob("./modules/*.ts", {eager: true}) // 导入文件
const api:any = {}
const apiGenerators:any = [] // modules目录下文件内容的数组,每一个文件是一个{}
for (const key in files) {
if (Object.prototype.hasOwnProperty.call(files, key)) {
apiGenerators.push(files[key].default || files[key])
}
}
apiGenerators.forEach((generator:any) => {
const apiInstance = generator({ // 创建axios实例
axios
})
for (const apiName in apiInstance) {
if (apiInstance.hasOwnProperty(apiName)) { // 通过名称找到定义的接口地址
api[apiName] = apiInstance[apiName]
}
}
})
export { api }
15.3、src/main.ts
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import router from "./routers/index"
import { api } from './api/index'
const app = createApp(App)
app.use(ElementPlus)
app.use(router)
app.config.globalProperties.$api = api
app.mount('#app')
15.4、页面使用及测试
<template>
<div class="container">
</div>
</template>
<script setup lang="ts">
let params = {
keyword:"snow",
}
import { getCurrentInstance } from 'vue'
let internalInstance = getCurrentInstance();
let Api = internalInstance && internalInstance.appContext.config.globalProperties.$api
Api.getUser(params).then((res: any)=>{
console.log('17res', res)
})
</script>
<style scoped lang="scss">
</style>
测试成功
十六、UnoCSS / 原子化CSS
Unocss(原子化css) 使用(vue3 + vite + ts)-CSDN博客
即时按需:UnoCSS的加载和渲染速度非常快,可以立即进行使用。它不需要预先编译,使得样式可以在需要时动态地生成。
原子级CSS:UnoCSS使用原子级CSS样式的概念,即通过将样式属性定义为独立的类来构建页面。每个类都只包含一项或几项样式属性,可以在组件中灵活地组合和应用这些类,以实现细粒度的样式控制。
轻量化和高性能:UnoCSS通过仅传递实际使用的样式属性,减小生成的CSS文件的体积,从而优化页面的加载速度,并减少不必要的网络传输和运行时的样式计算。
十七、vue-global-api
17.2、使用前
<script setup>
import { ref, computed, watch } from 'vue'
const counter = ref(0)
const doubled = computed(() => counter.value * 2)
watch(doubled, (v) => {
console.log('New value: ' + v)
})
</script>
17.3、使用后
<script setup>
const counter = ref(0)
const doubled = computed(() => counter.value * 2)
watch(doubled, (v) => {
console.log('New value: ' + v)
})
</script>
十八、layout 布局
vue3 + ts: layout布局_vue3 layout-CSDN博客
十九、directives
二十、vue3 - ts - lerna
二十一、rollup 插件
vue3-vite-ts:编写Rollup插件并使用 / 优化构建过程_vite 中 rollup-plugin-dts-CSDN博客
二十二、vue3 + three.js
WebGL-Vue3-TS-Threejs:基础练习 / Javascript 3D library / demo-CSDN博客
二十三、多语言
vue3-ts-vite:Google 多语言调试 / 网页中插入谷歌翻译元素 / 翻译_google-translate-element-CSDN博客
二十四、storybook
vue3-ts-storybook:理解storybook、实践 / 前端组件库-CSDN博客
二十五、多页面应用
vue3-ts-vite:vue 项目 配置 多页面应用_vite多页面配置-CSDN博客
二十六、TinyMCE
vue3.3-TinyMCE:TinyMCE富文本编辑器基础使用_tinymce中文文档-CSDN博客
二十七、shims-vue.d.ts
二十八、keepalive
vue3-ts:keepalive 列表页到详情,详情回到列表,列表恢复离开时的状态_vue3 从列表到详情-CSDN博客
二十九、qiankun
微前端-qiankun:vue3-vite 接入 vue3、nuxt3、vue2、nuxt2等子应用_qiankun.js-CSDN博客
三十、生命周期
vue3:生命周期(onErrorCaptured)-CSDN博客
三十一、vue2与vue3的区别
三十二、uuid
vue3 + ts:使用uuid_vue3 uuid-CSDN博客
三十三、watch
[vue3 + ts:watch(immediate、deep、watch)_vue3 watch immediate-CSDN博客](https://blog.csdn.net/snowball_li/article/details/123515231 "vue3 + ts:watch(immediate、deep、watch)_vue3 watch immediate-CSDN博客")
三十四、slot
vue:匿名slot、具名slot、作用域slot(技术栈Vue3 + TS)_具名插槽-CSDN博客
三十五、prettier
vue3-ts:husky + prettier / 代码格式化工具-CSDN博客
三十六、Stylelint
https://blog.csdn.net/snowball_li/article/details/141025848
三十七、Commitlint
工程化:Commitlint / 规范化Git提交消息格式_commitlint 规范-CSDN博客
三十八、TypeScript
TypeScript:熟练掌握TypeScript_typescript 学习-CSDN博客
Vue3-TypeScript-Threejs:导入外部的glb格式3D模型_threejs 导入3d模型-CSDN博客
三十九、git modules
git:git modules_.gitmodules-CSDN博客
四十、ElementUI 自定义主题
ElementUI: 自定义主题_elementui 主题-CSDN博客
四十一、TS错误信息列表
项目配置 - 错误信息列表 - 《TypeScript 3.1 官方文档中文版》 - 书栈网 · BookStack
四十二、过程记录
记录一、找不到模块"path"或其相应的类型声明
报错信息"找不到模块'path'或其相应的类型声明"通常意味着你的TypeScript项目中缺少对应模块的类型定义文件。
解决:
pnpm add @types/node
安装后问题解决
记录二、Module '"c:/gkht/project/m-basic-vue3-pc/m-basic-vue3-pc/src/components/HelloWorld.vue"' has no default export.Vetur(1192)
新项目有一条红色的波浪线,很难受
解决:
根据提示信息可以知道是Vetur的提示信息。
查找资料后,
viscode插件 Vetur(v0.35.0)不支持最新写法 卸载 并 安装 Volar 插件,使用Volar插件代替Vetur
但是,本文时间20240802 Volar被弃用了,改用了 Vue - Official
问题得到解决
记录三、lint-staged干啥的
lint-staged是一个在git暂存文件上运行linters的工具,它的主要作用是提高代码质量和开发效率。lint-staged通过自动化代码检查过程,可以显著提升开发效率并维护代码一致性。这个工具特别适用于前端开发,因为它能够过滤出Git代码暂存区的文件,只对这些文件进行lint检查,避免了全量文件的检查,从而提高了性能并减少了误操作的可能性。此外,lint-staged的使用可以整合到开发流程中,为开发者提供更流畅、更可靠的编码体验
记录四、报错 error Parsing error: Unexpected token :
使用 ESLint 9.0 时遇到"Parsing error: Unexpected token :"这类错误,通常是因为 ESLint 配置不正确或者没有正确解析 TypeScript 文件。
确保已经安装了 @typescript-eslint/parser
和 @typescript-eslint/eslint-plugin
。这两个包是 ESLint 对 TypeScript 支持的核心。
记录五、TypeError: (intermediate value).globEager is not a function
由 import.meta.globEager(参数)
改为 import.meta.glob(参数, {eager: true})
记录六、理解 import.meta.glob
import.meta.glob
是一个在 Vite、Snowpack 等现代前端构建工具中提供的特殊功能,它允许你基于 Glob 模式动态地导入多个模块。这个功能特别有用,当你需要基于文件系统的结构来动态地加载多个模块时,比如在一个 Vue、React 或其他前端框架的应用中,你可能需要导入一个目录下所有的组件或模块。
import.meta.glob
返回一个对象,其键是匹配到的文件路径(相对于当前文件),值是动态导入的模块。但是,默认情况下,这些模块是懒加载的,即它们只会在被实际请求时才会被加载。
当你将 {eager: true}
作为第二个参数传递给 import.meta.glob
时,它会改变这个行为,使得所有匹配的模块在调用 import.meta.glob
时立即被加载(即非懒加载)。
// 假设你有一个目录 `./components/`,里面包含了多个 Vue 组件
// 你想要在应用启动时立即加载这些组件
// 使用 Vite 或支持 import.meta.glob 的构建工具
const modules = import.meta.glob('./components/*.vue', { eager: true });
// modules 现在是一个对象,其键是组件文件的路径,值是已解析的模块
// 你可以遍历这个对象来访问这些组件
for (const path in modules) {
// 注意:由于模块是立即加载的,你可以直接访问 modules[path] 来获取模块导出的内容
// 例如,如果组件默认导出了一个 Vue 组件,你可以这样做:
const component = modules[path].default;
// 现在你可以将 component 用作 Vue 组件或其他用途
}
// 但是,请注意,由于所有组件都是立即加载的,这可能会影响你的应用的初始加载时间
// 因此,请谨慎使用 {eager: true}
记录七、files[key].default为什么有的是命名导出有的是default
解答
// 正确的默认导出
export default function myFunction() {
// ...
}
// 只有命名导出
export function myNamedFunction() {
// ...
}
记录八、Cannot find module '@/api/modules/m-abc-center.ts'
解决:增加一个声明
src/vite-env.d.ts
declare module "@/api/modules/m-abc-center.ts"
记录九、vite-env.d.ts的作用是什么
vite-env.d.ts
(或有时简称为env.d.ts
)是一个TypeScript声明文件,它在Vite项目中扮演着重要的角色。以下是vite-env.d.ts
的主要作用:
|----|-------------|---------------------------------------------------------------------------------------------------------------------------------------------------|
| 序号 | 作用 | 解释 |
| 1 | 声明环境变量类型 | vite-env.d.ts`文件通常用于声明项目中使用的环境变量类型。在Vite项目中,环境变量是在构建过程中进行动态替换的,而`vite-env.d.ts`则为这些环境变量提供了类型定义,使得TypeScript能够理解这些变量的类型,并在代码中使用时提供类型检查和智能提示。 |
| 2 | 提高代码安全性 | 通过为环境变量提供明确的类型定义,vite-env.d.ts
帮助开发者在编写代码时避免类型错误,从而提高代码的类型安全性和可维护性。例如,如果某个环境变量被声明为字符串类型,那么TypeScript就会在编译时检查该变量的使用是否符合字符串类型的规范。 |
| 3 | 支持全局类型声明 | vite-env.d.ts
文件还可以用于声明全局类型。在TypeScript中,全局类型声明通常用于定义那些在整个项目中都可用的类型,比如全局变量、全局函数等。通过在vite-env.d.ts
中声明这些全局类型,开发者可以在项目的任何位置使用它们,而无需在每个文件中都进行重复声明。 |
| 4 | 引入其他类型的声明文件 | 在vite-env.d.ts
文件中,开发者还可以使用/// <reference types="..." />
指令来引入其他类型声明文件。这有助于将多个类型声明文件组织在一起,使得项目的类型定义更加清晰和易于管理。 |
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_API_URL: string;
// 其他环境变量...
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
declare module "@/api/modules/m-abc-center.ts"
记录十、Vue 3项目中的.vue
文件类型声明
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
在Vue 3与TypeScript结合使用时,由于.vue
文件不是标准的TypeScript文件(它们包含模板、脚本和样式),因此TypeScript默认无法理解这些文件的结构。为了解决这个问题,我们需要通过声明文件来告诉TypeScript如何理解.vue
文件中的组件。
四十三、欢迎交流指正
四十四、参考链接
小程序-uni-app:uni-app-base项目基础配置及使用 / uni-app+vue3+ts+vite+vscode_uniapp-CSDN博客
从 vue 源码看问题 ------ 你真的了解 Vue 全局 Api 吗?_vue-global-api-CSDN博客
Vue 开发必须知道的36个技巧(小结)_vue.js_脚本之家
https://juejin.cn/post/7354298118236864566
Documentation - ESLint - Pluggable JavaScript Linter
配置文件 - ESLint - 插件化的 JavaScript 代码检查工具