手把手带你看Vue2是如何使用rollup打包的

1,703 阅读7分钟

image.png

上一章我们学习了快速上手rollup,学会如何使用rollup进行打包操作。相信学习一项新的技术,应该都会去查看对应的官方文档进行辅助学习,那么rollup最重要的三个配置是什么? 是的,就是inputoutputplugins,这期就来讲讲plugins在rollup是如何运行的。

Rollup

什么是plugins?

让我们先来看看plugins在配置中是如何使用的

//rollup.config.js
import myExample from './rollup-plugin-my-example.js';
export default ({
  input: 'index.js', // resolved by our plugin
  output: [{
    file: './dist/bundle.js',
    format: 'cjs'
  }],
  plugins: [myExample()]     // !!!
});

从上述代码中可以看出,首先需要import引入插件,那么前提是需要安装对应的插件,然后引入使用,plugins是一个数组,说明引用的插件可以是多个,基本的使用已经了解,那我们来具体看看插件到底是什么!

这时候当然要结合rollup文档来进行学习啦~

image.png

可以看到rollup插件是一个由一个或多个属性、钩子组成的对象。

image.png

可以自行到github上搜索rollup/plugins->packages文件夹下就是各种插件了,以json这个插件为例来看看代码

import { createFilter, dataToEsm } from '@rollup/pluginutils';

export default function json(options = {}) {
  const filter = createFilter(options.include, options.exclude);
  const indent = 'indent' in options ? options.indent : '\t';

  return {
    name: 'json',

    // eslint-disable-next-line no-shadow
    transform(json, id) {
      if (id.slice(-5) !== '.json' || !filter(id)) return null;

      try {
        const parsed = JSON.parse(json);
        return {
          code: dataToEsm(parsed, {
            preferConst: options.preferConst,
            compact: options.compact,
            namedExports: options.namedExports,
            indent
          }),
          map: { mappings: '' }
        };
      } catch (err) {
        const message = 'Could not parse JSON file';
        const position = parseInt(/[\d]/.exec(err.message)[0], 10);
        this.warn({ message, id, position });
        return null;
      }
    }
  };
}

不用每一行都读懂,只需要看看插件是如何实现的,可以传入一个option参数,默认空,看return返回了一个对象,这个对象中包含了属性name,和钩子(方法)transform,暂时先知道这些就足够了!

钩子的作用不知道没事,下面会详细介绍,看看我自己写的plugins

export default {
    input: './index.js', //入口
    output: { //出口
        file: './dist/bundle.js', //文件路径
        format: 'cjs' //输出格式
    },
    plugins: [{
            buildStart() {
                console.log(1, 'buildStart');
            },
            a() {
                console.log(a);
            }
        },
        {
            options() {
                console.log(2, 'options');
            }
        }
    ]
};

既然已经知道了插件是一个对象,对象中包含属性和钩子,那么我直接添加一个对象,里面写上钩子,是不是也可行呢?原理是不是一样的,如果无法验证,运行就知道结果了

image.png

这里输出了2 options1 buildStart, 首先得到一个结论,明明有三个函数,为什么只打印了两个?明自定义写的函数,内部是不存在该钩子的所以不会执行, 第二个结论是什么呢? 这里2比1 先打印,说明执行的顺序和插件引入的顺序无关,因为引入是1 是在前的。

相信大家现在对 rollup 的 plugins 应该有了一些了解了吧,

构建过程

2ddc2d310b52d5e6bf31d2d712d16d5.png

看完这张图,我们得先理一下rollup的打包流程,来结合理解。

options

这是构建前的第一个钩子,可以传入参数,用于替换默认配置参数,如果没有参数配置,就是用rollup.config.js的默认配置进行打包,

buildStart

开始构建

resolveId

解析路径 在打包过程中,一个js文件可能引入不同的依赖,这时候计算机是不知道这些依赖都是什么的,所以需要这个钩子,来先将所有文件的路径都解析出来,举个例子:

import jq from 'jquery'
import b from './b.js'
import JQ from 'http://ajax.googleapis.com/ajax/libs/jquery/1.8.0/jquery.min.js'

console.log(b);
console.log(jq);
console.log(JQ);

一个文件中可能引入本地文件,线上引入和第三方引入,这种时候该如何打包? 思考一下......

你可能会说,直接添加到打包后的文件中就行了,但是计算机是计算机,不像人眼睛看到的一样,所以需要转化成他能识别的路径。

