Vite 的工程化流程

Vite 的工程化流程

最近在学习 Vue 3 的相关内容,而在 Vue 3 当中则采用了 Vite 来作为构建工具,所以在这里简单的梳理一下 Vite 的相关内容,也算是记录一下

什么是 Vite

Vite 是一款开发构建工具,在开发期,它是利用浏览的 Native ES Modules 特性来导入并且组织代码,生产环境中则是利用 Rollup 作为打包工具,它主要有以下几个特点

  • 启动速度很快
  • 热模块替换
  • 按需编译

安装和使用的方式十分简单,并不需要过多的配置,安装流程如下

1
npm install -g create-vite-app

安装完成以后我们就可以使用它来初始化我们的 Vue 3 项目

1
2
3
4
5
6
7
$ npm init vite-app <project-name>

$ cd <project-name>

$ npm install

$ npm run dev

代码组织方式

这里我们来借助上面默认初始化完成以后的项目来进行简单的结构梳理,首先先从入口文件开始看起,也就是我们的 index.html,它在代码当中的引入方式是下面这样的

1
<script type="module" src="/src/main.js"></script>

这里可以发现,我们引用了 /src/main.js 这个文件,但是是使用了 type="module" 的方式来进行引用,我们再来看看 main.js

1
2
3
4
5
import { createApp } from 'vue'
import App from './App.vue'
import './index.css'

createApp(App).mount('#app')

我们先来简单的梳理几个可能会有疑惑的地方,首先就是这里直接采用了裸模块的方式直接进行了引用,也就是 import { createApp } from 'vue' 这样的使用方式,我们在上面也提到过,Vite 是利用浏览的 Native ES Modules 特性来导入并且组织代码,但是浏览器是如何知道这个文件具体是在什么位置的呢?

另外对于 import App from './App.vue' 这样的相对路径的引入方式我们很熟悉,但是它又是如何解析 App.vue 这样的文件的呢?同理下面的 index.css 也是一样的道理,也许它是一个纯的 CSS,但是如果是使用了预编译器的 Sass,那又该如何对它进行处理呢?

关于上面的这些疑问,我们可以在项目启动以后,借助 Chrome 浏览的开发者工具当中的 Network 标签来进行分析,我们先来看它的加载过程,这里我们主要关注以下几个文件的加载解析过程,它们按序加载的顺序如下

  • localhost
  • main.js
  • vue.js
  • App.vue
  • index.css?import
  • HelloWorld.vue
  • App.vue?type=template

一开始首先加载 localhost,也就是请求本地服务器上的 index.html,它发现内部引用了 main.js,所以又发送了另外一条请求去请求该文件,但是我们可以发现,现在返回的 main.js 有了一些变化,如下所示

1
2
3
4
5
import { createApp } from '/@modules/vue.js'
import App from '/src/App.vue'
import '/src/index.css?import'

createApp(App).mount('#app')

我们可以发现,返回的路径在 Vite 的处理下变成了 '/@modules/vue.js',所以此时浏览器便会再次发送请求,去请求一个相对路径下的 vue.js,所以就会去 node_modules 下寻找 vue 文件夹,接下来就会去访问 vue 文件夹下的 package.json,如下

1
2
3
4
5
6
7
8
9
// node_modules/vue/package.json
{
// ...

"main": "index.js",
"module": "dist/vue.runtime.esm-bundler.js",

// ...
}

通过 package.json 我们可以知道,所谓的入口文件也就是 vue.runtime.esm-bundler.js 这个文件,从命名上我们也可以看出,就是一个运行时的使用 ES 模块来打包的 vue 版本

所以我们在调用了 import { createApp } from '/@modules/vue.js' 这行代码以后就相当于在我们的代码当中从 vue 当中导出了 createApp 这个方法,然后就可以在后续过程当中执行 createApp(App).mount('#app') 来进行程序的创建了

接下来我们再来看看 import App from '/src/App.vue' 这一行,因为是相对路径,所以会去请求当前目录下的 App.vue,我们来对比看一下它的先后变化,首先是我们代码当中实现的 App.vue,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<img alt="Vue logo" src="./assets/logo.png" />
<HelloWorld msg="Hello Vue 3.0 + Vite" />
</template>

<script>
import HelloWorld from './components/HelloWorld.vue'

export default {
name: 'App',
components: {
HelloWorld
}
}
</script>

而下面则是经过 Vite 处理后返回的 App.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import HelloWorld from '/src/components/HelloWorld.vue'

