浏览器只认识 .html
,.css
,.js
,这就意味着当项目中使用了其他类型文件时(比如:.ts
,.vue
等)需要将其转换成浏览器能识别的文件才能正常运行项目,例如以下这些场景:
.ts
:需要安装 tsc
工具将 .ts
转化成 .js
.react
/ .vue
:需要安装 react-compiler
和 vue- compiler
,将 .jsx
、.vue
转化为 render()
函数(也就是 js
代码).tsc
:需要经过一系列编译 .tsc
- .jsx
- .js
.less
/ .scss
:需要安装 less-loader
,scss- loader
等一系列编译工具babel
将一些先进的 es 语法转化为不同版本浏览器都可以接受的语法uglify.js
等)将项目压缩使其体积更小传输性能更高构建工具不需要我们去手动一步一步处理这些编译过程,它会集成这些编译处理工具并完成这些流程,让开发人员只需要关注业务代码,代码经过构建工具处理后,就能给出最终的 .js
。
JavaScript 设计之初并没有包含模块的概念,基于越来越多的工程需要,为了使用模块化开发,JavaScript 社区涌现了多种模块标准(如 Node 端的 CommonJS)。直到2015年,发布了 ES6(ECMAScript 6.0),自此 JavaScript 语言才具备了模块这一特性(JavaScript模块),最新的浏览器开始原生支持模块功能了,这是一个好事情,浏览器能够最优化加载模块,使它比使用库更有效率,因为使用库通常需要做一些额外的客户端处理。
先来看一个例子:
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Documenttitle>
head>
<body>
<script type="module">
import _ from 'lodash-es'
console.log('_', _)
script>
body>
html>
安装 lodash
npm init -y
npm i lodash-es
浏览器打开 index.html
,会有报错提示:
提示需要通过相对路径去引入,因为浏览器是不知道 node_modules
这个路径的,我们在项目开发中可以实现这样的引入是因为项目中的构建工具已经帮我们做了这种路径的判断处理和引入。
问:既然都知道,直接引入库名的最佳实践就表示要去
node_modules
下去找这个库,那么为什么 ESModule 不将这种规范加入呢?
答:因为 CommonJS 最初只为服务端而设计,例如 Node.js 的实现中就采用了 CommonJS 标准的一部分,它可以不使用相对路径和绝对路径就能找到node_modules
,是因为服务端读取文件就是本地获取,但是如果 ESModule (也就是浏览器)也支持这种模块加载方式的话就表示浏览器需要通过网络请求来加载资源,另外每个 package 包还可能引入了很多额外的包,浏览器加载这些包都是需要网络请求的,这无疑是很大的一种性能消耗。这也就是为什么 ESModule 不敢将这种最佳实践的加载方案加入标准中,而是直接报错告诉你,我就是不支持这种引入方式。
浏览器是不支持 CommonJS 模块化代码的,所以如果在编写代码时使用 CommonJS 模块化方式引入库,比如:
const lodash = require('lodash-es')
console.log('_', lodash)
提示浏览器不认识 require
这种语法,无法通过这种方式引入库,而经过构建工具的处理,实现了 CommonJS 等多种模块化的支持。
在处理例如 .less
、.scss
、.jsx
、.vue
等这些浏览器无法识别的文件时,构建工具通过集成每种文件对应处理工具,可以流程化自动实现语法处理工作,同时针对不同浏览器的语法兼容问题通过 babel
语法降级工具的集成可以实现自动化代码兼容性处理。
经过处理后的项目经过打包处理,最终会生成可以被浏览器识别的文件类型,在整个打包过程中,构建工具可以压缩文件、代码分割、优化开发体验(例如:热更新、开发服务器解决跨域问题等)等。
总结:构建工具可以让开发者不用每次编码后还要关心如何让代码在浏览器端运行,而只需要关注业务逻辑即可。
Webpack 就是这样一个构建工具,在浏览器支持 ES 模块之前,JavaScript 并没有提供原生机制让开发者以模块化的方式进行开发,上述构建工具需要处理的工作 Webpack 都可以帮我们完成。
但是,当我们开始构建越来越大型的应用时,需要处理的 JavaScript 代码量也呈指数级增长,因此开始遇到技术瓶颈 —— 使用 JavaScript 开发的工具通常需要很长时间才能启动开发服务器(项目跑起来),即使使用 HMR,文件修改后的效果也需要几秒钟才能在浏览器中反映出来。如此循环往复,迟钝的反馈会极大地影响开发者的开发效率和幸福感。
而 Webpack 无法通过改善自身来解决上述问题,为什么?我们从 Webpack 的原理进行说明。
Webpack 是一种多模块化支持的构建工具,例如:
const lodash = require('lodash') // commonjs 规范
import Vue from 'vue' // es6 module
// webpack允许上面的两种写法,webpack通过 AST 抽象语法分析工具,分析出所有的导入导出操作
// 通过启动的服务器最终将其转化成统一的浏览器可以识别的代码
const lodash = webpack_require('lodash')
const Vue = webpack_require('vue')
这就意味着 Webpack 需要一开始统一模块化代码(统一成 webpack_require
函数处理的模块化方式),这也就意味着需要将所有依赖文件都读取完。这也就是为什么当项目文件越来越多时,Webpack 需要读取的文件就越多,从而需要解析转化的文件也越多,打包(开发时候打包看不到因为是在内存中)过程就越长,导致启动开发服务器所需的时间也就越长。
因为需要打包构建的项目并不一定总是跑在浏览器端的项目,而运行在不同端的项目构建工具处理的模块化可能也不同,因此,Webpack 考虑更多的是兼容性。
Vite 旨在利用生态系统中的新进展解决上述问题:浏览器开始原生支持 ES 模块。它是基于 ESModule 的,不允许 CommonJS 的代码,不需要去处理不同模块化的统一而遍历所有模块文件,而是按需加载的过程( index.html
通过esmodule 新特性可以直接去请求 main.js
,再通过 main.js
中引入的模块 Vite 再通过开发服务器去按需加载这些模块 ),以原生 ES 模块方式提供源码,这实际上时让浏览器接管了部分打包程序的工作,Vite 只需要在浏览器请求源码时进行转换并按需提供源码,因此能快速启动开发服务器。
总结:
关于更多为何选择 Vite 构建工具的原因,官网也给出了更详情的说明。
Vite 是一种新型前端构建工具,能够显著提升前端开发体验,主要由两部分组成:
Vite 是开箱即用(out of box
)的工具,不需要任何额外配置的情况下就可以帮你处理构建工作:
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vitetitle>
head>
<body>
<script src="./main.js" type="module">script>
body>
html>
// main.js
import {count} from './counter.js'
console.log('count', count)
// counter.js
export const count = 0
在没有使用构建工具的情况下如果想要直接运行 index.html
会因为 type="module"
给出跨域报错,具体原因及解决方法可以在 CommonJS,ES6 Module以及模块打包原理 一文中找到答案,在使用了插件以后打开 index.html
页面显示没有问题,这里不再赘述。
在一个 Vite 项目中,
index.html
在项目最外层而不是在public
文件夹内,是因为在开发期间 Vite 是一个服务器,这个index.html
是该 Vite 项目的入口文件。通过解析指向 JavaScript 源码已经
指向的 CSS 源码。
下面在项目中引入 npm 包
npm init -y
npm i lodash-es
// main.js
import {count} from './counter.js'
import _ from 'lodash-es'
console.log('count', count)
console.log('lodash', _)
依然使用查看运行打开 index.html
,浏览器报错提示:
报错原因在文章开头的例子中已经说过了,就是浏览器可不会主动去 node_modules
中加载资源,所以提示你要使用绝对路径或相对路径,下面通过引入 Vite 构建工具来解决这个问题:
// package.json
scripts: {
'dev': 'vite'
}
npm i vite -D
npm run dev
浏览器打开 http://127.0.0.1:5173/
那 Vite 是如何找到 lodash
的呢?我们先来看一下网络请求,看 main.js
到底是从哪里加载到的 lodash
这个库:
可以看出,Vite 在处理的过程中,对非绝对路径或相对路径的引用,进行了补全路径的操作,而为什么可以去 /node_modules/.vite/deps
路径下呢?另外,Vite 是基于 ESModule 的,但是我们不能限制别人的库是采用的哪种模块化导出方式,那么当遇到 CommonJS 的包时会怎么办呢?
这就是 Vite 执行所谓的“依赖预构建”,这一步由 esbuild (虽然 esbuild 快得惊人,但仍有一些重要功能例如代码分割和CSS处理等还在持续开发中,所以 Vite 打包还是会交给 Rollup 去做,因为它更灵活和成熟,但是不排除未来 esbuild 功能稳定后将其作为生产构建器的可能)执行,这也使得 Vite 的启动时间比任何基于 JavaScript 的打包器都要快得多,这个过程有两个目的:
node_modules/.vite/deps
目录中import
,最终都会被 Vite 集成为一个或几个文件的形式,以提高后续浏览器网络请求的页面加载性能针对上面第二点,通过下面这个例子进行说明,在引入 lodash-es
(ESModule 规范的 lodash
包)模块依赖预构建和不依赖预构建不同情况下的网络请求:
依赖预构建:
不依赖预构建(这也是原生 ESModule 不敢支持 node_modules
的原因):
是否排除依赖预构建某个包可以通过 vite.config.js
中配置。关于更多 vite.config.js
的配置可以查看 Vite 基本配置及原理 这一篇。
Vite 使用 Koa
或者 express
这种后端服务框架搭建了一个开发服务器,当我们执行 npm run dev
命令去启动这个开发服务器时,会提示我们访问 http://127.0.0.1:5173/ 这个服务地址打开项目,这就是我们的本地开发服务器,服务器启动后 node 服务器会去读取项目配置文件 vite.config.js
。
服务器端的路由对应的其实就是我们在浏览器端的请求地址,当我们请求 .html
、.js
、.vue
这些不同后缀名的文件时,Vite 这个开发服务器可以针对我们请求的路径对文件进行操作然后返回给前端,也就是浏览器。
而对于浏览器而言,任何后缀的文件都只是字符串而已,它只通过文件的 content-type
来判断使用哪种方式来读取这个文件,当访问的是 .html
文件时,设置 content-type: text/html
浏览器自然就会打开这个页面,这也就是为什么有时候我们打开一个下载链接时,浏览器会自动执行下载操作,也是因为后端返回给浏览器时设置了这个 content-type
值。
为什么 Vite 能让浏览器解析 .vue
文件就很好理解了,当请求 .vue
文件时,Vite 开发服务器会对里面的文件内容进行一个解析编译将其转化成 js 内容,并且在返回给浏览器时,设置了这个文件的 content-type: text/javascript
,所以浏览器在访问这个 .vue
文件时,即使后缀名是 .vue
,但其实浏览器不会看后缀名到底是什么,它只会按照 content-type
指定的类型也就是 JavaScript 脚本来执行它。