likes
comments
collection

webpack 和 rollup 使用比较 —— 从 0 搭建 web 项目

作者站长头像
站长
· 阅读数 16

本文代码:github.com/supersoup/j…

webpack 和 rollup 都有好些年的历史了,一般在 web 项目中使用 webpack,而库的开发则是使用 rollup 较多。

其实现在很多简单的功能,两者都可以使用。今天我们就来使用这两个工具,来创建一个从零开始的 web 项目。

基本结构

  • 创建一个文件夹作为项目的根文件夹
  • 使用 npm init -y 创建 package.json
  • 使用 npm i rollup webpack webpack-cli cross-env -D 安装基本的库
  • 创建 src 文件夹作为代码文件,dist-webpack 和 dist-rollup 两个文件夹存放分别构建后的代码
  • 创建 index-webpack.html 和 index-rollup.html 作为 html 入口;创建 webpack.config.js 和 rollup.config.js 作为构建配置文件
  • package.json scripts 字段增加 "build": "webpack; rollup --config" 命令

webpack 和 rollup 使用比较 —— 从 0 搭建 web 项目

第一步:输入和输出

我们在 src 目录下创建 a.js 和 index.js

// a.js
export default 'hello word'

// index.js
import a from './a';
console.log(a)

webpack

编写 webpack.config.js,这个配置文件只能用 cjs 的方式编写。

mode 是 webpack4 为了简化配置推出的一种预设,默认是 production。为了让构建后的产物有可读性,我们把它设置为 development,再把 devtool 设置为 false。;当然,也可以直接设置为 none

module.exports = {
   mode: 'development',
   devtool: false,
   entry: './src/index.js',
   output: {
      path: path.resolve(__dirname, 'dist-webpack'),
      filename: '[name].js'
   }
}

rollup

编写 rollup.config.js,这个配置文件可以用 esm 或 cjs 两种方式编写。

input 代表入口文件。 outputfile 代表输出的文件可以写相对路径;format 表示输出的代码形式。

module.exports {
   input: 'src/index.js',
   output: {
      file: 'dist-rollup/main.js',
      format: 'iife', // 立即执行函数
   },
}

查看结果

运行 npm run build 生成结果。

然后我们给两个 html 添加 <script> 引用构建后的 js,在根目录使用 http-server 启动静态资源服务器,就可以访问两个 html 查看控制台输出。两者都正常输出了 hello world

第二步:使用第三方库

我们将要使用 lodashjquery 这两个第三方库,首先安装它们:

$ npm i lodash jquery -S

在 index.js 中增加第三方库的使用:

import _ from 'lodash'
import $ from 'jquery'

console.log(a)
console.log(_.range(10))

我们可以把他们打入到 bundle 中一起使用,也可以通过 <script> 直接引入放在 cdn 或自己服务器上的资源。

方式一:把第三方库打入 bundle 中

webpack

webpack 直接就会把他们打包到 bundle 中。从构建产物中查看 __webpack_modules__ 的值,包含了这两个库和 ./src/a.js 这个自己的模块。

rollup

而 rollup 则要复杂一些。它有两个特点:

  • 第一是不会把 node_modules 里面的内容打包进来,所以需要使用 @rollup/plugin-node-resolve 插件
  • 第二是默认只支持 esm 的模块,而 lodash 和 jquery 的源码并未提供 esm 的支持(如果第三方库有 package.json 有 module 字段,rollup 和 webpack 这样的 es6 感知工具会直接导入 es6 的版本)。所以需要使用 @rollup/plugin-commonjs 支持 cjs 的模块。

安装这俩插件:

$  npm i @rollup/plugin-node-resolve @rollup/plugin-commonjs -D

然后在 rollup.config.js 中添加配置:

const nodeResolve = require('@rollup/plugin-node-resolve');
const commonjs = require('@rollup/plugin-commonjs');

module.exports.plugins = [
    nodeResolve(),
    commonjs(),
]

可以看到构建产物中,这两第三方模块的内容都打入了 bundle 中。

查看结果

刷新两个页面,都可以看到页面出现了 123 标题;控制台出现一个长度为 10 的数组。

方式二:将第三方库作为外部依赖

这是种什么方式?

将第三方库作为外部依赖,放在 cdn 上。一方面不同项目和页面都可以复用,另一方面构建速度会变快,打包之后的包体积都会变小。

首先,我们通过 <script> 标签引入 jquery 和 lodash 这两个库。

<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.1/jquery.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/lodash.js/4.17.21/lodash.min.js"></script>
// 下面才是 <script src="main.js">

我们引入了这两个库,就可以直接通过 window.jQuerywindow.$)和 window._ 对他们进行访问。

然后,我们要在构建配置文件中将这俩库排除。

webpack

