前端/Node打包工具的配置文件之谜本次分享的主题将以三种常见的前端打包构建工具为案例,介绍其配置文件,讲解为什么、是什
1 前言
本次分享的主题将以下边三种常见的前端打包构建工具为案例,介绍其配置文件,讲解为什么、是什么、怎么用这些配置文件。
- webpack,强配置型打包工具、严重依赖插件。
- vite,低配置型打包工具、对插件有一定需求。
- rollup,实用老牌打包工具。
2 Webpack
一般来讲,webpack的配置文件为webpack.config.js
,webpack之所以设计这么一个配置文件,一方面是因为webpack的设计理念灵活性,同时webpack是插件制的,在配置文件中,用户可以根据自己的需求对webpack进行扩展,以配置文件来满足不同项目的需求。
要学会配置webpack,首先我们要理解下边这些核心概念:
- 入口(entry)
- 输出(output)
- loader
- 插件(plugin)
- 模式(mode)
- 浏览器兼容性(browser compatibility)
- 环境(environment)
2.1 概念
2.1.1 入口(entry)
入口起点(entry point) 指示 webpack 应该使用哪个模块,来作为构建的开始。进入入口起点后,webpack 会找出有哪些模块和库是入口起点(直接和间接)依赖的。
默认值是 ./src/index.js
,可以通过在配置文件中配置 entry
属性,来指定一个(或多个)不同的入口起点。例如:
webpack.config.js
module.exports = {
entry: './path/to/my/entry/file.js',
};
2.1.2 输出(output)
output 属性告诉 webpack 在哪里输出它所创建的 bundle,以及如何命名这些文件。主要输出文件的默认值是 ./dist/main.js
,其他生成文件默认放置在 ./dist
文件夹中。
我们可以通过在配置中指定一个 output
字段,来配置这些处理过程:
webpack.config.js
const path = require('path');
module.exports = {
entry: './path/to/my/entry/file.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'my-first-webpack.bundle.js',
},
};
在上面的示例中,我们通过 output.filename
和 output.path
属性,来告诉 webpack bundle 的名称,以及我们想要生成到哪里。
2.1.3 加载器(loader)
webpack 只能理解 JavaScript 和 JSON 文件,这是 webpack 开箱可用的自带能力。loader 让 webpack 能够去处理其他类型的文件,并将它们转换为有效 模块,以供应用程序使用,以及被添加到依赖图中。 webpack 的其中一个强大的特性就是能通过 import
导入任何类型的模块(例如 .css
文件),webpack作为在这一块的先行者,一直在影响后来者框架的设计。
loader 有两个属性:
test
属性,识别出哪些文件会被转换。use
属性,定义出在进行转换时,应该使用哪个 loader。
webpack.config.js
const path = require('path');
module.exports = {
output: {
filename: 'my-first-webpack.bundle.js',
},
module: {
rules: [{ test: /\.txt$/, use: 'raw-loader' }],
},
};
以上配置中,对一个单独的 module 对象定义了 rules
属性,里面包含两个必须属性:test
和 use
。示例中规则的意思是:当你遇到.txt结尾的路径时,在对它打包之前,先使用raw-loader转换一下。
2.1.4 插件(plugin)
loader 用于转换某些类型的模块,而插件则可以用于执行范围更广的任务。包括:打包优化,资源管理,注入环境变量。
想要使用一个插件,我们只需要 require()
它,然后把它添加到 plugins
数组中。多数插件可以通过选项(option)自定义。我们也可以在一个配置文件中因为不同目的而多次使用同一个插件,这时需要通过使用 new
操作符来创建一个插件实例。
webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack'); // 用于访问内置插件
module.exports = {
module: {
rules: [{ test: /\.txt$/, use: 'raw-loader' }],
},
plugins: [new HtmlWebpackPlugin({ template: './src/index.html' })],
};
在上面的示例中,html-webpack-plugin
为应用程序生成一个 HTML 文件,并自动将生成的所有 bundle 注入到此文件中。
2.1.5 模式(mode)
通过选择 development
, production
或 none
之中的一个,来设置 mode
参数,可以启用 webpack 内置在相应环境下的优化。其默认值为 production
。
module.exports = {
mode: 'production',
};
2.1.6 浏览器兼容性(browser compatibility)
Webpack 支持所有符合 ES5 标准 的浏览器(不支持 IE8 及以下版本)。webpack 的 import()
和 require.ensure()
需要 Promise
。如果想要支持旧版本浏览器,在使用这些表达式之前,还需要 提前加载 polyfill。
2.1.7 环境(environment)
Webpack 5 运行于 Node.js v10.13.0+ 的版本。
2.2 配置
我们从手搭一个React启动项目来学习配置webpack。为方便理解,我绘制了一张原理图(劣质版本,原理其实比这复杂)。
首先,我们要安装webpack,并确保文件夹格式、package.json如下图,然后我们开始演示如何构建一个react项目。
pnpm add webpack webpack-cli webpack-dev-server -D
接着,配置最基本的输入输出,此时webpack已经完成了一个最小可用单元。如果我们尝试执行pnpm build
,会发现在/dist文件夹内生成了一个新文件index.bundle.js
。
const path = require('path');
module.exports = {
entry: './src/index.js',
mode: "development",
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'index.bundle.js',
},
};
但是我们的目标是能支持react的使用,也就是我们最起码要支持jsx。
那么这个时候就要用到 loader
了。
-
提问:有没有同学还记得loader的作用?
loader用于加载webpack本来无法识别的模块,比如css、less这些资源文件
2.2.1 支持JSX
首先,将src/index.js文件修改成src/index.jsx,并将以下内容写入,此时如果运行pnpm dev会报错:
- index.jsx
import React from "react";
import { createRoot } from "react-dom/client";
const App = () => {
return <div>App</div>;
};
createRoot(document.getElementById("app")).render(<App />);
接着,我们使用esbuild-loader
来加载jsx文件。安装后修改webpack的config文件,此时再次运行没有出现报错。
pnpm add esbuild-loader -D
- webpack.config.js
const path = require("path");
module.exports = {
entry: "./src/index.jsx",
mode: "development",
output: {
path: path.resolve(__dirname, "dist"),
filename: "index.bundle.js",
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
use: [
{
loader: "esbuild-loader",
options: {
loader: "jsx",
target: "es2015",
},
},
],
},
],
},
};
2.2.2 开发模式
此时,虽然我们可以把jsx文件打包编译成js文件,但是代码是被打包到index.bundle.js文件中,我们希望这个工程是可以预览和调试的。为了确保这点,我们需要引入webpack的server模式。
首先,还是修改webpack配置文件。
- webpack.config.js
const path = require("path");
module.exports = {
entry: "./src/index.jsx",
mode: "development",
output: {
path: path.resolve(__dirname, "dist"),
filename: "index.bundle.js",
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
use: [
{
loader: "esbuild-loader",
options: {
loader: "jsx",
target: "es2015",
},
},
],
},
],
},
devServer: {
static: {
directory: path.resolve(__dirname, "./dist"),
},
hot: true,
port: "auto",
open: true
},
};
会看到,这里我们配置了一个新字段devServer
,其中通过static指定了静态文件存储的位置,通过hot指定支持热更新,port代表服务的端口,open代表自动打开浏览器。
但是这样是还不够的,此时服务虽然跑起来了,但是却显示404。
2.2.3 加载HTML文件
为了让页面正常显示,我们需要把我们准备的index.html
文件加载进来。
我们需要使用HtmlWebpackPlugin来加载html文件。
pnpm add html-webpack-plugin -D
我们导入HtmlWebpackPlugin ,并在plugins列表中实例化,其中的template参数指定为我们index.html文件的地址。
- webpack.config.js
...
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
...
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, "./public/index.html"),
}),
],
};
此时执行dev指令,页面可以正常显示:
尝试build打包,可以发现dist文件夹下多出了index.html文件:
2.2.4 加载样式文件
在现代网站开发中,加载样式是必不可少的步骤,现阶段的CSS由于缺少嵌套等能力,以至于市场中衍生出了less、scss这些预处理器。
我们以加载less和css来演示如何加载多个loader。
首先,我们选择style-loader来将css代码解析进HTML文件中,使用css-loader加载css文件,用less-loader来加载less文件。
pnpm add style-loader less less-loader css-loader -D
可以看到,我们的配置代表,对于以less或css结尾的文件,先用less-loader处理它,再用css-loader处理,最后用style-loader处理。注意:loader的处理顺序是从后往前。
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
...
module: {
rules: [
...
{
test: /\.(le|c)ss$/,
use: ["style-loader","css-loader", "less-loader"],
},
],
},
...
};
此时修改代码,再次运行,样式均可以加载。
2.2.5 加载资源文件
如果,我们要加载一张图片,怎么做?
在webpack5之前的版本,这一块也是需要插件和loader配合的。不过webpack5之后,官方提供了这一块的支持。
const path = require("path");
module.exports = {
...
module: {
rules: [
...
{
test: /\.(png|jpg|gif|eot|ttf|woff|woff2)$/i,
type: "asset/resource",
},
],
},
...
};
除了通过use指令配置loader之外,也可以通过type指定将资源加载成不同的类型,比如"asset/resource"就是将资源加载成url。
更多的选项,可以参考官网。
2.2.6 One more thing
从能让项目跑起来的角度,这份配置是合格的。
从让项目很好地跑起来的角度,这份配置是不及格的。
- 提问:同学们觉得,这份配置存在哪些问题?
- Answer:
- 开发环境和生产环境没有区分
- 该项目不支持alias
- 缺少开发环境下的hash配置,所以热更新能力其实是异常的
- 缺乏生成环境优化
3 Vite
Vite是近年来兴起的前端构建工具,它主要由两部分组成:
- 一个开发服务器,它基于 原生 ES 模块提供了丰富的内建功能。
- 一套构建指令,它使用 Rollup 打包我们的代码,由于它是预配置的,可输出用于生产环境的高度优化过的静态资源。
Vite 旨在支持最常见的模式来构建开箱即用的 Web 应用程序,但Vite 核心必须保持精简,API 接口较小,以保持项目的长期可维护性。这个目标的实现得益于Vite 基于 rollup 的插件系统。
不得不说,vite的诞生解决了webpack繁杂配置的问题,以下边的配置为例,这个配置即可以胜任我们刚刚手搓的webpack-react基本项目,并且在各方面均不差。
所以,vite的配置文件一般情况下不需要过多操心,虽然vite仍然提供非常丰富的插件(基于Rollup)以及大量基础配置项。
3.1 配置文件组成
根据官方文档,vite提供了非常多的选项,相比webpack,vite似乎是更专业的现代前端构建工具。vite的配置项从SSR到Web Worker,可以说是紧跟前端发展。
3.2 基本&重要配置项
我们不可能把所有东西讲完 ——鲁迅
-
root
root选项,用于指定项目根目录(
index.html
文件所在的位置)。可以是一个绝对路径,或者一个相对于该配置文件本身的相对路径。 -
base,默认
/
开发或生产环境服务的公共基础路径。合法的值包括以下几种:
- 绝对 URL 路径名,例如
/foo/
- 完整的 URL,例如
https://foo.com/
- 空字符串或
./
(用于嵌入形式的开发)
base常用于在同一域名下部署了不同的子网站。比如,我们在github page免费部署页面时常常需要用到base和下边的publicDir
- 绝对 URL 路径名,例如
-
publicDir,默认
<root>/public
作为静态资源服务的文件夹。该目录中的文件在开发期间在
/
处提供,并在构建期间复制到outDir
的根目录,并且始终按原样提供或复制而无需进行转换。该值可以是文件系统的绝对路径,也可以是相对于项目根目录的相对路径。将
publicDir
设定为false
可以关闭此项功能。如果有下列这些资源:
- 不会被源码引用(例如
robots.txt
) - 必须保持原有文件名(没有经过 hash)
- ...或者你压根不想引入该资源,只是想得到其 URL。
那么你可以将该资源放在指定的
public
目录中,它应位于你的项目根目录。该目录中的资源在开发时能直接通过/
根路径访问到,并且打包时会被完整复制到目标目录的根目录下。请注意:
- 引入
public
中的资源永远应该使用根绝对路径 —— 举个例子,public/icon.png
应该在源码中被引用为/icon.png
。 public
中的资源不应该被 JavaScript 文件引用。
- 不会被源码引用(例如
-
cacheDir,默认
"node_modules/.vite”
存储缓存文件的目录。此目录下会存储预打包的依赖项或 vite 生成的某些缓存文件,使用缓存可以提高性能。如需重新生成缓存文件,你可以使用
--force
命令行选项或手动删除目录。此选项的值可以是文件的绝对路径,也可以是以项目根目录为基准的相对路径。当没有检测到 package.json 时,则默认为.vite
。 -
resolve.alias,模块路径别名配置项(非常实用)。
我们常在前端项目中,用
@/components
这样的路径导入模块,这种时候的@/
就是绝对路径的别名。目前各个模块打包工具,甚至包括node本身其实都提供了别名功能,而vite的别名规则一般可以用下边的方式配置:import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; import { resolve } from "path"; // <https://vitejs.dev/config/> export default defineConfig({ plugins: [react()], resolve: { alias: { "@": resolve(__dirname, "src"), }, }, });
-
resolve.extensions,导入时省略文件后缀。默认
['.mjs', '.js', '.mts', '.ts', '.jsx', '.tsx', '.json']
导入时想要省略的扩展名列表。注意,不建议忽略自定义导入类型的扩展名(例如:
.vue
),因为它会影响 IDE 和类型支持。 -
css.modules,配置 CSS modules 的行为。选项将被传递给 postcss-modules。
我们说过,Vite是一个高度专业的前端构建工具,其优化项也包括了样式领域,Vite内置了对css module的支持,用于解决全局样式冲突。
-
css.postcss,内联的 PostCSS 配置(格式同
postcss.config.js
)或者一个自定义的 PostCSS 配置路径。vite内置了postcss,可以直接在vite.config.ts中配置postcss。
对内联的 POSTCSS 配置,它期望接收与
postcss.config.js
一致的格式。但对于plugins
属性有些特别,只接收使用数组格式。搜索是使用 postcss-load-config 完成的,只有被支持的文件名才会被加载。
注意:如果提供了该内联配置,Vite 将不会搜索其他 PostCSS 配置源。
-
envDir,vite内置了加载.env文件的能力,默认值是根目录。
用于加载
.env
文件的目录。可以是一个绝对路径,也可以是相对于项目根的路径。Vite 使用 dotenv 从你的 环境目录中的下列文件加载额外的环境变量:
.env # 所有情况下都会加载 .env.local # 所有情况下都会加载,但会被 git 忽略 .env.[mode] # 只在指定模式下加载 .env.[mode].local # 只在指定模式下加载,但会被 git 忽略
注意!为了防止意外地将一些环境变量泄漏到客户端,只有以 VITE_
为前缀的变量才会暴露给经过 vite 处理的代码。
4 Rollup
相比前两者,rollup是更纯粹的模块打包工具,根据官方的描述,Rollup能整合各种标准的代码模块格式并自由转换成其他标准的模块格式。所以,相对于webpack和vite常用于开发web应用来说,rollup被更多用于构建JavaScript库和组件。
Rollup 是一个用于 JavaScript 的模块打包工具,它将小的代码片段编译成更大、更复杂的代码,例如库或应用程序。它使用 JavaScript 的 ES6 版本中包含的新标准化代码模块格式,而不是以前的 CommonJS 和 AMD 等特殊解决方案。ES 模块允许你自由无缝地组合你最喜欢的库中最有用的个别函数。这在未来将在所有场景原生支持,但 Rollup 让你今天就可以开始这样做。
rollup的配置项并不复杂,功能却十分强大,这依托于它本身良好的插件设计,其插件生态非常庞大。
4.1 配置文件组成
rollup的配置文件是在根目录的rollup.config.js
,其基本能力是对不同标准的模块进行整合。
这句话说着轻巧,做起来是真的繁杂,我想知道在座的同学们知道Js的模块有哪些标准吗?
-
es模块(ESModule)
- amd模块(异步模块)
- cjs模块(CommonJS)
- umd模块,通用模块。即支持各种标准的模块,一般来讲,umd模块会在打包产物作为备用模块。
- iife,自动执行的函数。适合作为
<script>
标签包含的自动执行功能。
- system模块,刚看到我也蒙了。
SystemJS是一个插件化的,基于标准的模块加载器。它提供了一个工作流,可以将为浏览器中编写的原始ES6模块代码转换为System.register模块格式,以在不支持原始ES6模块的旧版浏览器中运行,几乎可以达到运行原始ES模块的速度,同时支持
顶层 await
,动态导入,循环引用和实时绑定,import.meta.url,模块类型,导入映射,完整性和内容安全策略,并且在旧版浏览器中可兼容IE11。
4.2 基本配置&插件
我们上边说过,rollup的核心功能其实是在各个模块之间转换,所以使用rollup打包非常简单,大部分情况下只需要掌握output、input、plugins几个选项的部分配置就可以完成模块打包。
4.2.1 input
该选项用于指定 bundle 的入口文件(例如,你的 main.js
,app.js
或 index.js
文件)。如果值为一个入口文件的数组或一个将名称映射到入口文件的对象,那么它们将被打包到单独的输出 chunks。除非使用 output.file 选项,否则生成的 chunk 名称将遵循 output.entryFileNames 选项设置。当该选项的值为对象形式时,对象的属性名将作为文件名中的 [name]
,而对于值为数组形式,数组的值将作为入口文件名。
请注意,当选项的值使用对象形式时,可以通过在名称中添加 /
来将入口文件放入不同的子文件夹。以下例子将根据 entry-a.js
和 entry-b/index.js
,产生至少两个入口 chunks,即 index.js
文件将输出在 entry-b
文件夹中:
4.2.2 output.dir/output.file
和webpack、vite一样,这里同样支持多文件设置,形式上通过config[]设置。其中file选项只有在生成的chunk不超过一个的情况下才可以使用,dir选项则是在多个以上chunk时使用(搭配选项)。
file示例:
export default {
input: "index.js",
output: {
file: "dist/umd.min.js",
format: "umd",
name: "core",
},
};
dir示例:
export default {
input: "index.js",
output: {
dir: "dist",
format: "umd",
name: "core",
},
};
多文件示例:
export default {
input: "index.js",
output: [{
dir: "dist/es",
format: "es",
},{
dir: "dist/umd",
format: "umd",
}],
};
4.2.3 output.name
对于输出格式为 iife
/ umd
的 bundle 来说,若想要使用全局变量名来表示你的 bundle 时,该选项是必要的。同一页面上的其他脚本可以使用这个变量名来访问你的 bundle 输出。
// rollup.config.js
export default {
...,
output: {
file: 'bundle.js',
format: 'iife',
name: 'MyBundle'
}
};
// var MyBundle = (function () {...
该选项也支持命名空间,即可以包含点 .
的名字。最终生成的 bundle 将包含命名空间所需要的设置。
4.2.4 output.globals
该选项用于在 umd
/ iife
bundle 中,使用 id: variableName
键值对指定外部依赖。例如,在这样的情况下:
import $ from 'jquery';
我们需要告诉 Rollup jquery
是外部依赖,jquery
模块的 ID 为全局变量 $
:
// rollup.config.js
export default {
...
external: ['jquery'],
output: {
format: 'iife',
name: 'MyBundle',
globals: {
jquery: '$'
}
}
};
/*
var MyBundle = (function ($) {
// 这里编辑代码
}($));
*/
4.2.5 output.plugins
相比后边要将的plugins,output中的插件更特殊,该选项用于指定输出插件。output.plugins
仅限于在 bundle.generate()
或 bundle.write()
阶段,即在 Rollup 的主要分析完成后运行钩子的插件才可使用。比如下图就是一个压缩代码的示例。
4.2.5 plugins
如果我们需要打包更复杂的代码,通常需要更灵活的配置,例如导入使用 NPM 安装的模块、使用 Babel 编译代码、处理 JSON 文件等等。
为此,rollup提供了插件机制,用于在捆绑过程的关键点更改 Rollup 的行为。plugins是一个数组,数组内的插件可以用代码的方式做开关。
rollup的插件机制主要是基于生命周期钩子,插件可以在不同的生命周期钩子内改变上下文,进而处理代码。具体的生命周期请参考cn.rollupjs.org/plugin-deve…
4.2.6 external
该选项用于匹配需要排除在 bundle 外部的模块,它的值可以是一个接收模块 id
参数并返回 true
(表示外部依赖)或 false
(表示非外部依赖)的函数,也可以是一个模块 ID 数组或者正则表达式。除此之外,它还可以只是单个的模块 ID 或正则表达式。被匹配的模块 ID 应该满足以下条件之一:
- 外部依赖的名称,需要和引入语句中写法完全一致。例如,如果想标记
import "dependency.js"
为外部依赖,就需要使用"dependency.js"
作为模块 ID;而如果要标记import "dependency"
为外部依赖,则使用"dependency"
。 - 解析过的模块 ID(如文件的绝对路径)。
如果要通过 /node_modules/ 正则表达式过滤掉包的引入,例如 import {rollup} from 'rollup'
,需要先使用类似 @rollup/plugin-node-resolve 的插件,来将引入依赖解析到 node_modules
转载自:https://juejin.cn/post/7294166852157882379