一共有五种引入方式,分别是:虚拟模块用户模块第三方模块内置模块data协议 这个钩子会解析这些模块的引入方式路径,例如相对路径最终会转换成绝对路径,都由钩子内部实现。

load

路径解析完,当然就是要加载了,将所有的模块全部加载进来,因为前面如果没有resolveId的话,路径无法被识别就没有办法加载了。

举个例子

//a.js
import './b.js'
//b.js
import './c.js'

const b = 'b'
console.log(b);
//c.js
console.log('c');

这边a是入口文件,打包a.jsa.js中引入了b.js文件,b.js文件中引入了c.js文件,在load加载的时候,会将全部内容加载到同一个js文件下。看看结果:

//bundle.js
'use strict';

console.log('c');

const b = 'b';
console.log(b);

transform

转换 node 只能解析.js / .json / .node 的文件,其他文件是无法识别的,但是你一个项目中不可能只有这些文件,最简单的包括图片.jpg或者.ts文件等等,都是无法被解析的,所以这个时候就需要transform这个钩子来实现转换,把无法识别的文件,转换成可以识别的.js文件,这样才可以继续打包。

moduleParsed

模块解析 这里进行判断,如果引入的文件还有其他依赖,则返回·resolveId,继续执行,直到所有模块都被加载并且转换为止,这时候加载进来的所有内容时字符串,moduleParsed会将这个字符串,转换成AST(abstract syntax tree) 抽象语法树格式。

import a from '12313'

let tips = [
  "Click on any AST node with a '+' to expand it",

  "Hovering over a node highlights the \
   corresponding location in the source code",

  "Shift click on an AST node to expand the whole subtree"
];

function printTips() {
  tips.forEach((tip, i) => console.log(`Tip ${i}:` + tip));
}

这是一个基本的js文件结构,有引入,有变量,有函数,到这一步就已经将所有内容解析进来了,来看看生成的AST树结构,每个结构都有type属性和start end开始的字符数和结束位置,转换成清晰的树形结构

image.png

buildEnd

结束构建

小结

了解完构建阶段的这些钩子后,回过头来继续讲一下,前面的plugins。plugins是一个对象,包含属性和钩子,而其中的钩子就是构建阶段这些钩子,那什么时候会执行这些钩子呢?如何执行?

这里我自己写了一个例子,加深印象,大家也可以自己动手试试,这样才能更好的记忆。

export default {
    input: './a.js', //入口
    output: { //出口
        file: './dist/bundle.js', //文件路径
        format: 'cjs' //输出格式
    },
    plugins: [{
            buildStart() {
                console.log(1, 'buildStart');
            },
        },
        {
            options() {
                console.log(2, 'options');
            },
            resolveId() {
                console.log(2, 'resolveId');
            }
        },
        {
            transform() {
                console.log(3, 'transform');
            },
            load() {
                console.log(3, 'load');
            },
            moduleParsed() {
                console.log(3, 'moduleParsed');
            }
        },
        {
            buildStart() {
                console.log(4, 'buildStart');
            },
            buildEnd() {
                console.log(4, 'buildEnd');
            }
        }
    ]
};

这里是自己定义的插件,我们测试钩子的执行,就不需要其他内容了。这里基本上的钩子都添加了,并且我加上了顺序,第一个plugins就输出1 + 钩子名,以此类推,一共四个plugin,其中都包含不同的钩子,来看看打包结果:

image.png

这里第一个打印的是2 options,是第二个插件中的options钩子执行的代码,所以证实了插件引入顺序与执行顺序无关,而是与插件中包含的钩子有关,每执行到一个步骤的时候,都会遍历一次plugins,查看插件中是否有与之对应的钩子,并且执行他。
简单来说,假设执行到了buildStart,就会自动遍历插件中的每一个对象,看看他们之中是否有buildStart的钩子,如果有就执行,没有就继续。并且每一个钩子都是如此的。

vue2是如何使用rollup打包的

学习完rollup,就让我们来看一看,在别的项目中是如何使用rollup进行打包的,这里就以vue2为例,来看看更多rollup的高级使用或者你不知道的使用方式。

要查看一个项目的代码首先当然是先拉代码啦,这里附上vue2源码,先将代码拉下来,我们继续进行。

package.json

第一步要看一个项目,首先得从README或者package.json文件入手,README查看项目说明和使用,package.json查看一些命令,这里我们是要看看如何使用rollup进行打包的,所以我们就从package.json文件入手。

image.png