module.exports.externals = {
    jquery: 'jQuery',  // 设为 '$' 也可以
    lodash: '_'
}

可以看到构建的代码中,有大概这样的代码,相当于是把全局变量作为这个模块的导出值。

var __webpack_modules__ = {
    lodash: module => { module.exports = _; },
    jquery: module => { module.exports = jQuery; },
    // 其他模块
}

rollup

rollup 需要配置两个地方,output.golbals 告诉 rollup 这些是获取 window 上的属性;external 告诉 rollup 不要打包这些库。

module.exports.output.globals = {
    jquery: 'jQuery',  // 设为 '$' 也可以
    lodash: '_'
}
module.exports.external = ['lodash', 'jquery']

可以看到构建的代码中,有大概这样的代码,把全局变量作为这个 iife 的参数。同时 jquery 和 lodash 的源码也不会打包进去。

(function (_, $) {
   // 具体的内容
})(_, jQuery);

查看结果

和第一种方式显示效果相同。

第三步:使用 babel 进行处理

之前,前端开发很麻烦的一点就是兼容性处理。这个在 babel 出现之后得到了好转。现在虽然浏览器兼容性比较好了,但是仍然还是有部分需要转换的,使用 babel 可以让我们使用浏览器还没支持的最新的标准,甚至是草案;另外像 React 的 jsx 也需要转码。

目标:编译 es6 和 jsx 代码

我们为 index.js 添加如下代码:

import React from 'react';
import ReactDOM from 'react-dom';
const root = document.createElement('div')
document.body.appendChild(root)
ReactDOM.render((<h2>456</h2>), root, () => {
   console.log('render!')
})

同时用第二步的方式二的形式,添加 cdn 上的 react 和 react-dom 库,注意选择 umd 格式的资源:

<script src="https://cdn.bootcdn.net/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>

之后的配置除了要给 externalsoutput.globals + external 属性添加这两个库外,最重要的就是加入 babel 相关的配置。

加入 babel 家族的工具们

使用 babel 比较好的实践是把配置放在 babel.config.js 中。我们在根目录创建这个文件:

module.exports = {
   presets: [
      [ '@babel/env', { modules: false } ],
      '@babel/react'
   ]
}

注意 @babel/env 的配置参数 { modules: false } 表示希望 babel 保留 esm 的方式,不要对模块形式进行转换。其实也可以不设置这个参数,相当于直接用默认值 auto。它会识别你是在用打包器还是 @babel/cli,从而正确地选择模块的形式。

然后安装:

# 这个是 babel 本身使用需要的,由于是用作插件,所以不需要 @babel/cli
$ npm i @babel/core @babel/preset-env @babel/preset-react -D

# 这个是 webpack 的 loader 和 rollup 的 plugin
$ npm i babel-loader @rollup/plugin-babel -D

然后两个配置工具的配置方式如下:

webpack

webpack 里有两个不同的概念:loader 和 plugin:

  • loader 是在 fs 的 api 读取每一个文件之后,经过匹配,要进行的处理的一段管道。
  • plugin 是在 webpack 不同生命周期的 hooks 上进行订阅处理器,然后在 hooks 触发时进行额外操作。

其实 loader 做的事情,plugin 也能做。但是 loader 更加有序和清晰。

而 babel 在 webpack 里使用的是 babel-loader,这个不需要使用 require 引入。

module.exports.module = {
   rules: [
      { 
          test: /.js$/,
          exclude: /(node_modules|bower_components)/,
          use: 'babel-loader' 
      }
   ]
}

注意优化所用到的 exclude 参数,表示 node_modules 里的 js 是不需要通过 babel 来转换的。因为我们引用的 node_modules 的库,是已经经过了 babel 编译的。

rollup

rollup 中没有区分 loader 的概念。都是 plugin。

const babel = require('@rollup/plugin-babel')
module.exports.plugins = [
    babel({
        exclude: 'node_modules/**',
        babelHelpers: 'bundled'
    })
]

和 webpack 一样,它也有 exclude 参数。

查看结果

刷新,可以看到页面上出现了二级标题 456

第四步:使用 typescript

ts 配置和安装

和 babel 类似,typescript 也是推荐把配置文件放在 tsconfig.json 中

{
  "compilerOptions": {
    "target": "es5",
    "module": "ESNext",
    "strict": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  }
}

安装

# ts 本体
$ npm i typescript tslib -D

# webpack 和 rollup 构建所需
$ npm i ts-loader @rollup/plugin-typescript

然后我们添加一个 b.ts

// b.ts
const b: string = 'b'
export default b

// index.js
import b from './b'
console.log(b)

webpack

webpack 针对 .ts 后缀的文件,增加一个匹配规则。注意 use 的 loaders 的顺序是数组从右往左执行。最右侧的 loader 会读取源文件的内容,而最左侧的 loader 则需要返回合法的 js 代码字符串。

