gulp

流式工程化 pipeline

1
2
3
4
5
var gulp = require('gulp');
gulp.src('xxx.js') // 获取流的目标
.pipe( // 途径过程编排
gulp.dest('dist/xxx.js'); // 流目的地
)

src()

获取到想要处理的流文件

1
2
3
4
5
6
7
8
9
10
11
gulp.src(globs[, options]); // => stream 并非原本文件流,虚拟文件对象流 => 减少IO次数

// 1. globs 文件匹配模式
// 使用数组的方式匹配文件种类
gulp.src(['./js/*.js', './css/*.css'])

// 2. options 处理文件的配置项
// options.buffer - boolean 返回buffer,使用场景大文件 => false时,返回一个抽象的stream而非文件本身
// options.read - boolean文件内容是否返回 => 判断文件内容是否读取返回,file.content返回值
// options.base - 文件基础路径
gulp.src('./main/js/*.js', { base: 'main' })
  1. base 路径问题
  • 默认会以匹配模式中最左边的通配符之前的路径作为 base,并保留之前的目录结构
1
2
3
4
5
6
7
8
9
10
11
const gulp = require("gulp");

// base 路径是 src,最左边的通配符 * 之前的路径是 src,所以,src/js 目录下的所有 .js 文件会被复制到 dist/js 目录中,最终产物的路径会保留 src 之后的目录结构
gulp.task("default", function () {
return gulp.src("src/js/*.js").pipe(gulp.dest("dist"));
});

// base 路径是 src/js,那么 src/js 目录下的所有 .js 文件会被直接复制到 dist 目录中,而不会保留 src/js 这一层目录结构
gulp.task("default", function () {
return gulp.src("src/js/*.js", { base: "src/js" }).pipe(gulp.dest("dist"));
});
  1. 通配符分类
  • *: 匹配任意数量的任意字符,但不包括路径分隔符 /
  • **: 匹配任意数量的任意字符,包括路径分隔符 /,可以用于递归匹配
  • ?: 匹配单个任意字符

pipe()

把文件流通过 pipe 形式导入到 gulp 的通路中,途径插件

1
2
3
4
gulp
.src("./main/js/*.js", { base: "main" })
.pipe(minify())
.pipe(gulp.dest("build"));

dest()

用于将处理后的文件写入到指定的目标路径
文件产出的路径是 gulp.dest(path) 指定的目录与 base 之后的目录结构的组合

1
2
3
4
gulp.dest(path[, options])
// 1. path - 写入文件的路径
// 2. options可选参数
// options.mode - string 0777 所在目录的权限

watch()

监视文件变化,用以触发相应编排流程的开展

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
gulp.watch(glob[, opts], tasks);
// glob - 监视文件的匹配模式
// tasks - 文件变化后要执行的任务

// 创建任务
gulp.task('minify', function() {
// 任务内容
console.log('main minify');
})
gulp.task('uglify', function() {
// 任务内容
console.log('main uglify');
})

// < 4.0版本
gulp.watch('./page/**/*.js', ['uglify', 'minify']);

// > 4.0版本
gulp.watch(
'./page/**/*.js'
gulp.parallel(
['uglify', 'minify']
)
)

task()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
 gulp.task(name[, deps], fn)
// name - 任务名
// deps - 当前任务所依赖的前置 => 当前任务会在所有依赖的指向完成后,被调用执行
// fn - 执行函数


// 批量执行/打包任务
gulp.task('js', function() {
console.log('js');
})
gulp.task('es', function() {
console.log('es');
})
gulp.task('webpack', function() {
console.log('webpack');
})

gulp.task('main', gulp.parallel(
['js', 'es', 'webpack']
))

// 异步任务不会等待
gulp.task('async', funtion() {
setTimout(function() {
console.log('main async')
}, 1000)
})

