新晋打包工具
- 新晋打包工具
- 前端模块工具的发展历程
- 分类
- 初版构建工具
- grunt
- 使用场景
- gulp
- 采用管道机制
- 任务化配置与api简洁
- 现代打包构建工具基石--webpack
- 基于webpack改进的构建工具
- rollup 推荐
- 举例说明
- package.json
- rollup.config.mjs
- my-extract-css-rollup-plugin.mjs
- src/index.js
- src/utils.js
- src/index.css
- src/utils.css
- build/cjs.js
- build/esm.js
- build/umd.js
- build/index.css
- build/666.css
- 使用场景
- 不适用场景
- Parcel 不推荐
- 举例说明
- parcel/index.html
- parcel/index.js
- parcel/App.js
- parcel/index.css
- package.json
- 使用场景
- 突破JS语言特性的构建工具
- SWC 推荐使用 √ - 平替babel
- jsc-parser语法解析相关配置
- jsc-target 输出代码的es版本
- 典型配置案例
- ESbuild - 作为工具去使用的
- 基于ES Module的bundleless(no bundle)构建工具 => vite
- 基于bundle的解决方案
- vite - 重点掌握
- vite原理
- 为什么vite之前没有,到2021年后才有这样的开发链路呢?
- vite插件
- package.json
- vite.config.js
- 自定义插件 -plugins/myPlugin.js
- vite插件的相关钩子
- 通用钩子
- rspack - 推荐尝试使用
- 示例:通过 rsbuild 创建一个工程
- turpopack 国外的
新晋打包工具
构建为了将工程化的思想和自动化的思想应用在前端的工程链路中
前端模块工具的发展历程
- 09年,commonJS:指定浏览器外js的相关 api 规范, nodejs 就采用了这样的规范
- 11年,requireJS:作为客户端模块加载器,提供了异步加载模块的能力,之后就变成了 AMD 的规范
- 13年,grunt,gulp 诞生。
- 14年,UMD,统一模块定义,跨平台的前后端兼容
- 14年,6to5,ES6 语法 => ES5,经历了 词法分析,语法分析,AST => new AST => generator code。这也就是 babel 的能力
- 14年,system is 简化模块加载工具
- 14年,webpack,第一个稳定版本的
- 15年,ES6 规范正式发布的
- 15年,rollup 基于ES6模块化,并且提供 tree shaking相关能力
- 17年,Parcel,零配置,内部集成配置,能力进行收口,parcel,index.html
=> 做平台,开发基础能力,具备插件化机制 - 19年,构建工具深水区,不再使用js语言卷了,使用go,rust语言来卷。由于JS是高级语言,使用 babel 会经历各种AST转换
snowpack,使用rust语言,天生支持多线程能力 - 20年,浏览器对 ESM,http2 支持,使得 bundless 思路开始出现,esbuild 进入到大众视野中
- 21年,vite诞生
分类
- 初版构建工具
- 现代打包构建工具基石 webpack
- 突破JS语言特性的构建工具
- esmodule 的 bundless 构建工具
初版构建工具
grunt
最早的构建工具,构建工具的鼻祖
基于 nodejs 来开发的,借助nodejs实现跨系统,跨平台的操作文件系统
自动化的配置工具集,像官方所说的是一种 Task Runner,是基于任务的,整体配置json,由JSON配置设置驱动的。
基于 grunt 可以进行JS语法监测,或者合并一些JS文件,合并后的文件压缩,以及将我们预处理的sass,less文件进行编译
配置驱动、插件化、任务链
'use strict'
module.exports = function (grunt) {//构建的初始化配置grunt.initConfig({/*配置具体任务 */pkg: grunt.file.readIsON('package.json'),dirs: {src: 'path',dest: 'dest/<%= pkg.name >/<%= pkg.version 名>'},// clean任务(删除dest/test_grunt/0.0.1 目录下非min的文件)clean: {js: ['<%= dirs.dest &>/*.js', '!<%= dirs.dest %>/*.min.js'],css: ['<%= dirs,dest %>/*.css', '!<%= dirs.dest 名>/*.min.css'],},// copy任务(拷贝path目录下的文件到dest目录)copy: {main: {files: [// includes files within path{expand: true,src: ['path/*'],dest: '<%= dirs.dest %>/',filter: 'isFile',},],},},//concat任务(将dest目录下的a.js和b.js合并为built.js)concat: {options: {separator: '\n',},concatCss: {src: ['<%= dirs,dest &>/a.css', '<%= dirs.dest &>/path/b.css'],dest: '<%= dirs.dest %>/built.css',},concatJs: {src: ['<%= dirs,dest &>/a.js', '<%= dirs.dest &>/b.js'],dest: '<%= dirs.dest %>/built.is'}},// cssmin任务(压缩css)cssmin: {target: {files: [{expand: true,cwd: '<%= dirs.dest %>',src: ['*.css', '!*.min.css'],dest: '<%= dirs.dest %>',ext: '.min.css'}]},},// uglify任务(压缩js)uglify: {options: {mangle: {except: ['jQuery', 'Backbone'],},},my_target: {files: {'<%= dirs.dest %>/bulit.min.js': ['<%= dirs.dest %>/*.js']},},},})// 载入要使用的插件grunt.loadNpmTasks('grunt-contrib-clean')grunt.loadNpmTasks('grunt-contrib-copy')grunt.loadNpmTasks('grunt-contrib-concat')grunt.loadNpmTasks('grunt-contrib-cssmin')grunt.loadNpmTasks('grunt-contrib-uglify')//注册刚配置好的任务grunt.registerTask('cls', ['clean'])grunt.registerTask('cpy', ['copy'])grunt.registerTask('con', ['concat'])grunt.registerTask('cmpCSS', ['cssmin'])grunt.registerTask('cmpJS', ['uglify'])grunt.registerTask('default', ['copy', 'concat', 'cssmin', 'uglify', 'clean'])
}
缺点:
针对 文件处理模式
- grunt 任务,基于磁盘文件操作,先读取 => 再处理 => 后写入
效率是非常低下的
grunt.initConfig({uglify: {files:{'dest/output.min.js': ['src/input1.js','src/input2.js']}}
})
读取 less => 编译 css => 写入磁盘 => 读取 css => 压缩处理 => 写入磁盘
使用场景
- 传统项目维护 已经是使用grunt来处理
- 简单任务自动化 使用grunt也足够了
gulp
基于 nodejs 的流式前端构建工具。特点:代码驱动任务,高效流处理,基于task驱动
完成 测试,检查,合并,压缩 能力
采用管道机制
采用管道pipe机制
处理文件,所有操作在内存中处理,基于内存流的,避免频繁io操作
在管道 pipe 中 =>使用 less 插件=>转成 css =>使用 minicss 插件压缩css => 写入磁盘,由于是在内存中完成的,因此效率提升
任务化配置与api简洁
gulp.task('css',()=>gulp.src('./src/css/**').pipe(cssmin()).pipe(gulp.dest('./dist/css'))
)
插件生态庞大,包含文件压缩,语法编译等
基于流式的高效性和插件驱动的灵活性
var gulp = require('gulp')
var pug = require('gulp-pug')
var less = require('gulp-less')
var minifyCss = require('gulp-csso')gulp.task('html',function(){return gulp.src('client/templates/*.pug').pipe(pug()).pipe(gulp.dest('build/html'))
})
gulp.task('css',function(){return gulp.src('client/templates/*.less').pipe(less()).pipe(minifycss()).pipe(gulp.dest('build/css'))
})gulp.task('default', ['html''css'])
现代打包构建工具基石–webpack
上篇文章中已说到了,这里就不再赘述了。
特性:基于各种各样配置,包含loader对文件进行编译处理,webpack内容当中,所有内容皆为模块,需要转译成JS模块,需要使用不同的loader进行处理,另外,还有插件的能力,webpack基于事件流的,集成自 tapable 的,学会开发自定义插件,了解compiler,complation 各自的有哪些钩子,并且钩子能做哪些事情,落地一些插件才行
基于webpack改进的构建工具
rollup 推荐
vue2,vue3,react,babel等,源码层面上,都是使用 rollup 做构建工具的
专注于 js 模块打包的工具
特点:高效性,轻量性,一般都是在前端 Library 基础类库,工具函数等打包,打包出来的效果要优于webpack的,体积也要优于webpack。
对于基础类库/工具函数库需要被其他函数库引用,像引入 vue2,vue3,react。针对他们的诉求肯定是越小越好,没有用到的相关特性就不要打包进来了,所以 tree shaking 能力是必备的,能够对当前代码进行静态分析,esModule的导入导出,没有用到的功能(deadcode )就会精准剔除
-
高效 tree shaking 能力
-
减小包体积,避免冗余依赖,适用于按需加载的场景
-
支持输出 ESM commonjs AMD IIFE UMD模块格式,满足不同环境需求
配置时候也比较简单,只需要在配置文件中进行如下操作:rollup index.js -f cjs -o bundle.cjs.js #输出 CommonJS格式
-
轻量化代码输出
几乎不添加额外代码
打包仅包含一些必要的函数,辅助代码 -
强大的插件生态,vite线上发布使用rollup进行打包的,vite扩展了rollup插件生态,包含代码转换,依赖解析,压缩等场景
-
@rollup/plugin-babel
-
@rollup/plugin-terser 压缩代码
-
@rollup/plugin-commonjs,将commonjs => ESM
很多相关的插件
针对 rollup 有插件,没有loader,但是也能对 非 js 文件进行处理,有扩展的能力
- transform 对代码进行转换
- 语法转换
- 添加额外功能
- 等等
因此在开发插件的时候,需要重点关注 transform 方法
举例说明
pnpm init
package.json
- “rollup-plugin-cleaner”:“^1.0.0”, —— 清除当前目录下的dist文件的
- “rollup-plugin-cleanup”:“^3.2.1”, —— 清除代码注释,删除无效的console等等
- “rollup-plugin-postcss”:“^4.0.2” —— 针对css文件的插件
{"name": "about-builder","version": "1.0.0","description": "","main": "index.js","scripts": {"test": "echo \"Error: no test specified\" && exit 1","build":"npx rollup -c rollup.config.mjs --watch"},"keywords": [],"author": "","license": "ISC","dependencies":{"parcel": "^2.13.0","react":"^18.3.1","react-dom":"^18.3.1","rollup":"^4.27.4","rollup-plugin-cleaner":"^1.0.0", "rollup-plugin-cleanup":"^3.2.1","rollup-plugin-postcss":"^4.0.2"}, "devDependencies":{"process":"^0.11.10"}
}
rollup.config.mjs
使用rollup的话,就需要提供这样的一个配置文件
import postcss from "rollup-plugin-postcss"
import cleanup from "rollup-plugin-cleanup"
import cleaner from "rollup-plugin-cleaner"
import myExtractCssRollupPlugin from "./my-extract-css-rollup-plugin.mjs"/** @type {import("rollup").RollupOptions} */
export default {input: 'src/index.js',output: [{file: 'build/esm.js',format: 'esm'},{file: 'build/cjs.js',format: 'cjs' //指定当前模块规范},{file: 'build/umd.js',name: 'Echo',format: 'umd'}],plugins: [cleaner({targets: ['dist',"build"], //需清理的目录silent: false, //显示操作日志watch: true, //监听模式exclude: ['README.md'], //保留特定文件}),// 代码清理cleanup({comments: false,sourcemap: false,targets: ['build/*']}),// 处理css,将css内容从js文件中提取出来postcss({extract: true,extract: 'index.css'}),// 自定义插件myExtractCssRollupPlugin({filename: '666.css'})]
}
my-extract-css-rollup-plugin.mjs
/** 为什么 rollup 没有 loader 呢?* 因为 rollup 的 plugin 有 transform 方法,也就相当于 loader 的功能了。* Rollup 打包过程中对模块的代码进行转换操作
*/const extractArr=[]export default function myExtractCssRollupPlugin(opts) {return {name: 'my-extract-css-rollup-plugin',transform(code, id) {//在这里对代码进行转换操作if (!id.endsWith('.css')) {return null}// 将后缀为css的文件内容收集起来extractArr.push(code)return {// 转换后的代码code: '',// 可选的源映射信息,如果需要生成源映射的话map: { mappings: '' }}},//此方法在Rollup生成最终的输出文件之前被调用generateBundle(options, bundle) {this.emitFile({fileName: opts.filename,type:"asset",source:extractArr.join('/* #echo# */\n')})}}
}
src/index.js
import { add } from './utils.js'
// rollup 默认开启 tree shaking
import './index.css'function main() {console.log(add(1, 3))
}export default main
src/utils.js
import './utils.css'function add(a, b) {return a + b;
}export { add };
src/index.css
body{background: skyblue;
}
src/utils.css
.bbb{background: red;
}
执行
pnpm run build
得到:
build/cjs.js
'use strict';function add(a, b) {return a + b;
}function main() {console.log(add(1, 3));
}module.exports = main;
build/esm.js
function add(a, b) {return a + b;
}function main() {console.log(add(1, 3));
}export { main as default };
build/umd.js
(function (global, factory) {typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :typeof define === 'function' && define.amd ? define(factory) :(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Echo = factory());
})(this, (function () { 'use strict';function add(a, b) {return a + b;}function main() {console.log(add(1, 3));}return main;}));
build/index.css
.bbb{background: red;
}
body{background: skyblue;
}
build/666.css
export default undefined;/* #echo# */
export default undefined;
使用场景
- 开发 js 库,工具函数
- 需要 tree shaking 优化的项目
- 生成环境打包 vite
不适用场景
- 依赖非 js 资源 非常多
Parcel 不推荐
- 完全零配置
- 构建速度快
parcel 官网
举例说明
还是在上面的 about-builder 包下,使用 React 框架来写案例
parcel/index.html
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title><link rel="stylesheet" href="index.css">
</head>
<body><div id="app"></div><script type="module" src="./index.js"></script>
</body>
</html>
parcel/index.js
import { createRoot } from 'react-dom/client'
import App from './App.js'const container = document.getElementById('app')
const root = createRoot(container)
root.render(<App />)
parcel/App.js
export function App() {return <h1>Hello World!</h1>
}
parcel/index.css
body{background-color: skyblue;
}
package.json
去掉 main 那一行,也就是:"main": "index.js"
这个内容
执行:
npx parcel parcel/index.html
文件夹多了一个dist和.parcel-cache
页面:
热更新也是比较友好的
使用场景
适用小型项目
突破JS语言特性的构建工具
非 JS 语言相关的构建工具
SWC 推荐使用 √ - 平替babel
- speedy web Compiler 快速web编译器
=> Compiler + bundler (编译+构建 所组成的)
=> bundler 有一定的缺陷,推荐使用 Compiler 编译 能力
=> 强调 快速 的能力,使用rust 语言
实现的,使用多线程
-
简历中做一些优化,针对 webpack 做一些常规的优化,像进行分包,还有像通过引入cache提升构建速度,像leo-plugins等方式,只是针对webpack本身所作的优化,但是现在
webpack+babel
已经具备了性能瓶颈
=> 优化措施:webpack+swc
babel 对标 => swc
babel-loader => swc-loader -
文件比较多,使用 babel-loader 的话,需要经历 翻译、ast 是比较耗时的
使用swc的话,性能会得到质的飞跃 -
swc官方网站
-
性能表现原因:
- rust 语言编写,编译时确定运行的行为,不像js是解释执行,解释成机器语言再执行机器语言。rust 是多线程的这样的一个能力
-
功能覆盖
SWC 主要对 js 代码快速转换
,核心将es6+代码转换成 es5或者其他代码
,在这过程中会进行代码压缩优化
等相关的一些操作,比如swc能很好的处理箭头函数
,模板字符串
,解构赋值等es6+特性
的转换,还有针对ts语言
,tsx语言
等语言的处理,成熟度也是可以的 -
使用:简单转换代码
@swc/core @swc/clinpx swc source.js -o dist.js
const start = () => {console.log('app started') } // 转为 var start = function (){console.log('app started') }
jsc-parser语法解析相关配置
使用 swc-loader 时候,需要着重注意 JSC 相关配置
swc-loader
- JSC (javascript Compiler)
配置项:
options:{//jsc相关能力配置"jsc":{//当前需要转义哪些语言"parser":{//指定当前语言类型"syntax": "typescript", //ecmascript"tsx": true, //是否编译tsx"dynamicImport": true //是否支持动态导入}}
}
jsc-target 输出代码的es版本
配置对应的target
接着上面写:
options:{//jsc相关能力配置"jsc":{//当前需要转义哪些语言"parser":{//指定当前语言类型"syntax": "typescript", //ecmascript"tsx": true, //是否编译tsx"dynamicImport": true //是否支持动态导入}//配置对应target"target": "es2015" //输出代码的es版本"transform":{ //代码转换"react":{"runtime":"automatic"},//启动代码优化"optimizer":{"simplify": true //简化}}}
}
典型配置案例
.swcrc 配置文件
{"jsc": {"parser": {"syntax": "typescript","tsx": true,"decorators": true,},"transform":{ //代码转换"react":{"runtime":"automatic"}},"target": "es2018",//是否需要辅助函数"externalHelpers": true,"baseUrl": ".","paths": {"@/*": ["src/*"]}},"minify": true //进行代码压缩
}
也可以自己写一些插件
import{ readFilesync } from 'fs'
import { transform } from '@swc/core'const run = async () => {const code = readFileSync('./source.js','utf-8')const result= await transform(code,{filename:'source.js',})//·输出编译后代码console.log(result.code)
}
run()
ESbuild - 作为工具去使用的
vite 在开发环境下,使用 esbuild 预构建依赖
由于并发处理包的构建是非常快的,因此才会使用,而JS本质是解释型语言,执行代码的时候需要一边将源码翻译成机器语言,一边调度执行。
-
go编写程序,是编译型语言,少了动态解释过程
-
多线程
go语言具备多线程能力,将所有的包都进行深度开发,因为JS是单线程
,虽然也引入了webworker
做一些多线程的事情,但是还是有一些限制,比如说,go的多个线程之间是可以共享当前进程的内存空间
,但是JS的webworker是不能共享进程内存空间的
,如果想要数据共享
的话,需要通过postmessage
进行通信,但是这样的话,效率也比较低下的。因此,这也是JS的限制
=> 更高效的利用内存使用率 => 达到了更高的运行性能 -
全量定制
比如,webpack中会用到babel实现ES5的版本转义
,使用ESlint代码检查
,使用tsc完成typescript代码转义,检查
,使用less,scss
等,这些使用插件去实现的。
但是,esbuild中完全去重写,整套流程,工具都是重写的,意味着对这些文件的资源 tsx,jsx,js,ts等加载解析代码的生成逻辑,内部都会进行定制化开发,相对来说,成本也是非常高的,实现出来后,对编译的各个阶段都达到了非常好的性能。如果不去继续兼容webpack的loader,依然可能会达到不好的效果。
webpack尤其针对 babel 的代码编译,会频繁的经历 string => AST => AST => string =>AST => string 这样的阶段,因此,esbuild重写了大多数转译工具
,能够尽量共用相似的AST转换,减少AST结构的转换,进而提升内存利用率
-
ESbuild 特性
(1)极快的速度,无需缓存
(2)支持 ES6 commonjs 模块
(3)ES6 tree shaking
(4)API 可以同时用于 js 和 go
(5)兼容 ts,jsx语法
(6)支持plugins
这也是为什么 vite 使用 esbuild 作为包的转换
ESbuild官网
同时拷贝10个 three.js 库的扩展
基于ES Module的bundleless(no bundle)构建工具 => vite
http2 支持 多路复用 并发限制很大 10 50 100
浏览器 esm
基于bundle的解决方案
bundle based => entry 入口进行分析,分析当前的依赖内容,调用了哪些模块,对应的loader对当前进行处理 => modules,递归的完成这些模块的依赖分析 =>最终形成bundle => 启动 devServer 给到浏览器,然后浏览器去进行渲染
vite - 重点掌握
vite原理
而nobundle的思想:
本地启动一个服务,执行vite相关内容,会创建一个服务,启动devServer(本地请求资源服务),还有 websocket 两个服务(主要用于hm热更新)
no-bundle核心的两个特性:预构建、按需加载
使用按需加载的简单的vue3项目:
- 加载html,html中引入了main.js
还会引入 @vite/client,实现热更新
- 加载client资源(热更新)
监听消息
handleMessage方法:
在websocket中能看到payload.type,connect是建联,update是更新操作,等等。
先是建联:
更改 页面文字:
websocket会有update更新
类型是 js-update的话,会调用队列:
最终会发起 App.vue请求
App.vue请求会带着时间戳,不会复用之前的,避免了缓存的影响,就会拿到更改之后的数据替换之前的内容
-
加载main.js,引入了vue.js,style.css,等
-
加载vue.js,style.css等,比如,style.css使用css插件做处理,创建style标签用在header当中
为什么vite之前没有,到2021年后才有这样的开发链路呢?
- http2.x 支持,多路复用
之前webpack不拆包,将所有的都打包到一个bundle当中,热更新重新走整个链路的流程,最终形成bundle,然后再更新这个bundle,会受体积影响
现在都是使用websocket,支持单文件的热更新
多路复用
:
http1.0 会对单个域名有tcp请求的限制,限制 6-8 tcp请求链接的数量,因此,将多个文件合并到一个文件当中进行处理,避免限制对有些请求发送不出去
http2.x 有多路复用,同一个域名下对请求并发限制很大,10个,50个,100个同时请求服务器下的多个资源 - 浏览器支持 esm
webpack时候还不支持 esm 这样的一个特性,需要经历编译这一层
现在可以在浏览器中通过"import xxx"去加载到对应的资源内容
vite插件
使用 vite 创建 vue3 项目:
pnpm create vite my-vue3-app
使用vite构造的vue3项目:
package.json
这三个快捷指令
vite.config.js
内部集成了常见模块的插件,针对css等不需要单独额外处理
都是基于rollup插件去扩展的
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// 自定义插件
import myVitePlugin from './plugins/myPlugin'// https://vitejs.dev/config/
export default defineConfig({plugins: [vue(), myVitePlugin()],test: {environment: 'jsdom',coverage: {reporter: ['text', 'json', 'html'],// 设置覆盖文件夹reportsDirectory: './coverage',// 检查每个文件的阈值perFile: true,// 设置代码覆盖率阈值lines: 75,functions: 75,branches: 75,statements: 75}}
})
自定义插件 -plugins/myPlugin.js
在工程当中,打印当前工程版本号
import path from 'path'
import fs from 'fs'//控制台打印当前工程版本号
export default function myVitePlugin() {let version, configreturn {name: 'my-vite-plugin',configResolved(resolvedConfig) {config = resolvedConfigconst pkgPath = path.resolve(config.root, 'package.json')const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))version = pkg.version},buildStart() {console.log('当前工程版本号:1.0.0')},transform(code, id) {if (id.endsWith('main.js')) {const info = `console.log('当前工程版本号:${version}')`return `${code}\n${info}\n`}}}
}
vite插件的相关钩子
- config 解析vite相关配置时候
- configResolved 解析配置之后的钩子
- configuerserver 配置开发服务器
- handlehotupdate 执行热更新时候的钩子
通用钩子
- options
- buildstart 开始创建
- transform 每个模块传入请求时调用
- buildend 构建结束
rspack - 推荐尝试使用
基于 rust 语言,实现的高性能前端构建工具
特性:兼容webpack生态
完全从webpack配置快速迁移到 rust 的技术体系当中,在构建速度上得到了显著的提升
rspack 官网
示例:通过 rsbuild 创建一个工程
pnpm create rsbuild@latest
类似 vite:
rsbuild 与 webpack区别:
- 语言优势,rust 语言编译时会转为机器码,少了解释执行的过程
- 多线程
rsbuild 与 vite 的区别: - vite 在生产环境依赖 rollup,在开发环境使用 esbuild+热更新,no-bundle按需下载的思想
turpopack 国外的
相对来说使用的比较少
基于 rust
turpopack 官网
由 Vercel 赞助的
vercel
可以一键去部署自己的项目,无需写git-action的配置,已经内置了这样的能力,做了CI/CD