之前vite2
刚出的时候其实已经自学过一波,但是老实说学起来完全不入脑,一方面本来这方面的基础就很差(指项目配置),另一方面学的时候没有跟着去动手,纯理论的学那完全就是越看越困。最后就是急躁,想一下就学完。所以到现在来说基本上就是零基础。这次痛并思痛,跟着文档一步一步来。
Vitevitejs.dev/guide/
overview
faster and leaner development experience for modern web projects
faster and leaner
、development experience
、modern web projects
是用来干什么的一目了然:提高开发体验感的,并且仅支持标准(现代)浏览器(>= IE 9`
?不清楚9
算不算,但是没差,IE
已经不再维护了)。
注意这里是”(development
)开发“,而打包还是交由rollup
处理。
拥抱ES MODULE
[2] (现代浏览器天然支持)而非CommonJs
[3]。
话不多说,直接上手。如果你不想一步一步配置的话,可以直接看官方的demo
[4] 。
不过学习嘛,还是一步一步来好些。
先创建个文件夹,然后初始化下环境
mkdir vite-study
cd vite-study
npm init --yes
然后安装下vite
,推荐是全局安装,因为cli
做了各种框架的基础配置,能快速生成项目。
npm install vite --global
npm install vite -D
安装完之后我们先什么都不动,仅新增个index.html
文件,记住这里和webpack
不同,直接放根目录,vite
会在根目录里找,这是因为vite
将index.html
当做入口的原因。
vite study
555
然后package.json
中配置下script
"scripts": {
"dev": "vite"
},
然后直接终端执行指令npm run dev
run-result
browser
非常简单,非常傻瓜,这样就跑起来啦。
但其实只是起了个koa
[5]服务器而已,和你自己手动起的没差,甚至不需要koa
,直接就http
[6]模块开就行了。
现在我们再给这个index.html
加点js
和css
文件。
echo > index.css
echo > index.js
index.css
随便搞点样式
.test {
color: red;
}
index.js
中import
这个index.css
文件
import './index.css';
然后重新再跑一下指令npm run dev
。
然后你会发现没有任何变化,因为我们压根没有给他们之间建立联系
直接在index.html
中引入这个index.js
文件,不过需要注意一点,script
的type
需要是module
,这样才能被识别是ES Module
,才能支持export import
等。
vite study
555
然后ctrl + s
自动reload
img-2
如果script
的type
不是module
,你在运行的时候浏览器就会报错。
img-3
如果你细心的话应该会发现你的index.css
中的样式被打上了scoped-id
:data-vite-dev-id
。如果你之前看过我的文章你会知道这里搞个自定义id
的作用是啥,一是作用域样式不会污染其它模块样式;二则是热更新可以直接找到这个style
标签然后直接覆盖替换。
img-4
然后没啥好说的了,我们接着直接来接入框架了。
不过到现在都没用到vite.config.js
,有些憋屈,所以得找个理由改下配置~。
有了,dev
的域名端口太丑~,我们来改下。
我们先在根目录创建vite.config.js
,然后配置下servver
[7]
import { defineConfig } from 'vite'
export default defineConfig({
server: {
port: 6666
}
})
然后ctrl + s
即可~
vite
虽然可以和其他框架比如react、svelte
等框架配合,但最适合那自然还是vue
(非技术方面,纯感性~)。
先安装vue
以及能让vite
“识别”vue
的plugin
: @vitejs/plugin-vue
[8]
npm install vue --save
npm install @vitejs/plugin-vue -D
然后在vite.config.js
中引入,配置到plugins
[9]中。
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
server: {
port: 6666
},
plugins: [
vue()
]
})
接着我们在根目录创建views
文件夹,然后在里面创建一个App.vue
{{ text }}
然后老规矩index.js
中引入
import { createApp } from 'vue'
import './index.css';
import App from './views/App.vue';
createApp(App).mount('#app')
然后老规矩在index.js
中引入vue
组件入口。
最后别忘了index.html
中创建个id
为app
的元素。
vite study
ok
,让我们在终端运行下指令npm run dev
不过这里还是遇到了问题,同时配置server.port
和vue
插件时会找不到页面,可能刚没关好,端口还在。。换成port:7777
就正常了。。。
img-5
官方还是很推荐vue3 +typescript
的。
老规矩如果嫌麻烦可以直接去看线上的demo
[11] ,不过学习嘛,最好还是都过一遍。
有一点我们需要知道,vite
仅提供.ts
文件的加载识别处理,而类型判断需要IDE
或者通过tsc
等构建过程实现。
可能我说的不是很对。。。。所以还是贴下原文。。。如果说错了麻烦大佬评论里说下,谢谢~
img-6
其他就不多说了,直接来配置
先来安装下我们需要的东西,既然用到了typescript
那自然是得先安装它才行,而vue-tsc
刚上面提到了一些,是一个包裹了tsc
[12]的工具,专门用来识别处理vue
[13]文件的。
npm install typescript -D
npm install vue-tsc -D
安装完之后我们来配置下tsconfig.json
文件
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"moduleResolution": "Node",
"strict": true,
"jsx": "preserve",
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"lib": ["ESNext", "DOM"],
"skipLibCheck": true,
"noEmit": true
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
"references": [{ "path": "./tsconfig.node.json" }]
}
compilerOptions
[14]: 顾名思义,编译所需要的配置参数,当然这是可以不配置的,不配置会用默认参数。target
[15]: 编译的目标环境,比如es5
,常见的配置。ESNext
表示最新的ECMA
下一代标准。useDefineForClassFields
[16]: 这是用来告知ts
使用下一代标准runtime
,当然语法还是一样的所以不需要考虑兼容性问题。默认值会根据你的target
的配置改变,如果是ES2022
或者ESNext
也就是最新版或者下一代版本,默认值会是true
,如果不是默认值则是false
。 这里要设置为true
,因为vue
需要。其他框架具体看情况,比如lint-component
就不能是true
。module
[17]: 设置模块规范,大家应该都知道这一块了,commonJs
或者ES2015(ES6)
等。ESNext
下一代ECMA
标准。moduleResolution
[18]: 告知ts
要怎么处理模块。这里用的ESNext
,所以是Node
。moduleResolution-default
strict
[19]: 这个就不多说了,严格模式jsx
[20]: 这个也是一看就知道是干嘛用的,告知ts
如何处理jsx
文件和代码,默认是preserve
,保持原状。jsx-mode
resolveJsonModule
[21] : 这个也很好理解,就是告知ts
支持对json
文件的处理,不然当你import
了json
文件之后会报错处理。isolatedModules
[22] : 这个配置还是比较重要的,vite
是基于esbuild
实现的,而esbuild
对typescript
的一些东西并不支持,比如type-only
的导入或者enum
。所以得加上这个字段通知ts
提醒你这些语法不支持。esModuleInterop
[23]: ts
会将commonJs/AMD/CMD
转换成和ES6 module
类似的,因为这层转换,导致存在两个问题:1. ESM
中使用import * as x from 'a'
相当于commonJs
中的const x = require('a')
。乍一看貌似没啥问题,但实际上ESM
的 * as x
是只能用于对象导入的,而对应的CJS
中还可以是function
等,这就会有问题了。2. 你引入的第三方包不一定都有做这两种规范的入口,所以可能会存在第一点中的问题。开启这个字段后,会从两方面来处理这个问题,一是compiler
中,二是导入两个辅助函数。lib
[24]: 类型检测需要的包,默认会检测你的target
,会根据你的target
来判断引入对应的内置api
类型判断以及提示。默认也会有浏览器端代码提示,也就是DOM
提示。skipLibCheck
[25]: 看名字就知道,绕过整个包的检测,只会检测你引入的方法等,可以节省编译的时间。noEmit
[26]: 看名字也很好懂,就是不要输出,比如编译后的源码,sourcemap
等,这样你引入的typescript
就相当于只是类型检测和代码提示的包而已。 然后可以用其它包比如babel
转换。include
[27]: 这个就不多说了,就是限制只转换这部分的文件,其中/**/*.xx
是会检测所有这个文件夹下的文件的,但如果只是*.xx
,没有**/
就只会检测当前这个文件夹下的xx
后缀名字的文件,文件夹会直接绕过。references
[28][29]
这个要单独说下,这是typescript 3.0
开始支持的一个特性。
简单地说,你的项目可能有多个文件夹,这些文件夹有自己的想法,所以需要做不同的配置来构建单独的output
,而如果没有使用这个特性,你只能是给每个文件夹加上tsconfig.json
文件,并且执行多次tsc
才行。
由于你可能需要不同的配置针对不同的文件夹,但是配置文件只有一个的时候并不能满足这个需求,当然你可以给每个文件夹配置一个自己的conf
文件,但这依旧有一个问题就是tsc
并不能帮你给所有的配置文件都处理,有些地方需要你去手动处理,watch
也并不能一次性监听所有的配置文件,这也就意味着你也得手动添加才行。
针对上面的问题,3.0
开始支持了refercens
,能有效的帮你拆分你的项目,分别执行对应的配置。
path
:指向对应文件夹中的配置文件,如果只写了文件夹名字,比如./src
,那就只会读对应的tsconfig.json
,而如果你的配置文件是自定义名字,请务必加上文件名。
然后我们创建tsconfig.node.json
文件
{
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}
composite
[30]: 这是个必要字段,只要你是references
的就都需要配置,方便ts
去快速找到output
对应的文件夹。不过如果你的文件位置和文件夹位置不是在同一个地方,那么你可以设置rootDir
去改变。allowSyntheticDefaultImports
[31]: 允许你使用import x from 'a'
替换import * as x from 'a'
include
: 前面提到过include
指向需要参与tsc
构建的文件,这里单独开了一个reference
,就是为了这个vite.config.ts
能被正常处理,一般情况下不参与构建,所以这里得单独指明这个文件也是需要参与构建的。然后我们修改下vite.config.js
为vite.config.js
。
接着我们新建src
目录,将我们的App.vue
、index.css
迁移到src
下。然后新建main.ts
文件作为入口。
import { createApp } from 'vue'
import './index.css';
import App from './App.vue';
createApp(App).mount('#app')
这个时候的App.vue
会有错误下划线,这是因为typescript
并不知道这是什么,我们先不管,稍后会做声明。
然后改下index.html
的引入,这里你应该有些好奇居然可以直接引入ts
文件,那是因为vite
将html
作为入口,也是会参与打包的。
接着我们进入到App.vue
文件中,我们需要写点typescript
的代码
{{ text }}
这个时候,handleChangeText
方法中text.value
会有红色下划线,这是因为我们定义了text
是一个string
类型的ref
。
上面说到main.ts
中引入App.vue
时无法识别.vue
文件,那是因为我们并没有对vue-sfc
文件做声明,typescript
自然是不认识的。
我们在src
文件夹下新建vue-env.d.ts
文件
///
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
///
这一行代码是通知vite
这是一个client
端声明文件,之前讲vitest
时也有提到过,感兴趣的大佬可以去看下我之前的文章。
声明的代码也很好理解,就是声明*.vue
文件是Component
。
我们之前已经在tsconfig.json
中include
过了.d.ts
文件了,所以这里并不需要我们去哪里给他导入。
这个时候你再回去看main.ts
,发现已经没有报错了。
最后我们来改下package.json
中的一些地方
"main": "src/main.ts",
"module": "src/main.ts",
"scripts": {
"dev": "vue-tsc && vite"
},
这样就配置ok
啦,我们在终端运行下npm run dev
。
img-7
运行成功!
既然基础已经可以跑通了,那么我们就来搞个小而五脏俱全的。
npm install vue-router --save
这就安装完啦,然后我们在src
文件夹下创建一个views
文件夹,然后在里面创建Index.vue
以及创建A.vue
和B.vue
用来测试我们的路由。
mkdir src/views
echo > src/views/A.vue
echo > src/views/B.vue
echo > src/views/Index.vue
随便给A.vue
以及B.vue
写点东西,B
的代码类似,这里就不展示了。
{{ text }}
然后我们在src
下创建router
文件夹以及在这个文件夹里新建index.ts
mkdir src/router
echo > src/router/index.ts
然后我们在这个index.ts
中写下router
的配置。
import { createRouter, createWebHashHistory } from 'vue-router'
import type { RouteRecordRaw, Router } from 'vue-router'
const Index = () => import('../views/Index.vue');
const A = () => import('../views/A.vue');
const B = () => import('../views/B.vue');
const routes: RouteRecordRaw[] = [
{
path: '/',
component: Index,
children: [
{ path: 'a', name: 'a', component: A },
{ path: 'b', name: 'b', component: B },
],
redirect: 'a'
}
]
const router: Router = createRouter({
// 跳转方式,这里就不用H5了
history: createWebHashHistory(),
routes
})
export default router
主路由是Index.vue
然后两个子路由A
和B
然后老规矩将这个router
给vue
use
一下。main.ts
中引入router
import { createApp } from "vue";
import router from "./router/index";
import "./index.css";
import App from "./App.vue";
createApp(App).use(router).mount("#app");
然后让我们回过头去到Index.vue
中,我们需要写个click
事件让路由变化。
切换
由于组合式api
不再使用this
,所以这里只能是import
再create
了[33]。
另外别忘了在App.vue
中将路由组件渲染在template
中。
然后就完成啦,我们重新跑下指令npm run dev
router-change
运行成功!
没想到vuex
直接被偷家了,目前由于vuex
还是4.x
版本,存在一些问题,比如同步异步分割的问题(老问题了)。
而pinia
完全就是按照vuex5
的预案来实现的,所以说是vuex5
也不为过,目前这个包是官方接手并且推荐的。
总结下就是pinia
轻快好省。 话不多说,感兴趣的可以去看下官方的描述。
Piniapinia.vuejs.org/introduction.html正在上传…重新上传取消
我们直接来安装
npm install pinia --save
mkdir src/store
echo > src/store/index.ts
echo > src/store/user.ts
话不多说,先引入pinia
并且给vue
use
下。
import { createPinia } from 'pinia'
import type { Pinia } from 'pinia'
const pinia: Pinia = createPinia();
export default pinia
然后main.ts
引入
import { createApp } from "vue";
import router from "./router/index";
import pinia from "./store/index";
import "./index.css";
import App from "./App.vue";
createApp(App).use(router).use(pinia).mount("#app");
ok
,这就配置完啦~
接着我们来存储一些数据,进入src/store/user.ts
import { reactive, computed } from 'vue';
import { defineStore } from "pinia";
import type { ComputedRef } from 'vue';
// 相当于以前的`module`
const useUserStore = defineStore("user", () => {
// 会被判定为`state`
const user = reactive({
name: '鲲哥',
age: 24,
gender: 'man', // 一个真正的man~(神经错乱)
description: '全民制作人,大家好,我是cxk~',
})
// 会被转换为`action`
function growUp (): void {
user.age += 1
console.log(user)
}
// 会被替换为getters
const age: ComputedRef = computed(() => user.age);
return {
user,
age,
growUp
}
});
export default useUserStore
当然如果你不习惯写compositionApi
的话也是可以的,传入一个options
,字段为state, getters, action
[35]
然后让我们去到A.vue
方法中,将这个user
引入
点击立长一岁~~
当前岁数:{{ age }}
注意如果你用的是对象解构并且这个数据是一个非引用的数据,你就需要用storeToRefs
进行包裹,因为解构导致它失去了响应式,相当于const a = 1
这样。
ok
,我们直接终端运行npm run dev
store
这样就完成啦~
我们接着准备接入vitest
[37],不过在这之前,我们要配置下别名,不为什么,只是为了少写几个字而已~
话不多说,直接去到vite.config.ts
文件中。 vite
别名配置是基于rollup
的插件@rollup/plugin-alias
[38],所以写法一致。
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// import './server.js'
export default defineConfig({
resolve: {
alias: {
'@/': 'src/',
'@views/': 'src/views/',
'@router/': 'src/router/',
'@store/': 'src/store/',
}
},
plugins: [
vue()
],
server: {
port: 7777,
},
})
但是到这里还没有结束,我们还需要告知ts
这个@
字符代表什么意思。
{
"compilerOptions": {
// ...
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@views/*": ["src/views/*"],
"@store/*": ["src/store/*"],
"@router/*": ["src/router/*"],
}
},
}
这样就正常啦~,可以安心的将你的绝对路径改成@/xxx
了。
这是一个从vite
中抽出去的一个包,我之前也写过一篇文章学习vitest
的,感兴趣的大佬可以去看下。
坏蛋Dan:Vitest学习1 赞同 · 0 评论文章正在上传…重新上传取消
话不多说,我们直接配置。这里就不用inline
测试了,感兴趣的大佬可以看下我上面提到的这个文章。 另外下面用到的一些东西我就不解释了,这文章里都有说的。
npm install vitest -D
mkdir __tests__
echo > __tests__/testComponentA.spec.ts
首先,因为这个包是在src
外的,所以我们需要在tsconfig.json
文件中将他include
了导入文件才能正常。
{
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue", "__tests__/**/*.ts"],
}
然后我们直接进入vite.config.ts
中配置下。
///
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// import './server.js'
export default defineConfig({
resolve: {
alias: {
'@/': 'src/',
'@views/': 'src/views/',
'@router/': 'src/router/',
'@store/': 'src/store/',
}
},
test: {
environment: 'jsdom',
globals: true
},
plugins: [
vue()
],
server: {
port: 7777,
},
})
这里需要注意几点,如果你是在vite.config.ts
文件中配置的,你需要在文件头配置 ///
通知vite
这里有配置test
。当然你也可以重开一个vitest.config.ts
,然后把这个vite.config.ts
注释掉,将里面的配置迁移到vitest.config.ts
中。当然你还可以使用mergeConfig
将这两个配置文件中的配置合并,这里就不多说了。
environment
: jsdom
表示我们是在浏览器环境,需要测试dom
。
globals
: 如果是true
你就可以在全局都有vitest
的代码提示了。 如果设置了true
别忘了通知ts
,回到tsconfig.node.json
中配置types
。
{
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true,
"types": ["vitest/globals"]
},
"include": ["vite.config.ts"]
}
然后我们再来安装下我们测试需要的包jsdom
[40]和@vue/test-utils
[41] 。
npm install jsdom @vue/test-utils -D
然后我们进入__tests__/testComponentA.spec.ts
中
import { describe, it, expect } from 'vitest';
import { mount } from '@vue/test-utils'
import A from '../src/views/A.vue';
describe('test-component-a-grow-up', () => {
it('test-a-click', () => {
const wrapper = mount(A, {});
// 找到按钮并让他加一岁
const btn = wrapper.get('.btn');
btn.trigger('click');
expect(wrapper.get('.age').text()).contain('25');
})
})
最后让我们去到package.json
文件中新增一个script
: "test": "vitest"
然后终端执行npm run test
然而直接报错了。
test-run-error
为什么呢,因为这个时候pinia
没创建实例,这又是为什么呢?因为项目没有跑起来。。。
那么怎么办呢?那只能是将main.ts
也跑了。。另外还发现一个问题,就是点击事件需要异步处理。
import { describe, it, expect } from 'vitest';
import { mount } from '@vue/test-utils'
import '../src/main';
import A from '../src/views/A.vue';
describe('test-component-a-grow-up', () => {
it('test-a-click', async () => {
const wrapper = mount(A, {});
// 找到按钮并让他加一岁
const btn = wrapper.get('.btn');
await btn.trigger('click');
expect(wrapper.get('.age').text()).contain('26');
})
})
这回重新跑npm run test
test-img
成功啦~,age
成功变成25
了。
我们来搞个自己的插件玩下。
vite
的plugin
接口是基于rollup
[43]的接口,然后加了一些自己的东西。
我们先在根目录新建一个plugins
的文件夹,然后新建vite-plugin-vue-test.ts
。这里需要注意vite
的plugin
的文件名是一定要vite-plugin-*
[44]的,如果你的plugin
是只想用于某个框架,那就得是vite-plugin-vue/react-*
这样。当然你可以写rollup
的插件,命名和rollup
的规范一致。
先来看下编写规范,和rollup
的其实是一样的。
export default function myPlugin(options = {}) {
return {
name: 'my-plugin', // required, will show up in warnings and errors
resolveId(id) {
//...
},
load(id) {
//...
}
}
}
name
字段,这是必须的,用来标识你的plugin
。resolved、load
都是vite
抛出的hook
,你可以监听他们,然后返回数据来改变某些东西。ok
,基本了解完毕,接下来就是得想一下我们的plugin
要干嘛,就简单的log
下吧。。。学习嘛,就是不要有这么多技术含量doge
(误).
我们来log
下执行过程中的三个hook
,看下都是个啥。
export default function myPlugin() {
return {
name: 'my-plugin', // required, will show up in warnings and errors
resolveId(id) {
console.log(`resolveId: ${id}`)
},
load(id) {
console.log(`load ${id}`)
},
transform (...args) {
console.log(`transform ${args}`)
}
}
}
resolveId
[45]: 模块请求中自定义的解释器?可用来代码第三方包的解析。hook-resolveId
load
[46]: 加载的时候触发,热更新的时候只加载需要的。id
指向文件路径。hook-load
transform
[47]: 看名字就知道这肯定是和源码有关的。hook-transform
其他的hook
这里就不log
了,知道怎么实现一个plugin
就是我们的目标。