前端工程化的学习(偏向vite构建工具)

lxf2023-03-13 13:33:01

前端工程化的学习(偏向vite构建工具)

好早就听说了vite,也早就简单的使用并了解了一点,之前在公司实习团队也正在迁移webpack的项目到vite,但我自己却一直没有深入,毕竟还是初级前端工程师,功力还欠缺很多,但最近封装了一个小组件,整个项目不使用脚手架挺难受的,到处参考别人的代码希望能找到组件开发的最佳实践,整个过程举步维艰,所以开始先从vite入手学习一下前端工程化相关的东西了...

为什么需要构建工具

摘抄一段vite官网对打包的描述:

使用工具抓取、处理并将我们的源码模块串联成可以在浏览器中运行的文件

现阶段我们基本都不会直接编写可以浏览器上运行的文件,更多的是使用各种新的框架(Vue/React)、语法(TypeScript/less/sass),用这些工具编写出来的代码时不能直接在浏览器上直接运行的,我们需要每次都手动使用不同的解释器/编译器去将用高级语法编写的代码转换为能在浏览器中运行的代码: 前端工程化的学习(偏向vite构建工具) 所以简单理解构建工具(打包工具)要做的就是这样一件事:将这条工具链内置,面向开发者透明,避免开发者每次查看效果都要重复机械化地输入不同的命令,除此之外,构建工具还可以使用各种优化工具优化最终生成的文件。 一般来说,一个构建工具会有以下功能:

  • 模块化支持:兼容多种模块化规范写法,支持从node_modules中引入代码(浏览器本身只识别路径方式的模块导入,imoprt { forEach } from 'loadsh'这样直接以名字导入需构建工具识别)
  • 框架编译/语法转换:如:tsc->lessc->vueComplier
  • 构建产物性能优化:文件打包、代码压缩、code splitting、tree shaking...
  • 开发体验优化:hot module replacement、跨域解决等...
  • ... 总的来说,构建工具让我们开发人员可以更加关注代码的编写,而非代码的运行。

五花八门的构建工具

市面上常见的构建工具有如下(这里简单说一下各种构建工具的特点,具体展开就太多了,大家感兴趣可以直接去官网看看):

  • grunt:基于配置驱动的,开发者需要做的就是了解各种插件的功能,然后把配置整合到 Gruntfile.js 中,然后就可以自动处理一些需要反复重复的任务,例如代码压缩、编译、单元测试、linting等工作,配置复杂度较高且IO操作较多。
  • gulp:Gulp最大特点是引入了流的概念,同时提供了一系列常用的插件去处理流,流可以在插件之间传递。这使得它本身被设计的非常简单,但却拥有强大的功能,既可以单独完成构建,也可以和其他工具搭配使用
  • webpack:最主流的打包构建工具,兼容覆盖基本所有场景,前端工程化的核心,但相应带来的缺点就是配置繁琐
  • rollup:由于webpack配置繁琐,对于小型项目开发者较不友好,他们更倾向于rollup。其配置简单,易于上手,成为了目前最流行的JS库打包工具
  • esbuild:使用go语言并大量使用了其高并发的特性,速度极快。不过目前Esbuild还很年轻,没有达到1.0版本,并且其打包构建与Rollup类似更关注于JS本身,所以并不适合单独使用在前端项目的生产环境之中
  • parcel:...
  • ...
  • vite:开发环境基于esmodule规范按需加载,速度极快,具有极佳的开发体验,生产环境底层调用rollup,接下来主要介绍webpack与vite之间的一个对比。

其实官网介绍vite的优势已经非常详细了,我自身也没有额外的理解,这里就直接摘要一段官网的话:

当我们开始构建越来越大型的应用时,需要处理的 JavaScript 代码量也呈指数级增长。包含数千个模块的大型项目相当普遍。基于 JavaScript 开发的工具就会开始遇到性能瓶颈,Vite 以 原生 ESM 方式提供源码。这实际上是让浏览器接管了打包程序的部分工作:Vite 只需要在浏览器请求源码时进行转换并按需提供源码。根据情景动态导入代码,即只在当前屏幕上实际使用时才会被处理。