webpack / gulp

  1. 执行粒度上
  • webpack: 以功能依赖模块为粒度,区分 loaders 或 plugins
  • gulp: 以任务为配置粒度,配置是将依赖、工具、功能抽象组装成任务,编排成任务流
  1. 执行顺序上
  • webpack: 以文件类型负责区域来网状构造项目,弱化顺序项目复杂程度
  • gulp: 流式的方式串联每一项工具依赖,更适合作为自动化工具链来使用

工程化

webpack 升级

v3 => v4

  1. 零配置
  • 不强制依赖 webpack.config.js => 默认项: entry: ‘./src/index.js & output: ‘./dist/main.js’
  • wepack-cli 分离安装
  1. 状态上
  • 提供 mode 区分 development & production,提升开发编译效率,专注于编译产品
  1. scope hoisting
  • 作用域提升: 分析模块之间的依赖关系,将所有模块的代码按照顺序放在一个函数作用域中,减少函数闭包的数量,从而减小打包后的文件体积,提高代码的执行效率
  • 开启方式: Webpack 4 开始,默认开启,但需要使用 ES6 模块语法
  1. tree shaking
  • 摇树优化: 在打包过程中静态分析模块之间的导入和导出关系,找出那些没有被引用的代码 (死代码),并将其从打包结果中移除,从而减小打包后的文件体积
  • 开启方式: ES6 模块语法 + production 自动开启
  1. 细节配置差异
  • commonChunkPlugin => splitChunks
  • loaders => rules
  • css loader 增加 use
  • css-loader => style-loader => miniCssExtract.loader

v4 优化

对产品打包以及开发编译的速度做出了优化

v4 => v5

  1. 持久化缓存: 阶段性编译结果存放如持久化缓存磁盘区域
  • v4 需要使用插件,hard-source-webpack-plugin
  • development,cache.type 默认被设置为 ‘memory’;production,默认开启,且 cache.type 通常建议设置为 ‘filesystem’
1
2
3
4
5
module.exports = {
cache: {
type: "fileSystem",
},
};
  1. 资源模块: 通过规整统一处理资源文件,优化资源文件引入的方式,直接与文件目标结合处理
  • old: raw-loader、url-loader、file-loader
  • new: asset/resource => file-loader、asset/inline => url-loader、asset/source => raw-loader
  1. 打包优化
  • tree-shaking: 跨模块 shaking
  • splitchunk: 精细化配置
  1. 代码压缩

    自带 js 压缩功能,v4 需要下载安装 terser-webpack-plugin 插件

v5 优化

进一步优化配置,优化编译速度以及包大小

webpack 插件

缓存加速

  • cache-loader,缓存耗时的工作
  • terser-webpack-plugin / uglifyjs-webpack-loguin 的 cache 以及 parrallel

减肥瘦身

  • imagemin-webpack-plugin 批量压缩图片
  • purifycss-wepack,删除未使用的 css 代码
  • optimize-css-assets-webpack-plugin, 压缩 css

插件

  • cleanWebpackPlugin,打包前清理上次项目生成的 bundle 文件
  • happypack…

vite

  • 冷启动 / 冷服务: 开发状态下不重新走编译打包
  • 热更新: 更新源文件时依旧可以实时更新视图
  • 按需更新: 不刷新所有节点,只更新改动部分

vs webpack

webpack - 编译支撑开发

  • 打包生成 bundle => 启动 dev-server => 建立开发环境
  • HMR => 改动到的模块以及相关依赖重新打包编译

vite - 路由劫持 + 实时编译

  • 启动 dev-server => 直接请求所需模块并且实时编译(rollup js 模块打包器 AMD CJS => ESM)
  • HMR => 让浏览器重新请求当前页面所需模块 => 利用浏览器的强缓存和协商缓存优化请求(源码模块协商缓存,依赖模块用强缓存)

总结

  1. 省去打包生成 bundle 过程
  2. 通过 rollup 对模块进行按需实时编译
  3. 利用缓存机制对不同模块进行缓存优化

dev / prod

