Webpack五以及其他打包工具
# webpack5
# 模块联邦
Webpack5 模块联邦让 Webpack 达到了线上 Runtime 的效果,让代码直接在项目间利用 CDN 直接共享,不再需要本地安装 Npm 包、构建再发布了
Webpack 可以通过 DLL 或者 Externals 做代码共享时 Common Chunk,但不同应用和项目间这个任务就变得困难了,我们几乎无法在项目之间做到按需热插拔。
常见共享模块的场景
UMD 模块
微前端:多个项目共存于一个页面,有点类似iframe,共享的对象是项目级的,页面级的 子应用间的chunk以及对象可通过全局事件共享,但是公共包在项目安置以及打包编译很难放
webpack的方案是直接将一个应用的 bundle,应用于另一个应用,动态分发 runtime 子模块给其他应用。
使用方式,先设置本模块可以被导入
const HtmlWebpackPlugin = require("html-webpack-plugin");
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
module.exports = {
// other webpack configs...
plugins: [
new ModuleFederationPlugin({
// 1. name 当前应用名称,需要全局唯一
name: "app_one_remote",
// 2. remotes 可以将其他项目的 name 映射到当前项目中
remotes: {
app_two: "app_two_remote",
app_three: "app_three_remote"
},
// 3. exposes 表示导出的模块,只有在此申明的模块才可以作为远程依赖被使用
exposes: {
AppContainer: "./src/App"
},
// 4. shared可以让远程加载的模块对应依赖改为使用本地项目的 React或ReactDOM。
shared: ["react", "react-dom", "react-router-dom"]
}),
new HtmlWebpackPlugin({
template: "./public/index.html",
chunks: ["main"]
})
]
};
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
导入使用
import { Search } from "app_two/Search";
app_two/Search来自于app_two 的配置:
// app_two的webpack 配置
export default {
plugins: [
new ModuleFederationPlugin({
name: "app_two",
library: { type: "var", name: "app_two" },
filename: "remoteEntry.js",
exposes: {
Search: "./src/Search"
},
shared: ["react", "react-dom"]
})
]
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
模块联邦本身是一个普通的 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特性来加快压缩。(默认开启)
// 对babel-loader的工作进行缓存
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: ['cache-loader', 'babel-loader'],
include: path.resolve('src'),
},
],
},
};
optimization: {
minimizer: [
new TerserPlugin({
cache: true, // 开启该插件的缓存,默认缓存到node_modules/.cache中
parallel: true, // 开启“多线程”,提高压缩效率
exclude: /node_modules/
})
],
},
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
而在webpack5中可以使用cache 特性来将webpack工作缓存到硬盘中。存放的路径为node_modules/.cache/webpack
module.exports = {
//开发环境默认值为 cache.type = "memory"。
//生产环境可手动设为 cache.type = "filesystem"。
cache: {
type: 'filesystem',
version: 'your_version'
}
};
2
3
4
5
6
7
8
持久化缓存
在日常开发中我们会尽量减少文件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掉
// inner.js
export const a = 1;
export const b = 2;
// module.js
import * as inner from "./inner";
export { inner }
// user.js
import * as module from "./module";
console.log(module.inner.a);
2
3
4
5
6
7
8
9
10
11
2.内部模块。Webpack 4 不会去分析导入和导出模块之间的依赖关系,Webpack5 里面会通过 optimization.innerGraph
记录依赖关系。比如下面这个例子,只有 test
方法使用了 someting
。最终可以实现标记更多没有使用的导出项
import { something } from "./something";
function usingSomething() {
return something;
}
export function test() {
return usingSomething();
}
2
3
4
5
6
7
8
9
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)、压缩、测试;图片的压缩;浏览器自动刷新,还有许多强大的插件可以在这里 (opens new window)查找。
安装
npm install gulp -g
# Gulp Api
使用gulp,仅需知道4个API即可:gulp.task()
,gulp.src()
,gulp.dest()
,gulp.watch()
gulp.src()
方法正是用来获取流的,但要注意这个流里的内容不是原始的文件流,而是一个虚拟文件对象流(Vinyl files (opens new window)),这个虚拟文件对象中存储着原始文件的路径、文件名、内容等信息
gulp.dest()
方法是用来写文件的,其语法为:
gulp的使用流程一般是这样子的:首先通过gulp.src()
方法获取到我们想要处理的文件流,然后把文件流通过pipe方法导入到gulp的插件中,最后把经过插件处理后的流再通过pipe方法导入到gulp.dest()
中,gulp.dest()
方法则把流中的内容写入到文件中,
gulp.task
方法用来定义任务,内部使用的是Orchestrator (opens new window),其语法为:
gulp.watch()
用来监视文件的变化,当文件发生变化后,我们可以利用它来执行相应的任务,例如文件压缩等。其语法为
# 常用插件
安装gulp插件
CSS
- sass的编译(gulp-ruby-sass (opens new window))
- less的编译(gulp-less (opens new window))
- 自动添加css前缀(gulp-autoprefixer (opens new window))
- 压缩css(gulp-minify-css (opens new window))
js
- js代码校验(gulp-jshint (opens new window))
- 合并js文件(gulp-concat (opens new window))
- 压缩js代码(gulp-uglify (opens new window))
html
图片、资源
- 压缩图片(gulp-imagemin (opens new window))
- 图片缓存,只有图片替换了才压缩(gulp-cache (opens new window))
其他
- 自动加载插件(gulp-load-plugins (opens new window))
- 自动刷新页面(gulp-livereload (opens new window))
- 更改提醒(gulp-notify (opens new window))
- 重命名(gulp-rename (opens new window))
- 清除文件(del (opens new window))
安装这些插件
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在根目录下,然后在里面加载插件:
var gulp = require('gulp'),
sass = require('gulp-ruby-sass'),
autoprefixer = require('gulp-autoprefixer'),
minifycss = require('gulp-minify-css'),
jshint = require('gulp-jshint'),
uglify = require('gulp-uglify'),
imagemin = require('gulp-imagemin'),
rename = require('gulp-rename'),
concat = require('gulp-concat'),
notify = require('gulp-notify'),
cache = require('gulp-cache'),
livereload = require('gulp-livereload'),
del = require('del');
2
3
4
5
6
7
8
9
10
11
12
13
创建任务
// Load plugins
var gulp = require('gulp'),
sass = require('gulp-ruby-sass'),
autoprefixer = require('gulp-autoprefixer'),
minifycss = require('gulp-minify-css'),
jshint = require('gulp-jshint'),
uglify = require('gulp-uglify'),
imagemin = require('gulp-imagemin'),
rename = require('gulp-rename'),
concat = require('gulp-concat'),
notify = require('gulp-notify'),
cache = require('gulp-cache'),
livereload = require('gulp-livereload'),
del = require('del');
// Styles,编译sass,添加前缀,保存到我们指定的目录下面,还没结束,我们还要压缩,给文件添加.min后缀再输出压缩文件到指定目录,最后提醒任务完成了
gulp.task('styles', function() {
return gulp.src('src/styles/main.scss')
.pipe(sass({ style: 'expanded', }))
.pipe(autoprefixer('last 2 version', 'safari 5', 'ie 8', 'ie 9', 'opera 12.1', 'ios 6', 'android 4'))
.pipe(gulp.dest('dist/styles'))
.pipe(rename({ suffix: '.min' }))
.pipe(minifycss())
.pipe(gulp.dest('dist/styles'))
.pipe(notify({ message: 'Styles task complete' }));
});
// Scripts,js代码校验、合并和压缩
gulp.task('scripts', function() {
return gulp.src('src/scripts/**/*.js')
.pipe(jshint('.jshintrc'))
.pipe(jshint.reporter('default'))
.pipe(concat('main.js'))
.pipe(gulp.dest('dist/scripts'))
.pipe(rename({ suffix: '.min' }))
.pipe(uglify())
.pipe(gulp.dest('dist/scripts'))
.pipe(notify({ message: 'Scripts task complete' }));
});
// Images,使用imagemin插件把所有在src/images/目录以及其子目录下的所有图片(文件)进行压缩,我们可以进一步优化,利用缓存保存已经压缩过的图片
gulp.task('images', function() {
return gulp.src('src/images/**/*')
.pipe(cache(imagemin({ optimizationLevel: 3, progressive: true, interlaced: true })))
.pipe(gulp.dest('dist/images'))
.pipe(notify({ message: 'Images task complete' }));
});
// Clean,在任务执行前,最好先清除之前生成的文件
gulp.task('clean', function(cb) {
del(['dist/assets/css', 'dist/assets/js', 'dist/assets/img'], cb)
});
// Default task设置默认任务,在命令行下输入$ gulp执行的就是默认任务,现在我们为默认任务指定执行上面写好的三个任务:
gulp.task('default', ['clean'], function() {
gulp.start('styles', 'scripts', 'images');
});
// Watch,实现当文件修改时自动刷新页面,我们要修改watch任务,配置LiveReload:
gulp.task('watch', function() {
// Watch .scss files
gulp.watch('src/styles/**/*.scss', ['styles']);
// Watch .js files
gulp.watch('src/scripts/**/*.js', ['scripts']);
// Watch image files
gulp.watch('src/images/**/*', ['images']);
// Create LiveReload server
livereload.listen();
// Watch any files in dist/, reload on change
gulp.watch(['dist/**']).on('change', livereload.changed);
});
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# 其他插件
打包前拷贝到dist目录,之后所有的操作基于dist目录
const { series, src, dest } = require('gulp');
const clean = require('gulp-clean');
//清除文件夹插件
function clear() {
return src('dist',{ allowEmpty: true }).pipe(clean());
}
//拷贝静态资源
function assets() {
return src('src/assets/**/*.*').pipe(dest('dist/assets/'));
}
//拷贝css文件
function css() {
return src(['src/css/**/*.css']).pipe(dest('dist/css'));
}
//拷贝js文件
function js() {
return src(['src/js/**/*.js']).pipe(dest('dist/js'))
}
//拷贝html文件
function html() {
return src('src/views/**/*.html').pipe(dest('dist/views'))
}
exports.build = series(
clear,
assets,
css,
js,
html
)
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
28
29
30
给打包文件加hash
npm i gulp-dev -D
在gulpfile。js中添加以下代码
const rev = require('gulp-rev');
//给css文件加入hash
function cssRevHash() {
return src('dist/css/**/*.css')
.pipe(rev())
.pipe(dest('/dist/css'))
.pipe(rev.manifest())
.pipe(dev('rev/css'));
}
//给js文件加入hash
function jsRevHash() {
return src('dist/js/**/*.js')
.pipe(rev())
.pipe(dest('dist/js/'))
.pipe(rev.manifest())
.pipe(dev('rev/js'));
}
export.build = series(
...
cssRevHash,
jsRevHash
)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
替换文件中的源文件
npm i gulp-rev-collector -D
在gulpfilejs中加入代码
const revCollector = require('gulp-rev-collector');
function htmlRevInject() {
return src([`rev/${revDir}/**/*.json`,'dist/views/**/*.html'])
.pipe(revCollector({ replaceReved: true}))
.pipe(dest('dist/views'));
}
exports.build = series(
...
htmlRevInject
)
2
3
4
5
6
7
8
9
10
11
12
打包zip文件
npm i gulp-zip -D
在gulpfilejs中加入代码
const zip = require('gulp-zip')
function zipiupiu() {
return src('dist/**/*').pipe(zip(zipName)).pipe(dest('dist'));
}
exports.build = series(
...
zipiupiu
)
2
3
4
5
6
7
8
9
10
动态处理打包环境,有prod和dev两种
npm i cross-env -D
在packagejson中添加命令
{
"scripts": {
"build:env": "cross-env NODE_ENV=development gulp build",
"build:prod": "cross-env NODE_ENV=production gulp build"
};
}
2
3
4
5
6
根据不同环境执行不同的打包方式,如开发环境不需要压缩。
if (process.env.NODE_ENV === 'production') {
exports.build = series (
clear,
assets,
css,
js,
html,
cssRevHash,
jsRevHash,
htmlRevInject,
htmlMinify,
zipiupiu
)
}else {
exports.build = series (
clear,
assets,
css,
js,
html,
cssRevHash,
jsRevHash,
htmlRevInject,
zipiupiu
)
}
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
# gulp与webpack、grant的区别
与webpack
gulp严格上讲,模块化不是他强调的东西,他旨在规范前端开发流程。
webpack更是明显强调模块化开发,而那些文件压缩合并、预处理等功能,不过是他附带的功能。
gulp应该与grunt比较,而webpack应该与browserify
gulp与webpack上是互补的,还是可替换的,取决于你项目的需求。如果只是个vue或react的单页应用,webpack也就够用;如果webpack某些功能使用起来麻烦甚至没有(雪碧图就没有),那就可以结合gulp一起用。
与grunt
Grunt主要是以文件为媒介来运行它的工作流的,比如在Grunt中执行完一项任务后,会把结果写入到一个临时文件中,然后可以在这个临时文件内容的基础上执行其它任务,执行完成后又把结果写入到临时文件中,然后又以这个为基础继续执行其它任务...就这样反复下去。而在Gulp中,使用的是Nodejs中的stream (opens new window)(流),首先获取到需要的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 (opens new window)就是一种不错的解决方案
# 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, 提供静态服务器能力
# Grunt
安装cli工具
npm install -g grunt-cli
安装grunt及用到的插件
npm install gulp grunt-contrib-uglify --save-dev
创建Gruntfile.js
文件
module.exports = function(grunt) {
// Project configuration.
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
uglify: {
options: {
banner: '/*! <%= pkg.name %> <%= grunt.template.today("yyyy-mm-dd") %> */\n'
},
build: {
src: 'src/*.js',
dest: 'build/all.min.js'
}
}
});
// 加载包含 "uglify" 任务的插件。
grunt.loadNpmTasks('grunt-contrib-uglify');
// 默认被执行的任务列表。
grunt.registerTask('default', ['uglify']);
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
大部分的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
# Vite
冷启动
在浏览器支持 ES 模块之前,JavaScript 并没有提供的原生机制让开发者以模块化的方式进行开发。这也正是我们对 “打包” 这个概念熟悉的原因:使用工具抓取、处理并将我们的源码模块串联成可以在浏览器中运行的文件。
时过境迁,我们见证了诸如 webpack (opens new window)、Rollup (opens new window) 和 Parcel (opens new window) 等工具的变迁,它们极大地改善了前端开发者的开发体验。
然而,当我们开始构建越来越大型的应用时,需要处理的 JavaScript 代码量也呈指数级增长。包含数千个模块的大型项目相当普遍。我们开始遇到性能瓶颈 —— 使用 JavaScript 开发的工具通常需要很长时间(甚至是几分钟!)才能启动开发服务器,即使使用 HMR,文件修改后的效果也需要几秒钟才能在浏览器中反映出来。如此循环往复,迟钝的反馈会极大地影响开发者的开发效率和幸福感。
当冷启动开发服务器时,基于打包器的方式启动必须优先抓取并构建你的整个应用,然后才能提供服务。
Vite 通过在一开始将应用中的模块区分为 依赖 和 源码 两类,改进了开发服务器启动时间。
依赖 大多为在开发时不会变动的纯 JavaScript。一些较大的依赖(例如有上百个模块的组件库)处理的代价也很高。依赖也通常会存在多种模块化格式(例如 ESM 或者 CommonJS)。
Vite 将会使用 esbuild (opens new window) 预构建依赖 (opens new window)。Esbuild 使用 Go 编写,并且比以 JavaScript 编写的打包器预构建依赖快 10-100 倍。
源码 通常包含一些并非直接是 JavaScript 的文件,需要转换(例如 JSX,CSS 或者 Vue/Svelte 组件),时常会被编辑。同时,并不是所有的源码都需要同时被加载(例如基于路由拆分的代码模块)。
Vite 以 原生 ESM (opens new window) 方式提供源码。这实际上是让浏览器接管了打包程序的部分工作:Vite 只需要在浏览器请求源码时进行转换并按需提供源码。根据情景动态导入代码,即只在当前屏幕上实际使用时才会被处理。
更新
基于打包器启动时,重建整个包的效率很低。原因显而易见:因为这样更新速度会随着应用体积增长而直线下降。
一些打包器的开发服务器将构建内容存入内存,这样它们只需要在文件更改时使模块图的一部分失活[1] (opens new window),但它也仍需要整个重新构建并重载页面。这样代价很高,并且重新加载页面会消除应用的当前状态,所以打包器支持了动态模块热重载(HMR):允许一个模块 “热替换” 它自己,而不会影响页面其余部分。这大大改进了开发体验 —— 然而,在实践中我们发现,即使采用了 HMR 模式,其热更新速度也会随着应用规模的增长而显著下降。
在 Vite 中,HMR 是在原生 ESM 上执行的。当编辑一个文件时,Vite 只需要精确地使已编辑的模块与其最近的 HMR 边界之间的链失活[1] (opens new window)(大多数时候只是模块本身),使得无论应用大小如何,HMR 始终能保持快速更新。
Vite 同时利用 HTTP 头来加速整个页面的重新加载(再次让浏览器为我们做更多事情):源码模块的请求会根据 304 Not Modified
进行协商缓存,而依赖模块请求则会通过 Cache-Control: max-age=31536000,immutable
进行强缓存,因此一旦被缓存它们将不需要再次请求。
打包
尽管原生 ESM 现在得到了广泛支持,但由于嵌套导入会导致额外的网络往返,在生产环境中发布未打包的 ESM 仍然效率低下(即使使用 HTTP/2)。为了在生产环境中获得最佳的加载性能,最好还是将代码进行 tree-shaking、懒加载和 chunk 分割(以获得更好的缓存)。
虽然 esbuild
快得惊人,并且已经是一个在构建库方面比较出色的工具,但一些针对构建 应用 的重要功能仍然还在持续开发中 —— 特别是代码分割和 CSS 处理方面。就目前来说,Rollup 在应用打包方面更加成熟和灵活。尽管如此,当未来这些功能稳定后,我们也不排除使用 esbuild
作为生产构建器的可能。
# wp2vite
安装
npm install -g wp2vite
命令行使用
cd your_workspace/your_project
## 执行wp2vite的命令行
wp2vite
or
wp2vite init
2
3
4
5
然后启动项目就行
// 安装依赖
npm install
// 启动项目
npm run dev // 如果原先你的项目有dev script,请执行下面的命令
or
npm run vite-start
2
3
4
5
6
7
# snowpack
# WebRTC
# webpack学习资源
wepack:https://www.kancloud.cn/sllyli/webpack/1242354