再贴两张大家可能已经很熟的对比图: 前端工程化的学习(偏向vite构建工具) 相信大家看了上述官网的摘要差不多已经明白为什么vite在开发环境下启动速度非常快的原因了,主要就是使用了浏览器原生支持的esmodule规范,当然还少不了vite本身在这之上做的一些优化,比如依赖预构建 我在一篇文章中看到过这样一个问题:这个思路既然能解决开发启动速度上的问题,为什么webpack不能支持呢? 答:

  • webpack的设计理念就是大而全,它需要兼容不同的模块化,我们的工程既有可能跑在浏览器端,也有可能跑在服务端,所以webpack会将不同的模块化规范转换为独有的一个函数webpack_require进行处理,为了做到这一点,它必须一开始就要统一编译转换模块化代码,也就意味着它需要将所有的依赖全部读取一遍;
  • 而我们在使用vite项目的时候,就只能使用esmodule规范,但项目的依赖仍然可能使用了不同的模块规范,vite会在依赖预构建中处理这一步,将依赖树转换为单个模块并缓存在/node_modules/.vite下方便浏览器按需加载,将打包的部分工作交给了浏览器执行,优化了开发体验。而构建交给了rollup同样会兼容各种模块化规范...

总结:webpack更多的关注兼容性,而vite关注浏览器端的开发体验,侧重点不一样

vite处理细节

自身对前端工程化的理解也比较浅,从vite官网文档中可以学到不少前端工程化相关的知识,知识点总结至vite官网,快速入口

1. 导入路径补全

在处理的过程中如果说看到了有非绝对路径或者相对路径的引用, 他则会尝试开启路径补全:

import _ from "lodash"  // 补全前,浏览器并不认识这种裸模块导入
import _ from "/node_modules/.vite/lodash"; // 补全后,使用依赖预构建处理后的结果

2. 依赖预构建

主要就是为了解网络多包传输的性能问题,官网原话:

一些包将它们的 ES 模块构建作为许多单独的文件相互导入。例如,lodash-es 有超过 600 个内置模块!当我们执行 import { debounce } from 'lodash-es' 时,浏览器同时发出 600 多个 HTTP 请求!尽管服务器在处理这些请求时没有问题,但大量的请求会在浏览器端造成网络拥塞,导致页面的加载速度相当慢。

通过预构建 lodash-es 成为一个模块,我们就只需要一个 HTTP 请求了!

重写前:

// a.js 
export default function a() {}
​
export { default as a  } from "./a.js"

vite重写以后:

function a() {}

顺便解决了以下两个问题:

  • 不同的第三方包会有不同的导出格式,Vite 会将作为 CommonJS 或 UMD 发布的依赖项转换为 ESM
  • 对路径的处理上可以直接使用.vite/deps, 方便路径重写

其他:构建这一步由 esbuild 执行,这使得 Vite 的冷启动时间比任何基于 JavaScript 的打包器都要快得多

注意:这里都是指的开发环境,生产环境会交给rollup去执行

3. vite与ts

vite他天生就对ts支持非常良好, 因为vite在开发时态是基于esbuild, 而esbuild是天生支持对ts文件的转换的快速入口

4. 环境变量

一个产品可能要经过如下环境:

  1. 开发环境

  2. 测试环境

  3. 预发布环境

  4. 灰度环境

  5. 生产环境 不同的环境使用的数据应该是隔离的,或者是经过处理的,比如小流量环境,很显然,不同环境在一些密钥上的设置上是不同的,环境变量在这时候就尤为重要了,vite中内置了dotenv对环境变量进行处理: dotenv会自动读取.env文件, 并解析这个文件中的对应环境变量 并将其注入到process对象下(但是vite考虑到和其他配置的一些冲突问题, 他不会直接注入到process对象下) 配置:

    .env # 所有情况下都会加载 .env.local # 所有情况下都会加载,但会被 git 忽略 .env.[mode] # 只在指定模式下加载 .env.[mode].local # 只在指定模式下加载,但会被 git 忽略 然后文件里使用VITE前缀的命名变量VITE_SOME_KEY=123,可以在vite.config.ts中配置envPrefix: "ENV_"修改这个前缀 使用:

console.log(import.meta.env.VITE_SOME_KEY) // 123
console.log(import.meta.env.DB_PASSWORD) // undefined

其他:为什么vite.config.js可以书写成esmodule的形式(vite明明是运行在服务端的), 这是因为vite他在读取这个vite.config.js的时候会率先node去解析文件语法, 如果发现你是esmodule规范会直接将你的esmodule规范进行替换变成commonjs规范

5. vite对css的处理

基本流程:1. vite在读取到main.js中引用到了Index.css

  1. 直接去使用fs模块去读取index.css中文件内容
  2. 直接创建一个style标签, 将index.css中文件内容直接copy进style标签里
  3. 将style标签插入到index.html的head中
  4. 将该css文件中的内容直接替换为js脚本(方便热更新或者css模块化), 同时设置Content-Type为js 从而让浏览器以JS脚本的形式来执行该css后缀的文件 前端工程化的学习(偏向vite构建工具)

处理重复类名

前端工程化的学习(偏向vite构建工具) 前端工程化的学习(偏向vite构建工具) 全部都是基于node

  1. module.css (module是一种约定, 表示需要开启css模块化)
  2. 他会将你的所有类名进行一定规则的替换(将footer 替换成 _footer_i22st_1)
  3. 同时创建一个映射对象{ footer: "_footer_i22st_1" }
  4. 将替换过后的内容塞进style标签里然后放入到head标签中 (能够读到index.html的文件内容)
  5. 将componentA.module.css内容进行全部抹除, 替换成JS脚本
  6. 将创建的映射对象在脚本中进行默认导出

config参考

快速入口

// 摘自https://github.com/passerecho/vite-
    css: { // 对css的行为进行配置
        // modules配置最终会丢给postcss modules
        modules: { // 是对css模块化的默认行为进行覆盖
            localsConvention: "camelCaseOnly", // 修改生成的配置对象的key的展示形式(驼峰还是中划线形式)
            scopeBehaviour: "local", // 配置当前的模块化行为是模块化还是全局化 (有hash就是开启了模块化的一个标志, 因为他可以保证产生不同的hash值来控制我们的样式类名不被覆盖)
            // generateScopedName: "[name]_[local]_[hash:5]" // https://github.com/webpack/loader-utils#interpolatename
            // generateScopedName: (name, filename, css) => {
            //     // name -> 代表的是你此刻css文件中的类名
            //     // filename -> 是你当前css文件的绝对路径
            //     // css -> 给的就是你当前样式
            //     console.log("name", name, "filename", filename, "css", css); // 这一行会输出在哪??? 输出在node
            //     // 配置成函数以后, 返回值就决定了他最终显示的类型
            //     return `${name}_${Math.random().toString(36).substr(3, 8) }`;
            // }
            hashPrefix: "hello", // 生成hash会根据你的类名 + 一些其他的字符串(文件名 + 他内部随机生成一个字符串)去进行生成, 如果你想要你生成hash更加的独特一点, 你可以配置hashPrefix, 你配置的这个字符串会参与到最终的hash生成, (hash: 只要你的字符串有一个字不一样, 那么生成的hash就完全不一样, 但是只要你的字符串完全一样, 生成的hash就会一样)
            globalModulePaths: ["./componentB.module.css"], // 代表你不想参与到css模块化的路径
        },
        preprocessorOptions: { // key + config key代表预处理器的名
            less: { // 整个的配置对象都会最终给到less的执行参数(全局参数)中去
                // 在webpack里就给less-loader去配置就好了
                math: "always",
                globalVars: { // 全局变量
                    mainColor: "red",
                }
            },
        },
        devSourcemap: true,
    },

6. 静态资源

服务时引入一个静态资源会返回解析后的公共路径:

import imgUrl from './img.png'
document.getElementById('hero-img').src = imgUrl