const __script = {
name: 'App',
components: {
HelloWorld
}
}

import { render as __render } from '/src/App.vue?type=template'
__script.render = __render
__script.__hmrId = '/src/App.vue'
typeof __VUE_HMR_RUNTIME__ !== 'undefined' && __VUE_HMR_RUNTIME__.createRecord(__script.__hmrId, __script)
__script.__file = '/path/src/App.vue'
export default __script

可以发现,我们之前所写的 <template></template><script></script> 等相关代码全部被经过了一次编译,然后将它们组合,输出最终的代码,而返回的最终代码也就如上所示,简单梳理一下就是

  • 首先将我们的路径替换成了相对路径 '/src/components/HelloWorld.vue'
  • 之前 export default { } 的部分变成了一个名为 __script 的组件配置对象
  • 通过请求 App.vue 引入了 render 函数,但是添加了 type=template 的查询参数,这样 Vite 就会对这个请求做特殊处理,也就是解析 <template></template> 这个模板,将其变成渲染函数
  • 得到渲染函数以后,再将其合并到我们之前的组件配置对象当中
  • 以上就是任务的整个流程,剩余的一些都是一些标识文件,这里我们就不过多探讨了,然后最后将我们的组件配置对象导出

其实简单总结的话就是『解析当前组件,并且把我们的最终解析结果导出』,所以这样一来 App.vue 就变成了一个组件配置对象返回到了前台,所以我们在使用这个组件配置对象的时候就可以正常的去使用了

小结

最后我们来简单的总结一下上面梳理的内容,从开头部分开始说起,关键变化的是 index.html 中的入口文件的导入方式

1
<script type="module" src="/src/main.js"></script>

这样 main.js 中就可以使用 ES6 Module 的方式来组织代码

1
2
3
import { createApp } from 'vue'
import App from './App.vue'
import './index.css'

浏览器会自动加载这些导入,Vite 会启动一个本地服务器处理这些不同的加载请求

  • 对于相对地址的导入,要根据后缀名处理文件内容并返回
  • 而对于裸模块导入要修改它的路径为相对地址并再次请求处理
1
2
3
import { createApp } from '/@modules/vue.js'
import App from '/src/App.vue'
import '/src/index.css?import'

资源加载

下面我们再来看看 Vite 当中的资源加载,也就是图片,CSS 等一些静态资源是如何处理的,这也是工程化当中的一个十分重要的点

CSS 文件导入

Vite 中可以直接导入 CSS 文件,样式将影响导入的页面,还是以我们的 main.js 为例,它是以全局的方式进行引入的

1
2
3
import { createApp } from 'vue'
import App from './App.vue'
import './index.css'

JavaScript 的处理方式类似,Vite 会对这个 CSS 进行开发阶段的预处理,将其转换成 JavaScript 代码,然后热更新到界面当中,但是需要注意的是,最后还是会被打包到 style.css 当中,下面是处理后的 main.js 文件当中的引入方式

1
import '/src/index.css?import'

可以发现,针对于 CSS 的处理,添加了 import 的标识,再来看看现在的 index.css 的样子,如下

1
2
3
4
import { updateStyle } from "/vite/client"
const css = "#app {\n font-family: Avenir, Helvetica, Arial, sans-serif;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n text-align: center;\n color: #2c3e50;\n margin-top: 60px;\n}\n"
updateStyle("\"2418ba23\"", css)
export default css

可以发现,其实最终的处理是以 JavaScript 的形式传递到前端,下面我们再来看看 CSS Module 的使用方式

CSS Module

我们除了在模版当中的 style 里直接定义对应 class 的样式以外,还可以使用 Module 的方式,我们通常的使用方式是下面这样的

1
2
3
4
5
6
7
8
9
<template>
<img class="img" alt="Vue logo" src="./assets/logo.png" />
</template>

<style scoped>
.img {
width: 500px;
}
</style>

但是我们可以将其调整为 CSS Module 的形式,这样一来我们的 CSS 在将来编译的时候会将我们的 style 变成计算属性,所以在模板当中使用的时候就不再是使用单纯的 class 了,而是使用 $style 来进行使用,也就是下面这样的使用方式

1
2
3
4
5
6
7
8
9
<template>
<img :class="$style.img" alt="Vue logo" src="./assets/logo.png" />
</template>

<style module>
.img {
width: 500px;
}
</style>

运行以后可以发现,结果是和上面是一致的,并且最终生成的样式结果当中会自动帮助我们加上 hash,也就是下面这样的