另外 webpack 默认只能省略 ['.js', '.json', '.wasm'] 后缀,而我们引入 b.ts 的模块时没有加后缀;我们需要为 extensions 添加额外的后缀。可以采用 '...' 访问默认后缀列表。

module.exports.resolve.extensions = ['.ts', '...']
module.exports.module.rules = [
    {
       test: /.ts$/,
       exclude: /(node_modules|bower_components)/,
       use: [
          'babel-loader',
          'ts-loader',
       ]
    },
    // 其他 rule...
]

rollup

rollup 相对而言更加简单:

const typescript = require('@rollup/plugin-typescript')
module.exports.plugins = [
    // 其他插件...
    
    typescript()
]

第五步:样式处理

样式处理的目标

样式处理一般包括这些工作:

  • 读取 css 文件
  • 读取并编译 less, scss, styl 文件
  • css module
  • postcss 处理
  • 将 css 字符串插入 dom 中,或者将这些字符串提出生成 css 文件
  • 压缩 css

这里举一个例子,尽可能多地包含相关的内容。

我们使用 less 作为样式文件进行编写

然后创建 c.less,希望它作为全局样式:

@color: grey;
body {
  color: @color;
}

创建 d.module.less,希望把使用 css module:

@color: red;
.red {
  color: @color;
}

然后通过 index.js 来引用两者:

import './c.less'
import d from './d.module.less'
console.log(d.red)

为了实现这些功能,webpack 需要组合使用一堆独立的 loader 和 plugin。而 rollup 就非常简单,一个 plugin 包含了所有的功能。

方式一:脚本插入 <style>

webpack

安装以下内容:

$ npm i less-loader css-loader style-loader -D

然后添加一条 rule:

module.exports.module.rules = [
    {
       test: /.less$/,
       use: [
          'style-loader',
          {
             loader: 'css-loader',
             options: {
                modules: {
                   auto: true // 为所有满足 /\.module(s)?\.\w+$/i.test(filename) 条件的文件设置 modules.mode 选项为 local
                }
             }
          },
          'less-loader',
       ]
    },
    // 其他 rule
    
]

rollup

使用 rollup-plugin-postcss 插件一站式解决:

$ npm i rollup-plugin-postcss less -D

在配置文件中设置上 module 的配置即可。less 文件不需要额外的配置,只要确保 less 是安装了的,import less 文件时就会自动识别和处理。

const postcss = require('rollup-plugin-postcss')
module.exports.plugins = [
    postcss({
        modules: true,
        autoModules: /\.module\.\S+$/,
    })
    // 其他插件...
]

查看结果

打开页面,审查元素,可以看到 <head> 被插入两个 <style> 标签:

index-wepback.html 执行:

webpack 和 rollup 使用比较 —— 从 0 搭建 web 项目

index-rollup.html 执行:

webpack 和 rollup 使用比较 —— 从 0 搭建 web 项目

然后控制台也会打印出 css module 化的 className

方式二:抽离独立 css 文件

把 css 文件放在 <head> 中,在 <body> 的任何内容出现之前加载,是一直以来的最佳实践。可以避免页面闪动,减少页面重绘。

webpack

过去实现这个功能用的是:extract-text-webpack-plugin。现在,建议使用最新的 mini-css-extract-plugin 插件。

const MiniCssExtractPlugin = require("mini-css-extract-plugin")
module.exports.plugins = [ new MiniCssExtractPlugin() ]

另外把 style-loader 替换成 MiniCssExtractPlugin.loader

rollup

只需要给 postcss 插件的调用增加一项配置 extract: true

查看结果

在各自的 dist 目录中,均出现了 main.css 文件。内容就是方式一两个 <style> 的内容。

如果愿意可以配置压缩选项。

总结

webpack 从 v1 到现在的 v5,不断地优化性能,增加功能,简化配置;再加上社区努力地翻译文档,终于让初学者觉得上手轻松了一点。

但这个过程中配置不断地变化,也带来了很多迁移的成本。作者本人从 15 年开始使用 webpack,这中间也需要每年集中学习一遍,毕竟这玩意不是每天都会去修改的东西,不会天天都去研究,也总是记不住配置项,只能总结一个适合自己公司的最佳实践的模版各个项目使用。

现在基于 rollup 新构建工具的 vite 的异军突起,让我们这些对 webpack 又爱又恨的菜鸟喜不自禁,又有新的东西可以学了!

当然从上面的几个步骤可以看出,新的工具在使用上要简单很多。其实,构建工具的使用,更多的是学习“需求”,这个“需求”是咱们前端从我们的工作和任务中自己给自己的 ———— 明白要做什么事情,然后朝着这个目标,比较地学习两个技术,可以学得比较深刻。