找到了package.jsonscripts字段,但是如果毫无目的的话,还真是无从下手呢,这指令看的应该头都会大把,不过还好我们是带着目的来的,一般先看两条指令,一个是build打包指令,另一个是dev运行指令,这边我们主要研究打包,那就先看看build指令吧。

image.png

找到了build字段,看看指令内容是node scripts/build.js,很明显的看出,使用node命令执行了scripts下的build.js文件,我们跟随指令来到build.js文件。

build.js

image.png

虽然从build打包指令中还没有看到和rollup相关的信息,但是一进入build.js第一眼就看到了,在build.js中引入了rollup看源码不需要一行一行都读懂,只需要看到关键部分,那么就让我们继续往下看。

image.png

可以看到这里调用一个函数build(),并且传入了一个参数,build是构建打包的意思,那么这个函数一定和打包有关咯,那就找到这个函数,并且大概看看他的作用是什么

function build (builds) {
  let built = 0
  const total = builds.length
  const next = () => {
    buildEntry(builds[built]).then(() => {
      built++
      if (built < total) {
        next()
      }
    }).catch(logError)
  }
  next()
}

从代码中可以看出,这边调用了另一个函数buildEntry函数,并且还是一个异步函数,继续看到buildEntyr函数。

function buildEntry (config) {
  const output = config.output
  const { file, banner } = output
  const isProd = /(min|prod)\.js$/.test(file)
  return rollup.rollup(config)
    .then(bundle => bundle.generate(output))
    .then(({ output: [{ code }] }) => {
      if (isProd) {
        const minified = (banner ? banner + '\n' : '') + terser.minify(code, {
          toplevel: true,
          output: {
            ascii_only: true
          },
          compress: {
            pure_funcs: ['makeMap']
          }
        }).code
        return write(file, minified, true)
      } else {
        return write(file, code)
      }
    })
}

看一个函数最重要的是什么?入参出参,这边咱们入参可能不是很清楚是什么,那看看函数返回了什么,return了一个rollup.rollup的方法,看过rollup文档的话,应该会对这个方法有点熟悉,那我们就来详细看看rollup.rollup这个方法的作用吧!!!

rollup.rollup

image.png

rollup.rollup接收一个配置对象作为参数,并且返回一个promise对象,去解析有各种属性和方法的bundle对象,在这个步骤会执行tree-shaking

bundle对象中,可以调用bundle.generate方法实现打不同版本的包,

 rollup.rollup(config)
    .then(bundle => bundle.generate(output))
    .then(({ output: [{ code }] }) => {
      if (isProd) {
        const minified = (banner ? banner + '\n' : '') + terser.minify(code, {
          toplevel: true,
          output: {
            ascii_only: true
          },
          compress: {
            pure_funcs: ['makeMap']
          }
        }).code
        return write(file, minified, true)
      } else {
        return write(file, code)
      }
    })

rollup.rollup(config).then(bundle => bundle.generate(output)) 这里就可以看出返回了一个bundle对象,后续就是判断是否压缩(isProd),那么我们看完函数的出参,回过头我们看一看入参,传入的配置参数是什么?

image.png

顺着参数往上找,就可以找到这个一开始定义好的builds参数,这里引入了config.js文件,那么我们就去config.js文件中看看,到底定义了什么,而getAllBuilds()又是什么方法。
这里提示:一定要把vue的源码拉下来,结合着看,不然可能看不懂,因为代码太长了没法一一黏贴过来,所以要自己实际去操作一下,这里主要讲该如何去查看并且学习,vue2如何使用rollup打包的。

config.js

image.png

config.js中引入了大量的rollup的插件(plugin),前面也介绍了插件的作用,继续看:

image.png

在builds对象里包含了各种不同版本的包,接下来就是找到getAllBuilds这个方法在哪里,因为在build.js中引入了,来到最下面:

image.png

这里把使用Object.keys的方法将builds对象进行了遍历,生成map,把配置返回,上有有配置方法就不再看了~ 这样就实现了不同版本的包获取对应的配置传给build.js文件进行打包了。

总结

学习如何查看项目的源码,首先要确定自己的目标是什么,带着目的和问题进行查看,这样才有方向,毕竟一个项目这么大,一个文件一个文件看是不实际的也很浪费时间,学习效率并不高,甚至到最后还可能看不懂,具体的流程我已经一步一步详细的列出来,并且解析了一下,也是初学,还有很多提升的空间,大家可以借鉴。