1
<img class="img_7ac74a55" alt="Vue logo" src="/src/assets/logo.png">

而这也是模块化带来的好处,因为有 hash 的存在,所以我们也不用担心它未来会重名,而这也是与使用 scoped 的区别

不过这里需要注意的一点就是,如果我们之前定义的 class 是使用 - 命名的话,则需要将其调整为驼峰命名,另外如果需要在 JavaScript 当中导入 CSS Module,只需要将 CSS 文件命名为 *.module.css,这样一来 Vite 也会自动对其进行模块化的处理

1
2
3
4
5
6
7
8
9
10
import style from './HelloWorld.module.css'

export default {
emits: ['close'],
computed: {
$style() {
return style
}
}
}

PostCSS

Vite 自动会对 *.vue 文件和导入的 .css 文件应用 PostCSS 配置,我们只需要安装必要的插件和添加 postcss.config.js 文件即可

1
2
3
4
5
module.exports = {
plugins: [
require('autoprefixer')
]
}

这里需要注意的一个地方就是,如果需要安装 autoprefixer 这个插件,最好保持和 PostCSS 相同的版本,否则会有不兼容的错误提示

资源 URL 处理

我们可以在 *.vue 文件的 <template><style> 和纯的 .css 文件当中以相对和绝对路径方式引用静态资源,我们先来看看静态资源如何引用的

1
2
3
4
5
<!-- 相对路径 -->
<img src="./assets/logo.png" />

<!-- 绝对路径 -->
<img src="/src/assets/logo.png" />

另外一个就是 public 目录,public 目录下可以存放未在源码中引用的资源,它们会被留下且文件名不会有哈希处理,这些文件会原封不动的拷贝到发布目录的根目录下

1
<img src="./logo.png">

但是需要注意的是,引用放置在 public 下的文件需要使用绝对路径,例如 public/icon.png 应该使用 /icon.png 进行引用

eslint

其实在 Vite 当中使用 eslint 并不会对我们进行约束,该怎么配置还是怎样配置,通常我们借助 eslint 规范项目代码,通过 prettier 做代码格式化,所以我们就需要进行两部分配置,而且我们希望两者是相匹配的,因为如果不匹配的话,在我们格式化以后是通过不了 eslint 的检查,这样就会引起很多的麻烦,下面我们来看看如何进行配置,首先在项目当中安装依赖,package.json 如下

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"scripts": {
"lint": "elint \"src/**/*.{js,vue}\""
},
"devDependencies": {
"@vue/elint-config-prettier": "^6.0.0",
"babel-eslint": "^10.1.0",
"eslint": "^6.7.2",
"eslint-plugin-prettier": "^3.1.3",
"eslint-plugin-vue": "^7.0.0-0",
"prettier": "^1.19.1"
}
}

接下来就是进行 lint 的规则配置,主要有两种方式,一种是采用 .eslintrc 的方式,也就是没有后缀的形式,但是这种方式需要写 JSON,它的好处是代码提示很好,并且如果安装了 eslint 扩展可以很好的帮助我们来进行提示有哪些项是可以选择的,可以使用的

另外一种是采用 .eslintrc.js 的方式,也就是带后缀的方式,它的好处是可以在配置文件当中加上一些动态的配置,比如环境变量等,这里我们采用带有后缀的方式,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
module.exports = {
root: true,
env: {
node: true
},
// 引入三个扩展,一个是 vue3 核心的 lint 规则,exlint 的建议规则 和 prettier 建议的规则
extends: ['plugin: vue/vue3-essential', 'eslint: recommended', ' @vue/prettier'],
parserOptions: {
parser: 'babel-eslint'
},
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'prettier/prettier': [
'warn',
{
// 一些冲突项可以自定义设定
// singleQuote: true,
// semi: false,
trailingComma: 'es5 ',
},
],
}
}

另外如果有必要我们还可以配置 prettier.config.js 来修改 prettier 的默认格式化规则,因为开发工具的不一致可能导致格式化后的结果不一致,所以我们将其配置成一致的