例如,imgUrl 在开发时会是 /img.png,在生产构建后会是 /assets/img.2d8efhg.png

行为类似于 Webpack 的 file-loader。区别在于导入既可以使用绝对公共路径(基于开发期间的项目根路径),也可以使用相对路径。

为什么要使用hash

浏览器是有一个缓存机制 静态资源名字只要不改, 那么他就会直接用缓存的 刷新页面--> 请求的名字是不是同一个 --> 读取缓存 --> 所以我们要尽量去避免名字一致(每次开发完新代码并构建打包时)

1. 显式 URL 引入

未被包含在内部列表或 assetsInclude 中的资源,可以使用 ?url 后缀显式导入为一个 URL。这十分有用,例如,要导入 Houdini Paint Worklets 时:

import workletURL from 'extra-scalloped-border/worklet.js?url'
CSS.paintWorklet.addModule(workletURL)

2. 将资源引入为字符串

资源可以使用 ?raw 后缀声明作为字符串引入。

import shaderString from './shader.glsl?raw'

比如svg文件如果我们以url的方式导入文件,则相当于导入一张图片,只能对其进行图片的相关操作,如果我们想要对其进行svg相关的操作,我们则需要使用?raw的方式导入:

import svgIcon from "./assets/svgs/fullScreen.svg?url"; // 这种是以图片的方式加载svg,无其他特殊操作
import svgRaw from "./assets/svgs/fullScreen.svg?raw"; // 加载svg的源文件,这种方式的加载可以做到修改svg的颜色等操作console.log("svgIcon", svgIcon, svgRaw);
document.body.innerHTML = svgRaw;
​
const svgElement = document.getElementsByTagName("svg")[0];
​
svgElement.onmouseenter = function() {
    // 不是去改他的background 也不是color
    // 而是fill属性
    this.style.fill = "red";
}
  
// 第一种使用svg的方式
// const img = document.createElement("img");
// img.src = svgIcon;// document.body.appendChild(img);
// 第二种加载svg的方式

3. 导入脚本作为 Worker

脚本可以通过 ?worker 或 ?sharedworker 后缀导入为 web worker。

// 在生产构建中将会分离出 chunk
import Worker from './shader.js?worker'
const worker = new Worker()
// sharedworker
import SharedWorker from './shader.js?sharedworker'
const sharedWorker = new SharedWorker()
// 内联为 base64 字符串
import InlineWorker from './shader.js?worker&inline'

快速入口

性能优化

  1. 代码逻辑上的优化,如:

    1. 使用lodash工具中的防抖、节流而非自己编写;数组数据量大时,也可以使用lodash中的forEach方法等等
    2. for(let i = 0; i < arr.length; i++){}替换为for(let i = 0, len = arr.length; i < len; i++)这样只用通过作用域链获取一次父作用域中的arr变量
    3. ...
  2. 构建优化(构建工具关注的事):体积优化->压缩、treeshaking、图片资源压缩、cdn加载、分包...

  3. ...

其中分包知识我第一次接触到,这里记录一下: 主要是为了配合浏览器中的缓存策略

  • 假设这样一个场景,我们使用lodash中的forEach函数编写了console('1'),最终打包后的代码如果不分包则会将lodash中的相关实现和console('1')合并为一个文件传给浏览器;
  • 而我们的业务代码经常变化,比如console('1')-->console('2')这时候我们仍然需要将lodash中的相关实现和console('1')合并为一个文件传给浏览器;
  • 但显然lodash中的代码实现并没有更改,浏览器直接使用以前的就可以了
  • 所以分包就是把一些不会经常更新的文件,进行单独打包处理为一个文件,配置参考 前端工程化的学习(偏向vite构建工具)

最后

前端工程化我也是最近开始学习,如有理解错误希望各位大佬不吝赐教

参考文章

  • cn.vitejs.dev/
  • segmentfault.com/a/119000004…
  • Admin.net/post/708561…
  • github.com/passerecho/…
  • css-tricks.com/comparing-t…
  • Admin.net/post/708561…