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"]
    })
  ]
};
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

导入使用

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"]
    })
  ]
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

模块联邦本身是一个普通的 Webpack 插件 ModuleFederationPlugin,插件有几个重要参数:

  1. name 当前应用名称,需要全局唯一。
  2. remotes 可以将其他项目的 name 映射到当前项目中。
  3. exposes 表示导出的模块,只有在此申明的模块才可以作为远程依赖被使用。
  4. shared 是非常重要的参数,制定了这个参数,可以让远程加载的模块对应依赖改为使用本地项目的 React 或 ReactDOM。

# 构建速度优化

缓存

通常我们会借助一些其他的配置优化缓存

  1. cache-loader,针对一些耗时的工作进行缓存。比如缓存babel-loader的工作。
  2. 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/
    })
  ],
},
1
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'
  }
};
1
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);
1
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();
}
1
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
1

# 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

js

html

图片、资源

其他

安装这些插件

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
1

创建一个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');
1
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);
});
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
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
)
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
28
29
30

给打包文件加hash

npm i gulp-dev -D
1

在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
)
1
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
1

在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
)
1
2
3
4
5
6
7
8
9
10
11
12

打包zip文件

npm i gulp-zip -D
1

在gulpfilejs中加入代码

const zip = require('gulp-zip')

function zipiupiu() {
  return src('dist/**/*').pipe(zip(zipName)).pipe(dest('dist'));
}

exports.build = series(
	...
  zipiupiu
)
1
2
3
4
5
6
7
8
9
10

动态处理打包环境,有prod和dev两种

npm i cross-env -D
1

在packagejson中添加命令

{
  "scripts": {
		"build:env": "cross-env NODE_ENV=development gulp build",
    "build:prod": "cross-env NODE_ENV=production gulp build"
  };
}
1
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
  )
}
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

# 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
1

安装grunt及用到的插件

npm install gulp grunt-contrib-uglify --save-dev
1

创建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']);
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

大部分的Gruntfile.js都非常臃肿,有一大段一大段的嵌套配置结构,令人看起来很烦躁。这些冗余的Gruntfile.js文件其实维护起来并不是那么轻量的,主要体现在下面几个方面,

  1. 配置和运行分离
  2. 大部分插件的职责不单一,做了不仅一件事
  3. 配置项过多,做的事情越多,配置增长的就越快,且越不可控制
  4. 任务执行中会产生一些临时文件,较频繁的IO操作导致性能滞后

我们再来看看gulp中是如何解决这些问题的,

  1. gulp遵循code over configuration的原则,直接就在调用的地方配置
  2. gulp的插件严格遵循单一职责原则,一个插件仅作一件事,一个gulp一般只有20多行代码就搞定
  3. gulp基于流的思想,并没有过多的配置,任务更多的像是在写代码而不是定义配置
  4. 这个是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
1

命令行使用

cd your_workspace/your_project
## 执行wp2vite的命令行
wp2vite 
or 
wp2vite init
1
2
3
4
5

然后启动项目就行

// 安装依赖
npm install

// 启动项目
npm run dev // 如果原先你的项目有dev script,请执行下面的命令
or
npm run vite-start
1
2
3
4
5
6
7

# snowpack

# WebRTC

# webpack学习资源

wepack:https://www.kancloud.cn/sllyli/webpack/1242354

Last Updated: 11/7/2021, 3:25:01 PM