Webpack 5以及其他打包工具
https://www.kelen.cc/posts/webpack5-module-federation
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"]
})
]
};
导入使用
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"]
})
]
};
模块联邦本身是一个普通的 Webpack 插件 ModuleFederationPlugin
,插件有几个重要参数:
name
当前应用名称,需要全局唯一。remotes
可以将其他项目的 name
映射到当前项目中。exposes
表示导出的模块,只有在此申明的模块才可以作为远程依赖被使用。shared
是非常重要的参数,制定了这个参数,可以让远程加载的模块对应依赖改为使用本地项目的 React 或 ReactDOM。ModuleFederationPlugin
组合了 ContainerPlugin
和 ContainerReferencePlugin
ContainerPlugin
该插件使用指定的公开模块来创建一个额外的容器入口。
ContainerReferencePlugin
该插件将特定的引用添加到作为外部资源(externals)的容器中,并允许从这些容器中导入远程模块。它还会调用这些容器的 override
API 来为它们提供重载。本地的重载(当构建也是一个容器时,通过 __webpack_override__
或 override
API)和指定的重载被提供给所有引用的容器。
从使用者到容器的控制
概念适用于独立于环境
共享中的相对和绝对请求
config.context
requiredVersion
共享中的模块请求
requiredVersion
/
的模块请求将匹配所有具有这个前缀的模块请求一般来说,remote 是使用 URL 配置的,但是你也可以向 remote 传递一个 promise,其会在运行时被调用。你应该用任何符合上面描述的 get/init
接口的模块来调用这个 promise。例如,如果你想传递你应该使用哪个版本的联邦模块,你可以通过一个查询参数做以下事情
缓存
通常我们会借助一些其他的配置优化缓存
// 对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/
})
],
},
而在webpack5中可以使用cache 特性来将webpack工作缓存到硬盘中。存放的路径为node_modules/.cache/webpack
module.exports = {
//开发环境默认值为 cache.type = "memory"。
//生产环境可手动设为 cache.type = "filesystem"。
cache: {
type: 'filesystem',
version: 'your_version'
}
};
持久化缓存
在日常开发中我们会尽量减少文件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。
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.内部模块。Webpack 4 不会去分析导入和导出模块之间的依赖关系,Webpack5 里面会通过 optimization.innerGraph
记录依赖关系。比如下面这个例子,只有 test
方法使用了 someting
。最终可以实现标记更多没有使用的导出项
import { something } from "./something";
function usingSomething() {
return something;
}
export function test() {
return usingSomething();
}
3.commonjs tree shaking支持。Webpack不仅仅支持 ES module 的 tree Shaking,commonjs规范的模块开始支持了
在webpack5之前,webpack会自动的帮我们项目引入Node全局模块polyfill。我们可以通过node配置
但是webpack团队认为,现在大多数工具包多是为前端用途而编写的,所以不再自动引入polyfill。我们需要自行判断是否需要引入polyfill,当我们用weback5打包的时候,webpack会给提示
Webpack5 为我们提供了资源模块,用一种更简单、更方便的方式来替换原来的raw-loader、url-loader、file-loader等图文、字体文件资源
Webpack5 为我们提供了 asset/resource
、asset/inline
、asset/source
、asset
四种资源类型,具体介绍如下所示
asset/resource
:将资源文件输出到指定的输出目录,作用等同于 file—loader
;
asset/inline
:将资源文件内容以指定的格式进行编码(一般为 base64
),然后以 data URI 的形式嵌入到生成的 bundle
中,作用等同于 url-loader
;
asset/source
:将资源文件的内容以字符串的形式嵌入到生成的 bundle
中,作用相当于 raw-loader
;
asset
:作用等同于设置了 limit
属性的 url-loader
,即资源文件的大小如果小于 limit
的值(默认值为 8kb
),则采用 asset/inline
模式,否则采用 asset/resource
模式
资源模块的配置
原来使用file-loader如下
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.(png|jpe?g|gif)$/i,
use: [
{
loader: 'file-loader',
},
],
},
],
},
};
调整配置
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.(png|jpe?g|gif)$/i,
type: 'asset' // 可用值:asset/resource、asset/inline、asset/source、asset
},
],
},
};
默认情况下,在处理 resource
类型的资源时,相关资源会被输出到 output.path 目录下(文件命名规则为 [hash][ext][query]
),我们可通过设置 output.assetModuleFilename
来改变其输出目录
// webpack.config.js
module.exports = {
output: {
path: path.resolve(__dirname, 'dist'),
assetModuleFilename: './static/images/[hash][ext][query]',
},
};
这里需要注意的是,assetModuleFilename
的值须为相对路径
,如值为绝对路径
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通过执行栈管理多个执行上下文,执行栈最底端是全局上下文,全局上下文中执行函数的时候,相应上下文会入栈,其中不断调用函数,不同的函数不断创建不同的执行上下文,直到函数执行完毕,上下文出栈,即后进先出
如果有异步函数,在执行栈的空的时候查看任务队列,微任务优先入栈。
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基于Node.js的前端构建工具,通过Gulp的插件可以实现前端代码的编译(sass、less)、压缩、测试;图片的压缩;浏览器自动刷新,还有许多强大的插件可以在这里查找。
安装
npm install gulp -g
使用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
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
创建一个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');
创建任务
// 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);
});
打包前拷贝到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
)
给打包文件加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
)
替换文件中的源文件
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
)
打包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
)
动态处理打包环境,有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"
};
}
根据不同环境执行不同的打包方式,如开发环境不需要压缩。
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
)
}
与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
应用场景: 项目(特别是类库)只有 js,而没有其他的静态资源文件,使用 webpack 就有点大才小用了,因为 webpack bundle 文件的体积略大,运行略慢,可读性略低。这时候 rollup就是一种不错的解决方案
创建一个rollup.config.js文件
import json from '@rollup/plugin-json';
import terser from '@rollup/plugin-terser';
export default {
input: 'src/main.js',
output: [
{
file: 'bundle.js',
format: 'cjs'
},
{
file: 'bundle.min.js',
format: 'iife',
name: 'version',
plugins: [terser()]
}
],
plugins: [json()]
};
import resolve from '@rollup/plugin-node-resolve';
export default [
{
resolve({
extensions: ['.ts', '.js'],
preferBuiltins: false,
mainFields: ['jsnext:main', 'jsnext', 'module', 'main'],
}),
}
]
import multiInput from 'rollup-plugin-multi-input';
export default [
{
input: [
'miniprogram/**/*.ts',
],
plugins: [
multiInput(),
]
}
]
import esbuild from 'rollup-plugin-esbuild';
export default [
{
plugins: [
esbuild({
sourceMap: false,
minify: !isDev,
target: 'es2018',
legalComments: 'none',
}),
]
}
]
// rollup.config.js
import typescript from 'rollup-plugin-typescript2';
export default {
input: './main.ts',
plugins: [
typescript(/*{ plugin options }*/)
]
}
https://github.com/ezolenko/rollup-plugin-typescript2
// rollup-plugin-my-example.js
export default function myExample () {
return {
name: 'my-example', // this name will show up in warnings and errors
resolveId ( source ) {
if (source === 'virtual-module') {
return source; // this signals that rollup should not ask other plugins or check the file system to find this id
}
return null; // other ids should be handled as usually
},
load ( id ) {
if (id === 'virtual-module') {
return 'export default "This is virtual!"'; // the source code for "virtual-module"
}
return null; // other ids should be handled as usually
}
};
}
// rollup.config.js
import myExample from './rollup-plugin-my-example.js';
export default ({
input: 'virtual-module', // resolved by our plugin
plugins: [myExample()],
output: [{
file: 'bundle.js',
format: 'es'
}]
});
插件就是一个JavaScript函数,调用构建时的api。
插件的api可以有几种属性
同步还是异步的
并发的
load
resolveId
buildStart
buildEnd
options
resolveDynamicImport
transform
watchChange
rollup返回promise,所以可以gulp中可以监听
const gulp = require('gulp');
const rollup = require('rollup');
const rollupTypescript = require('@rollup/plugin-typescript');
gulp.task('build', async function () {
const bundle = await rollup.rollup({
input: './src/main.ts',
plugins: [rollupTypescript()]
});
await bundle.write({
file: './dist/library.js',
format: 'umd',
name: 'library',
sourcemap: true
});
});
安装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']);
};
大部分的Gruntfile.js都非常臃肿,有一大段一大段的嵌套配置结构,令人看起来很烦躁。这些冗余的Gruntfile.js
文件其实维护起来并不是那么轻量的,主要体现在下面几个方面,
我们再来看看gulp中是如何解决这些问题的,
wepack:https://www.kancloud.cn/sllyli/webpack/1242354