dev: esbuilder

  • 依赖预构建 cjs / AMD => ESM
  • 依赖缓存到 node_modules/.vite
  • package.json / lockfile / vite.config.js => 三者之一触发 => 重新预构建
  • 通过浏览器的缓存机制 => 缓存请求处理 => 提升页面性能

prod: rollup

特性

  • 原生的 ts 支持
1
<script lang="ts"></script>
  • 原生的文件引入支持
1
2
import "./src/assets/reset.css";
import "./src/assets/mock.json";
  • 新增依赖支持,vite.config.js

原理实践

vite 缓存控制依赖模块,浏览器缓存控制静态资源、业务代码、js 文件等

  1. ESM 静态定义,编译时做加载 => 生成只读引用
  2. 路由脚本进入只引用需要被加载模块 from 只读引用 => 实时编译运行包含依赖的代码
  3. 浏览器缓存优化 => 分模块按需加载,未变化直接用缓存 / 模块变化只更新模块

主流构建工具

底层 JS/TS 转译器

纯粹用于将 TypeScript/JavaScript/JSX 编译到某种特定运行环境下的底层转译器,代表有 BabelO(Js)、TSC(Ts)、esbuild(Go) 和 SWC(Rust) 等。虽然我将它们归类为转译器,但是它们大都也支持打包的能力,比如 esbuild 就把自己定位为打包器。

上层打包器

通常不会具备转译能力,而是借助上面提到的这些转译器来实现转移能力。它们更专注于完成一些范围更广、更加具体的打包任务。代表有 Webpack、Rollup、Parcel、esbuild、Snowpack、Vite、wmr、microbundle、tsup、tsdx、tsup 等。

dev 环境

  1. 通过监听源代码变化然后重新构建项目将打包后的代码推送到浏览器的传统模式
  • Webpack+Babel
  • Rollup: 通常打包第三方库
  • Parcel: 和 Webpack 的功能类似,但是它简化了配置,号称零配置、开箱即用。Parcel 2 的 JS 转译器部分基于 SWC 进行开发,性能有很大提升。
  1. 通过浏览器的原生 module 来实现动态打包的 bundleless 模式
  • Snowpack: 最大的特点就是闪电般的速度。最终部署它会使用 Webpack/Parcel 插件。不过已经不再积极维护了
  • Vite: 也是以速度著称,打包部分使用 Rollup,所以最终部署时打包体积相比 Snowpack 会更小。
  • wmr: 非常轻量级的打包工具,它没有任何 npm 依赖。所以它没有 Snowpack 和 Vite 成熟,但是 wmr 更适合用在 Preact 或者一些简单的项目上

ts 打包器

  • TSDX: 对自身的定位不是一个打包器那么简单,而是覆盖了一个 TypeScript 项目开发时所需的所有东西的零配置 CLI: Rollup、Jest、tsc、yarn、TSLint、VSCode……,有点 All in One 的感觉。
  • tsup: 优势也是零配置,并且底层是使用 esbuild 作为支持的。

优缺点:

Webpack

  • 热更新方面: webpack 支持 HMR,但是 webpack 需要全部重新编译并更新,效率较低
  • tree-shaking: webpack2 开始支持且消除效果不好,但是 webpack5 有更好的 tree-shaking(去除未使用代码)
  • 分包方面: webpack 支持代码切割。(分包)
  • ESM 打包: 现在 webpack 支持 es6module 输出

Rollup

优点

  • Rollup 是一款 ES Modules 打包器,从作用上来看,Rollup 与 Webpack 非常类似。不过相比于 Webpack,Rollup 要小巧的多,打包生成的文件更小。(识别 commonJs 需要插件)
  • 热更新: Rollup 不支持 HMR,在对 js 以外的模块的支持上不如 webpack,但是如果是打包纯 js 库例如 react,前期的 vue 的话,使用 rollup 是很合适的,打包的产物比较干净,没有 webpack 那么多工具函数
  • Rollup 的插件机制设计得相对更干净简洁,单个模块的 resolve / load / transform 跟打包环节完全解耦,所以 Vite 才能在开发时模拟 Rollup 的插件机制,并且兼容大部分 Rollup 插件
  • rollup 原生支持 tree-shaking