1
2
3
4
5
6
7
8
9
10
11
module.exports = {
printWidth: 80, // 毎行代长度(默认 80)
tabWidth: 2, // 每个 tab 相当于多少个空格(默认 2)
useTabs: false, // 是否使用 tab 进行缩进(默认 false)
singleQuote: true, // 使用单引号(默认 false)
semi: true, // 声明结尾使用分号(默认 true)
trailingComma: 'es5', // 多行使用拖尾逗号(默认 none),es5 表示只针对对象或数组才使用
bracketSpacing: true, // 对象字面量的大阔好间使用空格(默认 true)
jsxBracketSameLine: false, // 多行 jsx 中的 > 放置在最后一行的结尾,而不是另起一行(默认 false)
arrowParens: 'avoid', // 只有一个参数的箭头函数的参数是否带圆括号(默认 avoid)
}

测试

当我们当项目规模变大,或者我们在写一些通用组件的时候,我们肯定需要实现一些测试,避免一些新的调整影响到了之前的代码,下面我们就来看看如何在 Vite 项目当中配置测试

这里我们采用的是 jest 测试框架和针对于组件测试的 @vue/test-utils,我们先来看看需要安装哪些依赖,如下

1
2
3
4
5
6
7
{
"jest": "^24.0.0", // 包括测试运行库和断言库
"vue-jest": "^5.0.0-alpha.3", // 测试组件
"babel-jest": "^26.1.0", // 语法转换
"@babel/preset-env": "^7.10.4", // 配合 babel 转换
"@vue/test-utils": "^2.0.0-beta.9" // 测试套件
}

依赖安装完成以后我们还需要配置 babel.config.js

1
2
3
4
5
6
7
8
9
10
11
module.exports = {
presets: [
[
'@babel/preset-env', {
targets: {
node: 'current'
}
}
]
]
}

然后再来配置 jest.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
module.exports = {
testEnvironment: 'jsdom',
// 转换
transform: {
'^.+\\.vue$': 'vue-jest',
'^.+\\js$': 'babel-jest',
},
moduleFileExtensions: ['vue', 'js', 'json', 'jsx', 'ts', 'tsx', 'node'],
testMatch: ['**/tests/**/*.spec.js', '**/__tests__/**/* .spec.js'],
moduleNameMapper: {
'^main(.*)$': '<rootDir>/src$1',
}
}

启动脚本为

1
2
// runInBand 代表按照序列的方式来执行
"test": "jest --runInBand"

最后就是我们的测试代码,因为我们在上面配置了测试目录,所以我们直接将其放置在 tests/example.spec.js 目录下

1
2
3
4
5
6
7
8
9
10
11
12
13
import HelloWorld from 'main/components/HelloWorld.vue'
import { shallowMount } from 'vue/test-utils'

describe('aaa', () => {
test('should', () => {
const wrapper = shallowMount(HelloWorld, {
props: {
msg: 'hello, vue3',
},
})
expect(wrapper.text()).toMatch('hello, vue3')
})
})

另外需要注意的就是我们还需要在 lint 配置当中添加 jest 环境

1
2
3
4
5
module. exports = {
env: {
jest: true
}
}

完成以后就可以来执行我们的测试代码了

1
npm run test

但是这样我们每次都需要手动执行命令来进行测试,所以我们可以将 linttestgit 挂钩起来,让其每次在提交代码之前自动执行测试,首先我们需要来安装两个插件

1
npm install lint-staged yorkie -D

接着需要在 package.json 当中添加配置

1
2
3
4
5
6
7
8
9
10
// yorkie 的配置项,监控我们的提交
// commit 之前执行 lint-staged,也就是代码检查
// push 之前去执行我们的测试
"gitHooks": {
"pre-commit": "lint-staged",
"pre-push": "npm run test"
},
"lint-staged": {
"*.{js, vue}": "eslint"
}

这样一来,在我们提交代码之前就会自动的执行检查与测试,只有全部通过以后才会正常的提交

TypeScript 的整合

其实在 Vite 当中我们是可以直接导入 .ts 的文件来进行使用的,即直接在模版文件当中通过添加 <script lang="ts"> 来进行使用,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<template>
<div>
<ul>
<li v-for="item in list" :key="item.id">{{ item.name }}</li>
</ul>
</div>
</template>

<script lang="ts">

import { defineComponent, ref } from 'vue'

interface Course {
id: number,
name: string
}

export default defineComponent({
setup() {
const list = ref<Course[]>([])
setTimeout(() => {
list.value.push({
id: 1,
name: 'hello'
})
}, 1000);
return { list }
}
})

</script>

另外在我们使用过程当中,最好指定一下 TypeScript 的版本号

1
2
3
4
5
{
"devDependencies": {
"typescript": "^3.9.7"
}
}

