基于==ES-Module==的前端构建工具
在浏览器支持 ES 模块之前,JavaScript 并没有提供原生机制让开发者以模块化的方式进行开发。
当冷启动开发服务器时,基于打包器的方式启动必须优先抓取并构建你的整个应用,然后才能提供服务。
在 Vite 中,HMR 是在原生 ESM 上执行的。当编辑一个文件时,Vite 只需要精确地使已编辑的模块与其最近的 HMR 边界之间的链失活(大多数时候只是模块本身),使得无论应用大小如何,HMR 始终能保持快速更新。也就意味着,vite在启动的时候并不需要打包,也就意味着不用分析模块的依赖,也就是不用编译,因此,启动速度大大加快。按需编译。可以这样理解,vite就是对esbuild的进一步封装。
这样要从两者的机制来看。
js应用程序静态模块==打包器==。它会递归地构建一个模块关系图,然后将这些模块打包成一个包或者多个包。
可以看到,它的运行过程是一个==串行==过程(由于js是单线程的),要读取项目中的所有模块,然后打包,最后运行。
通过HTTP请求头来请求入口文件,模块中的import都是==动态引入==,即需要哪个,我最后就会引入哪个。
ESM称为实时绑定的规则,引入和导出的对象都指向同一个内存地址,也就是当值发生变化的时候,也会实时地发生变化。
但打包的过程,vite就没有webpack做的好。
所以vite也称为前端==构建==工具
前面一直所说构建工具,这里就来详细谈一下。
代码转换:将TypeScript编译成JavaScript、将SCSS编译成CSS等。
文件优化:==压缩==JavaScript、CSS、HTML代码,压缩合并图片等。
代码分割:提取多个页面的公共代码,提取首屏**不需要执行部分的代码让其异步加载**。
模块合并:在采用模块化的项目里会有很多个模块和文件,需要==通过构建功能将模块分类合并成一个文件==。
自动刷新:监听本地源代码的变化,自动重新构建、刷新浏览器。
代码校验:在代码被提交到仓库前需要校验代码==是否符合规范,以及单元测试是否通过==。
自动发布:更新代码后,自动构建出线上发布代码并传输给发布系统
pnpm create vite@latest
与webpack的差别
index.html放在根目录:官网的解释是vite是服务器,index.html是项目入口文件
vite.config.js替换vue.config.js,作为vite项目依赖文件
@vitejs/plugin-vue
提供支持vue3单文件组件
错误信息
Refused to apply style from ‘http://127.0.0.1:5500/assets/index-351bd726.css’ because its MIME type (‘text/html’) is not a supported stylesheet MIME type, and strict MIME checking is enabled.
引起此类报错的原因是==因为js、css等文件过多,需手动添加(./)获取当前文件路径,但js等文件过多手动添加路径==会引起冲突,所以直接在打包的配置文件中加上base:'./'
与common.js不同的是,es module模块是在编译阶段,效率比common.js高。
ESM 的==运行机制是,JS 引擎在对脚本静态分析的时候,遇到模块加载命令import, 会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值(也就是在编译阶段只是做一个标志)。 因此,ESM 是动态引用,并且不会缓存值==,模块里面的变量绑定其所在的模块。
首次执行 vite 时,服务启动后会对 node_modules 模块和配置 optimizeDeps 的目标进行预构建!
目的是为了兼容 CommonJS 和 UMD,以及提升性能。
兼容CommonJs UMD:因为vite的开发服务器将所有模块都视为esm,如果有CommonJs,需要将它转换成ESM.
性能:vite将有许多内置模块的依赖关系转换成单个模块,以便后续页面使用。一些模块将它们自己的文件相互导入,这样,当我们执行import的时候,执行的HTTP请求也会非常多。尽管服务器在处理这些请求时没有问题,但**大量的请求会在浏览器端造成网络堵塞**。导致页面的加载速度相当慢。通过预构建成为一个模块,我们就只需要一个HTTP请求了。
.vite/deps可以看预构建!!!
一个项目中,只有==裸模块==才会构建!
何为裸模块?
依赖预构建大致的流程是:
1、先收集打包的依赖,比如我在代码中引入了vue 那么匹配到vue后到node_modules中找到对应vue的esm模块js 2、然后把js都存储到一个对象中 (收集依赖的过程也用到了esbuild build方法,收集的过程是在一个esbuild插件中完成的)
3、得到一个所有依赖的对象后
4、再次用esbuild对这些依赖进行编译,
5、编译后的文件存储到.vite中 下次再使用依赖的时候直接从.vite中获取,不需要再次编译 预构建之后的产物代码,在 node_module 目录下的 .vite 文件夹 依赖预构建的产物会放在 它的deps 目录下
vite.config.js的配置:
server: {
open: false,
host: '127.0.0.1',
port: 3456,
proxy: {
'^/api/': {
target: 'https://www.bilibili.com/', // 后台服务器地址
changeOrigin: true, /* 允许跨域 */
rewrite: (path) => path.replace(/^\/api/, '')
}
}
}
开发环境:运行在本地的环境 npm run dev
生产环境:放到服务器上面的环境 npm run build
在项目根目录下(与package.json同级)新建配置文件 ----------
【1】 .env: 全局的,没有设置其他环境变量时,会加载这个文件里的内容, 比如所有版本都使用的是同一个接口地址时,就可以写在这一个文件里面就行
# .env
NODE_ENV = env
VITE_NAME='全局环境'
VITE_BASE_URL=''
【2】 .env.development: 开发环境下的配置文件,执行npm run dev命令,会自动加载.env.development文件 会覆盖.env这个文件里定义的环境变量
# .env.development
NODE_ENV = development
VITE_NAME='开发环境'
VITE_BASE_URL='/api'
【3】 .env.production: 生产环境下的配置文件,执行npm run build命令,会自动加载.env.production文件 会覆盖 .env这个文件里定义的环境变量
# .env.production
NODE_ENV = production
VITE_NAME='生产环境'
VITE_BASE_URL = 'http://xxxxxx/api'
".env.[name]"是可以自定义的,在package.json里面做对应的名称修改
根据Vite的约定规则,只有以“VITE_”开头的变量才会在客户端被捕获 捕获方式为:import.meta.env.{参数名},然后重新启动服务 执行 npm run dev 时候,vite自动去读取.env.development文件里面的配置 执行 npm run build 进行打包,vite自动将.env.production 的内容打包进去 --------------------
"scripts": {
"dev": "vite --mode development",
"build": "vite build --mode production",
"start": "vite --mode production",
"build:env": "vite build --mode development"
关于package.json
文件中的配置 dev 默认在本地开启测试环境的服务(mode=‘development’)
start 在本地开启正式环境服务 (mode=‘production’)
build 默认打包到正式环境(基础配置取.env.production 文件中内容)
build:env 默认打包到测试环境(基础配置取.env.development 文件中内容)
loadEnv 接收==三个参数==
mode:模式
envDir:环境变量配置文件所在目录
prefix:接受的环境变量前缀,默认为 VITE_ 在vite中默认是VITE_,为 ‘’,则加载所有环境变量
defineConfig 传入一个方法,方法可以接收一个对象,
对象中两个参数:
command, mode command :
server(run dev)
build(run build)
export default defineConfig(({ mode, command }) => {
const config = loadEnv(mode, './')
console.log(config);
return {
server: {
open: true,
host: '127.0.0.1',
port: 3456,
proxy: {
'^/api/': {
target: 'http://localhost:8080/', // 后台服务器地址
// target: config.VITE_TARGET,
changeOrigin: true, /* 允许跨域 */
rewrite: (path) => path.replace(/^\/api/, '')
}
}
},
base: './'
}
})
/*
*{
VITE_NAME: '开发环境',
VITE_BASE_URL: '/api',
VITE_USER_NODE_ENV: 'development'
}
/
然后就可以从config里面读取到配置文件的信息了。
打包后dist目录中index.html直接 live server,运行不ok
因为==不是一个服务器==,根本没有跨域
再package.json的scripts中,
添加"preview":"vite preview --mode development"
因为刚才执行打包命令为: build:env 默认打包到测试环境(基础配置取.env.development 文件中内容)
所以预览也添加 --mode development 然后,执行 npm run preview,打包文件执行跨域o
Vite 默认就支持对 css 代码的处理
可以通过.js文件中import引入css文件
也可以link标签导入css文件
CSS Module并不是css官方的标准,也不是浏览器的特性。而是通过一些构建工具(vite webpack)对它进行构建。对 CSS 类名和选择器的作用域进行限定的一种方式(类似命名空间)
命名规范为:css模块文件以.module.[css|less|scss]结尾
运行时的方案最典型的就是 BEM
它是通过 .block__element–modifier
这种==命名规范来实现的样式隔离, 不同的组件有不同的 blockName,只要按照这个规范来写 CSS,是能保证样式不冲突的==。 但是这种方案毕竟不是强制的,还是有样式冲突的隐患。
一类是==运行时的通过命名区分, 一类是编译时==的自动转换 CSS,添加上模块唯一标识。
编译时的方案有两种,一种是 scoped,一种是 css modules
scoped 是 vue-loader 支持的方案, 它是通过编译的方式在元素上添加了 data-xxx 的属性, 然后给 css 选择器加上[data-xxx] 的属性选择器的方式实现 css 的样式隔离
css-modules 在 vue、react 中都可以用, 它是**通过编译的方式修改选择器名字为全局唯一的方式来实现 css 的样式隔离**. CSS-Module引入的时候要以模块引入!!!
import.meta返回当前模块的信息,具体信息看看运行环境!
1、import.meta.url 返回当前模块的 URL 路径
2、import.meta.scriptElement 返回加载模块的那个
var()函数用于读取变量 还可以使用第二个参数,表示变量的默认值
定义变量:–变量名
全局变量
:root{
--m-bg:#f00;
}
div{
background:var(--m-bg)
}
resolve: {
alias: {
"@csssrc": 'a/b/c'
}
}
Vite 同时提供了对 .scss, .sass, .less, .styl 和 .stylus 文件的内置支持。 没有必要为它们安装特定的 Vite 插件,但必须安装相应的预处理器依赖
preprocessorOptions: {
scss: {
additionalData: `@import "./src/assets/inx.scss";`
}}
npm install -g sass
# .scss and .sass
npm add -D sass
# .less
npm add -D less
# .styl and .stylus
npm add -D stylus
1、Vite 支持 import.meta.glob
函数从文件系统导入多个模块 匹配到的文件默认是懒加载的,为动态导入, 通过遍历加 then 方法可拿到对应的模块文件详情信息 构建时,会分离为独立的 chunk 【使用 .then ,变成异步的了,会导致一些问题,此处不展开】
2、import.meta.globEager
直接引入所有的模块, 即静态 import 导入
==它们是 Vite 独有的功能==而不是一个 Web 或 ES 标准
const jsObj = import.meta.globEager("./assets/js/*")
console.log(jsObj);
使用
jsObj['./assets/js/1.js'].default()
jsObj['./assets/js/2.js'].default()
jsObj['./assets/js/3.js'].default()
const imgObj = import.meta.globEager('./assets/img/*')
console.log(imgObj);
const keys = Object.keys(imgObj)
for (let p of keys) {
images.value.push(imgObj[p].default)
}
// 如果我们只是想获取脚本的url,不想导入脚本,可以通过在导入路径后添加 后缀
//只是想获取脚本的url,不想导入脚本,添加?url后缀。
import jsUrl from './assets/js/a.js?url'
//import导入的图片会转换为一个路径
import imgsrc from './assets/vue.svg'
//?raw,以二进制的方式读取
import iconImg from './assets/images/icon.png?raw
Vite`插件是一个==拥有名称、创建钩子==(build hook)或 生成钩子(output generate hook)的对象
export default {
name: 'my-vite-plugin',
resolveId(id) {},
load(id) {},
transform(code) {}
}
如果插件需要==配置功能==,则是一个接受插件选项,返回插件函数的函数
eg:options是配置选项
export default function(options){
name:'my-vite-plugin',
resolveId(id){},
load(id){},
transform(code){}
}
开发时,vite dev server会创建一个钩子容器,按照Roll up调用==创建钩子函数的规则==来请求各个钩子函数。
- options:替换或操纵`rollup`选项
- buildStart:开始创建
- config: 修改Vite配置
- configResolved:Vite配置确认
- configureServer:用于配置dev server,可以进行中间件操作
- transformIndexHtml:用于转换宿主页
- handleHotUpdate:自定义HMR更新时调用
- resolveId:创建自定义确认函数,常用语定位第三方依赖(找到对应的文件)
- load:创建自定义加载函数,可用于返回自定义的内容(加载文件源码)
- transform:可用于转换已加载的模块内容(转变源码为需要的代码)
强制插件排序:enforce
pre:在 Vite 核心插件之前调用该插件
默认:在 Vite 核心插件之后调用该插件
post:在 Vite 构建插件之后调用该插件
plugins: [{enforce: 'pre'}
按需应用插件:apply
默认情况下插件在开发 (serve) 和生产 (build) 模式中都会调用。
apply 属性指明它们仅在 'build' 或 'serve' 模式时调用:
plugins: [{apply: 'build'}
export default function viteTransformCSSModulesPlugin() {
const name = 'vite-plugin-transform-css-modules';
return {
enforce: 'pre',
name,
// raw,文件里面的代码;id,文件的绝对路径
async transform(raw, id) { }
}
}
如果你的插件只适用于特定的框架:
vite-plugin-vue- 前缀作为 Vue 插件
vite-plugin-react- 前缀作为 React 插件
vite-plugin-svelte- 前缀作为 Svelte 插件
Vite 专属的插件:
Vite 插件应该有一个带 vite-plugin- 前缀、语义清晰的名称。
在 package.json 中包含 vite-plugin 关键字。
在插件文档增加一部分关于为什么本插件是一个 Vite 专属插件的详细说明
(如,本插件使用了 Vite 特有的插件钩子)。
处理==图片== CSS VUE组件
transform(code, id):在每个传入模块请求时被调用,主要是用来转换单个模块;
code 待转换的代码,就是导入的文件内容
id 当前正在解析文件的绝对路径 或者 由 load 返回的值
export default function () {
return {
name: 'vite-plugin',
transform(code, ids) {
// console.log(typeof ids);
if (ids.indexOf("1.png") > 0) {
console.log(code);
return `export default "/src/assets/3.png"`
}
// if (ids.indexof("1.png") > 0) {
// console.log(ids);
// console.log(code);
// }
}
}
}
浏览器发起请求以后,dev server 端会通过 middlewares 对请求做拦截,
然后对源文件做 resolve、load、transform 等操作,
然后再将转换以后的内容发送给浏览器。
resolveId 就是去找到对应的文件,输出本地的实际的路径,
load 输出是文件模块的代码字符串
加载本地文件到内存中
当然了你也可以在load里修改一下源码,
然后再传入到 transform
transform 就是将源码转变成目标代码
在这里进一步对源码进行操作
转换完成的内容直接返回给浏览器
它处理的是模块
【load 与 transform】:
共同点:
是它们都能修改内容;
区别是:
load,它处理的是文件
transform,它转换的是模块
所以在load中,可以直接写,<template>之类的代码
但在 transform 中,就要通过h渲染函数
load(id):
(ids.indexof(“1.png”) > 0) {
// console.log(ids);
// console.log(code);
// }
}
}
}
## 22.load钩子
```js
浏览器发起请求以后,dev server 端会通过 middlewares 对请求做拦截,
然后对源文件做 resolve、load、transform 等操作,
然后再将转换以后的内容发送给浏览器。
resolveId 就是去找到对应的文件,输出本地的实际的路径,
load 输出是文件模块的代码字符串
加载本地文件到内存中
当然了你也可以在load里修改一下源码,
然后再传入到 transform
transform 就是将源码转变成目标代码
在这里进一步对源码进行操作
转换完成的内容直接返回给浏览器
它处理的是模块
【load 与 transform】:
共同点:
是它们都能修改内容;
区别是:
load,它处理的是文件
transform,它转换的是模块
所以在load中,可以直接写,之类的代码
但在 transform 中,就要通过h渲染函数
load(id):
在==每个传入模块请求时被调用,可以自定义加载器,可用来返回自定义的内容 输出是文件模块的代码字符串,默认就是直接读取文件内容并返回==