缺点

  • 加载其他类型的资源文件或者支持导入 CommonJS 模块,又或是编译 ES 新特性,这些额外的需求 Rollup 需要使用插件去完成
  • rollup 并不适合开发应用使用,因为需要使用第三方模块,而目前第三方模块大多数使用 CommonJs 方式导出成员,并且 rollup 不支持 HMR,使开发效率降低

Vite

组成:

  • 一个开发服务器,它基于 原生 ES 模块 提供了丰富的内建功能,如速度很快的 【模块热更新 HMR】
  • 一套构建指令,它使用 Rollup 打包你的代码,并且它是预配置的,可以输出用于生产环境的优化过的静态资源

特点:

  • 快速的冷启动: vite 会直接启动开发服务器,不需要进行打包操作,所以不需要分析模块的依赖、不需要编译,因此启动速度非常快
  • 即时的模块热更新
  • 真正的按需编译: 利用现代浏览器支持 ES Module 的特性,当浏览器请求某个模块的时候,再根据需要对模块的内容进行编译,这种方式大大缩短了编译时间

优点

  1. vite 热更新,实现按需编译,按模块更新。(快)
  • 在 Vite 中,HMR 是在原生 ESM 上执行的。当编辑一个文件时,Vite 只需要精确地使已编辑的模块与其最近的 HMR 边界之间的链失效,使 HMR 更新始终快速,无论应用的大小。
  • Vite 同时利用 HTTP 头来加速整个页面的重新加载(再次让浏览器为我们做更多事情): 源码模块的请求会根据 304 Not Modified 进行协商缓存,而依赖模块请求则会通过 Cache-Control: max-age=31536000,immutable 进行强缓存,因此一旦被缓存它们将不需要再次请求。
  • 热更新原理: 在热模块 HMR 方面,当修改一个模块的时候,仅需让浏览器重新请求该模块即可,无须像 webpack 那样需要把该模块的相关依赖模块全部编译一次,效率更高
  1. vite 在生产环境通过 Rollup 进行打包(特点: 打包体积小),生成 esm 模块包。(特点: 快)
  • vite 在开发环境时,基于浏览器支持 esm,让浏览器解析模块,然后服务器按需编译返回。同时基于 esbuild(go)进行预构建打包不常变动的第三包,并用进行缓存。(缓存+快)
  • Vite 使用 esbuild 预构建依赖。Esbuild 使用 Go 编写,并且比以 Node.js 编写的打包器预构建依赖快 10-100 倍。

缺点

  • 生态: 生态不如 webpack,wepback 在于 loader 和 plugin 非常丰富
  • prod 环境的构建: 目前用的 Rollup,原因在于 esbuild 对于 css 和代码分割不是很友好
  • 还没有被大规模使用,很多问题或者诉求没有真正暴露出来

前端测试

单元测试

  1. 覆盖率: 测试案例覆盖的场景
1
2
3
4
5
6
7
8
9
function top10(number, sum) {
if (number < 10) {
return (sum += number);
}
return sum;
}
top10(8);
top10(9);
// 覆盖率 => 50%
  1. 单元拆分
  • 逻辑闭环的最小模块
  • 最小实现的视图组件
  1. 环境准备
1
2
3
4
5
6
7
8
// 1. 依赖安装
npm i --save-dev jest
npm i @type/jest babel-jest @vue/test-utlis@next @testing-library/jest-dom ts-jest vue-jest @babel/preset-env --save-dev

// 2. 配置babel
// jest文件支持es6语法

// 3. 配置jest

E2E 测试

业务功能触发,不关注具体实现,只验证是否实现业务功能 => 开发 or 测试,例如 cypress 库