然后在加上 TypeScript 的相关配置即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"compilerOptions": {
"target": "esnext ",
"module": "esnext",
"moduleResolution": "node",
"isolatedModules": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"experimentalDecorators": true,
"lib": ["dom", "esnext"]
},
"exclude": ["node_modules", "dist"]
}

项目配置

对于使用 Vite 构建的项目,我们只需要在项目的根目录下添加 vite.config.js 文件,即可针对项目进行深度配置,下面我们就来看看如何使用

定义别名

我们通常在导入组件的时候,可能会像下面这样使用

1
import HelloWorld from './components/HelloWorld.vue'

但在这样操作可能会出现大量相对路径,非常的不优雅和容易出错,所以我们可以通过导入别名的方式来进行简化,首先我们先来配置我们的 vite.config.js,如下

1
2
3
4
5
6
7
8
const path = require('path')

module.exports = {
alias: {
// 路径映射必须以 / 开头和结尾
"/comps/": path.resolve(__dirname, "src/components")
}
}

比如上面的配置就表示我们给 src/components 进行别名定义,这样一来我们在导入组件的时候就可以直接使用别名当中定义的方式

1
2
3
4
5
// 配置之前
import HelloWorld from './components/HelloWorld.vue'

// 配置之后
import HelloWorld from '/comps/HelloWorld.vue'

代理配置

下面我们再来看看代理服务器的配置,这个也是使用较多的一个配置,配置如下

1
2
3
4
5
6
7
8
proxy: {
'/api': {
target: 'http://paoxy.com',
changeOrigin: true,
// 使用对象方式进行配置也可
rewrite: path => path.replace(/^\/api/, '')
}
}

使用也很简单

1
2
3
fetch('/api/users')
.then(res => res.json())
.then(data => console.log(data))

数据配置

我们通常在开发过程当中会使用 mock 来进行数据模拟,下面我们来看看如何配置,首先安装依赖

1
2
3
4
npm i mockjs -S

// 因为需要安装环境变量,所以我们加上 cross-env 这个库
npm i vite-plugin-mock cross-env -D

依赖安装完成以后,我们就可以在我们的 vite.config.js 当中来进行插件配置

1
2
3
4
5
6
7
8
9
10
11
const { createMockServer } = require('vite-plugin-mock')

module.exports = {
plugins: [
createMockServer({
// 由于该库默认支持 TS,所以项目不是使用 TS 开发的话就将其关掉
// 如果项目使用 TS 开发则无需配置这项
supportTs: false
})
]
}

然后在 package.json 当中进行环境变量的配置,这个算是一个小坑,需要注意

1
2
3
"scripts": {
"dev": "cross-env NODE_ENV=development vite"
},

最后就可以来定义我们的 mock 数据了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// mock/test.js
export default [
{
url: '/api/users',
method: 'GET',
response: req => {
return {
code: 0,
data: [
{ name: 'zhangsan' },
{ name: 'lisi' }
]
}
}
},
{
url: '/api/post',
method: 'POST',
timeout: 2000,
response: {
code: 0,
data: {
name: 'zhangsan'
}
}
},
]

然后重启我们的服务,这时就可以在控制台当中看到以下信息

1
[vite: mock-server]: request invoke: /api/users

说明此时已经检测到了我们的 mock 数据,这时我们就可以在浏览器当中来验证我们的数据了

模式和环境变量

我们在使用模式做多环境配置的时候,vite serve 的模式默认是 development,而 vite build 的时候则是 production,所以我们可以针对开发环境来创建对应的配置文件,我们可以在根目录当中创建一个名为 .emv.development 的文件,其中包括的内容如下

1
2
// 需要注意的是,需要在前面添加 VITE_ 才可以在代码中去使用这个变量
VITE_TOKEN = this is token

这样在代码中我们就可以来使用它了,测试如下

1
console.log(import.meta.env.VITE_TOKEN)

类似这样的方式我们还可以去配置 .emv.production 文件,这时候两种不同的配置就可以分别在这两种环境下生效

打包和部署

打包的话很简单,直接执行如下命令即可

1
npm run build

这里我们主要来看看部署方面的内容,当然我们可以使用类似 FTPSSH 等工具直接连接到我们的服务器,然后将打包后的 dist 文件传上去即可,但是这里我们来看看如何使用自动化处理流程来避免前面的那些繁琐的操作,这里我们主要采用的是 GitHubActions 来实现 CI/CD 的过程

GitHub Actions 可以让我们在 GitHub 仓库中直接创建自定义的软件开发生命周期工作流程,如下图所示

待续

# Vue

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×