Webpack五以及其他打包工具
webpack5
模块联邦
Webpack5 模块联邦让 Webpack 达到了线上 Runtime 的效果,让代码直接在项目间利用 CDN 直接共享,不再需要本地安装 Npm 包、构建再发布了
Webpack 可以通过 DLL 或者 Externals 做代码共享时 Common Chunk,但不同应用和项目间这个任务就变得困难了,我们几乎无法在项目之间做到按需热插拔。
常见共享模块的场景
UMD 模块
微前端:多个项目共存于一个页面,有点类似iframe,共享的对象是项目级的,页面级的
子应用间的chunk以及对象可通过全局事件共享,但是公共包在项目安置以及打包编译很难放
webpack的方案是直接将一个应用的 bundle,应用于另一个应用,动态分发 runtime 子模块给其他应用。
使用方式,先设置本模块可以被导入
1 | const HtmlWebpackPlugin = require("html-webpack-plugin"); |
导入使用
1 | import { Search } from "app_two/Search"; |
模块联邦本身是一个普通的 Webpack 插件 ModuleFederationPlugin
,插件有几个重要参数:
name
当前应用名称,需要全局唯一。remotes
可以将其他项目的name
映射到当前项目中。exposes
表示导出的模块,只有在此申明的模块才可以作为远程依赖被使用。shared
是非常重要的参数,制定了这个参数,可以让远程加载的模块对应依赖改为使用本地项目的 React 或 ReactDOM。
构建速度优化
缓存
通常我们会借助一些其他的配置优化缓存
- cache-loader,针对一些耗时的工作进行缓存。比如缓存babel-loader的工作。
- terser-webpack-plugin 或 uglifyjs-webpack-plugin的cache以及parallel。terserPlugin继承自uglifyjsPlugin,我们可以开启插件的cache以及parallel特性来加快压缩。(默认开启)
1 | // 对babel-loader的工作进行缓存 |
而在webpack5中可以使用cache 特性来将webpack工作缓存到硬盘中。存放的路径为node_modules/.cache/webpack
1 | module.exports = { |
持久化缓存
在日常开发中我们会尽量减少文件hash发生变化的情况,以最大化的利用缓存,节省流量。这就是我们常说的“优化持久化缓存”。首先最简单的措施就是使用contenthash来作为文件哈希后缀,只有当文件内容发生变化的时候,哈希才会发生改变。
在webpack4中,当我们新增一个模块或者一个入口,所有文件的哈希后缀都发生了改变,但这不符合期望,原有文件hash不应发生变化。
在webpack4 中,chunkId与moduleId都是自增id。也就是只要我们新增一个模块,那么代码中module的数量就会发生变化,从而导致moduleId发生变化,于是文件内容就发生了变化。chunkId也是如此,新增一个入口的时候,chunk数量的变化造成了chunkId的变化,导致了文件内容变化。
webpack4可以通过设置optimization.moduleIds = ‘hashed’与optimization.namedChunks=true来解决这些问题,但都有性能损耗等副作用。
而webpack5 在production模式下optimization.chunkIds和optimization.moduleIds默认会设为’deterministic’,webpack会采用新的算法来计算确定性的chunkI和moduleId。
tree shaking优化
1: 对于模块引入嵌套场景,如嵌套后未引用则可以shaking掉
1 | // inner.js |
2.内部模块。Webpack 4 不会去分析导入和导出模块之间的依赖关系,Webpack5 里面会通过 optimization.innerGraph
记录依赖关系。比如下面这个例子,只有 test
方法使用了 someting
。最终可以实现标记更多没有使用的导出项
1 | import { something } from "./something"; |
3.commonjs tree shaking支持。Webpack不仅仅支持 ES module 的 tree Shaking,commonjs规范的模块开始支持了
包体积优化
在webpack5之前,webpack会自动的帮我们项目引入Node全局模块polyfill。我们可以通过node配置
但是webpack团队认为,现在大多数工具包多是为前端用途而编写的,所以不再自动引入polyfill。我们需要自行判断是否需要引入polyfill,当我们用weback5打包的时候,webpack会给提示
其他特性
- Webpack 生成的代码不再仅仅是ES5,也会生成 ES6 的代码
- Node.js 的最小支持版本从 6 升级到了 10
- 新的 Web 平台支持。在Webpack 5 里面开始原生支持 JSON Modules、Asset Modules、Native Worker 和 异步模块等等
js底层原理
js执行过程
JavaScript是一种解释型语言,执行时按照解释器-编译器-执行的顺序进行。
解释器:
JavaScript的解释器包含在即时编译器(JIT,just in time)中,在一句一句编译源代码的同时又会将编译后的一些代码保存下来。
即时编译器中首先有监视器监控着代码的运行状况。如果一段代码运行了几次,那么这段代码就会被标记为warm代码,标记为warm的代码会被编译器送到基线编译器编译,同时把结果缓存起来,如果一个函数被基线编译器编译,那么函数的每一行都会被编译成一个表,这个表的索引是行号和变量类型,如果监视器发现有代码使用相同的变量类型命中这段代码时,JIT会直接提取这段代码编译后的内容。
如果标记为warm的代码还在不断被调用,那么它会被标记为hot,标记为hot的代码,意味着可能会被经常调用,所以可能需要更多时间优化编译结果。监视器会把该段代码送到优化编译器,编译成一个高效的版本并存储下来。
编译:
parse阶段-
词法分析和语法分析:词法分析是把需要执行的代码字符串分割出来,生成一系列token,便于做语法分析
语法分析分析上述输出,输出AST抽象语法树,如果分析发现异常则抛出错误。
Ignition:Ignition是v8的解释器,它会根据抽象语法树生成对应的字节码并执行。
所谓字节码,就是机器码的抽象,机器码虽然运行效率最高,但是占用空间较大,导致十分占内存,所以以时间换空间,引入字节码
Turbofan:Turbofan能够把字节码翻译成机器码,当发现hot代码时,turbofan会把字节码翻译成机器码,然后调用生成的机器码。如果发现假设失效,则退回字节码
Orinoco:Orinoco是v8的垃圾回收模块
执行:
执行阶段有执行上下文和执行栈两个概念
执行上下文就是JavaScript的运行环境,通常有
全局执行上下文,即window对象
函数执行上下文,即函数执行的时候被创建,每次调用函数就会创建一个新的执行上下文
eval执行上下文
对于每个执行上下文创建时,都有三个比较重要的属性:
变量对象:表示执行上下文的数据作用域,
作用域链
确定this指向:
执行栈:
js通过执行栈管理多个执行上下文,执行栈最底端是全局上下文,全局上下文中执行函数的时候,相应上下文会入栈,其中不断调用函数,不同的函数不断创建不同的执行上下文,直到函数执行完毕,上下文出栈,即后进先出
如果有异步函数,在执行栈的空的时候查看任务队列,微任务优先入栈。
AST的应用
Es-lint就是使用ast查询器查询AST,AST选择器类似CSS选择器,每个ESlint规则都是通过选择器操作的
Babel:Babel对于一段代码的工作流程是:
1.输入代码 2.词法分析,将代码分成token 3.语法分析:把token转换成AST
4.遍历AST 5.改变AST,增删改查。 6.AST转换为源代码
webpack:webpack主要通过遍历AST分析出模块的依赖、消除无用代码等
Gulp
Gulp基于Node.js的前端构建工具,通过Gulp的插件可以实现前端代码的编译(sass、less)、压缩、测试;图片的压缩;浏览器自动刷新,还有许多强大的插件可以在这里查找。
安装
1 | npm install gulp -g |
Gulp Api
使用gulp,仅需知道4个API即可:gulp.task()
,gulp.src()
,gulp.dest()
,gulp.watch()
gulp.src()
方法正是用来获取流的,但要注意这个流里的内容不是原始的文件流,而是一个虚拟文件对象流(Vinyl files),这个虚拟文件对象中存储着原始文件的路径、文件名、内容等信息
gulp.dest()
方法是用来写文件的,其语法为:
gulp的使用流程一般是这样子的:首先通过gulp.src()
方法获取到我们想要处理的文件流,然后把文件流通过pipe方法导入到gulp的插件中,最后把经过插件处理后的流再通过pipe方法导入到gulp.dest()
中,gulp.dest()
方法则把流中的内容写入到文件中,
gulp.task
方法用来定义任务,内部使用的是Orchestrator,其语法为:
gulp.watch()
用来监视文件的变化,当文件发生变化后,我们可以利用它来执行相应的任务,例如文件压缩等。其语法为
常用插件
安装gulp插件
CSS
- sass的编译(gulp-ruby-sass)
- less的编译(gulp-less)
- 自动添加css前缀(gulp-autoprefixer)
- 压缩css(gulp-minify-css)
js
- js代码校验(gulp-jshint)
- 合并js文件(gulp-concat)
- 压缩js代码(gulp-uglify)
html
- 压缩html(gulp-minify-html)
图片、资源
- 压缩图片(gulp-imagemin)
- 图片缓存,只有图片替换了才压缩(gulp-cache)
其他
- 自动加载插件(gulp-load-plugins)
- 自动刷新页面(gulp-livereload)
- 更改提醒(gulp-notify)
- 重命名(gulp-rename)
- 清除文件(del)
安装这些插件
1 | npm install gulp-ruby-sass gulp-autoprefixer gulp-minify-css gulp-jshint gulp-concat gulp-uglify gulp-imagemin gulp-notify gulp-rename gulp-livereload gulp-cache del --save-dev |
创建一个gulpfile.js在根目录下,然后在里面加载插件:
1 | var gulp = require('gulp'), |
创建任务
1 | // Load plugins |
其他插件
打包前拷贝到dist目录,之后所有的操作基于dist目录
1 | const { series, src, dest } = require('gulp'); |
给打包文件加hash
1 | npm i gulp-dev -D |
在gulpfile。js中添加以下代码
1 | const rev = require('gulp-rev'); |
替换文件中的源文件
1 | npm i gulp-rev-collector -D |
在gulpfilejs中加入代码
1 | const revCollector = require('gulp-rev-collector'); |
打包zip文件
1 | npm i gulp-zip -D |
在gulpfilejs中加入代码
1 | const zip = require('gulp-zip') |
动态处理打包环境,有prod和dev两种
1 | npm i cross-env -D |
在packagejson中添加命令
1 | { |
根据不同环境执行不同的打包方式,如开发环境不需要压缩。
1 | if (process.env.NODE_ENV === 'production') { |
gulp与webpack、grant的区别
与webpack
gulp严格上讲,模块化不是他强调的东西,他旨在规范前端开发流程。
webpack更是明显强调模块化开发,而那些文件压缩合并、预处理等功能,不过是他附带的功能。
gulp应该与grunt比较,而webpack应该与browserify
gulp与webpack上是互补的,还是可替换的,取决于你项目的需求。如果只是个vue或react的单页应用,webpack也就够用;如果webpack某些功能使用起来麻烦甚至没有(雪碧图就没有),那就可以结合gulp一起用。
与grunt
Grunt主要是以文件为媒介来运行它的工作流的,比如在Grunt中执行完一项任务后,会把结果写入到一个临时文件中,然后可以在这个临时文件内容的基础上执行其它任务,执行完成后又把结果写入到临时文件中,然后又以这个为基础继续执行其它任务…就这样反复下去。而在Gulp中,使用的是Nodejs中的stream(流),首先获取到需要的stream,然后可以通过stream的pipe()
方法把流导入到你想要的地方,比如Gulp的插件中,经过插件处理后的流又可以继续导入到其他插件中,当然也可以把流写入到文件中。所以Gulp是以stream为媒介的,它不需要频繁的生成临时文件,这也是Gulp的速度比Grunt快的一个原因。
https://markpop.github.io/2014/09/17/Gulp%E5%85%A5%E9%97%A8%E6%95%99%E7%A8%8B/
https://www.cnblogs.com/2050/p/4198792.html
Rollup
应用场景:
项目(特别是类库)只有 js,而没有其他的静态资源文件,使用 webpack 就有点大才小用了,因为 webpack bundle 文件的体积略大,运行略慢,可读性略低。这时候 rollup就是一种不错的解决方案
Rollup 的好处
- Tree Shaking: 自动移除未使用的代码, 输出更小的文件
- Scope Hoisting: 所有模块构建在一个函数内, 执行效率更高
- Config 文件支持通过 ESM 模块格式书写
- 可以一次输出多种格式:IIFE, AMD, CJS, UMD, ESM
- Development 与 production 版本: .js, .min.js
- 文档精简
基础插件
- rollup-plugin-alias: 提供 modules 名称的 alias 和 reslove 功能.
- rollup-plugin-babel: 提供 Babel 能力, 需要安装和配置 Babel (这部分知识不在本文涉及)
- rollup-plugin-eslint: 提供 ESLint 能力, 需要安装和配置 ESLint (这部分知识不在本文涉及)
- rollup-plugin-node-resolve: 解析 node_modules 中的模块
- rollup-plugin-commonjs: 转换 CJS -> ESM, 通常配合上面一个插件使用
- rollup-plugin-replace: 类比 Webpack 的 DefinePlugin , 可在源码中通过 process.env.NODE_ENV 用于构建区分 Development 与 Production 环境.
- rollup-plugin-filesize: 显示 bundle 文件大小
- rollup-plugin-uglify: 压缩 bundle 文件
- rollup-plugin-serve: 类比 webpack-dev-server, 提供静态服务器能力
- Rollup-plugin-vue: 解析vue文件
- rollup-plugin-json:可以让rollup从json文件中读取数据
Grunt
安装cli工具
1 | npm install -g grunt-cli |
安装grunt及用到的插件
1 | npm install gulp grunt-contrib-uglify --save-dev |
创建Gruntfile.js
文件
1 | module.exports = function(grunt) { |
大部分的Gruntfile.js都非常臃肿,有一大段一大段的嵌套配置结构,令人看起来很烦躁。这些冗余的Gruntfile.js
文件其实维护起来并不是那么轻量的,主要体现在下面几个方面,
- 配置和运行分离
- 大部分插件的职责不单一,做了不仅一件事
- 配置项过多,做的事情越多,配置增长的就越快,且越不可控制
- 任务执行中会产生一些临时文件,较频繁的IO操作导致性能滞后
我们再来看看gulp中是如何解决这些问题的,
- gulp遵循code over configuration的原则,直接就在调用的地方配置
- gulp的插件严格遵循单一职责原则,一个插件仅作一件事,一个gulp一般只有20多行代码就搞定
- gulp基于流的思想,并没有过多的配置,任务更多的像是在写代码而不是定义配置
- 这个是grunt的致命伤,gulp的流式构建改变了底层的流程控制,不再频繁的去进行IO操作了
Parcel
ESbuild
esbuild是新一代的Javascript打包工具。
esbuild以速度快著称,耗时只有webpack的2%到3%。很多工具都内置了esbuild,比如vite和snowpack
esbuild快的原因:
1.使用Go语言编写,可以编译为原生代码
2.解析、打印和源映射完全并行化
3.无须昂贵的数据转换,只需很少的几步完成
目前支持:
commonjs模块、es6模块
使用–bundle与es6模块的静态绑定打包
使用–minify完全压缩(空格、标识符和修饰符)
使用–sourcemap完全支持源映射
支持jsx转向js
使用–define进行编译时标识符替换
使用package.json中的browser字段进行替换
自动监测tsconfig中的baseUrl
安装es-build
1 | npm install esbuild |
安装完可以检查一下es build的版本
1 | ./node_modules/.bin/esbuild --version |
执行打包命令
1 | ./node_modules/.bin/esbuild app.jsx --bundle --outfile=out.js |
wasm版本
1 | npm install esbuild-wasm |
在deno中使用
1 | import * as esbuild from 'https://deno.land/x/esbuild@v0.14.22/mod.js' |
Vite
冷启动
在浏览器支持 ES 模块之前,JavaScript 并没有提供的原生机制让开发者以模块化的方式进行开发。这也正是我们对 “打包” 这个概念熟悉的原因:使用工具抓取、处理并将我们的源码模块串联成可以在浏览器中运行的文件。
时过境迁,我们见证了诸如 webpack、Rollup 和 Parcel 等工具的变迁,它们极大地改善了前端开发者的开发体验。
然而,当我们开始构建越来越大型的应用时,需要处理的 JavaScript 代码量也呈指数级增长。包含数千个模块的大型项目相当普遍。我们开始遇到性能瓶颈 —— 使用 JavaScript 开发的工具通常需要很长时间(甚至是几分钟!)才能启动开发服务器,即使使用 HMR,文件修改后的效果也需要几秒钟才能在浏览器中反映出来。如此循环往复,迟钝的反馈会极大地影响开发者的开发效率和幸福感。
当冷启动开发服务器时,基于打包器的方式启动必须优先抓取并构建你的整个应用,然后才能提供服务。
Vite 通过在一开始将应用中的模块区分为 依赖 和 源码 两类,改进了开发服务器启动时间。
依赖 大多为在开发时不会变动的纯 JavaScript。一些较大的依赖(例如有上百个模块的组件库)处理的代价也很高。依赖也通常会存在多种模块化格式(例如 ESM 或者 CommonJS)。
Vite 将会使用 esbuild 预构建依赖。Esbuild 使用 Go 编写,并且比以 JavaScript 编写的打包器预构建依赖快 10-100 倍。
源码 通常包含一些并非直接是 JavaScript 的文件,需要转换(例如 JSX,CSS 或者 Vue/Svelte 组件),时常会被编辑。同时,并不是所有的源码都需要同时被加载(例如基于路由拆分的代码模块)。
Vite 以 原生 ESM 方式提供源码。这实际上是让浏览器接管了打包程序的部分工作:Vite 只需要在浏览器请求源码时进行转换并按需提供源码。根据情景动态导入代码,即只在当前屏幕上实际使用时才会被处理。
更新
基于打包器启动时,重建整个包的效率很低。原因显而易见:因为这样更新速度会随着应用体积增长而直线下降。
一些打包器的开发服务器将构建内容存入内存,这样它们只需要在文件更改时使模块图的一部分失活[1],但它也仍需要整个重新构建并重载页面。这样代价很高,并且重新加载页面会消除应用的当前状态,所以打包器支持了动态模块热重载(HMR):允许一个模块 “热替换” 它自己,而不会影响页面其余部分。这大大改进了开发体验 —— 然而,在实践中我们发现,即使采用了 HMR 模式,其热更新速度也会随着应用规模的增长而显著下降。
在 Vite 中,HMR 是在原生 ESM 上执行的。当编辑一个文件时,Vite 只需要精确地使已编辑的模块与其最近的 HMR 边界之间的链失活[1](大多数时候只是模块本身),使得无论应用大小如何,HMR 始终能保持快速更新。
Vite 同时利用 HTTP 头来加速整个页面的重新加载(再次让浏览器为我们做更多事情):源码模块的请求会根据 304 Not Modified
进行协商缓存,而依赖模块请求则会通过 Cache-Control: max-age=31536000,immutable
进行强缓存,因此一旦被缓存它们将不需要再次请求。
打包
尽管原生 ESM 现在得到了广泛支持,但由于嵌套导入会导致额外的网络往返,在生产环境中发布未打包的 ESM 仍然效率低下(即使使用 HTTP/2)。为了在生产环境中获得最佳的加载性能,最好还是将代码进行 tree-shaking、懒加载和 chunk 分割(以获得更好的缓存)。
虽然 esbuild
快得惊人,并且已经是一个在构建库方面比较出色的工具,但一些针对构建 应用 的重要功能仍然还在持续开发中 —— 特别是代码分割和 CSS 处理方面。就目前来说,Rollup 在应用打包方面更加成熟和灵活。尽管如此,当未来这些功能稳定后,我们也不排除使用 esbuild
作为生产构建器的可能。
wp2vite
安装
1 | npm install -g wp2vite |
命令行使用
1 | cd your_workspace/your_project |
然后启动项目就行
1 | // 安装依赖 |
snowpack
WebRTC
webpack学习资源
wepack:https://www.kancloud.cn/sllyli/webpack/1242354